From 55cf7a2e2bd7061efa706f2f8b7f851bb3f08e64 Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 10 Nov 2021 18:00:21 +0900 Subject: [PATCH 01/17] add bundle NTKUltraCubeFaceBundle face type representing a portrait watchface --- Watchface/Face.swift | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Watchface/Face.swift b/Watchface/Face.swift index 8555975..f741bf0 100644 --- a/Watchface/Face.swift +++ b/Watchface/Face.swift @@ -4,16 +4,21 @@ extension Watchface { public struct Face: Codable { public var version: Int = 4 public var face_type: FaceType + /// non-nil when face type = bundle + public var bundle_id: BundleID? /// infograph: nil public var resource_directory: Bool? = true public var customization: Customization public var complications: Complications? + /// unknown values + public var argon: Argon? private enum CodingKeys: String, CodingKey { case version case customization case complications case face_type = "face type" + case bundle_id = "bundle id" case resource_directory = "resource directory" } @@ -24,6 +29,12 @@ extension Watchface { case kaleidoscope /// aka infograph case whistler_analog = "whistler-analog" + /// portrait (has bundle id) + case bundle + } + + public enum BundleID: String, Codable { + case comAppleNTKUltraCubeFaceBundle = "com.apple.NTKUltraCubeFaceBundle" } public struct Customization: Codable { @@ -121,12 +132,26 @@ extension Watchface { } } - public init(version: Int = 4, face_type: FaceType, resource_directory: Bool? = true, customization: Customization, complications: Complications? = nil) { + public struct Argon { + /// unknown base64. (possibly constant value) + public var k: String? + /// unknown {hash}.aea. (possibly constant value) + public var n: String? + + public init(k: String? = nil, n: String? = nil) { + self.k = k + self.n = n + } + } + + public init(version: Int = 4, face_type: FaceType, bundle_id: BundleID? = nil, resource_directory: Bool? = true, customization: Customization, complications: Complications? = nil, argon: Argon? = nil) { self.version = version self.face_type = face_type + self.bundle_id = bundle_id self.resource_directory = resource_directory self.customization = customization self.complications = complications + self.argon = argon } } } From 6d5a1415ddeb69ae2ee94e5b735b3be0f3b13c91 Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 17 Nov 2021 18:18:25 +0900 Subject: [PATCH 02/17] now --- WatchFaceDumper.xcodeproj/project.pbxproj | 4 + WatchFaceDumper/ViewController.swift | 11 +- Watchface/Face.swift | 29 +--- Watchface/Metadata.swift | 18 +- Watchface/Resources.swift | 57 ++++--- .../PortraitWatchface.swift | 158 ++++++++++++++++++ 6 files changed, 212 insertions(+), 65 deletions(-) create mode 100644 Watchface/SpecificWatchfaces/PortraitWatchface.swift diff --git a/WatchFaceDumper.xcodeproj/project.pbxproj b/WatchFaceDumper.xcodeproj/project.pbxproj index 349bb9e..bacf737 100644 --- a/WatchFaceDumper.xcodeproj/project.pbxproj +++ b/WatchFaceDumper.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ EA9D41352520BFD300838E68 /* Face.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41342520BFD300838E68 /* Face.swift */; }; EA9D41382520C00800838E68 /* Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41372520C00800838E68 /* Resources.swift */; }; EA9D41442520C11A00838E68 /* Watchface+FileWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41432520C11A00838E68 /* Watchface+FileWrapper.swift */; }; + EAA4F2912744F57900AE226B /* PortraitWatchface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA4F2902744F57900AE226B /* PortraitWatchface.swift */; }; EAD954F1256E7D5D004EBB02 /* EditableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD954F0256E7D5D004EBB02 /* EditableImageView.swift */; }; /* End PBXBuildFile section */ @@ -57,6 +58,7 @@ EA9D41342520BFD300838E68 /* Face.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Face.swift; sourceTree = ""; }; EA9D41372520C00800838E68 /* Resources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resources.swift; sourceTree = ""; }; EA9D41432520C11A00838E68 /* Watchface+FileWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Watchface+FileWrapper.swift"; sourceTree = ""; }; + EAA4F2902744F57900AE226B /* PortraitWatchface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitWatchface.swift; sourceTree = ""; }; EAD954F0256E7D5D004EBB02 /* EditableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableImageView.swift; sourceTree = ""; }; FE8E86D8C8D1F1C494C8E5E4 /* Pods-WatchFaceDumper.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchFaceDumper.release.xcconfig"; path = "Target Support Files/Pods-WatchFaceDumper/Pods-WatchFaceDumper.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -133,6 +135,7 @@ isa = PBXGroup; children = ( EA52949D2520DAA400CE938E /* PhotosWatchface.swift */, + EAA4F2902744F57900AE226B /* PortraitWatchface.swift */, ); path = SpecificWatchfaces; sourceTree = ""; @@ -292,6 +295,7 @@ EAD954F1256E7D5D004EBB02 /* EditableImageView.swift in Sources */, EA9D412C2520BF5100838E68 /* ImageProvider.swift in Sources */, EA76F8D8251B94E700B395EC /* MetadataViewModel.swift in Sources */, + EAA4F2912744F57900AE226B /* PortraitWatchface.swift in Sources */, EA4D31BE24FFE37100104A33 /* Document.swift in Sources */, EA76F8D3251B906800B395EC /* ImageListOutlineViewModel.swift in Sources */, ); diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 0903901..95924c4 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -270,8 +270,13 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa originalCropX: 0, originalCropY: 0) watchface.resources?.images.imageList.append(item) - watchface.resources?.files[item.imageURL] = imageData - watchface.resources?.files[item.irisVideoURL] = movieData + // TODO: add resource URLs for UltraCube such as baseImage + if let u = item.imageURL { + watchface.resources?.files[u] = imageData + } + if let u = item.irisVideoURL { + watchface.resources?.files[u] = movieData + } } reloadDocument() } @@ -280,7 +285,7 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa guard case 0.. Value? { @@ -49,6 +51,7 @@ extension Watchface { case .slot2: return slot2 case .slot3: return slot3 case .bezel: return bezel + case .date: return date } } set { @@ -64,23 +67,10 @@ extension Watchface { case .slot2: slot2 = newValue case .slot3: slot3 = newValue case .bezel: bezel = newValue + case .date: date = newValue } } } - - public init(top: Value? = nil, bottom: Value? = nil, top_left: Value? = nil, top_right: Value? = nil, bottom_left: Value? = nil, bottom_center: Value? = nil, bottom_right: Value? = nil, slot1: Value? = nil, slot2: Value? = nil, slot3: Value? = nil, bezel: Value? = nil) { - self.top = top - self.bottom = bottom - self.top_left = top_left - self.top_right = top_right - self.bottom_left = bottom_left - self.bottom_center = bottom_center - self.bottom_right = bottom_right - self.slot1 = slot1 - self.slot2 = slot2 - self.slot3 = slot3 - self.bezel = bezel - } } public struct Color: Codable { diff --git a/Watchface/Resources.swift b/Watchface/Resources.swift index 6809887..5b95b84 100644 --- a/Watchface/Resources.swift +++ b/Watchface/Resources.swift @@ -50,12 +50,23 @@ extension Watchface { /// photos has some, kaleidoscope has none public var rightAnalysis: Analysis? - public var imageURL: String + /// photos has some, UltraCube has none + public var imageURL: String? + /// photos has none, UltraCube has some + public var baseImageURL: String? + /// photos has none, UltraCube may have some paired with backgroundImageURL + public var maskImageURL: String? + /// photos has none, UltraCube may have some paired with maskImageURL + public var backgroundImageURL: String? - public var irisDuration: Double = 3 - public var irisStillDisplayTime: Double = 0 - public var irisVideoURL: String - public var isIris: Bool = true + /// photos has some + public var irisDuration: Double? = 3 + /// photos has some + public var irisStillDisplayTime: Double? = 0 + /// photos has some + public var irisVideoURL: String? + /// photos has some + public var isIris: Bool? = true /// required for watchface sharing... it seems like PHAsset local identifier "UUID/L0/001". an empty string should work anyway. public var localIdentifier: String @@ -70,27 +81,21 @@ extension Watchface { public var originalCropX: Double public var originalCropY: Double - public init(topAnalysis: Analysis? = nil, leftAnalysis: Analysis? = nil, bottomAnalysis: Analysis? = nil, rightAnalysis: Analysis? = nil, imageURL: String, irisDuration: Double = 3, irisStillDisplayTime: Double = 0, irisVideoURL: String, isIris: Bool = true, localIdentifier: String, modificationDate: Date? = Date(), cropH: Double = 480, cropW: Double = 384, cropX: Double = 0, cropY: Double = 0, originalCropH: Double, originalCropW: Double, originalCropX: Double, originalCropY: Double) { - self.topAnalysis = topAnalysis - self.leftAnalysis = leftAnalysis - self.bottomAnalysis = bottomAnalysis - self.rightAnalysis = rightAnalysis - self.imageURL = imageURL - self.irisDuration = irisDuration - self.irisStillDisplayTime = irisStillDisplayTime - self.irisVideoURL = irisVideoURL - self.isIris = isIris - self.localIdentifier = localIdentifier - self.modificationDate = modificationDate - self.cropH = cropH - self.cropW = cropW - self.cropX = cropX - self.cropY = cropY - self.originalCropH = originalCropH - self.originalCropW = originalCropW - self.originalCropX = originalCropX - self.originalCropY = originalCropY - } + /// UltraCube has some + public var baseImageZorder: Int? = 0 + /// UltraCube has some + public var maskedImageZorder: Int? = 1 + /// UltraCube has some + public var timeElementImageZorder: Int? = 2 + /// UltraCube has some. 0-1? + public var imageAOTBrightness: Double? = 0.5 + /// UltraCube has some. constant false? + public var parallaxFlat: Bool? = false + /// UltraCube has some. constant 1.075? + public var parallaxScale: Double? = 1.075 + /// UltraCube has some + public var userAdjusted: Bool? = false + } public init(imageList: [Item], version: Int = 1) { diff --git a/Watchface/SpecificWatchfaces/PortraitWatchface.swift b/Watchface/SpecificWatchfaces/PortraitWatchface.swift new file mode 100644 index 0000000..aa42577 --- /dev/null +++ b/Watchface/SpecificWatchfaces/PortraitWatchface.swift @@ -0,0 +1,158 @@ +import Foundation + +public struct PortraitWatchface { + public var device_size: Int = 2 + public var style: Style + public var snapshot: Data + public var no_borders_snapshot: Data + public var dateComplication: Complication? + public var bottomComplication: Complication? + public var resources: Resources + + public enum Style: String { + /// Classic + case style1 = "style 1" + /// Modern + case style2 = "style 2" + /// Rounded + case style3 = "style 3" + } + + public struct Resources { + public var images: Metadata + /// filename -> content + public var files: [String: Data] + + public struct Metadata: Codable { + public var imageList: [Item] + public var version: Int = 1 + + public struct Item: Codable { + /// UltraCube has some + public var baseImageURL: String + /// UltraCube may have some paired with backgroundImageURL + public var maskImageURL: String? + /// UltraCube may have some paired with maskImageURL + public var backgroundImageURL: String? + + /// required for watchface sharing... it seems like PHAsset local identifier "UUID/L0/001". an empty string should work anyway. + public var localIdentifier: String + public var modificationDate: Date? = Date() + + public var originalCropH: Double + public var originalCropW: Double + public var originalCropX: Double + public var originalCropY: Double + + /// UltraCube has some + public var baseImageZorder: Int = 0 + /// UltraCube has some + public var maskedImageZorder: Int = 1 + /// UltraCube has some + public var timeElementImageZorder: Int = 2 + /// UltraCube has some. 0-1? + public var imageAOTBrightness: Double = 0.5 + /// UltraCube has some. constant false? + public var parallaxFlat: Bool = false + /// UltraCube has some. constant 1.075? + public var parallaxScale: Double = 1.075 + /// UltraCube has some + public var userAdjusted: Bool = false + } + + public init(imageList: [Item], version: Int = 1) { + self.imageList = imageList + self.version = version + } + } + + public init(images: Metadata, files: [String: Data]) { + self.images = images + self.files = files + } + + } + + public struct Complication { + public var name: String + public var template: Watchface.Metadata.ComplicationTemplate + // TODO: eliminate nil? + public var faceItem: Watchface.Face.Complications.Item? + /// (filename -> content) + public var data: [String: Data]? + + public init(name: String, template: Watchface.Metadata.ComplicationTemplate, faceItem: Watchface.Face.Complications.Item? = nil, data: [String: Data]? = nil) { + self.name = name + self.template = template + self.faceItem = faceItem + self.data = data + } + } +} + +extension PortraitWatchface { + public init?(watchface: Watchface) { + guard let style = watchface.face.customization.style.flatMap(Self.Style.init), + let resources = watchface.resources, + let resourcesMetadata = Resources.Metadata(images: resources.images) else { return nil } + self.init( + device_size: watchface.metadata.device_size, + style: style, + snapshot: watchface.snapshot, + no_borders_snapshot: watchface.no_borders_snapshot, + dateComplication: watchface.metadata.complication_sample_templates.date.map { + Complication( + name: watchface.metadata.complications_names.date ?? "Off", + template: $0, + faceItem: watchface.face.complications?.date, + data: watchface.complicationData?.date) + }, + bottomComplication: watchface.metadata.complication_sample_templates.bottom.map { + Complication( + name: watchface.metadata.complications_names.bottom ?? "Off", + template: $0, + faceItem: watchface.face.complications?.bottom, + data: watchface.complicationData?.bottom) + }, + resources: .init(images: resourcesMetadata, files: resources.files)) + } +} +extension PortraitWatchface.Resources.Metadata { + public init?(images: Watchface.Resources.Metadata) { + let imageList = images.imageList.compactMap { item -> Item? in + guard let baseImageURL = item.baseImageURL, + let baseImageZorder = item.baseImageZorder, + let maskedImageZorder = item.maskedImageZorder, + let timeElementImageZorder = item.timeElementImageZorder, + let imageAOTBrightness = item.imageAOTBrightness, + let parallaxFlat = item.parallaxFlat, + let parallaxScale = item.parallaxScale, + let userAdjusted = item.userAdjusted else { return nil } + return Item( + baseImageURL: baseImageURL, + maskImageURL: item.maskImageURL, + backgroundImageURL: item.backgroundImageURL, + localIdentifier: item.localIdentifier, + modificationDate: item.modificationDate, + originalCropH: item.originalCropH, + originalCropW: item.originalCropW, + originalCropX: item.originalCropX, + originalCropY: item.originalCropY, + baseImageZorder: baseImageZorder, + maskedImageZorder: maskedImageZorder, + timeElementImageZorder: timeElementImageZorder, + imageAOTBrightness: imageAOTBrightness, + parallaxFlat: parallaxFlat, + parallaxScale: parallaxScale, + userAdjusted: userAdjusted) + } + guard imageList.count == images.imageList.count else { return nil } + self.init( + imageList: imageList, + version: images.version) + } +} + +extension Watchface { + +} From 918ebd9bd3fd91ddbebacb52464c5282c3e4c90d Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 24 Nov 2021 18:21:14 +0900 Subject: [PATCH 03/17] now --- WatchFaceDumper/Document.swift | 2 +- .../ImageListOutlineViewModel.swift | 9 +- Watchface/Resources.swift | 88 +++++++++++------ .../PortraitWatchface.swift | 98 +++++-------------- 4 files changed, 89 insertions(+), 108 deletions(-) diff --git a/WatchFaceDumper/Document.swift b/WatchFaceDumper/Document.swift index 0a706fc..af92cfc 100644 --- a/WatchFaceDumper/Document.swift +++ b/WatchFaceDumper/Document.swift @@ -5,7 +5,7 @@ import Ikemen class Document: NSDocument { var watchface: Watchface = .init( photosWatchface: PhotosWatchface( - device_size: 2, position: .top, snapshot: Data(), no_borders_snapshot: Data(), topComplication: nil, bottomComplication: nil, resources: .init(images: .init(imageList: []), files: [:]))) + device_size: 2, position: .top, snapshot: Data(), no_borders_snapshot: Data(), topComplication: nil, bottomComplication: nil, resources: .init(images: .photos(.init(imageList: [])), files: [:]))) private var isLossyReading = false private var allowLossyAutosaving = false diff --git a/WatchFaceDumper/ImageListOutlineViewModel.swift b/WatchFaceDumper/ImageListOutlineViewModel.swift index 600fc9e..4bff7c7 100644 --- a/WatchFaceDumper/ImageListOutlineViewModel.swift +++ b/WatchFaceDumper/ImageListOutlineViewModel.swift @@ -11,8 +11,13 @@ final class ImageListOutlineViewModel: NSObject, NSOutlineViewDelegate, NSOutlin } func setWatchface(_ watchface: Watchface) { - imageListPropertyList = (try? PropertyListEncoder().encode(watchface.resources?.images.imageList)) - .flatMap {try? PropertyListSerialization.propertyList(from: $0, options: [], format: nil)} as? [Any] + let imageList: Data? + switch watchface.resources?.images { + case .photos(let v)?: imageList = try? PropertyListEncoder().encode(v.imageList) + case .ultraCube(let v)?: imageList = try? PropertyListEncoder().encode(v.imageList) + case nil: imageList = nil + } + imageListPropertyList = imageList.flatMap {try? PropertyListSerialization.propertyList(from: $0, options: [], format: nil)} as? [Any] } func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { diff --git a/Watchface/Resources.swift b/Watchface/Resources.swift index 5b95b84..12dd6c6 100644 --- a/Watchface/Resources.swift +++ b/Watchface/Resources.swift @@ -6,7 +6,28 @@ extension Watchface { /// filename -> content public var files: [String: Data] - public struct Metadata: Codable { + public enum Metadata: Codable { + /// photos or kaleidoscope (can be separated into cases...) + case photos(PhotosV1) + /// UltraCube aka Portrait + case ultraCube(UltraCubeV2) + + public init(from decoder: Decoder) throws { + self = try (try? PhotosV1(from: decoder)).map(Self.photos) + ?? (try? UltraCubeV2(from: decoder)).map(Self.ultraCube) + // generate an exception as photos + ?? Self.photos(PhotosV1(from: decoder)) + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .photos(let v): try v.encode(to: encoder) + case .ultraCube(let v): try v.encode(to: encoder) + } + } + } + + public struct PhotosV1: Codable { public var imageList: [Item] public var version: Int = 1 @@ -52,12 +73,6 @@ extension Watchface { /// photos has some, UltraCube has none public var imageURL: String? - /// photos has none, UltraCube has some - public var baseImageURL: String? - /// photos has none, UltraCube may have some paired with backgroundImageURL - public var maskImageURL: String? - /// photos has none, UltraCube may have some paired with maskImageURL - public var backgroundImageURL: String? /// photos has some public var irisDuration: Double? = 3 @@ -81,32 +96,45 @@ extension Watchface { public var originalCropX: Double public var originalCropY: Double - /// UltraCube has some - public var baseImageZorder: Int? = 0 - /// UltraCube has some - public var maskedImageZorder: Int? = 1 - /// UltraCube has some - public var timeElementImageZorder: Int? = 2 - /// UltraCube has some. 0-1? - public var imageAOTBrightness: Double? = 0.5 - /// UltraCube has some. constant false? - public var parallaxFlat: Bool? = false - /// UltraCube has some. constant 1.075? - public var parallaxScale: Double? = 1.075 - /// UltraCube has some - public var userAdjusted: Bool? = false - - } - - public init(imageList: [Item], version: Int = 1) { - self.imageList = imageList - self.version = version } } - public init(images: Metadata, files: [String: Data]) { - self.images = images - self.files = files + public struct UltraCubeV2: Codable { + public var imageList: [Item] + public var version: Int = 2 + + public struct Item: Codable { + public var baseImageURL: String + /// paired with backgroundImageURL. nil for some photos + public var maskImageURL: String? + /// paired with maskImageURL. nil for some photos + public var backgroundImageURL: String? + + /// required for watchface sharing... it seems like PHAsset local identifier "UUID/L0/001". an empty string should work anyway. + public var localIdentifier: String + public var modificationDate: Date? = Date() + + public var cropH: Double = 480 + public var cropW: Double = 384 + public var cropX: Double = 0 + public var cropY: Double = 0 + public var originalCropH: Double + public var originalCropW: Double + public var originalCropX: Double + public var originalCropY: Double + + public var baseImageZorder: Int = 0 + public var maskedImageZorder: Int = 1 + public var timeElementImageZorder: Int = 2 + /// 0-1? + public var imageAOTBrightness: Double = 0.5 + /// constant false? + public var parallaxFlat: Bool = false + /// constant 1.075? + public var parallaxScale: Double = 1.075 + public var userAdjusted: Bool? = false + + } } } } diff --git a/Watchface/SpecificWatchfaces/PortraitWatchface.swift b/Watchface/SpecificWatchfaces/PortraitWatchface.swift index aa42577..a5a7bbf 100644 --- a/Watchface/SpecificWatchfaces/PortraitWatchface.swift +++ b/Watchface/SpecificWatchfaces/PortraitWatchface.swift @@ -19,58 +19,15 @@ public struct PortraitWatchface { } public struct Resources { + public typealias Metadata = Watchface.Resources.UltraCubeV2 public var images: Metadata /// filename -> content public var files: [String: Data] - public struct Metadata: Codable { - public var imageList: [Item] - public var version: Int = 1 - - public struct Item: Codable { - /// UltraCube has some - public var baseImageURL: String - /// UltraCube may have some paired with backgroundImageURL - public var maskImageURL: String? - /// UltraCube may have some paired with maskImageURL - public var backgroundImageURL: String? - - /// required for watchface sharing... it seems like PHAsset local identifier "UUID/L0/001". an empty string should work anyway. - public var localIdentifier: String - public var modificationDate: Date? = Date() - - public var originalCropH: Double - public var originalCropW: Double - public var originalCropX: Double - public var originalCropY: Double - - /// UltraCube has some - public var baseImageZorder: Int = 0 - /// UltraCube has some - public var maskedImageZorder: Int = 1 - /// UltraCube has some - public var timeElementImageZorder: Int = 2 - /// UltraCube has some. 0-1? - public var imageAOTBrightness: Double = 0.5 - /// UltraCube has some. constant false? - public var parallaxFlat: Bool = false - /// UltraCube has some. constant 1.075? - public var parallaxScale: Double = 1.075 - /// UltraCube has some - public var userAdjusted: Bool = false - } - - public init(imageList: [Item], version: Int = 1) { - self.imageList = imageList - self.version = version - } - } - public init(images: Metadata, files: [String: Data]) { self.images = images self.files = files } - } public struct Complication { @@ -119,40 +76,31 @@ extension PortraitWatchface { } extension PortraitWatchface.Resources.Metadata { public init?(images: Watchface.Resources.Metadata) { - let imageList = images.imageList.compactMap { item -> Item? in - guard let baseImageURL = item.baseImageURL, - let baseImageZorder = item.baseImageZorder, - let maskedImageZorder = item.maskedImageZorder, - let timeElementImageZorder = item.timeElementImageZorder, - let imageAOTBrightness = item.imageAOTBrightness, - let parallaxFlat = item.parallaxFlat, - let parallaxScale = item.parallaxScale, - let userAdjusted = item.userAdjusted else { return nil } - return Item( - baseImageURL: baseImageURL, - maskImageURL: item.maskImageURL, - backgroundImageURL: item.backgroundImageURL, - localIdentifier: item.localIdentifier, - modificationDate: item.modificationDate, - originalCropH: item.originalCropH, - originalCropW: item.originalCropW, - originalCropX: item.originalCropX, - originalCropY: item.originalCropY, - baseImageZorder: baseImageZorder, - maskedImageZorder: maskedImageZorder, - timeElementImageZorder: timeElementImageZorder, - imageAOTBrightness: imageAOTBrightness, - parallaxFlat: parallaxFlat, - parallaxScale: parallaxScale, - userAdjusted: userAdjusted) - } - guard imageList.count == images.imageList.count else { return nil } + guard case .ultraCube(let metadata) = images else { return nil } self.init( - imageList: imageList, - version: images.version) + imageList: metadata.imageList.map { + Item( + baseImageURL: $0.baseImageURL, + maskImageURL: $0.maskImageURL, + backgroundImageURL: $0.backgroundImageURL, + localIdentifier: $0.localIdentifier, + modificationDate: $0.modificationDate, + originalCropH: $0.originalCropH, + originalCropW: $0.originalCropW, + originalCropX: $0.originalCropX, + originalCropY: $0.originalCropY, + baseImageZorder: $0.baseImageZorder, + maskedImageZorder: $0.maskedImageZorder, + timeElementImageZorder: $0.timeElementImageZorder, + imageAOTBrightness: $0.imageAOTBrightness, + parallaxFlat: $0.parallaxFlat, + parallaxScale: $0.parallaxScale, + userAdjusted: $0.userAdjusted) + }, + version: metadata.version) } } extension Watchface { - + // TODO } From b9fd76b1df033018e717522c2f3250807f97b673 Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 8 Dec 2021 18:16:01 +0900 Subject: [PATCH 04/17] now --- WatchFaceDumper/ViewController.swift | 178 ++++++++++++------ Watchface/Resources.swift | 29 ++- .../PortraitWatchface.swift | 2 +- Watchface/Watchface+FileWrapper.swift | 13 +- 4 files changed, 150 insertions(+), 72 deletions(-) diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 95924c4..86ac6ec 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -173,9 +173,13 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa metadataViewModel.setWatchface(watchface) imageItems = watchface.resources.map { resources in - resources.images.imageList - .map {(resources.files[$0.imageURL], resources.files[$0.irisVideoURL])} - .map {ImageItem(image: $0.0.flatMap {NSImage(data: $0)}, movie: $0.1.map {($0, nil)})} + switch resources.images { + case .photos(let v): return v.imageList + .map {(resources.files[$0.imageURL], resources.files[$0.irisVideoURL])} + .map {ImageItem(image: $0.0.flatMap {NSImage(data: $0)}, movie: $0.1.map {($0, nil)})} + case .ultraCube(let v): return v.imageList + .map {ImageItem(image: resources.files[$0.baseImageURL].flatMap {NSImage(data: $0)}, movie: nil)} // TODO: back, mask + } } ?? [] imageListOutlineViewModel.setWatchface(watchface) @@ -199,18 +203,39 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa $0.imageDidChange = { [weak self] image in guard let self = self else { return } self.document.watchface = self.document.watchface ※ { watchface in - guard let imageURL = watchface.resources?.images.imageList[row].imageURL else { return } + guard let resources = watchface.resources else { return } let jpeg = image?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) - watchface.resources?.files[imageURL] = jpeg - // TODO: resize - watchface.resources?.images.imageList[row].cropX = 0 - watchface.resources?.images.imageList[row].cropY = 0 - watchface.resources?.images.imageList[row].cropW = Double(image?.size.width ?? 0) - watchface.resources?.images.imageList[row].cropH = Double(image?.size.height ?? 0) - watchface.resources?.images.imageList[row].originalCropX = 0 - watchface.resources?.images.imageList[row].originalCropY = 0 - watchface.resources?.images.imageList[row].originalCropW = Double(image?.size.width ?? 0) - watchface.resources?.images.imageList[row].originalCropH = Double(image?.size.height ?? 0) + watchface.resources = resources ※ { resources in + switch resources.images { + case .photos(let v): + resources.files[v.imageList[row].imageURL] = jpeg + resources.images = .photos(v ※ { + // TODO: resize + $0.imageList[row].cropX = 0 + $0.imageList[row].cropY = 0 + $0.imageList[row].cropW = Double(image?.size.width ?? 0) + $0.imageList[row].cropH = Double(image?.size.height ?? 0) + $0.imageList[row].originalCropX = 0 + $0.imageList[row].originalCropY = 0 + $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) + $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) + }) + case .ultraCube(let v): + // TODO: back, mask + resources.files[v.imageList[row].baseImageURL] = jpeg + resources.images = .ultraCube(v ※ { + // TODO: resize + $0.imageList[row].cropX = 0 + $0.imageList[row].cropY = 0 + $0.imageList[row].cropW = Double(image?.size.width ?? 0) + $0.imageList[row].cropH = Double(image?.size.height ?? 0) + $0.imageList[row].originalCropX = 0 + $0.imageList[row].originalCropY = 0 + $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) + $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) + }) + } + } } self.reloadDocument() } @@ -218,15 +243,20 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa guard let self = self else { return } let (movie, duration) = ($0?.data, $0?.duration.flatMap {$0 > 0 ? $0 : nil}) self.document.watchface = self.document.watchface ※ { watchface in - guard let irisVideoURL = watchface.resources?.images.imageList[row].irisVideoURL else { return } - watchface.resources?.files[irisVideoURL] = movie - watchface.resources?.images.imageList[row].isIris = movie != nil - watchface.resources?.images.imageList[row].irisDuration = duration ?? 3.0 - watchface.resources?.images.imageList[row].irisStillDisplayTime = (duration ?? 3.0) - 0.1 - // NOTE: 3 secs in 30fps is best for watchface resources that is cropped & created as watchface by iOS - // TODO: re-compress: should be less than 3 secs? - // TODO: update duration metadata - + switch watchface.resources?.images { + case .photos(let v): + watchface.resources?.files[v.imageList[row].irisVideoURL] = movie + watchface.resources?.images = .photos(v ※ { + $0.imageList[row].isIris = movie != nil + $0.imageList[row].irisDuration = duration ?? 3.0 + $0.imageList[row].irisStillDisplayTime = (duration ?? 3.0) - 0.1 + // NOTE: 3 secs in 30fps is best for watchface resources that is cropped & created as watchface by iOS + // TODO: re-compress: should be less than 3 secs? + // TODO: update duration metadata + }) + case .ultraCube?, nil: + break + } } self.reloadDocument() } @@ -247,35 +277,61 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa let imageData: Data? = Data() let movieData: Data? = nil - let filenameBase = UUID().uuidString - - let item = Watchface.Resources.Metadata.Item( - topAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), - leftAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), - bottomAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), - rightAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), - imageURL: "\(filenameBase).jpg", - irisDuration: 0, - irisStillDisplayTime: 0, - irisVideoURL: "\(filenameBase).mov", - isIris: movieData != nil, - localIdentifier: "", - modificationDate: Date(), - cropH: 0, - cropW: 0, - cropX: 0, - cropY: 0, - originalCropH: 0, - originalCropW: 0, - originalCropX: 0, - originalCropY: 0) - watchface.resources?.images.imageList.append(item) - // TODO: add resource URLs for UltraCube such as baseImage - if let u = item.imageURL { - watchface.resources?.files[u] = imageData - } - if let u = item.irisVideoURL { - watchface.resources?.files[u] = movieData + switch watchface.resources?.images { + case .photos(let v)?: + let filenameBase = UUID().uuidString + let item = Watchface.Resources.PhotosV1.Item( + topAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), + leftAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), + bottomAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), + rightAnalysis: .init(bgBrightness: 0, bgHue: 0, bgSaturation: 0, coloredText: false, complexBackground: false, shadowBrightness: 0, shadowHue: 0, shadowSaturation: 0, textBrightness: 0, textHue: 0, textSaturation: 0), + imageURL: "\(filenameBase).jpg", + irisDuration: 0, + irisStillDisplayTime: 0, + irisVideoURL: "\(filenameBase).mov", + isIris: movieData != nil, + localIdentifier: "", + modificationDate: Date(), + cropH: 0, + cropW: 0, + cropX: 0, + cropY: 0, + originalCropH: 0, + originalCropW: 0, + originalCropX: 0, + originalCropY: 0) + watchface.resources?.images = .photos(v ※ {$0.imageList.append(item)}) + watchface.resources?.files[item.imageURL] = imageData + watchface.resources?.files[item.irisVideoURL] = movieData + case .ultraCube(let v)?: + let filenameBase = UUID().uuidString + let item = Watchface.Resources.UltraCubeV2.Item( + baseImageURL: "\(filenameBase).jpg", + maskImageURL: nil, + backgroundImageURL: nil, + localIdentifier: "", + modificationDate: Date(), + cropH: 0, + cropW: 0, + cropX: 0, + cropY: 0, + originalCropH: 0, + originalCropW: 0, + originalCropX: 0, + originalCropY: 0, + baseImageZorder: 0, + maskedImageZorder: 1, + timeElementZorder: 2, + timeElementUnitBaseline: 0.8035714285714286, + timeElementUnitHeight: 0.2411167512690355, + imageAOTBrightness: 0.5, + parallaxFlat: false, + parallaxScale: 1.075, + userAdjusted: false) + watchface.resources?.images = .ultraCube(v ※ {$0.imageList.append(item)}) + watchface.resources?.files[item.baseImageURL] = imageData + case nil: + break } } reloadDocument() @@ -284,8 +340,22 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa @IBAction func removeImage(_ sender: Any?) { guard case 0.. Date: Wed, 15 Dec 2021 18:03:03 +0900 Subject: [PATCH 05/17] now --- WatchFaceDumper/ImageItemRowView.swift | 49 +++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/WatchFaceDumper/ImageItemRowView.swift b/WatchFaceDumper/ImageItemRowView.swift index 8e0904e..9f1f6e2 100644 --- a/WatchFaceDumper/ImageItemRowView.swift +++ b/WatchFaceDumper/ImageItemRowView.swift @@ -20,7 +20,7 @@ final class ImageItemRowView: NSTableRowView { && lhs.movie?.data == rhs.movie?.data && lhs.movie?.duration == rhs.movie?.duration } - + var image: NSImage? var movie: (data: Data, duration: Double?)? } @@ -79,3 +79,50 @@ final class ImageItemRowView: NSTableRowView { movieView.controlsStyle = item.movie != nil ? .minimal : .none } } + +final class UltraCubeImageItemRowView: NSTableRowView { + private let baseImageView = EditableImageView() + private let backImageView = EditableImageView() + private let maskImageView = EditableImageView() + + struct ImageItem: Equatable { + var baseImage: NSImage + var backImage: NSImage? + var maskImage: NSImage? + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.baseImage.tiffRepresentation == rhs.baseImage.tiffRepresentation + && lhs.backImage?.tiffRepresentation == rhs.backImage?.tiffRepresentation + && lhs.maskImage?.tiffRepresentation == rhs.maskImage?.tiffRepresentation + } + } + + var item: ImageItem { + didSet { + reloadItem() + } + } + + init(item: ImageItem) { + self.item = item + super.init(frame: .zero) + + let autolayout = northLayoutFormat([:], [ + "base": baseImageView, + "back": backImageView, + "mask": maskImageView]) + autolayout("H:|-[base]-[back(base)]-[mask]-|") + autolayout("V:|-[base(240)]-|") + autolayout("V:|-[back(base)]-|") + autolayout("V:|-[back(mask)]-|") + + reloadItem() + } + + required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")} + + private func reloadItem() { + baseImageView.image = item.baseImage + // TODO + } +} From 544f6888590019b8cea019f64a05b436cfe57c09 Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 5 Jan 2022 17:44:03 +0900 Subject: [PATCH 06/17] add preservation for argon, analytics id, and typeface for Portrait Watchface --- Watchface/Face.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Watchface/Face.swift b/Watchface/Face.swift index 9509038..a7c74aa 100644 --- a/Watchface/Face.swift +++ b/Watchface/Face.swift @@ -12,14 +12,18 @@ extension Watchface { public var complications: Complications? /// unknown values public var argon: Argon? + /// UltraCulbe: "ultracube" + public var analytics_id: String? private enum CodingKeys: String, CodingKey { case version case customization case complications + case argon case face_type = "face type" case bundle_id = "bundle id" case resource_directory = "resource directory" + case analytics_id = "analytics id" } public enum FaceType: String, Codable { @@ -44,14 +48,17 @@ extension Watchface { public var content: String? /// "top" public var position: String? - /// kaleidoscope: "radial", UltraCulbe: "style 2" + /// kaleidoscope: "radial" public var style: String? + /// UltraCulbe: "style 2" + public var typeface: String? - public init(color: String? = nil, content: String? = nil, position: String? = nil, style: String? = nil) { + public init(color: String? = nil, content: String? = nil, position: String? = nil, style: String? = nil, typeface: String? = nil) { self.color = color self.content = content self.position = position self.style = style + self.typeface = typeface } } @@ -117,7 +124,7 @@ extension Watchface { } } - public struct Argon { + public struct Argon: Codable { /// unknown base64. (possibly constant value) public var k: String? /// unknown {hash}.aea. (possibly constant value) From ac8b80ba4bd91cac4ed271ffec1dd5c0041f19dc Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 5 Jan 2022 18:17:32 +0900 Subject: [PATCH 07/17] add Portrait image viewer --- WatchFaceDumper/ImageItemRowView.swift | 22 ++-- WatchFaceDumper/ViewController.swift | 153 ++++++++++++++----------- 2 files changed, 103 insertions(+), 72 deletions(-) diff --git a/WatchFaceDumper/ImageItemRowView.swift b/WatchFaceDumper/ImageItemRowView.swift index 9f1f6e2..90cd27a 100644 --- a/WatchFaceDumper/ImageItemRowView.swift +++ b/WatchFaceDumper/ImageItemRowView.swift @@ -81,17 +81,18 @@ final class ImageItemRowView: NSTableRowView { } final class UltraCubeImageItemRowView: NSTableRowView { + private let titleLabel = NSTextField(labelWithString: "") private let baseImageView = EditableImageView() private let backImageView = EditableImageView() private let maskImageView = EditableImageView() struct ImageItem: Equatable { - var baseImage: NSImage + var baseImage: NSImage? var backImage: NSImage? var maskImage: NSImage? static func == (lhs: Self, rhs: Self) -> Bool { - lhs.baseImage.tiffRepresentation == rhs.baseImage.tiffRepresentation + lhs.baseImage?.tiffRepresentation == rhs.baseImage?.tiffRepresentation && lhs.backImage?.tiffRepresentation == rhs.backImage?.tiffRepresentation && lhs.maskImage?.tiffRepresentation == rhs.maskImage?.tiffRepresentation } @@ -108,13 +109,15 @@ final class UltraCubeImageItemRowView: NSTableRowView { super.init(frame: .zero) let autolayout = northLayoutFormat([:], [ + "title": titleLabel, "base": baseImageView, "back": backImageView, "mask": maskImageView]) - autolayout("H:|-[base]-[back(base)]-[mask]-|") - autolayout("V:|-[base(240)]-|") - autolayout("V:|-[back(base)]-|") - autolayout("V:|-[back(mask)]-|") + autolayout("H:|-[title]-|") + autolayout("H:|-[base]-[back(base)]-[mask(base)]-|") + autolayout("V:|-[title]-[base(240)]-|") + autolayout("V:|-[title]-[back(base)]-|") + autolayout("V:|-[title]-[mask(base)]-|") reloadItem() } @@ -122,7 +125,12 @@ final class UltraCubeImageItemRowView: NSTableRowView { required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")} private func reloadItem() { + titleLabel.stringValue = [ + item.baseImage.map {"\(Int($0.size.width))×\(Int($0.size.height))"} ?? "no image (Portrait)", + (item.backImage != nil && item.maskImage != nil) ? "Portrait Photo" : "(Missing Portrait Support)" + ].joined(separator: ", ") baseImageView.image = item.baseImage - // TODO + backImageView.image = item.backImage + maskImageView.image = item.maskImage } } diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 86ac6ec..4a6b764 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -172,15 +172,22 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa metadataViewModel.setWatchface(watchface) - imageItems = watchface.resources.map { resources in + resourceItems = watchface.resources.map { resources in switch resources.images { - case .photos(let v): return v.imageList + case .photos(let v): return .photos( + v.imageList .map {(resources.files[$0.imageURL], resources.files[$0.irisVideoURL])} - .map {ImageItem(image: $0.0.flatMap {NSImage(data: $0)}, movie: $0.1.map {($0, nil)})} - case .ultraCube(let v): return v.imageList - .map {ImageItem(image: resources.files[$0.baseImageURL].flatMap {NSImage(data: $0)}, movie: nil)} // TODO: back, mask + .map {.init(image: $0.0.flatMap {NSImage(data: $0)}, movie: $0.1.map {($0, nil)})}) + case .ultraCube(let v): return .ultraCube( + v.imageList + .map {(resources.files[$0.baseImageURL], + $0.backgroundImageURL.flatMap {resources.files[$0]}, + $0.maskImageURL.flatMap {resources.files[$0]})} + .map {.init(baseImage: $0.0.flatMap {NSImage(data: $0)}, + backImage: $0.1.flatMap {NSImage(data: $0)}, + maskImage: $0.2.flatMap {NSImage(data: $0)})}) } - } ?? [] + } ?? .photos([]) imageListOutlineViewModel.setWatchface(watchface) @@ -188,77 +195,93 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa complicationsBottomLabel.stringValue = "complications.bottom: " + (watchface.metadata.complications_names.bottom ?? "" as String) + " " + (watchface.metadata.complication_sample_templates.bottom?.sampleText.map {"(\($0))"} ?? "") } - typealias ImageItem = ImageItemRowView.ImageItem - var imageItems: [ImageItem] = [] { + var resourceItems: ResourceItems = .photos([]) { didSet { - guard imageItems != oldValue else { return } + guard resourceItems != oldValue else { return } imageListTableView.reloadData() } } + enum ResourceItems: Equatable { + case photos([ImageItemRowView.ImageItem]) + case ultraCube([UltraCubeImageItemRowView.ImageItem]) - func numberOfRows(in tableView: NSTableView) -> Int {imageItems.count} + var count: Int { + switch self { + case .photos(let items): return items.count + case .ultraCube(let items): return items.count + } + } + } + + func numberOfRows(in tableView: NSTableView) -> Int {resourceItems.count} func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {nil} func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { - ImageItemRowView(item: imageItems[row]) ※ { - $0.imageDidChange = { [weak self] image in - guard let self = self else { return } - self.document.watchface = self.document.watchface ※ { watchface in - guard let resources = watchface.resources else { return } - let jpeg = image?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) - watchface.resources = resources ※ { resources in - switch resources.images { + switch resourceItems { + case .photos(let items): + return ImageItemRowView(item: items[row]) ※ { + $0.imageDidChange = { [weak self] image in + guard let self = self else { return } + self.document.watchface = self.document.watchface ※ { watchface in + guard let resources = watchface.resources else { return } + let jpeg = image?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + watchface.resources = resources ※ { resources in + switch resources.images { + case .photos(let v): + resources.files[v.imageList[row].imageURL] = jpeg + resources.images = .photos(v ※ { + // TODO: resize + $0.imageList[row].cropX = 0 + $0.imageList[row].cropY = 0 + $0.imageList[row].cropW = Double(image?.size.width ?? 0) + $0.imageList[row].cropH = Double(image?.size.height ?? 0) + $0.imageList[row].originalCropX = 0 + $0.imageList[row].originalCropY = 0 + $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) + $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) + }) + case .ultraCube(let v): + resources.files[v.imageList[row].baseImageURL] = jpeg + resources.images = .ultraCube(v ※ { + // TODO: resize + $0.imageList[row].cropX = 0 + $0.imageList[row].cropY = 0 + $0.imageList[row].cropW = Double(image?.size.width ?? 0) + $0.imageList[row].cropH = Double(image?.size.height ?? 0) + $0.imageList[row].originalCropX = 0 + $0.imageList[row].originalCropY = 0 + $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) + $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) + }) + } + } + } + self.reloadDocument() + } + $0.movieDidChange = { [weak self] in + guard let self = self else { return } + let (movie, duration) = ($0?.data, $0?.duration.flatMap {$0 > 0 ? $0 : nil}) + self.document.watchface = self.document.watchface ※ { watchface in + switch watchface.resources?.images { case .photos(let v): - resources.files[v.imageList[row].imageURL] = jpeg - resources.images = .photos(v ※ { - // TODO: resize - $0.imageList[row].cropX = 0 - $0.imageList[row].cropY = 0 - $0.imageList[row].cropW = Double(image?.size.width ?? 0) - $0.imageList[row].cropH = Double(image?.size.height ?? 0) - $0.imageList[row].originalCropX = 0 - $0.imageList[row].originalCropY = 0 - $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) - $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) - }) - case .ultraCube(let v): - // TODO: back, mask - resources.files[v.imageList[row].baseImageURL] = jpeg - resources.images = .ultraCube(v ※ { - // TODO: resize - $0.imageList[row].cropX = 0 - $0.imageList[row].cropY = 0 - $0.imageList[row].cropW = Double(image?.size.width ?? 0) - $0.imageList[row].cropH = Double(image?.size.height ?? 0) - $0.imageList[row].originalCropX = 0 - $0.imageList[row].originalCropY = 0 - $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) - $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) + watchface.resources?.files[v.imageList[row].irisVideoURL] = movie + watchface.resources?.images = .photos(v ※ { + $0.imageList[row].isIris = movie != nil + $0.imageList[row].irisDuration = duration ?? 3.0 + $0.imageList[row].irisStillDisplayTime = (duration ?? 3.0) - 0.1 + // NOTE: 3 secs in 30fps is best for watchface resources that is cropped & created as watchface by iOS + // TODO: re-compress: should be less than 3 secs? + // TODO: update duration metadata }) + case .ultraCube?, nil: + break } } + self.reloadDocument() } - self.reloadDocument() } - $0.movieDidChange = { [weak self] in - guard let self = self else { return } - let (movie, duration) = ($0?.data, $0?.duration.flatMap {$0 > 0 ? $0 : nil}) - self.document.watchface = self.document.watchface ※ { watchface in - switch watchface.resources?.images { - case .photos(let v): - watchface.resources?.files[v.imageList[row].irisVideoURL] = movie - watchface.resources?.images = .photos(v ※ { - $0.imageList[row].isIris = movie != nil - $0.imageList[row].irisDuration = duration ?? 3.0 - $0.imageList[row].irisStillDisplayTime = (duration ?? 3.0) - 0.1 - // NOTE: 3 secs in 30fps is best for watchface resources that is cropped & created as watchface by iOS - // TODO: re-compress: should be less than 3 secs? - // TODO: update duration metadata - }) - case .ultraCube?, nil: - break - } - } - self.reloadDocument() + case .ultraCube(let items): + return UltraCubeImageItemRowView(item: items[row]) ※ { _ in + // TODO: apply editing } } } @@ -338,7 +361,7 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa } @IBAction func removeImage(_ sender: Any?) { - guard case 0.. Date: Wed, 12 Jan 2022 18:27:07 +0900 Subject: [PATCH 08/17] now --- WatchFaceDumper/ImageItemRowView.swift | 9 +++-- WatchFaceDumper/ViewController.swift | 52 ++++++++++++++++++-------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/WatchFaceDumper/ImageItemRowView.swift b/WatchFaceDumper/ImageItemRowView.swift index 90cd27a..cb2b8bb 100644 --- a/WatchFaceDumper/ImageItemRowView.swift +++ b/WatchFaceDumper/ImageItemRowView.swift @@ -1,5 +1,6 @@ import AppKit import Ikemen +import Combine final class ImageItemRowView: NSTableRowView { private let titleLabel = NSTextField(labelWithString: "") @@ -98,7 +99,7 @@ final class UltraCubeImageItemRowView: NSTableRowView { } } - var item: ImageItem { + @Published var item: ImageItem { didSet { reloadItem() } @@ -110,9 +111,9 @@ final class UltraCubeImageItemRowView: NSTableRowView { let autolayout = northLayoutFormat([:], [ "title": titleLabel, - "base": baseImageView, - "back": backImageView, - "mask": maskImageView]) + "base": baseImageView ※ {$0.imageDidChange = {[weak self] in self?.item.baseImage = $0}}, + "back": backImageView ※ {$0.imageDidChange = {[weak self] in self?.item.backImage = $0}}, + "mask": maskImageView ※ {$0.imageDidChange = {[weak self] in self?.item.maskImage = $0}}]) autolayout("H:|-[title]-|") autolayout("H:|-[base]-[back(base)]-[mask(base)]-|") autolayout("V:|-[title]-[base(240)]-|") diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 4a6b764..1ea3f2a 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -2,6 +2,7 @@ import Cocoa import NorthLayout import Ikemen import AVKit +import Combine final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSSplitViewDelegate { var document: Document { @@ -93,6 +94,8 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa $0.setContentCompressionResistancePriority(.init(rawValue: 9), for: .horizontal) } + private var cancellables: Set = [] + init(document: Document) { self.document = document super.init(nibName: nil, bundle: nil) @@ -239,19 +242,8 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) }) - case .ultraCube(let v): - resources.files[v.imageList[row].baseImageURL] = jpeg - resources.images = .ultraCube(v ※ { - // TODO: resize - $0.imageList[row].cropX = 0 - $0.imageList[row].cropY = 0 - $0.imageList[row].cropW = Double(image?.size.width ?? 0) - $0.imageList[row].cropH = Double(image?.size.height ?? 0) - $0.imageList[row].originalCropX = 0 - $0.imageList[row].originalCropY = 0 - $0.imageList[row].originalCropW = Double(image?.size.width ?? 0) - $0.imageList[row].originalCropH = Double(image?.size.height ?? 0) - }) + case .ultraCube: + break } } } @@ -280,8 +272,38 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa } } case .ultraCube(let items): - return UltraCubeImageItemRowView(item: items[row]) ※ { _ in - // TODO: apply editing + return UltraCubeImageItemRowView(item: items[row]) ※ { + $0.$item.scan((UltraCubeImageItemRowView.ImageItem?, UltraCubeImageItemRowView.ImageItem)?.none) {($0?.1, $1)}.compactMap {$0}.sink { old, new in + let baseJpeg = new.baseImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + let backJpeg = new.backImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + let maskJpeg = new.maskImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + self.document.watchface = self.document.watchface ※ { watchface in + switch watchface.resources?.images { + case .ultraCube(let v): + let backgroundImageURL: String? = v.imageList[row].backgroundImageURL ?? backJpeg.map {_ in "back_" + UUID().uuidString + ".jpeg"} + let maskImageURL: String? = v.imageList[row].maskImageURL ?? maskJpeg.map {_ in "mask_" + UUID().uuidString + ".jpeg"} + watchface.resources?.files[v.imageList[row].baseImageURL] = baseJpeg + _ = backgroundImageURL.map {watchface.resources?.files[$0] = backJpeg} + _ = maskImageURL.map {watchface.resources?.files[$0] = maskJpeg} + watchface.resources?.images = .ultraCube(v ※ { + // TODO: resize + $0.imageList[row].cropX = 0 + $0.imageList[row].cropY = 0 + $0.imageList[row].cropW = Double(new.baseImage?.size.width ?? 0) + $0.imageList[row].cropH = Double(new.baseImage?.size.height ?? 0) + $0.imageList[row].originalCropX = 0 + $0.imageList[row].originalCropY = 0 + $0.imageList[row].originalCropW = Double(new.baseImage?.size.width ?? 0) + $0.imageList[row].originalCropH = Double(new.baseImage?.size.height ?? 0) + $0.imageList[row].backgroundImageURL = backgroundImageURL + $0.imageList[row].maskImageURL = maskImageURL + }) + case .photos?, nil: + break + } + } + // TODO: apply editing + }.store(in: &cancellables) } } } From 9ce591b19d64af435761fe26ac7f9b9145abdd4e Mon Sep 17 00:00:00 2001 From: banjun Date: Wed, 26 Jan 2022 18:31:04 +0900 Subject: [PATCH 09/17] now --- WatchFaceDumper/ViewController.swift | 39 ++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 1ea3f2a..8472f04 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -3,6 +3,7 @@ import NorthLayout import Ikemen import AVKit import Combine +import CoreGraphics final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSSplitViewDelegate { var document: Document { @@ -274,17 +275,38 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa case .ultraCube(let items): return UltraCubeImageItemRowView(item: items[row]) ※ { $0.$item.scan((UltraCubeImageItemRowView.ImageItem?, UltraCubeImageItemRowView.ImageItem)?.none) {($0?.1, $1)}.compactMap {$0}.sink { old, new in - let baseJpeg = new.baseImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) - let backJpeg = new.backImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) - let maskJpeg = new.maskImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + let base = new.baseImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + let back = new.backImage?.tiffRepresentation.flatMap {NSBitmapImageRep(data: $0)}?.representation(using: .jpeg, properties: [.compressionFactor: 0.95]) + let maskPng = new.maskImage.flatMap { image -> Data? in + let width = Int(image.size.width) + let height = Int(image.size.height) + guard let rep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: width, + pixelsHigh: height, + bitsPerSample: 8, + samplesPerPixel: 1, + hasAlpha: false, + isPlanar: false, + colorSpaceName: .calibratedWhite, + bytesPerRow: width, + bitsPerPixel: 8) else { return nil } + let contextImage = NSImage() + contextImage.addRepresentation(rep) + contextImage.lockFocus() + image.draw(in: NSRect(origin: .zero, size: image.size)) + contextImage.unlockFocus() + return rep.representation(using: .png, properties: [:]) // TODO + } self.document.watchface = self.document.watchface ※ { watchface in switch watchface.resources?.images { case .ultraCube(let v): - let backgroundImageURL: String? = v.imageList[row].backgroundImageURL ?? backJpeg.map {_ in "back_" + UUID().uuidString + ".jpeg"} - let maskImageURL: String? = v.imageList[row].maskImageURL ?? maskJpeg.map {_ in "mask_" + UUID().uuidString + ".jpeg"} - watchface.resources?.files[v.imageList[row].baseImageURL] = baseJpeg - _ = backgroundImageURL.map {watchface.resources?.files[$0] = backJpeg} - _ = maskImageURL.map {watchface.resources?.files[$0] = maskJpeg} + let baseImageURL = base.map {_ in "base_" + UUID().uuidString + ".jpeg"} ?? v.imageList[row].baseImageURL // TODO: heic + let backgroundImageURL: String? = back.map {_ in "back_" + UUID().uuidString + ".jpeg"} // TODO: heic + let maskImageURL: String? = maskPng.map {_ in "mask_" + UUID().uuidString + ".png"} + watchface.resources?.files[baseImageURL] = base + _ = backgroundImageURL.map {watchface.resources?.files[$0] = back} + _ = maskImageURL.map {watchface.resources?.files[$0] = maskPng} watchface.resources?.images = .ultraCube(v ※ { // TODO: resize $0.imageList[row].cropX = 0 @@ -295,6 +317,7 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa $0.imageList[row].originalCropY = 0 $0.imageList[row].originalCropW = Double(new.baseImage?.size.width ?? 0) $0.imageList[row].originalCropH = Double(new.baseImage?.size.height ?? 0) + $0.imageList[row].baseImageURL = baseImageURL $0.imageList[row].backgroundImageURL = backgroundImageURL $0.imageList[row].maskImageURL = maskImageURL }) From ace1e8df0af762bc094acf8975a41c6795ac31b4 Mon Sep 17 00:00:00 2001 From: banjun Date: Thu, 17 Mar 2022 20:42:08 +0900 Subject: [PATCH 10/17] convert UltraCube mask image into a compatible format, that is 8-bit gray without alpha --- WatchFaceDumper/ViewController.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index 8472f04..e32d456 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -284,25 +284,24 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, - bitsPerSample: 8, + bitsPerSample: 8, // mask png should be 8-bit grayscale samplesPerPixel: 1, - hasAlpha: false, - isPlanar: false, + hasAlpha: false, // mask png should not have alpha channel + isPlanar: false, // suitable to be NSGraphicsContext.current colorSpaceName: .calibratedWhite, bytesPerRow: width, bitsPerPixel: 8) else { return nil } - let contextImage = NSImage() - contextImage.addRepresentation(rep) - contextImage.lockFocus() + NSGraphicsContext.saveGraphicsState() + defer { NSGraphicsContext.restoreGraphicsState() } + NSGraphicsContext.current = .init(bitmapImageRep: rep) image.draw(in: NSRect(origin: .zero, size: image.size)) - contextImage.unlockFocus() - return rep.representation(using: .png, properties: [:]) // TODO + return rep.representation(using: .png, properties: [:]) } self.document.watchface = self.document.watchface ※ { watchface in switch watchface.resources?.images { case .ultraCube(let v): - let baseImageURL = base.map {_ in "base_" + UUID().uuidString + ".jpeg"} ?? v.imageList[row].baseImageURL // TODO: heic - let backgroundImageURL: String? = back.map {_ in "back_" + UUID().uuidString + ".jpeg"} // TODO: heic + let baseImageURL = base.map {_ in "base_" + UUID().uuidString + ".jpeg"} ?? v.imageList[row].baseImageURL // TODO?: heic + let backgroundImageURL: String? = back.map {_ in "back_" + UUID().uuidString + ".jpeg"} // TODO?: heic let maskImageURL: String? = maskPng.map {_ in "mask_" + UUID().uuidString + ".png"} watchface.resources?.files[baseImageURL] = base _ = backgroundImageURL.map {watchface.resources?.files[$0] = back} From 1de0ae0fc276360a4fc21002b4cc06d18266d5fe Mon Sep 17 00:00:00 2001 From: banjun Date: Thu, 17 Mar 2022 21:19:45 +0900 Subject: [PATCH 11/17] new document supports Portrait watchface --- WatchFaceDumper/Document.swift | 20 +++++++++++--- WatchFaceDumper/ViewController.swift | 9 +++++-- .../PortraitWatchface.swift | 27 ++++++++++++++++++- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/WatchFaceDumper/Document.swift b/WatchFaceDumper/Document.swift index af92cfc..689c1b1 100644 --- a/WatchFaceDumper/Document.swift +++ b/WatchFaceDumper/Document.swift @@ -3,9 +3,7 @@ import ZIPFoundation import Ikemen class Document: NSDocument { - var watchface: Watchface = .init( - photosWatchface: PhotosWatchface( - device_size: 2, position: .top, snapshot: Data(), no_borders_snapshot: Data(), topComplication: nil, bottomComplication: nil, resources: .init(images: .photos(.init(imageList: [])), files: [:]))) + var watchface: Watchface private var isLossyReading = false private var allowLossyAutosaving = false @@ -18,6 +16,22 @@ class Document: NSDocument { } } + convenience override init() { + // TODO: choose a watchface by ui +// self.init(photos: ()) + self.init(portrait: ()) + } + + init(photos: Void) { + watchface = .init(photosWatchface: PhotosWatchface(device_size: 2, position: .top, snapshot: Data(), no_borders_snapshot: Data(), topComplication: nil, bottomComplication: nil, resources: .init(images: .photos(.init(imageList: [])), files: [:]))) + super.init() + } + + init(portrait: Void) { + watchface = .init(portraitWatchface: PortraitWatchface(device_size: 2, style: .style3, snapshot: Data(), no_borders_snapshot: Data(), dateComplication: nil, bottomComplication: nil, resources: .init(images: .init(imageList: []), files: [:]))) + super.init() + } + override func makeWindowControllers() { addWindowController(WindowController(document: self)) } diff --git a/WatchFaceDumper/ViewController.swift b/WatchFaceDumper/ViewController.swift index e32d456..9332105 100644 --- a/WatchFaceDumper/ViewController.swift +++ b/WatchFaceDumper/ViewController.swift @@ -166,8 +166,13 @@ final class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDa let watchface = document.watchface // NSLog("%@", "\(watchface)") - let faceType = watchface.face.face_type.rawValue - faceTypeLabel.stringValue = faceType.first!.uppercased() + faceType.dropFirst() + " watch face" + let faceTypeName: String = { + switch watchface.face.face_type { + case .bundle where watchface.face.bundle_id == .comAppleNTKUltraCubeFaceBundle: return "Portrait" + default: return watchface.face.face_type.rawValue + } + }() + faceTypeLabel.stringValue = faceTypeName.first!.uppercased() + faceTypeName.dropFirst() + " watch face" snapshot.image = NSImage(data: watchface.snapshot) snapshot.imageFrameStyle = snapshot.image.map {_ in .none} ?? .grayBezel diff --git a/Watchface/SpecificWatchfaces/PortraitWatchface.swift b/Watchface/SpecificWatchfaces/PortraitWatchface.swift index f981de4..ed07a36 100644 --- a/Watchface/SpecificWatchfaces/PortraitWatchface.swift +++ b/Watchface/SpecificWatchfaces/PortraitWatchface.swift @@ -100,7 +100,32 @@ extension PortraitWatchface.Resources.Metadata { version: metadata.version) } } +extension Watchface.Resources.Metadata { + public init(images: PortraitWatchface.Resources.Metadata) { + self = .ultraCube(.init(imageList: images.imageList, version: images.version)) + } +} extension Watchface { - // TODO + public init(portraitWatchface portrait: PortraitWatchface) { + self.init( + metadata: .init( + complication_sample_templates: .init(bottom: portrait.bottomComplication?.template, date: portrait.dateComplication?.template), + complications_names:.init( + bottom: portrait.bottomComplication?.name, + date: portrait.dateComplication?.name), + complications_item_ids: .init(), + complications_bundle_ids: nil), + face: .init( + face_type: .bundle, + bundle_id: .comAppleNTKUltraCubeFaceBundle, + resource_directory: true, + customization: .init(color: nil, content: "custom", position: nil, style: portrait.style.rawValue, typeface: nil), + complications: .init(bottom: portrait.bottomComplication?.faceItem, date: portrait.dateComplication?.faceItem)), + snapshot: portrait.snapshot, + no_borders_snapshot: portrait.no_borders_snapshot, + resources: .init(images: .init(images: portrait.resources.images), files: portrait.resources.files), + complicationData: [portrait.bottomComplication?.data, portrait.dateComplication?.data].compactMap {$0}.isEmpty ? nil : .init( + bottom: portrait.bottomComplication?.data, date: portrait.dateComplication?.data)) + } } From 07666eae06849d866f7c574e42f687a203b75e31 Mon Sep 17 00:00:00 2001 From: banjun Date: Thu, 17 Mar 2022 21:47:43 +0900 Subject: [PATCH 12/17] choose watchface type on new document --- WatchFaceDumper/AppDelegate.swift | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/WatchFaceDumper/AppDelegate.swift b/WatchFaceDumper/AppDelegate.swift index ab9699f..f4e5619 100644 --- a/WatchFaceDumper/AppDelegate.swift +++ b/WatchFaceDumper/AppDelegate.swift @@ -2,4 +2,64 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + @IBAction func newDocument(_ sender: Any?) { + let wc = NSWindowController(window: .init(contentViewController: NewDocumentViewController())) + wc.showWindow(nil) + } +} + +import NorthLayout +import Ikemen + +class NewDocumentViewController: NSViewController { + private lazy var photosRadioButton: NSButton = NSButton(radioButtonWithTitle: "Photos", target: self, action: #selector(faceTypeChanged(_:))) ※ { + $0.state = .on + } + private lazy var portraitRadioButton: NSButton = NSButton(radioButtonWithTitle: "Portrait", target: self, action: #selector(faceTypeChanged(_:))) + + override func loadView() { + view = NSView() + } + + override func viewDidLoad() { + super.viewDidLoad() + + let autolayout = view.northLayoutFormat(["p": 20], [ + "header": NSTextField(labelWithString: "New Watch Face:"), + "photos": photosRadioButton, + "portrait": portraitRadioButton, + "cancel": NSButton(title: "Cancel", target: self, action: #selector(cancel(_:))) ※ { + $0.keyEquivalent = "\u{1b}" // esc + }, + "open": NSButton(title: "Open", target: self, action: #selector(open(_:))) ※ { + $0.keyEquivalent = "\r" + },]) + autolayout("H:|-p-[header]-p-|") + autolayout("H:|-p-[photos]-p-|") + autolayout("H:|-p-[portrait]-p-|") + autolayout("H:|-(>=p)-[cancel(open)]-p-[open]-p-|") + autolayout("V:|-p-[header]-p-[photos]-p-[portrait]-(>=p)-[cancel]-p-|") + autolayout("V:[open]-p-|") + } + + @IBAction func faceTypeChanged(_ sender: Any?) { + } + + @IBAction func cancel(_ sender: Any?) { + view.window?.close() + } + + @IBAction func open(_ sender: Any?) { + if photosRadioButton.state == .on { + openNewDocument(.init(photos: ())) + } else if portraitRadioButton.state == .on { + openNewDocument(.init(portrait: ())) + } + view.window?.close() + } + + func openNewDocument(_ document: Document) { + document.makeWindowControllers() + document.showWindows() + } } From e7148fd37f3cb0863357883af2b72e556daab510 Mon Sep 17 00:00:00 2001 From: banjun Date: Thu, 17 Mar 2022 22:15:49 +0900 Subject: [PATCH 13/17] separate NewDocumentViewController --- WatchFaceDumper.xcodeproj/project.pbxproj | 4 ++ WatchFaceDumper/AppDelegate.swift | 58 +------------------ WatchFaceDumper/Document.swift | 4 +- .../NewDocumentViewController.swift | 53 +++++++++++++++++ 4 files changed, 60 insertions(+), 59 deletions(-) create mode 100644 WatchFaceDumper/NewDocumentViewController.swift diff --git a/WatchFaceDumper.xcodeproj/project.pbxproj b/WatchFaceDumper.xcodeproj/project.pbxproj index bacf737..deb2dba 100644 --- a/WatchFaceDumper.xcodeproj/project.pbxproj +++ b/WatchFaceDumper.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ EA9D41352520BFD300838E68 /* Face.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41342520BFD300838E68 /* Face.swift */; }; EA9D41382520C00800838E68 /* Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41372520C00800838E68 /* Resources.swift */; }; EA9D41442520C11A00838E68 /* Watchface+FileWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D41432520C11A00838E68 /* Watchface+FileWrapper.swift */; }; + EA9D8B7527E36AB5002917A9 /* NewDocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D8B7427E36AB5002917A9 /* NewDocumentViewController.swift */; }; EAA4F2912744F57900AE226B /* PortraitWatchface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA4F2902744F57900AE226B /* PortraitWatchface.swift */; }; EAD954F1256E7D5D004EBB02 /* EditableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD954F0256E7D5D004EBB02 /* EditableImageView.swift */; }; /* End PBXBuildFile section */ @@ -58,6 +59,7 @@ EA9D41342520BFD300838E68 /* Face.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Face.swift; sourceTree = ""; }; EA9D41372520C00800838E68 /* Resources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resources.swift; sourceTree = ""; }; EA9D41432520C11A00838E68 /* Watchface+FileWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Watchface+FileWrapper.swift"; sourceTree = ""; }; + EA9D8B7427E36AB5002917A9 /* NewDocumentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDocumentViewController.swift; sourceTree = ""; }; EAA4F2902744F57900AE226B /* PortraitWatchface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitWatchface.swift; sourceTree = ""; }; EAD954F0256E7D5D004EBB02 /* EditableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableImageView.swift; sourceTree = ""; }; FE8E86D8C8D1F1C494C8E5E4 /* Pods-WatchFaceDumper.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchFaceDumper.release.xcconfig"; path = "Target Support Files/Pods-WatchFaceDumper/Pods-WatchFaceDumper.release.xcconfig"; sourceTree = ""; }; @@ -115,6 +117,7 @@ isa = PBXGroup; children = ( EA4D31B924FFE37100104A33 /* AppDelegate.swift */, + EA9D8B7427E36AB5002917A9 /* NewDocumentViewController.swift */, EA4E96FF251267B5008D822B /* WindowController.swift */, EA4D31BB24FFE37100104A33 /* ViewController.swift */, EA4E96F82512642D008D822B /* ImageItemRowView.swift */, @@ -283,6 +286,7 @@ EA9D41252520BE8900838E68 /* ComplicationTemplate.swift in Sources */, EA4E96F92512642D008D822B /* ImageItemRowView.swift in Sources */, EA9D41442520C11A00838E68 /* Watchface+FileWrapper.swift in Sources */, + EA9D8B7527E36AB5002917A9 /* NewDocumentViewController.swift in Sources */, EA4E96FC25126454008D822B /* EditableAVPlayerView.swift in Sources */, EA9D41382520C00800838E68 /* Resources.swift in Sources */, EA4E9700251267B5008D822B /* WindowController.swift in Sources */, diff --git a/WatchFaceDumper/AppDelegate.swift b/WatchFaceDumper/AppDelegate.swift index f4e5619..7a0964f 100644 --- a/WatchFaceDumper/AppDelegate.swift +++ b/WatchFaceDumper/AppDelegate.swift @@ -4,62 +4,8 @@ import Cocoa class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func newDocument(_ sender: Any?) { let wc = NSWindowController(window: .init(contentViewController: NewDocumentViewController())) + wc.window?.title = "New watchface" + wc.window?.styleMask = [.titled] wc.showWindow(nil) } } - -import NorthLayout -import Ikemen - -class NewDocumentViewController: NSViewController { - private lazy var photosRadioButton: NSButton = NSButton(radioButtonWithTitle: "Photos", target: self, action: #selector(faceTypeChanged(_:))) ※ { - $0.state = .on - } - private lazy var portraitRadioButton: NSButton = NSButton(radioButtonWithTitle: "Portrait", target: self, action: #selector(faceTypeChanged(_:))) - - override func loadView() { - view = NSView() - } - - override func viewDidLoad() { - super.viewDidLoad() - - let autolayout = view.northLayoutFormat(["p": 20], [ - "header": NSTextField(labelWithString: "New Watch Face:"), - "photos": photosRadioButton, - "portrait": portraitRadioButton, - "cancel": NSButton(title: "Cancel", target: self, action: #selector(cancel(_:))) ※ { - $0.keyEquivalent = "\u{1b}" // esc - }, - "open": NSButton(title: "Open", target: self, action: #selector(open(_:))) ※ { - $0.keyEquivalent = "\r" - },]) - autolayout("H:|-p-[header]-p-|") - autolayout("H:|-p-[photos]-p-|") - autolayout("H:|-p-[portrait]-p-|") - autolayout("H:|-(>=p)-[cancel(open)]-p-[open]-p-|") - autolayout("V:|-p-[header]-p-[photos]-p-[portrait]-(>=p)-[cancel]-p-|") - autolayout("V:[open]-p-|") - } - - @IBAction func faceTypeChanged(_ sender: Any?) { - } - - @IBAction func cancel(_ sender: Any?) { - view.window?.close() - } - - @IBAction func open(_ sender: Any?) { - if photosRadioButton.state == .on { - openNewDocument(.init(photos: ())) - } else if portraitRadioButton.state == .on { - openNewDocument(.init(portrait: ())) - } - view.window?.close() - } - - func openNewDocument(_ document: Document) { - document.makeWindowControllers() - document.showWindows() - } -} diff --git a/WatchFaceDumper/Document.swift b/WatchFaceDumper/Document.swift index 689c1b1..f02ec68 100644 --- a/WatchFaceDumper/Document.swift +++ b/WatchFaceDumper/Document.swift @@ -17,9 +17,7 @@ class Document: NSDocument { } convenience override init() { - // TODO: choose a watchface by ui -// self.init(photos: ()) - self.init(portrait: ()) + self.init(photos: ()) } init(photos: Void) { diff --git a/WatchFaceDumper/NewDocumentViewController.swift b/WatchFaceDumper/NewDocumentViewController.swift new file mode 100644 index 0000000..f308736 --- /dev/null +++ b/WatchFaceDumper/NewDocumentViewController.swift @@ -0,0 +1,53 @@ +import NorthLayout +import Ikemen + +class NewDocumentViewController: NSViewController { + private lazy var photosRadioButton: NSButton = NSButton(radioButtonWithTitle: "Photos", target: self, action: #selector(faceTypeChanged(_:))) ※ { + $0.state = .on + } + private lazy var portraitRadioButton: NSButton = NSButton(radioButtonWithTitle: "Portrait", target: self, action: #selector(faceTypeChanged(_:))) + + override func loadView() { + view = NSView() + } + + override func viewDidLoad() { + super.viewDidLoad() + + let autolayout = view.northLayoutFormat(["p": 20], [ + "photos": photosRadioButton, + "portrait": portraitRadioButton, + "cancel": NSButton(title: "Cancel", target: self, action: #selector(cancel(_:))) ※ { + $0.keyEquivalent = "\u{1b}" // esc + }, + "open": NSButton(title: "Open", target: self, action: #selector(open(_:))) ※ { + $0.keyEquivalent = "\r" + },]) + autolayout("H:|-p-[photos]-p-|") + autolayout("H:|-p-[portrait]-p-|") + autolayout("H:|-(>=p)-[cancel(open)]-p-[open]-p-|") + autolayout("V:|-p-[photos]-[portrait]-(>=p)-[cancel]-p-|") + autolayout("V:[open]-p-|") + } + + @IBAction func faceTypeChanged(_ sender: Any?) { + } + + @IBAction func cancel(_ sender: Any?) { + view.window?.close() + } + + @IBAction func open(_ sender: Any?) { + if photosRadioButton.state == .on { + openNewDocument(.init(photos: ())) + } else if portraitRadioButton.state == .on { + openNewDocument(.init(portrait: ())) + } + view.window?.close() + } + + func openNewDocument(_ document: Document) { + document.makeWindowControllers() + document.showWindows() + } +} From 9a770580355eb237a506080c73706371c2c59f82 Mon Sep 17 00:00:00 2001 From: banjun Date: Thu, 17 Mar 2022 22:23:29 +0900 Subject: [PATCH 14/17] editorial --- WatchFaceDumper/AppDelegate.swift | 2 +- WatchFaceDumper/NewDocumentViewController.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WatchFaceDumper/AppDelegate.swift b/WatchFaceDumper/AppDelegate.swift index 7a0964f..c327dde 100644 --- a/WatchFaceDumper/AppDelegate.swift +++ b/WatchFaceDumper/AppDelegate.swift @@ -4,7 +4,7 @@ import Cocoa class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func newDocument(_ sender: Any?) { let wc = NSWindowController(window: .init(contentViewController: NewDocumentViewController())) - wc.window?.title = "New watchface" + wc.window?.title = "New watch face" wc.window?.styleMask = [.titled] wc.showWindow(nil) } diff --git a/WatchFaceDumper/NewDocumentViewController.swift b/WatchFaceDumper/NewDocumentViewController.swift index f308736..f7fe4cf 100644 --- a/WatchFaceDumper/NewDocumentViewController.swift +++ b/WatchFaceDumper/NewDocumentViewController.swift @@ -20,14 +20,14 @@ class NewDocumentViewController: NSViewController { "cancel": NSButton(title: "Cancel", target: self, action: #selector(cancel(_:))) ※ { $0.keyEquivalent = "\u{1b}" // esc }, - "open": NSButton(title: "Open", target: self, action: #selector(open(_:))) ※ { + "new": NSButton(title: "New", target: self, action: #selector(new(_:))) ※ { $0.keyEquivalent = "\r" },]) autolayout("H:|-p-[photos]-p-|") autolayout("H:|-p-[portrait]-p-|") - autolayout("H:|-(>=p)-[cancel(open)]-p-[open]-p-|") + autolayout("H:|-(>=p)-[cancel(new)]-p-[new]-p-|") autolayout("V:|-p-[photos]-[portrait]-(>=p)-[cancel]-p-|") - autolayout("V:[open]-p-|") + autolayout("V:[new]-p-|") } @IBAction func faceTypeChanged(_ sender: Any?) { @@ -37,7 +37,7 @@ class NewDocumentViewController: NSViewController { view.window?.close() } - @IBAction func open(_ sender: Any?) { + @IBAction func new(_ sender: Any?) { if photosRadioButton.state == .on { openNewDocument(.init(photos: ())) } else if portraitRadioButton.state == .on { From a26b85c3fe710515613e05c0ef60d7cacc29c7e7 Mon Sep 17 00:00:00 2001 From: BAN Jun Date: Thu, 20 Oct 2022 21:35:42 +0900 Subject: [PATCH 15/17] add comment for Apple Watch Ultra device size --- Watchface/Metadata.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Watchface/Metadata.swift b/Watchface/Metadata.swift index faed212..74bc73a 100644 --- a/Watchface/Metadata.swift +++ b/Watchface/Metadata.swift @@ -3,7 +3,7 @@ import Foundation extension Watchface { public struct Metadata: Codable { public var version: Int = 2 - // 38mm, 42mm? + // 38mm = 2, 42mm?, Ultra = 6 public var device_size = 2 public var complication_sample_templates: ComplicationPositionDictionary public var complications_names: ComplicationPositionDictionary From ffdf7646835fca04bd1804326ceedafb908012dd Mon Sep 17 00:00:00 2001 From: BAN Jun Date: Thu, 20 Oct 2022 21:54:34 +0900 Subject: [PATCH 16/17] add inpaint button --- WatchFaceDumper/ImageItemRowView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WatchFaceDumper/ImageItemRowView.swift b/WatchFaceDumper/ImageItemRowView.swift index cb2b8bb..5d9899b 100644 --- a/WatchFaceDumper/ImageItemRowView.swift +++ b/WatchFaceDumper/ImageItemRowView.swift @@ -109,16 +109,22 @@ final class UltraCubeImageItemRowView: NSTableRowView { self.item = item super.init(frame: .zero) + let inpaintButton = NSButton(title: "Inpaint...", target: self, action: nil) + let autolayout = northLayoutFormat([:], [ "title": titleLabel, "base": baseImageView ※ {$0.imageDidChange = {[weak self] in self?.item.baseImage = $0}}, "back": backImageView ※ {$0.imageDidChange = {[weak self] in self?.item.backImage = $0}}, - "mask": maskImageView ※ {$0.imageDidChange = {[weak self] in self?.item.maskImage = $0}}]) + "mask": maskImageView ※ {$0.imageDidChange = {[weak self] in self?.item.maskImage = $0}}, + "inpaint": inpaintButton]) autolayout("H:|-[title]-|") autolayout("H:|-[base]-[back(base)]-[mask(base)]-|") autolayout("V:|-[title]-[base(240)]-|") autolayout("V:|-[title]-[back(base)]-|") autolayout("V:|-[title]-[mask(base)]-|") + autolayout("V:[inpaint]-|") + inpaintButton.centerXAnchor.constraint(equalTo: backImageView.centerXAnchor).isActive = true + addSubview(inpaintButton, positioned: .above, relativeTo: nil) reloadItem() } From 3f4ef54a023ea7b326409d44634d752d19f6a343 Mon Sep 17 00:00:00 2001 From: Basil Arackal Date: Fri, 6 Jan 2023 15:10:51 +0530 Subject: [PATCH 17/17] position typo fix --- Watchface/SpecificWatchfaces/PhotosWatchface.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Watchface/SpecificWatchfaces/PhotosWatchface.swift b/Watchface/SpecificWatchfaces/PhotosWatchface.swift index fba7e38..5a594ce 100644 --- a/Watchface/SpecificWatchfaces/PhotosWatchface.swift +++ b/Watchface/SpecificWatchfaces/PhotosWatchface.swift @@ -28,7 +28,7 @@ public struct PhotosWatchface { } public enum Position: String { - case top, bototm + case top, bottom } public init(device_size: Int = 2, position: Position, snapshot: Data, no_borders_snapshot: Data, topComplication: Complication? = nil, bottomComplication: Complication? = nil, resources: Watchface.Resources) {