feat(ios): Add App Intents support for Conduit interactions
This commit is contained in:
@@ -64,6 +64,8 @@ PODS:
|
||||
- onnxruntime-c (= 1.22.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- quick_actions_ios (0.0.1):
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.1):
|
||||
@@ -113,6 +115,7 @@ DEPENDENCIES:
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||
@@ -162,6 +165,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/objective_c/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
quick_actions_ios:
|
||||
:path: ".symlinks/plugins/quick_actions_ios/ios"
|
||||
record_ios:
|
||||
:path: ".symlinks/plugins/record_ios/ios"
|
||||
share_handler_ios:
|
||||
@@ -204,6 +209,7 @@ SPEC CHECKSUMS:
|
||||
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
||||
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../providers/app_providers.dart';
|
||||
import '../../features/auth/providers/unified_auth_providers.dart';
|
||||
import '../services/navigation_service.dart';
|
||||
import '../services/app_intents_service.dart';
|
||||
import '../services/quick_actions_service.dart';
|
||||
import '../models/conversation.dart';
|
||||
import '../services/background_streaming_handler.dart';
|
||||
import '../services/persistent_streaming_service.dart';
|
||||
@@ -171,6 +172,7 @@ class AppStartupFlow extends _$AppStartupFlow {
|
||||
keepAlive(apiTokenUpdaterProvider);
|
||||
keepAlive(silentLoginCoordinatorProvider);
|
||||
keepAlive(appIntentCoordinatorProvider);
|
||||
keepAlive(quickActionsCoordinatorProvider);
|
||||
|
||||
// Kick background model loading flow (non-blocking)
|
||||
Future<void>.delayed(const Duration(milliseconds: 120), () {
|
||||
|
||||
@@ -362,6 +362,20 @@ class AppIntentCoordinator extends _$AppIntentCoordinator {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openChatFromExternal({
|
||||
String? prompt,
|
||||
bool focusComposer = false,
|
||||
bool resetChat = false,
|
||||
}) {
|
||||
return _prepareChatWithOptions(
|
||||
prompt: prompt,
|
||||
focusComposer: focusComposer,
|
||||
resetChat: resetChat,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> startVoiceCallFromExternal() => _startVoiceCall();
|
||||
|
||||
Future<void> _prepareChatWithOptions({
|
||||
String? prompt,
|
||||
bool focusComposer = false,
|
||||
@@ -412,7 +426,7 @@ class AppIntentCoordinator extends _$AppIntentCoordinator {
|
||||
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const VoiceCallPage(),
|
||||
builder: (_) => const VoiceCallPage(startNewConversation: true),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
108
lib/core/services/quick_actions_service.dart
Normal file
108
lib/core/services/quick_actions_service.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../providers/app_providers.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
import 'app_intents_service.dart';
|
||||
import 'navigation_service.dart';
|
||||
|
||||
part 'quick_actions_service.g.dart';
|
||||
|
||||
const _quickActionNewChat = 'conduit_new_chat';
|
||||
const _quickActionVoiceCall = 'conduit_voice_call';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class QuickActionsCoordinator extends _$QuickActionsCoordinator {
|
||||
final QuickActions _quickActions = const QuickActions();
|
||||
|
||||
@override
|
||||
FutureOr<void> build() {
|
||||
if (kIsWeb) return Future<void>.value();
|
||||
if (!Platform.isIOS && !Platform.isAndroid) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
_quickActions.initialize(_handleAction);
|
||||
unawaited(_setShortcuts());
|
||||
|
||||
ref.listen<Locale?>(appLocaleProvider, (prev, next) {
|
||||
unawaited(_setShortcuts());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _setShortcuts() async {
|
||||
final titles = _resolveTitles();
|
||||
try {
|
||||
await _quickActions.setShortcutItems([
|
||||
ShortcutItem(type: _quickActionNewChat, localizedTitle: titles.newChat),
|
||||
ShortcutItem(
|
||||
type: _quickActionVoiceCall,
|
||||
localizedTitle: titles.voiceCall,
|
||||
),
|
||||
]);
|
||||
} catch (error, stackTrace) {
|
||||
DebugLogger.error(
|
||||
'quick-actions-register',
|
||||
scope: 'platform',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_QuickActionTitles _resolveTitles() {
|
||||
final context = NavigationService.context;
|
||||
final l10n = context != null ? AppLocalizations.of(context) : null;
|
||||
return _QuickActionTitles(
|
||||
newChat: l10n?.newChat ?? 'New Chat',
|
||||
voiceCall: l10n?.voiceCallTitle ?? 'Voice Call',
|
||||
);
|
||||
}
|
||||
|
||||
void _handleAction(String type) {
|
||||
unawaited(_handleActionAsync(type));
|
||||
}
|
||||
|
||||
Future<void> _handleActionAsync(String? type) async {
|
||||
if (type == null || type.isEmpty) return;
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 16));
|
||||
|
||||
switch (type) {
|
||||
case _quickActionNewChat:
|
||||
await ref
|
||||
.read(appIntentCoordinatorProvider.notifier)
|
||||
.openChatFromExternal(focusComposer: true, resetChat: true);
|
||||
break;
|
||||
case _quickActionVoiceCall:
|
||||
try {
|
||||
await ref
|
||||
.read(appIntentCoordinatorProvider.notifier)
|
||||
.startVoiceCallFromExternal();
|
||||
} catch (error, stackTrace) {
|
||||
DebugLogger.error(
|
||||
'quick-actions-voice',
|
||||
scope: 'platform',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DebugLogger.info('Unknown quick action: $type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickActionTitles {
|
||||
const _QuickActionTitles({required this.newChat, required this.voiceCall});
|
||||
|
||||
final String newChat;
|
||||
final String voiceCall;
|
||||
}
|
||||
@@ -156,10 +156,10 @@ class AndroidAssistantHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to voice call page
|
||||
// Navigate to voice call page with new conversation flag
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const VoiceCallPage(),
|
||||
builder: (context) => const VoiceCallPage(startNewConversation: true),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,10 +8,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
import '../../../core/utils/markdown_to_text.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../providers/chat_providers.dart';
|
||||
import '../services/voice_call_service.dart';
|
||||
|
||||
class VoiceCallPage extends ConsumerStatefulWidget {
|
||||
const VoiceCallPage({super.key});
|
||||
const VoiceCallPage({super.key, this.startNewConversation = false});
|
||||
|
||||
final bool startNewConversation;
|
||||
|
||||
@override
|
||||
ConsumerState<VoiceCallPage> createState() => _VoiceCallPageState();
|
||||
@@ -54,6 +57,11 @@ class _VoiceCallPageState extends ConsumerState<VoiceCallPage>
|
||||
|
||||
Future<void> _initializeCall() async {
|
||||
try {
|
||||
// Start a new conversation if requested
|
||||
if (widget.startNewConversation) {
|
||||
startNewChat(ref);
|
||||
}
|
||||
|
||||
_service = ref.read(voiceCallServiceProvider);
|
||||
|
||||
// Subscribe to service streams
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@@ -1181,6 +1181,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
quick_actions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: quick_actions
|
||||
sha256: "7e35dd6a21f5bbd21acf6899039eaf85001a5ac26d52cbd6a8a2814505b90798"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
quick_actions_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_android
|
||||
sha256: "23f04632ada7fc16665d84ba54a0c792c09727e7fda6c989c6e6ba1853aa15dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.27"
|
||||
quick_actions_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_ios
|
||||
sha256: a2e08ceb01f9d26e1b1826b1c4f5da6b7b6bbf61bcbaacd8e93dfff58b91f996
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
quick_actions_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_platform_interface
|
||||
sha256: "1fec7068db5122cd019e9340d3d7be5d36eab099695ef3402c7059ee058329a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
record:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -75,6 +75,7 @@ dependencies:
|
||||
http: ^1.5.0
|
||||
flutter_callkit_incoming: ^3.0.0
|
||||
flutter_app_intents: ^0.7.0
|
||||
quick_actions: 1.1.0
|
||||
|
||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user