diff --git a/.gitignore b/.gitignore index 8b53cbf..9e1de48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ xcuserdata project.xcworkspace +Build/ +.DS_Store diff --git a/Actions.xcodeproj/project.pbxproj b/Actions.xcodeproj/project.pbxproj index d23f301..9481268 100644 --- a/Actions.xcodeproj/project.pbxproj +++ b/Actions.xcodeproj/project.pbxproj @@ -234,6 +234,12 @@ 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 */; }; + FD3BD4322A966507001A7F03 /* SoulverCore in Frameworks */ = {isa = PBXBuildFile; productRef = FD3BD4312A966507001A7F03 /* SoulverCore */; }; + FDEDC6862A9560AE0063530F /* RMShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDC6832A955C530063530F /* RMShape.swift */; }; + FDEDC6872A9560AE0063530F /* RMIconContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDC67D2A9552200063530F /* RMIconContainer.swift */; }; + FDEDC6882A9560AE0063530F /* RMStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDC67B2A9551F50063530F /* RMStyle.swift */; }; + FDEDC68A2A9560AE0063530F /* CreateMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDC6712A95519B0063530F /* CreateMenuItem.swift */; }; + FDEDC68E2A9565E80063530F /* RMIconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDC68D2A9565E80063530F /* RMIconType.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -407,6 +413,11 @@ 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 = ""; }; + FDEDC6712A95519B0063530F /* CreateMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateMenuItem.swift; sourceTree = ""; }; + FDEDC67B2A9551F50063530F /* RMStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMStyle.swift; sourceTree = ""; }; + FDEDC67D2A9552200063530F /* RMIconContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMIconContainer.swift; sourceTree = ""; }; + FDEDC6832A955C530063530F /* RMShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RMShape.swift; sourceTree = ""; }; + FDEDC68D2A9565E80063530F /* RMIconType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RMIconType.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -424,6 +435,7 @@ buildActionMask = 2147483647; files = ( E31749C22916A6B400F6319E /* Sentry in Frameworks */, + FD3BD4322A966507001A7F03 /* SoulverCore in Frameworks */, E31749C42916A6B700F6319E /* ExceptionCatcher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -446,6 +458,7 @@ E30FD0D527908D1A00C01D80 /* Actions */ = { isa = PBXGroup; children = ( + FDEDC6702A95518D0063530F /* Rich Menu */, E33D080E2A11629500FBCAD7 /* AskChatGPT.swift */, E352420928F6EEDE00A957A7 /* AskForText.swift */, E34AB51528F58B500082AE78 /* Authenticate.swift */, @@ -635,6 +648,26 @@ name = Products; sourceTree = ""; }; + FDEDC6702A95518D0063530F /* Rich Menu */ = { + isa = PBXGroup; + children = ( + FDEDC6712A95519B0063530F /* CreateMenuItem.swift */, + FDEDC6732A9551AA0063530F /* Models */, + ); + path = "Rich Menu"; + sourceTree = ""; + }; + FDEDC6732A9551AA0063530F /* Models */ = { + isa = PBXGroup; + children = ( + FDEDC67B2A9551F50063530F /* RMStyle.swift */, + FDEDC6832A955C530063530F /* RMShape.swift */, + FDEDC67D2A9552200063530F /* RMIconContainer.swift */, + FDEDC68D2A9565E80063530F /* RMIconType.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -675,6 +708,7 @@ packageProductDependencies = ( E31749C12916A6B400F6319E /* Sentry */, E31749C32916A6B700F6319E /* ExceptionCatcher */, + FD3BD4312A966507001A7F03 /* SoulverCore */, ); productName = "Intents Extension"; productReference = E31749642916A5FB00F6319E /* Intents Extension macOS.appex */; @@ -997,9 +1031,11 @@ E3EA7D7B28EAC2210043F782 /* BlurImages.swift in Sources */, E33D080F2A11629500FBCAD7 /* AskChatGPT.swift in Sources */, E3C33CDC28D48C3200386C59 /* GetRunningApps.swift in Sources */, + FDEDC68A2A9560AE0063530F /* CreateMenuItem.swift in Sources */, E3BFF7B527428F5200B830DE /* WelcomeScreen.swift in Sources */, E3F64D7628D9F9930009B500 /* CreateColorImage.swift in Sources */, E3CB449228D7803C0031D55F /* ParseJSON5.swift in Sources */, + FDEDC6882A9560AE0063530F /* RMStyle.swift in Sources */, E3963A5A292CAF9F00210AC2 /* RemoveDuplicatesFromList.swift in Sources */, E34839A12A885DD90040DF6E /* InvertImages.swift in Sources */, E3CB449F28D791D40031D55F /* GenerateCSV.swift in Sources */, @@ -1023,6 +1059,7 @@ E303EED329529D0B007BE918 /* GetDefaultPrinter.swift in Sources */, E3DB885228E6EFBB00FEE8D6 /* ScanDocuments.swift in Sources */, E3B8FD1929ED4305001249CF /* GetTitleOfURL.swift in Sources */, + FDEDC68E2A9565E80063530F /* RMIconType.swift in Sources */, E3B8FD1829ED42DA001249CF /* GetUnsplashImage.swift in Sources */, E38035CB29278B1700D29A07 /* MergeDictionaries.swift in Sources */, E343F9B829713AFF00AD82F4 /* GetMapImageOfLocation.swift in Sources */, @@ -1036,9 +1073,11 @@ E33D08142A11679300FBCAD7 /* SettingsScreen.swift in Sources */, E303EED529529D1B007BE918 /* SetDefaultPrinter.swift in Sources */, E3FC71DD271EBE5C00C9D255 /* Utilities.swift in Sources */, + FDEDC6872A9560AE0063530F /* RMIconContainer.swift in Sources */, E324C9FF271E972300E7CA9B /* App.swift in Sources */, E3CB44AB28D7A10C0031D55F /* TranscribeAudio.swift in Sources */, E352420A28F6EEDE00A957A7 /* AskForText.swift in Sources */, + FDEDC6862A9560AE0063530F /* RMShape.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1068,7 +1107,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = YG56YK5RN5; + DEVELOPMENT_TEAM = ME2UKFKL5H; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Intents Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "Intents Extension"; @@ -1079,7 +1118,7 @@ "@executable_path/../../Frameworks", "@executable_path/../../../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.sindresorhus.Actions.Intents-Extension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).Intents-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; @@ -1128,7 +1167,7 @@ CODE_SIGN_ENTITLEMENTS = "Intents Extension/Intents_Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = YG56YK5RN5; + DEVELOPMENT_TEAM = ME2UKFKL5H; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Intents Extension/Info.plist"; @@ -1139,7 +1178,7 @@ "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.sindresorhus.Actions.Intents-Extension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).Intents-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; @@ -1310,7 +1349,7 @@ CODE_SIGN_ENTITLEMENTS = Shared/Actions.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = YG56YK5RN5; + DEVELOPMENT_TEAM = ME2UKFKL5H; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; @@ -1335,7 +1374,7 @@ "@executable_path/Frameworks", "@executable_path/../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.Actions; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = Actions; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; @@ -1533,6 +1572,11 @@ package = E3327E95272C68DE00AD5CC7 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; productName = Sentry; }; + FD3BD4312A966507001A7F03 /* SoulverCore */ = { + isa = XCSwiftPackageProductDependency; + package = E317495D2916A4A300F6319E /* XCRemoteSwiftPackageReference "SoulverCore" */; + productName = SoulverCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E324C9EA271E972100E7CA9B /* Project object */; diff --git a/Shared/Actions/Rich Menu/CreateMenuItem.swift b/Shared/Actions/Rich Menu/CreateMenuItem.swift new file mode 100644 index 0000000..862ef75 --- /dev/null +++ b/Shared/Actions/Rich Menu/CreateMenuItem.swift @@ -0,0 +1,255 @@ +// +// CreateMenuItem.swift +// Actions +// +// Created by Noah Kamara on 22.08.23. +// + +import Foundation +import AppIntents +import SwiftUI +import Contacts + +struct CreateMenuItem: AppIntent { + static var title: LocalizedStringResource { + "Create Menu Item" + } + + static let description = IntentDescription( + """ + Create a Menu Item with a Title, Subtitle and Icon + + You can later use one or more of these 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 + """, + categoryName: "Rich Menu", + searchKeywords: [ + "menu", + "menu item", + "choose from menu", + "rich menu" + ] + ) + + static var parameterSummary: some ParameterSummary { + When(\.$menuTitle, .hasAnyValue) { + Switch(\.$iconType) { + // MARK: SFSymbol + Case(RMIconType.sfSymbol) { + When(\.$backgroundShape, .notEqualTo, .noBackground) { + Summary("Create \(\.$systemName) with \(\.$menuTitle) and \(\.$subtitle)") { + \.$iconType + \.$foreground + \.$backgroundShape + \.$background + \.$data + } + } otherwise: { + Summary("Create \(\.$systemName) with \(\.$menuTitle) and \(\.$subtitle)") { + \.$iconType + \.$foreground + \.$backgroundShape + \.$background + \.$data + } + } + } + + // MARK: Emoji + Case(RMIconType.emoji) { + When(\.$backgroundShape, .notEqualTo, .noBackground) { + Summary("Create \(\.$emoji) with \(\.$menuTitle) and \(\.$subtitle)") { + \.$iconType + \.$backgroundShape + \.$background + \.$data + } + } otherwise: { + Summary("Create \(\.$emoji) with \(\.$menuTitle) and \(\.$subtitle)") { + \.$iconType + \.$foreground + \.$backgroundShape + \.$background + \.$data + } + } + } + + DefaultCase { + Summary("Create Item with \(\.$menuTitle) and \(\.$subtitle)") { + \.$iconType + \.$backgroundShape + \.$background + \.$data + } + } + } + } otherwise: { + // MARK: No Title + Summary("Create Menu Item with \(\.$menuTitle)") + } + } + + @Parameter(title: "Title") + var menuTitle: String + + @Parameter(title: "Subtitle") + var subtitle: String? + + @Parameter( + title: "Icon", + default: .sfSymbol + ) + var iconType: RMIconType + + @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 + ) + var background: RMStyle + + // SF Symbol + @Parameter( + title: "SF Symbol Name", + description: """ + The Name of a SF Symbol + + For available Symbols see Apple's Website (https://developer.apple.com/sf-symbols/) ]) + """, + default: "plus" + ) + var systemName: String + + @Parameter( + title: "Foreground", + description: "The Color for your SF Symbol", + default: .default + ) + var foreground: RMStyle + + // Emoji + @Parameter( + title: "Emoji", + description: """ + Any Emoji 😀. + + Tap the Emoji button on your keyboard and select one emoji. + """, + default: "😀", + inputOptions: .init(keyboardType: .default) + ) + var emoji: String + + @Parameter( + title: "Data" + ) + var data: String? + + @Parameter( + title: "Background Style", + description: """ + The style of the icon's background. + """, + default: .circle + ) + var backgroundShape: RMBackgroundShape + + func makeIcon() async -> Data? { + switch iconType { + case .sfSymbol: + return await RMIconContainer( + sfSymbol: systemName, + foregroundColor: foreground.color(), + backgroundColor: background.color(isBackground: true), + backgroundShape: backgroundShape + ) + .render() + case .emoji: + return await RMIconContainer( + emoji: emoji, + backgroundColor: background.color(isBackground: true), + backgroundShape: backgroundShape + ) + .render() + } + } + + func perform() async throws -> some IntentResult & ReturnsValue { + let icon = await makeIcon() + + let item = MenuItem( + title: menuTitle, + subtitle: subtitle, + icon: icon + ) + + return .result(value: item) + } +} + +struct MenuItem: TransientAppEntity { + init() { + self.init(title: nil, subtitle: nil) + } + + static var 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) + } else { + .init(data: icon.data) + } + } else { + nil + } + + return if let subtitle { + DisplayRepresentation( + title: title, + subtitle: subtitle, + image: image + ) + } else { + DisplayRepresentation( + title: title, + subtitle: nil, + image: image + ) + } + } + + @Property(title: "Title") + var title: String? + + @Property(title: "Subtitle") + var subtitle: String? + + @Property(title: "Icon") + var icon: IntentFile? + + init( + title: String? = nil, + subtitle: String? = nil, + icon: Data? = nil + ) { + if let icon { + self.icon = .init(data: icon, filename: "icon.png", type: .image) + } else { + self.icon = nil + } + self.title = title + self.subtitle = subtitle + } +} diff --git a/Shared/Actions/Rich Menu/Models/RMIconContainer.swift b/Shared/Actions/Rich Menu/Models/RMIconContainer.swift new file mode 100644 index 0000000..2c493c0 --- /dev/null +++ b/Shared/Actions/Rich Menu/Models/RMIconContainer.swift @@ -0,0 +1,122 @@ +import SwiftUI + +/// Container for rendering Icons in Menu Items +struct RMIconContainer: View { + let icon: Icon + var backgroundColor: Color + var backgroundShape: RMBackgroundShape + + /// 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 + private init( + @ViewBuilder + icon: () -> Icon, + background: Color, + backgroundShape: RMBackgroundShape + ) { + 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 { + self.init( + icon: { RMEmojiIconView(emoji: emoji) }, + background: backgroundColor, backgroundShape: backgroundShape + ) + } + + /// Initializes with a an SFSymbol icon with the specified background color + /// - Parameters: + /// - sfSymbol: SF Symbol Name , + /// - background: a color + /// - backgroundShape: Shape for the background + init( + sfSymbol systemName: String, + foregroundColor: Color, + backgroundColor: Color, + backgroundShape: RMBackgroundShape + ) where Icon == RMSybmolIconView { + self.init(icon: { + RMSybmolIconView( + systemName: systemName, + foregroundColor: foregroundColor == .primary ? .primary : foregroundColor + ) + }, background: backgroundColor, backgroundShape: backgroundShape) + } + + + var body: some View { + icon + .frame(width: 93, height: 93) + .background { + if let shape = backgroundShape.shape { + shape + .foregroundStyle(.quaternary) + .foregroundColor(backgroundColor) + } + } + } + + @MainActor func render() async -> Data? { + let renderer = ImageRenderer(content: self) + + // scale doesn't really matter + renderer.scale = 1 + + let data: Data? + + +#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 + } +} + + +struct RMEmojiIconView: View { + var emoji: String + + var body: some View { + Text(String(emoji.prefix(2))) + .font(.system(size: 70)) + .fontWeight(.semibold) + } +} + +struct RMSybmolIconView: View { + let systemName: String + let foregroundColor: Color + + var body: some View { + Image(systemName: systemName) + .font(.system(size: 50)) + .fontWeight(.semibold) + .foregroundColor(foregroundColor) + .padding(5) + } +} diff --git a/Shared/Actions/Rich Menu/Models/RMIconType.swift b/Shared/Actions/Rich Menu/Models/RMIconType.swift new file mode 100644 index 0000000..0b8c0e8 --- /dev/null +++ b/Shared/Actions/Rich Menu/Models/RMIconType.swift @@ -0,0 +1,24 @@ +import AppIntents +import Foundation + +enum RMIconType: String, AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Icon Type" + } + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] { + [ + .sfSymbol: .init( + title: "SF Symbol", + image: .init(named: "circle.fill") + ), + .emoji: .init( + title: "Emoji", + image: .init(named: "face.smiling") + ) + ] + } + + case sfSymbol + case emoji +} diff --git a/Shared/Actions/Rich Menu/Models/RMShape.swift b/Shared/Actions/Rich Menu/Models/RMShape.swift new file mode 100644 index 0000000..a40687c --- /dev/null +++ b/Shared/Actions/Rich Menu/Models/RMShape.swift @@ -0,0 +1,32 @@ +import Foundation +import AppIntents +import SwiftUI + +/// Shape for an Icon's Background +enum RMBackgroundShape: String, AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Background Shape" + } + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .circle: DisplayRepresentation(title: "Circle"), + .square: DisplayRepresentation(title: "Square"), + .noBackground: DisplayRepresentation(title: "No Background") + ] + + case circle + case square + case noBackground + + + var shape: AnyShape? { + switch self { + case .circle: + AnyShape(Circle()) + case .square: + AnyShape(RoundedRectangle(cornerRadius: 10)) + case .noBackground: + nil + } + } +} diff --git a/Shared/Actions/Rich Menu/Models/RMStyle.swift b/Shared/Actions/Rich Menu/Models/RMStyle.swift new file mode 100644 index 0000000..21e89ae --- /dev/null +++ b/Shared/Actions/Rich Menu/Models/RMStyle.swift @@ -0,0 +1,88 @@ +import AppIntents +import SwiftUI + + +/// A Style for an Icon or it's background +enum RMStyle: String, AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Style" + } + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] { + [ + .default: "default", + .red: "red", + .orange: "orange", + .yellow: "yellow", + .green: "green", + .mint: "mint", + .teal: "teal", + .cyan: "cyan", + .blue: "blue", + .purple: "purple", + .pink: "pink", + .brown: "brown", + .white: "white", + .gray: "gray", + .black: "black", + .clear: "clear" + ] + } + + case `default` + case red + case orange + case yellow + case green + case mint + case teal + case cyan + case blue + case purple + case pink + case brown + case white + case gray + case black + case 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 + case .red: + return .red + case .orange: + return .orange + case .yellow: + return .yellow + case .green: + return .green + case .mint: + return .mint + case .teal: + return .teal + case .cyan: + return .cyan + case .blue: + return .blue + case .purple: + return .purple + case .pink: + return .pink + case .brown: + return .brown + case .white: + return .white + case .gray: + return .gray + case .black: + return .black + case .clear: + return .clear + } + } +}