diff --git a/Actions.xcodeproj/project.pbxproj b/Actions.xcodeproj/project.pbxproj index 32e473b..981b449 100644 --- a/Actions.xcodeproj/project.pbxproj +++ b/Actions.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ E352420A28F6EEDE00A957A7 /* AskForText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E352420928F6EEDE00A957A7 /* AskForText.swift */; }; E352420C28F6F6AD00A957A7 /* AskForTextScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E352420B28F6F6AD00A957A7 /* AskForTextScreen.swift */; }; E365C11B2A4C683200A902D4 /* WaitMilliseconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = E365C11A2A4C683200A902D4 /* WaitMilliseconds.swift */; }; + E36A41B32AC605AF00A29195 /* CreateMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD691FCD2A9CCD4C00C55178 /* CreateMenuItem.swift */; }; E376FD8329524B6400C0F81F /* IsDarkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3C33CA928D3A81700386C59 /* IsDarkMode.swift */; }; E37A1E222A2D214800E68E92 /* GetDominantColorsOfImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E37A1E212A2D214800E68E92 /* GetDominantColorsOfImage.swift */; }; E38035CB29278B1700D29A07 /* MergeDictionaries.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3CB44A128D794DA0031D55F /* MergeDictionaries.swift */; }; @@ -149,7 +150,6 @@ E3F64D7628D9F9930009B500 /* CreateColorImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3F64D7528D9F9930009B500 /* CreateColorImage.swift */; }; E3F64D7928D9FCA20009B500 /* GetSymbolImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3F64D7728D9FCA20009B500 /* GetSymbolImage.swift */; }; E3FC71DD271EBE5C00C9D255 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3FC71DC271EBE5B00C9D255 /* Utilities.swift */; }; - FD691FCE2A9CCD4C00C55178 /* CreateMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD691FCD2A9CCD4C00C55178 /* CreateMenuItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -318,7 +318,6 @@ E3F64D7528D9F9930009B500 /* CreateColorImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateColorImage.swift; sourceTree = ""; }; E3F64D7728D9FCA20009B500 /* GetSymbolImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSymbolImage.swift; sourceTree = ""; }; E3FC71DC271EBE5B00C9D255 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; - FD691FC82A9CB9C500C55178 /* CreateMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateMenuItem.swift; sourceTree = ""; }; FD691FCD2A9CCD4C00C55178 /* CreateMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CreateMenuItem.swift; path = Shared/Actions/CreateMenuItem.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -350,12 +349,12 @@ isa = PBXGroup; children = ( E33D080E2A11629500FBCAD7 /* AskChatGPT.swift */, - FD691FC82A9CB9C500C55178 /* CreateMenuItem.swift */, E352420928F6EEDE00A957A7 /* AskForText.swift */, E34AB51528F58B500082AE78 /* Authenticate.swift */, E3EA7D7A28EAC2210043F782 /* BlurImages.swift */, E3DB885528E727E200FEE8D6 /* ChooseFromListExtended.swift */, E3F64D7528D9F9930009B500 /* CreateColorImage.swift */, + FD691FCD2A9CCD4C00C55178 /* CreateMenuItem.swift */, E3CB449D28D791D40031D55F /* GenerateCSV.swift */, E3CF4F9728FDA7EB00BD88D5 /* GenerateRandomData.swift */, E3B8FD1D29F11DCA001249CF /* GetAverageColorOfImage.swift */, @@ -418,7 +417,6 @@ E3EC2DDD28D24CD900A88E25 /* ConvertDateToUnixTime.swift */, E3EC2DDF28D253D600A88E25 /* ConvertUnixTimeToDate.swift */, E3EC2DE128D2588800A88E25 /* CreateURL.swift */, - FD691FCD2A9CCD4C00C55178 /* CreateMenuItem.swift */, E3F64D7128D9F3810009B500 /* EditURL.swift */, E3C33C9B28D376C400386C59 /* FilterList.swift */, E3D514C728EEA8F8003F7C2E /* FlashScreen.swift */, @@ -724,7 +722,6 @@ E317492A2916A20C00F6319E /* GetQueryItemValueFromURL.swift in Sources */, E31749342916A20C00F6319E /* EditURL.swift in Sources */, E317494F2916A20C00F6319E /* ReverseList.swift in Sources */, - FD691FCE2A9CCD4C00C55178 /* CreateMenuItem.swift in Sources */, E31749132916A20C00F6319E /* TrimWhitespace.swift in Sources */, E31749242916A20C00F6319E /* CalculateWithSoulver.swift in Sources */, E317491B2916A20C00F6319E /* RemoveNonPrintableCharacters.swift in Sources */, @@ -800,6 +797,7 @@ E3DB884D28E6E3AD00FEE8D6 /* HapticFeedback.swift in Sources */, E3CB448B28D71A180031D55F /* IsScreenLocked.swift in Sources */, E350B2522AC3005800BFC34D /* IsDeviceLocked.swift in Sources */, + E36A41B32AC605AF00A29195 /* CreateMenuItem.swift in Sources */, E3DB885428E7057100FEE8D6 /* WriteText.swift in Sources */, E3CC6D6229A7E293002D8C67 /* GlobalVariable.swift in Sources */, E324CA01271E972300E7CA9B /* MainScreen.swift in Sources */, diff --git a/Shared/Actions/CreateMenuItem.swift b/Shared/Actions/CreateMenuItem.swift index 979f389..f89d79c 100644 --- a/Shared/Actions/CreateMenuItem.swift +++ b/Shared/Actions/CreateMenuItem.swift @@ -1,77 +1,70 @@ -import Foundation import AppIntents import SwiftUI -// MARK: Action +// NOTE: It's intentionally in the app and not extension because when it's in the extension it causes the "Intents Extension" icon to show in the Dock. (macOS 14.0) + struct CreateMenuItem: AppIntent { static let title: LocalizedStringResource = "Create Menu Item" static let description = IntentDescription( """ -Create a menu item with a title, subtitle and icon. +Create a menu item with a title, subtitle, and icon. 4 -You can later use one or more of these Items in a "Choose from List" action. +You can later use one or more of these menu items in a “Choose from List” action. -Add an "Add to Variable" action below this to populate a list and then use that variable in the "Choose From List" action. +Add an "Add to Variable" action below this one to populate a list and then use that variable in the “Choose From List” action. """, - categoryName: "Rich Menu", + categoryName: "Miscellaneous", searchKeywords: [ - "menu", - "menu item", - "choose from menu", - "rich menu" + "choose", + "rich", + "list" ] ) static var parameterSummary: some ParameterSummary { Switch(\.$iconType) { - // MARK: SFSymbol - Case(RMIconType.sfSymbol) { + Case(.sfSymbol) { When(\.$backgroundShape, .equalTo, .noBackground) { - Summary("Create \(\.$menuTitle) with \(\.$systemName) and \(\.$subtitle)") { + Summary("Create menu item with title \(\.$menuTitle) and icon \(\.$sfSymbolName)") { + \.$subtitle \.$iconType \.$foreground \.$backgroundShape \.$data } } otherwise: { - Summary("Create \(\.$menuTitle) with \(\.$systemName) and \(\.$subtitle)") { + Summary("Create menu item with title \(\.$menuTitle) and icon \(\.$sfSymbolName)") { + \.$subtitle \.$iconType \.$foreground - \.$backgroundShape \.$background + \.$backgroundShape \.$data } } } - - // MARK: Emoji - Case(RMIconType.emoji) { + Case(.emoji) { When(\.$backgroundShape, .equalTo, .noBackground) { - Summary("Create \(\.$menuTitle) with \(\.$emoji) and \(\.$subtitle)") { + Summary("Create menu item with title \(\.$menuTitle) and icon \(\.$emoji)") { + \.$subtitle \.$iconType - \.$foreground \.$backgroundShape \.$data } } otherwise: { - Summary("Create \(\.$menuTitle) with \(\.$emoji) and \(\.$subtitle)") { + Summary("Create menu item with title \(\.$menuTitle) and icon \(\.$emoji)") { + \.$subtitle \.$iconType - \.$foreground - \.$backgroundShape \.$background + \.$backgroundShape \.$data } } } - + // I don't think this can ever be hit. DefaultCase { - Summary("Create Item with \(\.$menuTitle) and \(\.$subtitle)") { - \.$iconType - \.$backgroundShape - \.$background - \.$data - } + Summary("Create menu item with title \(\.$menuTitle)") {} } } } @@ -79,85 +72,84 @@ Add an "Add to Variable" action below this to populate a list and then use that @Parameter(title: "Title") var menuTitle: String - @Parameter(title: "Subtitle") - var subtitle: String? - @Parameter( - title: "Icon", - default: .sfSymbol + title: "SF Symbol", + description: "Find symbol names here: https://developer.apple.com/sf-symbols/", + inputOptions: .init( + keyboardType: .asciiCapable, + capitalizationType: .none, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) ) - var iconType: RMIconType + var sfSymbolName: String @Parameter( - title: "Background", - description: -""" -A Background for your Icon - -Use this in combination with background shape to show a background behind your icon -""", - default: .default + title: "Emoji", + description: "Tap the emoji button on your keyboard and select one emoji." ) - var background: RMStyle + var emoji: String? - // SF Symbol - @Parameter( - title: "SF Symbol", - description: """ - The name of a SF Symbol + @Parameter(title: "Subtitle") + var subtitle: String? - For available symbols see Apple's website (https://developer.apple.com/sf-symbols/) - """ - ) - var systemName: String + @Parameter(title: "Icon Type", default: .sfSymbol) + var iconType: MenuItemIconType @Parameter( - title: "Foreground", - description: "The color for your SF Symbol", + title: "Foreground Color", default: .default ) - var foreground: RMStyle + var foreground: MenuItemStyle - // Emoji @Parameter( - title: "Emoji", - description: -""" -Any Emoji 😀. - -Tap the Emoji button on your keyboard and select one emoji. -""", - inputOptions: .init(keyboardType: .default) + title: "Background Color", + description: "Use this in combination with the background shape to show a background behind the icon.", + default: .default ) - var emoji: String? + var background: MenuItemStyle @Parameter( - title: "Data" + title: "Background Style", + description: "The style of the icon's background.", + default: .circle ) - var data: String? + var backgroundShape: MenuItemBackgroundShape @Parameter( - title: "Background Style", - description: -""" -The style of the icon's background. -""", - default: .circle + title: "Data", + inputOptions: .init( + capitalizationType: .none, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) ) - var backgroundShape: RMBackgroundShape + var data: String? + + func perform() async throws -> some IntentResult & ReturnsValue { + let menuItem = MenuItem( + title: menuTitle, + subtitle: subtitle, + icon: await makeIcon() + ) + + return .result(value: menuItem) + } - func makeIcon() async -> Data? { + private func makeIcon() async -> Data? { switch iconType { case .sfSymbol: - return await RMIconContainer( - sfSymbol: systemName, + await IconContainerView( + sfSymbol: sfSymbolName, foregroundColor: foreground.color(), backgroundColor: background.color(isBackground: true), backgroundShape: backgroundShape ) .render() case .emoji: - return await RMIconContainer( + await IconContainerView( emoji: emoji ?? "", backgroundColor: background.color(isBackground: true), backgroundShape: backgroundShape @@ -165,31 +157,35 @@ The style of the icon's background. .render() } } +} - func perform() async throws -> some IntentResult & ReturnsValue { - let icon = await makeIcon() +struct MenuItem: TransientAppEntity { + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Menu Item" - let item = MenuItem( - title: menuTitle, - subtitle: subtitle, - icon: icon - ) + @Property(title: "Title") + var title: String - return .result(value: item) + @Property(title: "Subtitle") + var subtitle: String? + + @Property(title: "Icon") + var icon: IntentFile? + + init( + title: String, + subtitle: String? = nil, + icon: Data? = nil + ) { + self.icon = icon.flatMap { .init(data: $0, filename: "icon.png", type: .png) } + self.title = title + self.subtitle = subtitle } -} -// MARK: Menu Item -struct MenuItem: TransientAppEntity { init() { self.init(title: "", subtitle: nil) } - static let typeDisplayRepresentation: TypeDisplayRepresentation = "Menu Item" - var displayRepresentation: DisplayRepresentation { - let title: LocalizedStringResource = "\(title)" - let subtitle: LocalizedStringResource? = if let subtitle { "\(subtitle)" } else { nil } let image: DisplayRepresentation.Image? = if let icon { if #available(iOS 17.0, macOS 14.0, *) { .init(data: icon.data, displayStyle: .default) @@ -200,42 +196,15 @@ struct MenuItem: TransientAppEntity { nil } - return DisplayRepresentation( - title: title, - subtitle: subtitle, + return .init( + title: "\(title)", + subtitle: subtitle.flatMap { "\($0)" } ?? "", // The `""` is required as otherwise the icon doesn't show. (macOS 14.0) image: image ) } - - @Property(title: "Title") - var title: String - - @Property(title: "Subtitle") - var subtitle: String? - - @Property(title: "Icon") - var icon: IntentFile? - - init( - title: String, - subtitle: String? = nil, - icon: Data? = nil - ) { - if let icon { - self.icon = .init(data: icon, filename: "icon.png", type: .png) - } else { - self.icon = nil - } - self.title = title - self.subtitle = subtitle - } } -// MARK: Style -/** - A Style for an Icon or it's background - */ -enum RMStyle: String, AppEnum { +enum MenuItemStyle: String, AppEnum { case `default` case red case orange @@ -253,7 +222,7 @@ enum RMStyle: String, AppEnum { case black case clear - static let typeDisplayRepresentation: TypeDisplayRepresentation = "Style" + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Menu Item Style" static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .default: "Default", @@ -274,68 +243,56 @@ enum RMStyle: String, AppEnum { .clear: "Clear" ] - /** - Converts to color - - Parameter isBackground: differentiator for default color - - Returns: Color - */ func color(isBackground: Bool = false) -> Color { switch self { case .default: - return isBackground ? .white : .gray + isBackground ? .white : .gray case .red: - return .red + .red case .orange: - return .orange + .orange case .yellow: - return .yellow + .yellow case .green: - return .green + .green case .mint: - return .mint + .mint case .teal: - return .teal + .teal case .cyan: - return .cyan + .cyan case .blue: - return .blue + .blue case .purple: - return .purple + .purple case .pink: - return .pink + .pink case .brown: - return .brown + .brown case .white: - return .white + .white case .gray: - return .gray + .gray case .black: - return .black + .black case .clear: - return .clear + .clear } } } - -// MARK: Background Shape -/** - Shape for an Icon's Background - */ -enum RMBackgroundShape: String, AppEnum { - static let typeDisplayRepresentation: TypeDisplayRepresentation = "Background Shape" - - - static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ - .circle: "circle", - .square: "square", - .noBackground: "no background" - ] - +enum MenuItemBackgroundShape: String, AppEnum { case circle case square case noBackground + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Menu Item Background Shape" + + static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .circle: "Circle", + .square: "Square", + .noBackground: "No Background" + ] var shape: AnyShape? { switch self { @@ -349,10 +306,11 @@ enum RMBackgroundShape: String, AppEnum { } } +enum MenuItemIconType: String, AppEnum { + case sfSymbol + case emoji -// MARK: Icon -enum RMIconType: String, AppEnum { - static let typeDisplayRepresentation: TypeDisplayRepresentation = "Icon Type" + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Menu Item Icon Type" static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .sfSymbol: .init( @@ -364,77 +322,56 @@ enum RMIconType: String, AppEnum { image: .init(named: "face.smiling") ) ] - - case sfSymbol - case emoji } - -/** - Container for rendering Icons in Menu Items - */ -struct RMIconContainer: View { - let icon: Icon - var backgroundColor: Color - var backgroundShape: RMBackgroundShape +private struct IconContainerView: View { + private let icon: Icon + private let backgroundColor: Color + private let backgroundShape: MenuItemBackgroundShape /** - Initializes with a custom view as the icon and the specified background color - - Parameters: - - icon: Any view. Will be clipped to 93x93. - - background: a color - - backgroundShape: Shape for the background - */ + The icon will be clipped to 93x93. + */ private init( - @ViewBuilder - icon: () -> Icon, + @ViewBuilder icon: () -> Icon, background: Color, - backgroundShape: RMBackgroundShape + backgroundShape: MenuItemBackgroundShape ) { self.icon = icon() self.backgroundColor = background self.backgroundShape = backgroundShape } - /** - Initializes with a an emoji icon with the specified background color - - Parameters: - - emoji: Emoji String :-) - - background: a color - - backgroundShape: Shape for the background - */ + init( emoji: String, backgroundColor: Color, - backgroundShape: RMBackgroundShape - ) where Icon == RMEmojiIconView { + backgroundShape: MenuItemBackgroundShape + ) where Icon == IconView { self.init( - icon: { RMEmojiIconView(emoji: emoji) }, - background: backgroundColor, backgroundShape: backgroundShape + icon: { IconView(emoji: emoji) }, + background: backgroundColor, + backgroundShape: backgroundShape ) } - /** - Initializes with a an SFSymbol icon with the specified background color - - Parameters: - - sfSymbol: SF Symbol , - - background: a color - - backgroundShape: Shape for the background - */ init( sfSymbol systemName: String, foregroundColor: Color, backgroundColor: Color, - backgroundShape: RMBackgroundShape - ) where Icon == RMSymbolIconView { - self.init(icon: { - RMSymbolIconView( - systemName: systemName, - foregroundColor: foregroundColor == .primary ? .primary : foregroundColor - ) - }, background: backgroundColor, backgroundShape: backgroundShape) + backgroundShape: MenuItemBackgroundShape + ) where Icon == SymbolIconView { + self.init( + icon: { + SymbolIconView( + systemName: systemName, + foregroundColor: foregroundColor == .primary ? .primary : foregroundColor + ) + }, + background: backgroundColor, + backgroundShape: backgroundShape + ) } - var body: some View { icon .frame(width: 93, height: 93) @@ -447,35 +384,18 @@ struct RMIconContainer: View { } } - @MainActor func render() async -> Data? { + @MainActor + func render() async -> Data? { let renderer = ImageRenderer(content: self) - // scale doesn't really matter - renderer.scale = 1 - - let data: Data? - + // We cannot fetch the scale here, so we just default to the best resolution. + renderer.scale = 3 -#if os(macOS) - guard let image = renderer.nsImage else { - return nil - } - - data = image.tiffRepresentation -#else - guard let image = renderer.uiImage else { - return nil - } - - data = image.pngData() -#endif - - return data + return renderer.xImage?.pngData() } } - -struct RMEmojiIconView: View { +private struct IconView: View { var emoji: String var body: some View { @@ -485,8 +405,7 @@ struct RMEmojiIconView: View { } } - -struct RMSymbolIconView: View { +private struct SymbolIconView: View { let systemName: String let foregroundColor: Color diff --git a/Shared/AskForTextScreen.swift b/Shared/AskForTextScreen.swift index 618e42a..cee4628 100644 --- a/Shared/AskForTextScreen.swift +++ b/Shared/AskForTextScreen.swift @@ -41,7 +41,7 @@ struct AskForTextScreen: View { .lineLimit(4, reservesSpace: true) // Has no effect. (iOS 16.0) .focused($isFocused) #if canImport(UIKit) - .textContentType(data.type.toContentType) + .textContentType(data.type.toContentType) // TODO: Enable on macOS when targeting macOS 14. .keyboardType(data.type.toKeyboardType ?? .default) .textInputAutocapitalization(data.type.shouldDisableAutocorrectionAndAutocapitalization ? .never : nil) .autocorrectionDisabled(data.type.shouldDisableAutocorrectionAndAutocapitalization)