feat(ios): add ios shortcuts support

This commit is contained in:
cogwheel0
2025-11-25 00:08:51 +05:30
parent 0ab2619049
commit 2d88519abe
14 changed files with 792 additions and 46 deletions

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>15.1</string>
<string>16.0</string>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.1'
platform :ios, '16.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -40,6 +40,8 @@ PODS:
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_app_intents (0.1.0):
- Flutter
- flutter_callkit_incoming (0.0.1):
- CryptoSwift
- Flutter
@@ -102,6 +104,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_app_intents (from `.symlinks/plugins/flutter_app_intents/ios`)
- flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -141,6 +144,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_app_intents:
:path: ".symlinks/plugins/flutter_app_intents/ios"
flutter_callkit_incoming:
:path: ".symlinks/plugins/flutter_callkit_incoming/ios"
flutter_local_notifications:
@@ -188,6 +193,7 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_intents: e77f999f398c841ab584a1925dbce33ee0168fb5
flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@@ -212,6 +218,6 @@ SPEC CHECKSUMS:
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
PODFILE CHECKSUM: a6ecbec6401c6461e69650e9ef66360aee70610f
PODFILE CHECKSUM: 467be2eaee7e6ac5b7b2d2ef8bea817d4d70b736
COCOAPODS: 1.16.2

View File

@@ -585,7 +585,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -722,7 +722,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -773,7 +773,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -865,7 +865,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -908,7 +908,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -948,7 +948,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@@ -1,7 +1,10 @@
import AVFoundation
import BackgroundTasks
import Flutter
import AppIntents
import flutter_app_intents
import UIKit
import UniformTypeIdentifiers
final class VoiceBackgroundAudioManager {
static let shared = VoiceBackgroundAudioManager()
@@ -305,6 +308,248 @@ class BackgroundStreamingHandler: NSObject {
NotificationCenter.default.removeObserver(self)
endBackgroundTask()
VoiceBackgroundAudioManager.shared.deactivate()
}
}
@available(iOS 16.0, *)
enum AppIntentError: Error {
case executionFailed(String)
}
@available(iOS 16.0, *)
struct AskConduitIntent: AppIntent {
static var title: LocalizedStringResource = "Ask Conduit"
static var description = IntentDescription(
"Start a Conduit chat with an optional prompt."
)
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(
title: "Prompt",
requestValueDialog: IntentDialog("What should Conduit answer?")
)
var prompt: String?
init() {}
init(prompt: String?) {
self.prompt = prompt
}
func perform() async throws
-> some IntentResult & ReturnsValue<String> & OpensIntent
{
let parameters: [String: Any] = prompt?.isEmpty == false
? ["prompt": prompt ?? ""]
: [:]
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "app.cogwheel.conduit.ask_chat",
parameters: parameters
)
if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Opening chat"
return .result(value: value)
}
let message = result["error"] as? String
?? "Unable to open Conduit chat"
throw AppIntentError.executionFailed(message)
}
}
@available(iOS 16.0, *)
struct StartVoiceCallIntent: AppIntent {
static var title: LocalizedStringResource = "Start Voice Call"
static var description = IntentDescription(
"Start a live voice call with Conduit."
)
static var isDiscoverable = true
static var openAppWhenRun = true
func perform() async throws
-> some IntentResult & ReturnsValue<String> & OpensIntent
{
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "app.cogwheel.conduit.start_voice_call",
parameters: [:]
)
if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Starting voice call"
return .result(value: value)
}
let message = result["error"] as? String
?? "Unable to start voice call"
throw AppIntentError.executionFailed(message)
}
}
@available(iOS 16.0, *)
struct ConduitSendTextIntent: AppIntent {
static var title: LocalizedStringResource = "Send to Conduit"
static var description = IntentDescription(
"Start a Conduit chat with provided text."
)
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(
title: "Text",
requestValueDialog: IntentDialog("What should Conduit process?")
)
var text: String?
func perform() async throws
-> some IntentResult & ReturnsValue<String> & OpensIntent
{
let plugin = FlutterAppIntentsPlugin.shared
let trimmed = text?.trimmingCharacters(in: .whitespacesAndNewlines)
let result = await plugin.handleIntentInvocation(
identifier: "app.cogwheel.conduit.send_text",
parameters: ["text": trimmed ?? ""]
)
if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Sent to Conduit"
return .result(value: value)
}
let message = result["error"] as? String ?? "Unable to send text"
throw AppIntentError.executionFailed(message)
}
}
@available(iOS 16.0, *)
struct ConduitSendUrlIntent: AppIntent {
static var title: LocalizedStringResource = "Send Link to Conduit"
static var description = IntentDescription(
"Send a URL into Conduit for summary or analysis."
)
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(
title: "URL",
requestValueDialog: IntentDialog("Which link should Conduit analyze?")
)
var url: URL
func perform() async throws
-> some IntentResult & ReturnsValue<String> & OpensIntent
{
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "app.cogwheel.conduit.send_url",
parameters: ["url": url.absoluteString]
)
if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Sent link to Conduit"
return .result(value: value)
}
let message = result["error"] as? String ?? "Unable to send link"
throw AppIntentError.executionFailed(message)
}
}
@available(iOS 16.0, *)
struct ConduitSendImageIntent: AppIntent {
static var title: LocalizedStringResource = "Send Image to Conduit"
static var description = IntentDescription(
"Send an image into Conduit for analysis."
)
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(
title: "Image",
requestValueDialog: IntentDialog("Choose an image for Conduit.")
)
var image: IntentFile
func perform() async throws
-> some IntentResult & ReturnsValue<String> & OpensIntent
{
if let type = image.type, !type.conforms(to: .image) {
throw AppIntentError.executionFailed(
"Only image files are supported."
)
}
let data = try image.data
let base64 = data.base64EncodedString()
let name = image.filename ?? "shared_image.jpg"
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "app.cogwheel.conduit.send_image",
parameters: [
"filename": name,
"bytes": base64,
]
)
if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Sent image to Conduit"
return .result(value: value)
}
let message = result["error"] as? String ?? "Unable to send image"
throw AppIntentError.executionFailed(message)
}
}
@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
return [
AppShortcut(
intent: AskConduitIntent(),
phrases: [
"Ask with \(.applicationName)",
"Start chat in \(.applicationName)",
"Open composer in \(.applicationName)",
]
),
AppShortcut(
intent: StartVoiceCallIntent(),
phrases: [
"Start voice call in \(.applicationName)",
"Call with \(.applicationName)",
"Begin voice chat in \(.applicationName)",
]
),
AppShortcut(
intent: ConduitSendTextIntent(),
phrases: [
"Send text to \(.applicationName)",
"Share text with \(.applicationName)",
"Summarize this in \(.applicationName)",
]
),
AppShortcut(
intent: ConduitSendUrlIntent(),
phrases: [
"Summarize link in \(.applicationName)",
"Analyze link with \(.applicationName)",
"Send URL to \(.applicationName)",
]
),
AppShortcut(
intent: ConduitSendImageIntent(),
phrases: [
"Send image to \(.applicationName)",
"Analyze image with \(.applicationName)",
"Share photo to \(.applicationName)",
]
),
]
}
}