feat(ios): Add App Intents support for Conduit interactions
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user