diff --git a/Actions.xcodeproj/project.pbxproj b/Actions.xcodeproj/project.pbxproj index b2bf5ad..866ce00 100644 --- a/Actions.xcodeproj/project.pbxproj +++ b/Actions.xcodeproj/project.pbxproj @@ -96,7 +96,6 @@ E337A3E627E8BA6B0073E865 /* FuzzyFind in Frameworks */ = {isa = PBXBuildFile; productRef = E337A3E527E8BA6B0073E865 /* FuzzyFind */; }; E33D080D2A11623200FBCAD7 /* SimpleKeychain in Frameworks */ = {isa = PBXBuildFile; productRef = E33D080C2A11623200FBCAD7 /* SimpleKeychain */; }; E33D080F2A11629500FBCAD7 /* AskChatGPT.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33D080E2A11629500FBCAD7 /* AskChatGPT.swift */; }; - E33D08122A1163FD00FBCAD7 /* OpenAISwift in Frameworks */ = {isa = PBXBuildFile; productRef = E33D08112A1163FD00FBCAD7 /* OpenAISwift */; }; E33D08142A11679300FBCAD7 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33D08132A11679300FBCAD7 /* SettingsScreen.swift */; }; E33D4BA62744238300CC7A8A /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33D4BA42744238300CC7A8A /* AppState.swift */; }; E343F9B829713AFF00AD82F4 /* GetMapImageOfLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E317489E2913BEAB00F6319E /* GetMapImageOfLocation.swift */; }; @@ -104,6 +103,8 @@ E34839A32A88DF7D0040DF6E /* GetBooleanFromInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34839A22A88DF7D0040DF6E /* GetBooleanFromInput.swift */; }; E34839A62A88E6A70040DF6E /* IsDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34839A52A88E6A70040DF6E /* IsDay.swift */; }; E34AB51628F58B500082AE78 /* Authenticate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34AB51528F58B500082AE78 /* Authenticate.swift */; }; + E350B24F2AC2AF7D00BFC34D /* IsCellularLowDataModeOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E350B24E2AC2AF7D00BFC34D /* IsCellularLowDataModeOn.swift */; }; + E350B2522AC3005800BFC34D /* IsDeviceLocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = E350B2512AC3005800BFC34D /* IsDeviceLocked.swift */; }; E351083128FD14A2007C85AE /* HexEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E351083028FD14A2007C85AE /* HexEncode.swift */; }; E351083628FD79D8007C85AE /* OverwriteFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E351083528FD79D8007C85AE /* OverwriteFile.swift */; }; E351083928FD8089007C85AE /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = E351083828FD8089007C85AE /* Sentry */; }; @@ -212,6 +213,8 @@ E34839A52A88E6A70040DF6E /* IsDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsDay.swift; sourceTree = ""; }; E34AB51528F58B500082AE78 /* Authenticate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticate.swift; sourceTree = ""; }; E34AB51728F5AA170082AE78 /* ConvertCoordinatesToLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertCoordinatesToLocation.swift; sourceTree = ""; }; + E350B24E2AC2AF7D00BFC34D /* IsCellularLowDataModeOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsCellularLowDataModeOn.swift; sourceTree = ""; }; + E350B2512AC3005800BFC34D /* IsDeviceLocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsDeviceLocked.swift; sourceTree = ""; }; E351082E28FD099B007C85AE /* IsCellularDataOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsCellularDataOn.swift; sourceTree = ""; }; E351083028FD14A2007C85AE /* HexEncode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexEncode.swift; sourceTree = ""; }; E351083528FD79D8007C85AE /* OverwriteFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverwriteFile.swift; sourceTree = ""; }; @@ -335,7 +338,6 @@ buildActionMask = 2147483647; files = ( E33D080D2A11623200FBCAD7 /* SimpleKeychain in Frameworks */, - E33D08122A1163FD00FBCAD7 /* OpenAISwift in Frameworks */, E337A3E627E8BA6B0073E865 /* FuzzyFind in Frameworks */, E317495F2916A4A300F6319E /* SoulverCore in Frameworks */, E351083928FD8089007C85AE /* Sentry in Frameworks */, @@ -370,6 +372,7 @@ E3DB884F28E6EE7B00FEE8D6 /* HideShortcutsApp.swift */, E34839A02A885DD90040DF6E /* InvertImages.swift */, E3C33CA928D3A81700386C59 /* IsDarkMode.swift */, + E350B2512AC3005800BFC34D /* IsDeviceLocked.swift */, E3CB448A28D71A180031D55F /* IsScreenLocked.swift */, E3CB44A128D794DA0031D55F /* MergeDictionaries.swift */, E351083528FD79D8007C85AE /* OverwriteFile.swift */, @@ -458,6 +461,7 @@ E3C33CA528D3A52300386C59 /* IsAudioPlaying.swift */, E3C33CA728D3A6CB00386C59 /* IsBluetoothOn.swift */, E351082E28FD099B007C85AE /* IsCellularDataOn.swift */, + E350B24E2AC2AF7D00BFC34D /* IsCellularLowDataModeOn.swift */, E3C33CAF28D3B3EF00386C59 /* IsConnectedToVPN.swift */, E34839A52A88E6A70040DF6E /* IsDay.swift */, E3CB448628D717CA0031D55F /* IsDeviceOrientation.swift */, @@ -517,10 +521,10 @@ E324C9F0271E972200E7CA9B /* MainScreen.swift */, E33D08132A11679300FBCAD7 /* SettingsScreen.swift */, E3BFF7B327428F5200B830DE /* WelcomeScreen.swift */, - E3BFF7B62742A79100B830DE /* AppIcon.swift */, E3BFF7B02742616400B830DE /* WriteTextScreen.swift */, E337A3DF27E850EC0073E865 /* ChooseFromListScreen.swift */, E352420B28F6F6AD00A957A7 /* AskForTextScreen.swift */, + E3BFF7B62742A79100B830DE /* AppIcon.swift */, E3FC71DC271EBE5B00C9D255 /* Utilities.swift */, E30FD0D527908D1A00C01D80 /* Actions */, E324C9F1271E972300E7CA9B /* Assets.xcassets */, @@ -585,7 +589,6 @@ E351083828FD8089007C85AE /* Sentry */, E317495E2916A4A300F6319E /* SoulverCore */, E33D080C2A11623200FBCAD7 /* SimpleKeychain */, - E33D08112A1163FD00FBCAD7 /* OpenAISwift */, ); productName = "Actions (iOS)"; productReference = E324C9F6271E972300E7CA9B /* Actions.app */; @@ -624,7 +627,6 @@ E32B4F3227FD77700057F08D /* XCRemoteSwiftPackageReference "ExceptionCatcher" */, E317495D2916A4A300F6319E /* XCRemoteSwiftPackageReference "SoulverCore" */, E33D080B2A11623200FBCAD7 /* XCRemoteSwiftPackageReference "SimpleKeychain" */, - E33D08102A1163FD00FBCAD7 /* XCRemoteSwiftPackageReference "OpenAISwift" */, ); productRefGroup = E324C9F7271E972300E7CA9B /* Products */; projectDirPath = ""; @@ -685,6 +687,7 @@ E34839A62A88E6A70040DF6E /* IsDay.swift in Sources */, E31749432916A20C00F6319E /* IsAudioPlaying.swift in Sources */, E31749372916A20C00F6319E /* FormatPersonName.swift in Sources */, + E350B24F2AC2AF7D00BFC34D /* IsCellularLowDataModeOn.swift in Sources */, E31749182916A20C00F6319E /* IsDeviceOrientation.swift in Sources */, E31749552916A20C00F6319E /* CombineLists.swift in Sources */, E34839A32A88DF7D0040DF6E /* GetBooleanFromInput.swift in Sources */, @@ -798,6 +801,7 @@ E3DB886C28E7549800FEE8D6 /* Constants.swift in Sources */, E3DB884D28E6E3AD00FEE8D6 /* HapticFeedback.swift in Sources */, E3CB448B28D71A180031D55F /* IsScreenLocked.swift in Sources */, + E350B2522AC3005800BFC34D /* IsDeviceLocked.swift in Sources */, E3DB885428E7057100FEE8D6 /* WriteText.swift in Sources */, E3CC6D6229A7E293002D8C67 /* GlobalVariable.swift in Sources */, E324CA01271E972300E7CA9B /* MainScreen.swift in Sources */, @@ -963,8 +967,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MACOSX_DEPLOYMENT_TARGET = 13.3; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MACOSX_DEPLOYMENT_TARGET = 13.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1024,8 +1028,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; - MACOSX_DEPLOYMENT_TARGET = 13.3; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MACOSX_DEPLOYMENT_TARGET = 13.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; @@ -1057,6 +1061,7 @@ INFOPLIST_KEY_NSCameraUsageDescription = "Required to use the “Scan Documents” action."; INFOPLIST_KEY_NSContactsUsageDescription = "Required to get the user's name in the “Get User Details” action."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Required to use the “Authenticate” action."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Sindre Sorhus"; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Required to use the “Transcribe Audio” action."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -1104,6 +1109,7 @@ INFOPLIST_KEY_NSCameraUsageDescription = "Required to use the “Scan Documents” action."; INFOPLIST_KEY_NSContactsUsageDescription = "Required to get the user's name in the “Get User Details” action."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Required to use the “Authenticate” action."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Sindre Sorhus"; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Required to use the “Transcribe Audio” action."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -1202,14 +1208,6 @@ minimumVersion = 1.0.0; }; }; - E33D08102A1163FD00FBCAD7 /* XCRemoteSwiftPackageReference "OpenAISwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/adamrushy/OpenAISwift"; - requirement = { - kind = revision; - revision = 73dbe257e6cc612294f771f2dc295dcf1540af33; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1238,11 +1236,6 @@ package = E33D080B2A11623200FBCAD7 /* XCRemoteSwiftPackageReference "SimpleKeychain" */; productName = SimpleKeychain; }; - E33D08112A1163FD00FBCAD7 /* OpenAISwift */ = { - isa = XCSwiftPackageProductDependency; - package = E33D08102A1163FD00FBCAD7 /* XCRemoteSwiftPackageReference "OpenAISwift" */; - productName = OpenAISwift; - }; E351083828FD8089007C85AE /* Sentry */ = { isa = XCSwiftPackageProductDependency; package = E3327E95272C68DE00AD5CC7 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; diff --git a/Config.xcconfig b/Config.xcconfig index 9712d2e..7127370 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -1,5 +1,5 @@ -MARKETING_VERSION = 2.8.1 -CURRENT_PROJECT_VERSION = 47 +MARKETING_VERSION = 2.9.0 +CURRENT_PROJECT_VERSION = 48 // Source for the base of all bundle IDs // -> Intents extensions and app will derive bundleID from here diff --git a/Intents Extension/Actions/GetHighResolutionTimestamp.swift b/Intents Extension/Actions/GetHighResolutionTimestamp.swift index 0e69aa7..1df7e2c 100644 --- a/Intents Extension/Actions/GetHighResolutionTimestamp.swift +++ b/Intents Extension/Actions/GetHighResolutionTimestamp.swift @@ -9,7 +9,7 @@ Returns a timestamp representing the current instant in nanoseconds. Example: 434055845120916 -The most common use-case is to substract two instances of this to get a highly accurate difference. +The most common use-case is to subtract two instances of this to get a highly accurate difference. The timestamp is not meant to be stored for a long time. It's only unique for the current computer session. """, diff --git a/Intents Extension/Actions/IsCellularDataOn.swift b/Intents Extension/Actions/IsCellularDataOn.swift index 49f629b..5bef71d 100644 --- a/Intents Extension/Actions/IsCellularDataOn.swift +++ b/Intents Extension/Actions/IsCellularDataOn.swift @@ -12,11 +12,10 @@ On macOS, it always returns false. categoryName: "Device" ) - static var parameterSummary: some ParameterSummary { - Summary("Is cellular data on?") - } - func perform() async throws -> some IntentResult & ReturnsValue { - .result(value: await Device.isCellularDataEnabled) + // Give the system time to change the mode when used in an automation. + sleep(.milliseconds(30)) + + return .result(value: await Device.isCellularDataEnabled) } } diff --git a/Intents Extension/Actions/IsCellularLowDataModeOn.swift b/Intents Extension/Actions/IsCellularLowDataModeOn.swift new file mode 100644 index 0000000..c527c8c --- /dev/null +++ b/Intents Extension/Actions/IsCellularLowDataModeOn.swift @@ -0,0 +1,21 @@ +import AppIntents + +struct IsCellularLowDataModeOn: AppIntent { + static let title: LocalizedStringResource = "Is Cellular Low Data Mode On" + + static let description = IntentDescription( +""" +Returns whether cellular low data mode is enabled on the device. + +On macOS, it always returns false. +""", + categoryName: "Device" + ) + + func perform() async throws -> some IntentResult & ReturnsValue { + // Give the system time to change the mode when used in an automation. + sleep(.milliseconds(30)) + + return .result(value: await Device.isCellularLowDataModeEnabled) + } +} diff --git a/Intents Extension/Actions/IsSilentModeOn.swift b/Intents Extension/Actions/IsSilentModeOn.swift index a29cb58..2477540 100644 --- a/Intents Extension/Actions/IsSilentModeOn.swift +++ b/Intents Extension/Actions/IsSilentModeOn.swift @@ -7,7 +7,11 @@ struct IsSilentModeOn: AppIntent, CustomIntentMigratedAppIntent { static let title: LocalizedStringResource = "Is Silent Mode On (iOS-only)" static let description = IntentDescription( - "Returns whether the silent switch (mute) is enabled on the device.", +""" +Returns whether the silent switch (mute) is enabled on the device. + +Known limitation: This will return true even if silent mode is not enabled if it's run while Voice Memos is recording. +""", categoryName: "Device" ) diff --git a/Intents Extension/Actions/TransformLists.swift b/Intents Extension/Actions/TransformLists.swift index 8135254..06fcb34 100644 --- a/Intents Extension/Actions/TransformLists.swift +++ b/Intents Extension/Actions/TransformLists.swift @@ -14,6 +14,8 @@ Note that duplicates will be removed from the result. Tap and hold a list parameter to select a variable to a list. Don't quick tap it. Note: If you get the error “The operation failed because Shortcuts couldn't convert from Text to NSString.”, just change the preview to show a list view instead. This is a bug in the Shortcuts app. + +Known limitation: It does not work with iTunes Media items. """, categoryName: "List" ) diff --git a/Shared/Actions/AskChatGPT.swift b/Shared/Actions/AskChatGPT.swift index 2fde6f0..db5ad57 100644 --- a/Shared/Actions/AskChatGPT.swift +++ b/Shared/Actions/AskChatGPT.swift @@ -1,22 +1,12 @@ import AppIntents -import SimpleKeychain -import OpenAISwift +@available(*, deprecated, message: "This action was moved to a separate app called “AI Actions” by the same author. It's a drop-in replacement.") struct AskChatGPT: AppIntent { static let title: LocalizedStringResource = "Ask ChatGPT" static let description = IntentDescription( """ -Send a prompt to ChatGPT and get a text reply. - -It does not remember previous conversations. - -IMPORTANT: You must add your open OpenAI API key in the app settings before using this action. - -NOTE: Using the GPT-4 model requires access to the beta: https://openai.com/waitlist/gpt-4 -NOTE: The GPT-4 model generally costs 14x more than GPT-3.5. - -TIP: If you want a dictionary back, end your prompt with: “Return the result as a JSON object. Don't include any other text than the JSON object.” and then pass the result to the “Get Dictionary from Input” action. For more consistent result, also describe the shape of the JSON and include an example. +DEPRECATED. WILL BE REMOVED SOON. """, categoryName: "AI" ) @@ -29,7 +19,7 @@ TIP: If you want a dictionary back, end your prompt with: “Return the result a @Parameter( title: "Max Tokens", - description: "The maximum number of tokens allowed for the generated answer. More about tokens: https://platform.openai.com/tokenizer" + description: "The maximum number of tokens allowed for the generated answer." ) var maxTokens: Int? @@ -52,7 +42,7 @@ TIP: If you want a dictionary back, end your prompt with: “Return the result a @Parameter( title: "Presence Penalty", - description: "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. More info: https://platform.openai.com/docs/api-reference/parameter-details Default: 0", + description: "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. Default: 0", default: 0, // controlStyle: .slider, inclusiveRange: (-2, 2) @@ -61,7 +51,7 @@ TIP: If you want a dictionary back, end your prompt with: “Return the result a @Parameter( title: "Frequency Penalty", - description: "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. More info: https://platform.openai.com/docs/api-reference/parameter-details Default: 0", + description: "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. Default: 0", default: 0, // controlStyle: .slider, inclusiveRange: (-2, 2) @@ -103,68 +93,11 @@ As an example, you can pass {"50256": -100} to prevent the <|endoftext|> token f } func perform() async throws -> some IntentResult & ReturnsValue { - let keychain = SimpleKeychain(synchronizable: true) - - guard - try keychain.hasItem(forKey: Constants.keychainKey_openAI), - let token = try keychain.string(forKey: Constants.keychainKey_openAI).nilIfEmptyOrWhitespace - else { - throw "Please add your OpenAI API key in the settings of the main Actions app.".toError - } - - let openAI = OpenAISwift(authToken: token) - - let logitBiasFinal = try logitBias? - .nilIfEmptyOrWhitespace? - .toData - .jsonToDictionary() - .compactMapKeys { Int($0) } - .compactMapValues { - if let double = $0 as? Double { - return double - } - - if let string = $0 as? String { - return Double(string) - } - - return nil - } - - let response: OpenAI - do { - response = try await openAI.sendChat( - with: [ - .init(role: .system, content: "Keep it short."), - .init(role: .user, content: prompt) - ], - model: model == .gpt3_5 ? .chat(.chatgpt) : .gpt4(.gpt4), - temperature: temperature, - topProbabilityMass: topProbabilityMass, - maxTokens: maxTokens, - presencePenalty: presencePenalty, - frequencyPenalty: frequencyPenalty, - logitBias: logitBiasFinal - ) - } catch OpenAIError.genericError(let error) { - throw error.presentableMessage.toError - } catch OpenAIError.decodingError(let error) { - throw error.presentableMessage.toError - } catch OpenAIError.chatError(let error) { - var message = error.message - - if error.code == "model_not_found" { - message += ". Make sure you have access to this model. GPT-4 requires special access." - } - - throw error.message.toError - } - - guard let reply = response.choices?.first?.message.content else { - throw "Missing reply.".toError - } + #if os(macOS) + "https://sindresorhus.com/ai-actions".openUrl() + #endif - return .result(value: reply) + return .result(value: "PLEASE READ: This action was moved to a separate app called “AI Actions” by the same author. It's a drop-in replacement. The action in that app is called “Ask AI”. https://sindresorhus.com/ai-actions") } } diff --git a/Shared/Actions/GlobalVariable.swift b/Shared/Actions/GlobalVariable.swift index dea7464..e42dad1 100644 --- a/Shared/Actions/GlobalVariable.swift +++ b/Shared/Actions/GlobalVariable.swift @@ -80,11 +80,21 @@ struct GlobalVariableSetBoolean: AppIntent { """ Sets a global variable with the given boolean. +You can also toggle a boolean. + Global variables persist across your shortcuts and devices, with a limit of 1000 variables and a total storage capacity of 1 MB. Avoid using this for large amounts of data. For large data, use iCloud Drive, Notes, or Data Jar. """, categoryName: "Global Variable" ) + @Parameter( + title: "Action", + default: false, + // TODO: macOS 14, use .init + displayName: Bool.IntentDisplayName(true: "Toggle", false: "Set") + ) + var shouldToggle: Bool + @Parameter( title: "Key", description: "Maximum 20 characters.", @@ -104,11 +114,21 @@ Global variables persist across your shortcuts and devices, with a limit of 1000 var value: Bool static var parameterSummary: some ParameterSummary { - Summary("Set global boolean variable \(\.$key) to \(\.$value)") + When(\.$shouldToggle, .equalTo, true) { + Summary("\(\.$shouldToggle) global boolean variable \(\.$key)") + } otherwise: { + Summary("\(\.$shouldToggle) global boolean variable \(\.$key) to \(\.$value)") + } } func perform() async throws -> some IntentResult { - try setValue(key: key, value: value) + if shouldToggle { + let value = NSUbiquitousKeyValueStore.default.strictBool(forKey: "\(keyPrefix)\(key)") ?? false + try setValue(key: key, value: !value) + } else { + try setValue(key: key, value: value) + } + return .result() } } @@ -158,11 +178,19 @@ struct GlobalVariableSetNumber: AppIntent { """ Sets a global variable with the given number. +You can also increment or decrement a number. + Global variables persist across your shortcuts and devices, with a limit of 1000 variables and a total storage capacity of 1 MB. Avoid using this for large amounts of data. For large data, use iCloud Drive, Notes, or Data Jar. """, categoryName: "Global Variable" ) + @Parameter( + title: "Action", + default: .set + ) + var action: GlobalVariableSetNumberAction_AppEnum + @Parameter( title: "Key", description: "Maximum 20 characters.", @@ -179,15 +207,49 @@ Global variables persist across your shortcuts and devices, with a limit of 1000 var value: Double static var parameterSummary: some ParameterSummary { - Summary("Set global number variable \(\.$key) to \(\.$value)") + Switch(\.$action) { + Case(.increment) { + Summary("\(\.$action) global number variable \(\.$key) by \(\.$value)") + } + Case(.decrement) { + Summary("\(\.$action) global number variable \(\.$key) by \(\.$value)") + } + DefaultCase { + Summary("\(\.$action) global number variable \(\.$key) to \(\.$value)") + } + } } func perform() async throws -> some IntentResult { - try setValue(key: key, value: value) + lazy var number = NSUbiquitousKeyValueStore.default.strictNumber(forKey: "\(keyPrefix)\(key)") ?? 0 + + switch action { + case .set: + try setValue(key: key, value: value) + case .increment: + try setValue(key: key, value: number + value) + case .decrement: + try setValue(key: key, value: number - value) + } + return .result() } } +enum GlobalVariableSetNumberAction_AppEnum: String, AppEnum { + case set + case increment + case decrement + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Global Variable Set Number Action") + + static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .set: "Set", + .increment: "Increment", + .decrement: "Decrement" + ] +} + struct GlobalVariableGetNumber: AppIntent { static let title: LocalizedStringResource = "Global Variable: Get Number" diff --git a/Shared/Actions/HapticFeedback.swift b/Shared/Actions/HapticFeedback.swift index 0ae889a..be47b51 100644 --- a/Shared/Actions/HapticFeedback.swift +++ b/Shared/Actions/HapticFeedback.swift @@ -30,8 +30,13 @@ On macOS, it does nothing. func perform() async throws -> some IntentResult { #if canImport(UIKit) - Device.hapticFeedback(type.toNative) - try? await Task.sleep(for: .seconds(1)) + if #available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) { + try? await Task.sleep(for: .seconds(0.5)) + Device.hapticFeedback(type.toNative) + } else { + Device.hapticFeedback(type.toNative) + try? await Task.sleep(for: .seconds(1)) + } await ShortcutsApp.open() #endif diff --git a/Shared/Actions/IsDeviceLocked.swift b/Shared/Actions/IsDeviceLocked.swift new file mode 100644 index 0000000..fb66f7d --- /dev/null +++ b/Shared/Actions/IsDeviceLocked.swift @@ -0,0 +1,22 @@ +import AppIntents +import SwiftUI + +struct IsDeviceLocked: AppIntent { + static let title: LocalizedStringResource = "Is Device Locked" + + static let description = IntentDescription( +""" +Returns whether the device is currently locked. + +Limitations: +- It takes about 10 seconds from when you lock the screen until the device is actually locked. +- This will not work if you don't have any authentication (passcode, Face ID, or Touch ID) for the device. +""", + categoryName: "Device" + ) + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue { + .result(value: !XApplication.shared.isProtectedDataAvailable) + } +} diff --git a/Shared/Actions/ScanDocuments.swift b/Shared/Actions/ScanDocuments.swift index 5004064..12d254b 100644 --- a/Shared/Actions/ScanDocuments.swift +++ b/Shared/Actions/ScanDocuments.swift @@ -12,6 +12,8 @@ struct ScanDocuments: AppIntent, CustomIntentMigratedAppIntent { Scans one or more documents using the iOS document scanner. IMPORTANT: The resulting images are copied to the clipboard. Add the “Wait to Return” and “Get Clipboard” actions after this one. + +NOTE: In contrast to the built-in “Scan Document” action, this one makes it possible to use the scanned document in the shortcut. """, categoryName: "Utility" ) diff --git a/Shared/App.swift b/Shared/App.swift index 51ad33b..2f65b14 100644 --- a/Shared/App.swift +++ b/Shared/App.swift @@ -41,10 +41,10 @@ struct AppMain: App { } } #endif - #if os(macOS) - Settings { - SettingsScreen() - } - #endif +// #if os(macOS) +// Settings { +// SettingsScreen() +// } +// #endif } } diff --git a/Shared/AskForTextScreen.swift b/Shared/AskForTextScreen.swift index 5011f41..618e42a 100644 --- a/Shared/AskForTextScreen.swift +++ b/Shared/AskForTextScreen.swift @@ -92,8 +92,6 @@ struct AskForTextScreen: View { } } -//struct AskForTextScreen_Previews: PreviewProvider { -// static var previews: some View { -// AskForTextScreen() -// } +//#Preview { +// AskForTextScreen() //} diff --git a/Shared/Constants.swift b/Shared/Constants.swift index e8fd494..db6b00b 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -1,10 +1,6 @@ import Foundation import AppIntents -enum Constants { - static let keychainKey_openAI = "openai-api-token" -} - func initSentry() { SSApp.initSentry("https://12c8785fd2924c9a9c0f6bb1d91be79e@o844094.ingest.sentry.io/6041555") } @@ -28,7 +24,6 @@ Intent categories: - Parse / Generate - Math - Location -- AI - Meta - Global Variable - Miscellaneous diff --git a/Shared/MainScreen.swift b/Shared/MainScreen.swift index 52902bf..e44e8b3 100644 --- a/Shared/MainScreen.swift +++ b/Shared/MainScreen.swift @@ -4,7 +4,7 @@ struct MainScreen: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var appState: AppState @State private var error: Error? - @State private var isSettingsPresented = false +// @State private var isSettingsPresented = false var body: some View { NavigationStack { @@ -57,54 +57,52 @@ struct MainScreen: View { .task { debug() } - .sheet(isPresented: $isSettingsPresented) { - SettingsScreen() - } - .toolbar { - #if os(iOS) - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button("Settings", systemImage: "gear") { - isSettingsPresented = true - } - .keyboardShortcut(",") - } - #endif - } +// .sheet(isPresented: $isSettingsPresented) { +// SettingsScreen() +// } +// .toolbar { +// #if os(iOS) +// ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button("Settings", systemImage: "gear") { +// isSettingsPresented = true +// } +// .keyboardShortcut(",") +// } +// #endif +// } } } private func debug() { #if DEBUG - // For testing the “Write or Edit Text” action. -// appState.writeTextData = .init( -// title: "Test", -// text: "" -// ) + // For testing the “Write or Edit Text” action. +// appState.writeTextData = .init( +// title: "Test", +// text: "" +// ) // - // For testing the “Choose from List Extended” action. -// appState.chooseFromListData = .init( -// list: [ -// "Foo", -// "Bar" -// ], -// title: "Test", -// selectMultiple: false, -// selectAllInitially: false, -// allowCustomItems: false, -// timeoutReturnValue: .nothing -// ) + // For testing the “Choose from List Extended” action. +// appState.chooseFromListData = .init( +// list: [ +// "Foo", +// "Bar" +// ], +// title: "Test", +// selectMultiple: false, +// selectAllInitially: false, +// allowCustomItems: false, +// timeoutReturnValue: .nothing +// ) // -// appState.askForTextData = .init( -// text: "X", -// title: "X", -// timeoutReturnValue: "X" -// ) +// appState.askForTextData = .init( +// text: "X", +// title: "X", +// timeoutReturnValue: "X" +// ) #endif } } -struct MainScreen_Previews: PreviewProvider { - static var previews: some View { - MainScreen() - } +#Preview { + MainScreen() } diff --git a/Shared/SettingsScreen.swift b/Shared/SettingsScreen.swift index 8a2dd6d..d5ad327 100644 --- a/Shared/SettingsScreen.swift +++ b/Shared/SettingsScreen.swift @@ -1,5 +1,4 @@ import SwiftUI -import SimpleKeychain struct SettingsScreen: View { @Environment(\.dismiss) private var dismiss @@ -7,7 +6,7 @@ struct SettingsScreen: View { var body: some View { NavigationStack { Form { - OpenAIAPITokenSetting() + // ... } .formStyle(.grouped) #if os(macOS) @@ -27,75 +26,6 @@ struct SettingsScreen: View { } } -struct SettingsScreen_Previews: PreviewProvider { - static var previews: some View { - SettingsScreen() - } -} - -private struct OpenAIAPITokenSetting: View { - @State private var token = "" - @State private var error: Error? - @State private var keychain = SimpleKeychain(synchronizable: true) - @FocusState private var isFocused - - var body: some View { - Section { - HStack { - TextField("Key", text: $token) - .focused($isFocused) - .alert(error: $error) - .task { - do { - guard try keychain.hasItem(forKey: Constants.keychainKey_openAI) else { - return - } - - token = try keychain.string(forKey: Constants.keychainKey_openAI) - } catch { - self.error = error - } - } - // Neither `onChange` or `onDisappear` are called on macOS because the settings window views never disappears. (macOS 13.3) - .onChange(of: isFocused) { - guard !$0 else { - return - } - - save() - } - .onDisappear { - save() - } - .debouncingTask(id: token, interval: .seconds(1)) { - save() - } - PasteButton(payloadType: String.self) { - guard let string = $0.first else { - return - } - - token = string - } - .labelStyle(.iconOnly) - .controlSize(.small) - } - } header: { - Text("OpenAI API Key") - } footer: { - HStack(spacing: 16) { - Link("Get API Key", destination: "https://platform.openai.com/account/api-keys") - Link("API Usage", destination: "https://platform.openai.com/account/usage") - } - .controlSize(.small) - } - } - - private func save() { - do { - try keychain.set(token.trimmed, forKey: Constants.keychainKey_openAI) - } catch { - self.error = error - } - } +#Preview { + SettingsScreen() } diff --git a/Shared/Utilities.swift b/Shared/Utilities.swift index 8a03d94..a552836 100644 --- a/Shared/Utilities.swift +++ b/Shared/Utilities.swift @@ -3774,6 +3774,8 @@ extension Device { /** Whether the silent switch on the device is enabled. + + - Note: This will report true even if silent mode is not enabled if run while Voice Memos is recording. */ @available(macOS, unavailable) static var isSilentModeEnabled: Bool { @@ -5513,6 +5515,12 @@ extension NWPathMonitor { } } } + + static var currentCellularPath: NWPath? { + get async { + await NWPathMonitor.changes(requiredInterfaceType: .cellular).first() + } + } } @@ -5572,10 +5580,17 @@ extension NWConnection { extension Device { static var isCellularDataEnabled: Bool { get async { - let path = await NWPathMonitor.changes(requiredInterfaceType: .cellular).first() + let path = await NWPathMonitor.currentCellularPath return path?.status == .satisfied } } + + static var isCellularLowDataModeEnabled: Bool { + get async { + let path = await NWPathMonitor.currentCellularPath + return path?.isConstrained ?? false + } + } } diff --git a/Shared/WelcomeScreen.swift b/Shared/WelcomeScreen.swift index 3afd4f8..b281fd1 100644 --- a/Shared/WelcomeScreen.swift +++ b/Shared/WelcomeScreen.swift @@ -94,8 +94,6 @@ struct WelcomeScreen: View { } } -struct WelcomeScreen_Previews: PreviewProvider { - static var previews: some View { - WelcomeScreen() - } +#Preview { + WelcomeScreen() } diff --git a/app-store-description.txt b/app-store-description.txt index e6506a7..fc4ed57 100644 --- a/app-store-description.txt +++ b/app-store-description.txt @@ -13,7 +13,6 @@ The app is free without ads because I love making apps. Consider leaving a nice - Add to List - Apply Capture Date -- Ask ChatGPT - Ask for Text with Timeout - Authenticate - Blur Images @@ -82,9 +81,11 @@ The app is free without ads because I love making apps. Consider leaving a nice - Is Audio Playing (iOS-only) - Is Bluetooth On - Is Cellular Data On +- Is Cellular Low Data Mode On - Is Connected to VPN (iOS-only) - Is Dark Mode On - Is Day +- Is Device Locked - Is Device Orientation - Is Host Reachable - Is Low Power Mode On diff --git a/readme.md b/readme.md index 4b9c852..e7945cb 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,6 @@ And for high-quality transcription, see my [Aiko](https://sindresorhus.com/aiko) - Add to List - Apply Capture Date -- Ask ChatGPT - Ask for Text with Timeout - Authenticate - Blur Images @@ -110,9 +109,11 @@ And for high-quality transcription, see my [Aiko](https://sindresorhus.com/aiko) - Is Audio Playing (iOS-only) - Is Bluetooth On - Is Cellular Data On +- Is Cellular Low Data Mode On - Is Connected to VPN (iOS-only) - Is Dark Mode On - Is Day +- Is Device Locked - Is Device Orientation - Is Host Reachable - Is Low Power Mode On @@ -159,6 +160,7 @@ And for high-quality transcription, see my [Aiko](https://sindresorhus.com/aiko) #### Want more shortcut actions? +- Use the ChatGPT API → [AI Actions](https://sindresorhus.com/ai-actions) - High-quality transcription (speech to text) in 100 languages → [Aiko](https://sindresorhus.com/aiko) - Trigger shortcuts on your Mac from your iOS device → [Hyperduck](https://sindresorhus.com/hyperduck#shortcuts) - Show text in menu bar → [One Thing](https://sindresorhus.com/one-thing)