Merge pull request #175 from cogwheel0/ios-shortcuts-support
ios-shortcuts-support
This commit is contained in:
@@ -64,6 +64,8 @@ PODS:
|
|||||||
- onnxruntime-c (= 1.22.0)
|
- onnxruntime-c (= 1.22.0)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- quick_actions_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.21.1):
|
- SDWebImage (5.21.1):
|
||||||
@@ -113,6 +115,7 @@ DEPENDENCIES:
|
|||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/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`)
|
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||||
@@ -162,6 +165,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/objective_c/ios"
|
:path: ".symlinks/plugins/objective_c/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
|
quick_actions_ios:
|
||||||
|
:path: ".symlinks/plugins/quick_actions_ios/ios"
|
||||||
record_ios:
|
record_ios:
|
||||||
:path: ".symlinks/plugins/record_ios/ios"
|
:path: ".symlinks/plugins/record_ios/ios"
|
||||||
share_handler_ios:
|
share_handler_ios:
|
||||||
@@ -204,6 +209,7 @@ SPEC CHECKSUMS:
|
|||||||
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
||||||
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
|
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../providers/app_providers.dart';
|
|||||||
import '../../features/auth/providers/unified_auth_providers.dart';
|
import '../../features/auth/providers/unified_auth_providers.dart';
|
||||||
import '../services/navigation_service.dart';
|
import '../services/navigation_service.dart';
|
||||||
import '../services/app_intents_service.dart';
|
import '../services/app_intents_service.dart';
|
||||||
|
import '../services/quick_actions_service.dart';
|
||||||
import '../models/conversation.dart';
|
import '../models/conversation.dart';
|
||||||
import '../services/background_streaming_handler.dart';
|
import '../services/background_streaming_handler.dart';
|
||||||
import '../services/persistent_streaming_service.dart';
|
import '../services/persistent_streaming_service.dart';
|
||||||
@@ -171,6 +172,7 @@ class AppStartupFlow extends _$AppStartupFlow {
|
|||||||
keepAlive(apiTokenUpdaterProvider);
|
keepAlive(apiTokenUpdaterProvider);
|
||||||
keepAlive(silentLoginCoordinatorProvider);
|
keepAlive(silentLoginCoordinatorProvider);
|
||||||
keepAlive(appIntentCoordinatorProvider);
|
keepAlive(appIntentCoordinatorProvider);
|
||||||
|
keepAlive(quickActionsCoordinatorProvider);
|
||||||
|
|
||||||
// Kick background model loading flow (non-blocking)
|
// Kick background model loading flow (non-blocking)
|
||||||
Future<void>.delayed(const Duration(milliseconds: 120), () {
|
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({
|
Future<void> _prepareChatWithOptions({
|
||||||
String? prompt,
|
String? prompt,
|
||||||
bool focusComposer = false,
|
bool focusComposer = false,
|
||||||
@@ -412,7 +426,7 @@ class AppIntentCoordinator extends _$AppIntentCoordinator {
|
|||||||
|
|
||||||
await navigator.push(
|
await navigator.push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => const VoiceCallPage(),
|
builder: (_) => const VoiceCallPage(startNewConversation: true),
|
||||||
fullscreenDialog: 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to voice call page
|
// Navigate to voice call page with new conversation flag
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const VoiceCallPage(),
|
builder: (context) => const VoiceCallPage(startNewConversation: true),
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/utils/markdown_to_text.dart';
|
import '../../../core/utils/markdown_to_text.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
import '../providers/chat_providers.dart';
|
||||||
import '../services/voice_call_service.dart';
|
import '../services/voice_call_service.dart';
|
||||||
|
|
||||||
class VoiceCallPage extends ConsumerStatefulWidget {
|
class VoiceCallPage extends ConsumerStatefulWidget {
|
||||||
const VoiceCallPage({super.key});
|
const VoiceCallPage({super.key, this.startNewConversation = false});
|
||||||
|
|
||||||
|
final bool startNewConversation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<VoiceCallPage> createState() => _VoiceCallPageState();
|
ConsumerState<VoiceCallPage> createState() => _VoiceCallPageState();
|
||||||
@@ -54,6 +57,11 @@ class _VoiceCallPageState extends ConsumerState<VoiceCallPage>
|
|||||||
|
|
||||||
Future<void> _initializeCall() async {
|
Future<void> _initializeCall() async {
|
||||||
try {
|
try {
|
||||||
|
// Start a new conversation if requested
|
||||||
|
if (widget.startNewConversation) {
|
||||||
|
startNewChat(ref);
|
||||||
|
}
|
||||||
|
|
||||||
_service = ref.read(voiceCallServiceProvider);
|
_service = ref.read(voiceCallServiceProvider);
|
||||||
|
|
||||||
// Subscribe to service streams
|
// Subscribe to service streams
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@@ -1181,6 +1181,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
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:
|
record:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ dependencies:
|
|||||||
http: ^1.5.0
|
http: ^1.5.0
|
||||||
flutter_callkit_incoming: ^3.0.0
|
flutter_callkit_incoming: ^3.0.0
|
||||||
flutter_app_intents: ^0.7.0
|
flutter_app_intents: ^0.7.0
|
||||||
|
quick_actions: 1.1.0
|
||||||
|
|
||||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user