✨ App intents (aims to support Siri on iOS) (untested)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
platform :ios, '15.0'
|
platform :ios, '16.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ PODS:
|
|||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesSwift (~> 2.1)
|
- PromisesSwift (~> 2.1)
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_app_intents (0.1.0):
|
||||||
|
- Flutter
|
||||||
- flutter_app_update (0.0.1):
|
- flutter_app_update (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_inappwebview_ios (0.0.1):
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
@@ -336,6 +338,7 @@ DEPENDENCIES:
|
|||||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_app_intents (from `.symlinks/plugins/flutter_app_intents/ios`)
|
||||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
@@ -428,6 +431,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_app_intents:
|
||||||
|
:path: ".symlinks/plugins/flutter_app_intents/ios"
|
||||||
flutter_app_update:
|
flutter_app_update:
|
||||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||||
flutter_inappwebview_ios:
|
flutter_inappwebview_ios:
|
||||||
@@ -525,6 +530,7 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4
|
FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4
|
||||||
FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d
|
FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
flutter_app_intents: e77f999f398c841ab584a1925dbce33ee0168fb5
|
||||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||||
@@ -578,6 +584,6 @@ SPEC CHECKSUMS:
|
|||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 585198f58dca90ac6492607c83a8d17045ab3852
|
PODFILE CHECKSUM: 5a3652182a0f0e1093d3ac36dcabc72f6cc46b3a
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -216,8 +216,6 @@
|
|||||||
};
|
};
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = "Solian Watch App";
|
path = "Solian Watch App";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -759,10 +757,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -820,10 +822,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -874,10 +880,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
||||||
@@ -1096,7 +1106,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1597,7 +1607,7 @@
|
|||||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1642,7 +1652,7 @@
|
|||||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1684,7 +1694,7 @@
|
|||||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1950,7 +1960,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1981,7 +1991,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import Flutter
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import UIKit
|
import UIKit
|
||||||
import WatchConnectivity
|
import WatchConnectivity
|
||||||
|
import AppIntents
|
||||||
|
import flutter_app_intents
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@@ -107,7 +109,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
let token = UserDefaults.standard.getFlutterToken()
|
let token = UserDefaults.standard.getFlutterToken()
|
||||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
var data: [String: Any] = ["serverUrl": serverUrl]
|
||||||
if let token = token {
|
if let token = token {
|
||||||
data["token"] = token
|
data["token"] = token
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
let token = UserDefaults.standard.getFlutterToken()
|
let token = UserDefaults.standard.getFlutterToken()
|
||||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
var data: [String: Any] = ["serverUrl": serverUrl]
|
||||||
if let token = token {
|
if let token = token {
|
||||||
data["token"] = token
|
data["token"] = token
|
||||||
}
|
}
|
||||||
@@ -138,3 +140,240 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - App Intents
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct OpenChatIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Open Chat"
|
||||||
|
static var description = IntentDescription("Open a specific chat room")
|
||||||
|
static var isDiscoverable = true
|
||||||
|
static var openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Channel ID")
|
||||||
|
var channelId: String?
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
|
||||||
|
let plugin = FlutterAppIntentsPlugin.shared
|
||||||
|
let result = await plugin.handleIntentInvocation(
|
||||||
|
identifier: "open_chat",
|
||||||
|
parameters: ["channelId": channelId ?? ""]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Chat opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to open chat"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct OpenPostIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Open Post"
|
||||||
|
static var description = IntentDescription("Open a specific post")
|
||||||
|
static var isDiscoverable = true
|
||||||
|
static var openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Post ID")
|
||||||
|
var postId: String?
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
|
||||||
|
let plugin = FlutterAppIntentsPlugin.shared
|
||||||
|
let result = await plugin.handleIntentInvocation(
|
||||||
|
identifier: "open_post",
|
||||||
|
parameters: ["postId": postId ?? ""]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Post opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to open post"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct OpenComposeIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Open Compose"
|
||||||
|
static var description = IntentDescription("Open compose post screen")
|
||||||
|
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: "open_compose",
|
||||||
|
parameters: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Compose screen opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to open compose"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct ComposePostIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Compose Post"
|
||||||
|
static var description = IntentDescription("Create a new post")
|
||||||
|
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: "compose_post",
|
||||||
|
parameters: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Compose screen opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to compose post"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct SearchContentIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Search Content"
|
||||||
|
static var description = IntentDescription("Search for content")
|
||||||
|
static var isDiscoverable = true
|
||||||
|
static var openAppWhenRun = true
|
||||||
|
|
||||||
|
@Parameter(title: "Search Query")
|
||||||
|
var query: String?
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
|
||||||
|
let plugin = FlutterAppIntentsPlugin.shared
|
||||||
|
let result = await plugin.handleIntentInvocation(
|
||||||
|
identifier: "search_content",
|
||||||
|
parameters: ["query": query ?? ""]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Search opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to search"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct ViewNotificationsIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "View Notifications"
|
||||||
|
static var description = IntentDescription("View notifications")
|
||||||
|
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: "view_notifications",
|
||||||
|
parameters: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "Notifications opened"
|
||||||
|
return .result(value: value)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to view notifications"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct CheckNotificationsIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Check Notifications"
|
||||||
|
static var description = IntentDescription("Check notification count")
|
||||||
|
static var isDiscoverable = true
|
||||||
|
static var openAppWhenRun = false
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
|
||||||
|
let plugin = FlutterAppIntentsPlugin.shared
|
||||||
|
let result = await plugin.handleIntentInvocation(
|
||||||
|
identifier: "check_notifications",
|
||||||
|
parameters: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if let success = result["success"] as? Bool, success {
|
||||||
|
let value = result["value"] as? String ?? "You have new notifications"
|
||||||
|
return .result(
|
||||||
|
value: value,
|
||||||
|
dialog: "Dialog: \(value)"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let errorMessage = result["error"] as? String ?? "Failed to check notifications"
|
||||||
|
throw AppIntentError.executionFailed(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppIntentError: Error {
|
||||||
|
case executionFailed(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct AppShortcuts: AppShortcutsProvider {
|
||||||
|
static var appShortcuts: [AppShortcut] {
|
||||||
|
return [
|
||||||
|
// Open chat
|
||||||
|
AppShortcut(
|
||||||
|
intent: OpenChatIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Open chat with \(.applicationName)",
|
||||||
|
"Go to chat using \(.applicationName)",
|
||||||
|
"Show chat in \(.applicationName)"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
// Open post
|
||||||
|
AppShortcut(
|
||||||
|
intent: OpenPostIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Open post with \(.applicationName)",
|
||||||
|
"Show post using \(.applicationName)"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
// Compose
|
||||||
|
AppShortcut(
|
||||||
|
intent: OpenComposeIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Open compose with \(.applicationName)",
|
||||||
|
"New post using \(.applicationName)",
|
||||||
|
"Write post in \(.applicationName)"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
// Search
|
||||||
|
AppShortcut(
|
||||||
|
intent: SearchContentIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Search in \(.applicationName)",
|
||||||
|
"Find content using \(.applicationName)"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
// Check notifications
|
||||||
|
AppShortcut(
|
||||||
|
intent: CheckNotificationsIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Check notifications with \(.applicationName)",
|
||||||
|
"Get notifications using \(.applicationName)",
|
||||||
|
"Do I have notifications in \(.applicationName)"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,18 @@
|
|||||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||||
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
<string>Solian uses speech recognition for Siri integration</string>
|
||||||
|
<key>NSAppIntentsConfiguration</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAppIntentsPackage</key>
|
||||||
|
<string>dev.solsynth.solian</string>
|
||||||
|
</dict>
|
||||||
|
<key>NSAppIntentsMetadata</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAppIntentsSupported</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:island/route.dart';
|
|||||||
import 'package:island/services/notify.dart';
|
import 'package:island/services/notify.dart';
|
||||||
import 'package:island/services/widget_sync_service.dart';
|
import 'package:island/services/widget_sync_service.dart';
|
||||||
import 'package:island/services/timezone.dart';
|
import 'package:island/services/timezone.dart';
|
||||||
|
import 'package:island/services/app_intents.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
@@ -99,6 +100,17 @@ void main() async {
|
|||||||
talker.error("[SplashScreen] Failed to load timezone database... $err");
|
talker.error("[SplashScreen] Failed to load timezone database... $err");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
talker.info("[AppIntents] Initializing App Intents service...");
|
||||||
|
final appIntentsService = AppIntentsService();
|
||||||
|
await appIntentsService.initialize();
|
||||||
|
talker.info("[AppIntents] App Intents service is ready!");
|
||||||
|
} catch (err) {
|
||||||
|
talker.error(
|
||||||
|
"[AppIntents] Failed to initialize App Intents service... $err",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||||
|
|||||||
1
lib/services/app_intents.dart
Normal file
1
lib/services/app_intents.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export 'app_intents/ios.dart';
|
||||||
443
lib/services/app_intents/ios.dart
Normal file
443
lib/services/app_intents/ios.dart
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_app_intents/flutter_app_intents.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:island/models/auth.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:island/route.dart';
|
||||||
|
|
||||||
|
class AppIntentsService {
|
||||||
|
static final AppIntentsService _instance = AppIntentsService._internal();
|
||||||
|
factory AppIntentsService() => _instance;
|
||||||
|
AppIntentsService._internal();
|
||||||
|
|
||||||
|
FlutterAppIntentsClient? _client;
|
||||||
|
bool _initialized = false;
|
||||||
|
Dio? _dio;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (!Platform.isIOS) {
|
||||||
|
talker.warning('[AppIntents] App Intents only supported on iOS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_initialized) {
|
||||||
|
talker.info('[AppIntents] Already initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
talker.info('[AppIntents] Initializing App Intents client...');
|
||||||
|
_client = FlutterAppIntentsClient.instance;
|
||||||
|
|
||||||
|
// Initialize Dio for API calls
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final serverUrl =
|
||||||
|
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
|
final tokenString = prefs.getString(kTokenPairStoreKey);
|
||||||
|
|
||||||
|
final headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tokenString != null) {
|
||||||
|
try {
|
||||||
|
final token = AppToken.fromJson(jsonDecode(tokenString));
|
||||||
|
headers['Authorization'] = 'AtField ${token.token}';
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[AppIntents] Failed to parse token: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: serverUrl,
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
headers: headers,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _registerIntents();
|
||||||
|
_initialized = true;
|
||||||
|
talker.info('[AppIntents] All intents registered successfully');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Initialization failed', e, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _registerIntents() async {
|
||||||
|
if (_client == null) {
|
||||||
|
throw StateError('Client not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation Intents
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('open_chat')
|
||||||
|
.title('Open Chat')
|
||||||
|
.description('Open a specific chat room')
|
||||||
|
.parameter(
|
||||||
|
const AppIntentParameter(
|
||||||
|
name: 'channelId',
|
||||||
|
title: 'Channel ID',
|
||||||
|
type: AppIntentParameterType.string,
|
||||||
|
isOptional: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
_handleOpenChatIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('open_post')
|
||||||
|
.title('Open Post')
|
||||||
|
.description('Open a specific post')
|
||||||
|
.parameter(
|
||||||
|
const AppIntentParameter(
|
||||||
|
name: 'postId',
|
||||||
|
title: 'Post ID',
|
||||||
|
type: AppIntentParameterType.string,
|
||||||
|
isOptional: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
_handleOpenPostIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('open_compose')
|
||||||
|
.title('Open Compose')
|
||||||
|
.description('Open compose post screen')
|
||||||
|
.build(),
|
||||||
|
_handleOpenComposeIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Action Intent
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('compose_post')
|
||||||
|
.title('Compose Post')
|
||||||
|
.description('Create a new post')
|
||||||
|
.build(),
|
||||||
|
_handleComposePostIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query Intents
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('search_content')
|
||||||
|
.title('Search Content')
|
||||||
|
.description('Search for content')
|
||||||
|
.parameter(
|
||||||
|
const AppIntentParameter(
|
||||||
|
name: 'query',
|
||||||
|
title: 'Search Query',
|
||||||
|
type: AppIntentParameterType.string,
|
||||||
|
isOptional: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
_handleSearchContentIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('view_notifications')
|
||||||
|
.title('View Notifications')
|
||||||
|
.description('View notifications')
|
||||||
|
.build(),
|
||||||
|
_handleViewNotificationsIntent,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _client!.registerIntent(
|
||||||
|
AppIntentBuilder()
|
||||||
|
.identifier('check_notifications')
|
||||||
|
.title('Check Notifications')
|
||||||
|
.description('Check notification count')
|
||||||
|
.build(),
|
||||||
|
_handleCheckNotificationsIntent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_client = null;
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleOpenChatIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final channelId = parameters['channelId'] as String?;
|
||||||
|
if (channelId == null) {
|
||||||
|
throw ArgumentError('channelId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.info('[AppIntents] Opening chat: $channelId');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNavigatorKey.currentContext!.push('/chat/$channelId');
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: 'Opening chat $channelId',
|
||||||
|
needsToContinueInApp: true,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to open chat', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to open chat: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleOpenPostIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final postId = parameters['postId'] as String?;
|
||||||
|
if (postId == null) {
|
||||||
|
throw ArgumentError('postId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.info('[AppIntents] Opening post: $postId');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNavigatorKey.currentContext!.push('/posts/$postId');
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: 'Opening post $postId',
|
||||||
|
needsToContinueInApp: true,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to open post', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to open post: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleOpenComposeIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
talker.info('[AppIntents] Opening compose screen');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNavigatorKey.currentContext!.push('/posts/compose');
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: 'Opening compose screen',
|
||||||
|
needsToContinueInApp: true,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to open compose', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to open compose: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleComposePostIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
talker.info('[AppIntents] Composing new post');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNavigatorKey.currentContext!.push('/posts/compose');
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: 'Opening compose screen',
|
||||||
|
needsToContinueInApp: true,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to compose post', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to compose post: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleSearchContentIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final query = parameters['query'] as String?;
|
||||||
|
if (query == null) {
|
||||||
|
throw ArgumentError('query is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
talker.info('[AppIntents] Searching for: $query');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNavigatorKey.currentContext!.push('/search?q=$query');
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: 'Searching for "$query"',
|
||||||
|
needsToContinueInApp: true,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to search', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to search: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleViewNotificationsIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
talker.info('[AppIntents] Opening notifications');
|
||||||
|
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return AppIntentResult.failed(error: 'App context not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: You may need to adjust the route based on your actual notifications route
|
||||||
|
// This is a common pattern - check your route.dart for exact path
|
||||||
|
// If you don't have a dedicated notifications route, you might need to add one
|
||||||
|
return AppIntentResult.failed(
|
||||||
|
error: 'Notifications route not implemented',
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to view notifications', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to view notifications: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppIntentResult> _handleCheckNotificationsIntent(
|
||||||
|
Map<String, dynamic> parameters,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
talker.info('[AppIntents] Checking notifications count');
|
||||||
|
|
||||||
|
if (_dio == null) {
|
||||||
|
return AppIntentResult.failed(error: 'API client not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _dio!.get('/ring/notifications/count');
|
||||||
|
final count = (response.data as num).toInt();
|
||||||
|
final countValue = count;
|
||||||
|
|
||||||
|
String message;
|
||||||
|
if (countValue == 0) {
|
||||||
|
message = 'You have no new notifications';
|
||||||
|
} else if (countValue == 1) {
|
||||||
|
message = 'You have 1 new notification';
|
||||||
|
} else {
|
||||||
|
message = 'You have $countValue new notifications';
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppIntentResult.successful(
|
||||||
|
value: message,
|
||||||
|
needsToContinueInApp: false,
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
talker.error('[AppIntents] API error checking notifications', e);
|
||||||
|
return AppIntentResult.failed(
|
||||||
|
error:
|
||||||
|
'Failed to fetch notifications: ${e.message ?? 'Network error'}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to check notifications', e, stack);
|
||||||
|
return AppIntentResult.failed(error: 'Failed to check notifications: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Donation Methods - to be called manually from your app code
|
||||||
|
|
||||||
|
Future<void> donateOpenChat(String channelId) async {
|
||||||
|
if (!_initialized) return;
|
||||||
|
try {
|
||||||
|
await FlutterAppIntentsService.donateIntentWithMetadata(
|
||||||
|
'open_chat',
|
||||||
|
{'channelId': channelId},
|
||||||
|
relevanceScore: 0.8,
|
||||||
|
context: {'feature': 'chat', 'userAction': true},
|
||||||
|
);
|
||||||
|
talker.info('[AppIntents] Donated open_chat intent');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to donate open_chat', e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> donateOpenPost(String postId) async {
|
||||||
|
if (!_initialized) return;
|
||||||
|
try {
|
||||||
|
await FlutterAppIntentsService.donateIntentWithMetadata(
|
||||||
|
'open_post',
|
||||||
|
{'postId': postId},
|
||||||
|
relevanceScore: 0.8,
|
||||||
|
context: {'feature': 'posts', 'userAction': true},
|
||||||
|
);
|
||||||
|
talker.info('[AppIntents] Donated open_post intent');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to donate open_post', e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> donateCompose() async {
|
||||||
|
if (!_initialized) return;
|
||||||
|
try {
|
||||||
|
await FlutterAppIntentsService.donateIntentWithMetadata(
|
||||||
|
'open_compose',
|
||||||
|
{},
|
||||||
|
relevanceScore: 0.9,
|
||||||
|
context: {'feature': 'compose', 'userAction': true},
|
||||||
|
);
|
||||||
|
talker.info('[AppIntents] Donated compose intent');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to donate compose', e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> donateSearch(String query) async {
|
||||||
|
if (!_initialized) return;
|
||||||
|
try {
|
||||||
|
await FlutterAppIntentsService.donateIntentWithMetadata(
|
||||||
|
'search_content',
|
||||||
|
{'query': query},
|
||||||
|
relevanceScore: 0.7,
|
||||||
|
context: {'feature': 'search', 'userAction': true},
|
||||||
|
);
|
||||||
|
talker.info('[AppIntents] Donated search intent');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error('[AppIntents] Failed to donate search', e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> donateCheckNotifications() async {
|
||||||
|
if (!_initialized) return;
|
||||||
|
try {
|
||||||
|
await FlutterAppIntentsService.donateIntentWithMetadata(
|
||||||
|
'check_notifications',
|
||||||
|
{},
|
||||||
|
relevanceScore: 0.6,
|
||||||
|
context: {'feature': 'notifications', 'userAction': true},
|
||||||
|
);
|
||||||
|
talker.info('[AppIntents] Donated check_notifications intent');
|
||||||
|
} catch (e, stack) {
|
||||||
|
talker.error(
|
||||||
|
'[AppIntents] Failed to donate check_notifications',
|
||||||
|
e,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -774,6 +774,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.2"
|
version: "4.5.2"
|
||||||
|
flutter_app_intents:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_app_intents
|
||||||
|
sha256: bec5a1ec991b93d475435205dbdca6efdd8979749f2a3c73ebb9f8334b9d3fa2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
flutter_app_update:
|
flutter_app_update:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ dependencies:
|
|||||||
shake: ^3.0.0
|
shake: ^3.0.0
|
||||||
in_app_review: ^2.0.11
|
in_app_review: ^2.0.11
|
||||||
snow_fall_animation: ^0.0.1+3
|
snow_fall_animation: ^0.0.1+3
|
||||||
|
flutter_app_intents: ^0.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user