💄 Optimized CMP

 CMP now available to search the web
This commit is contained in:
2025-12-21 16:17:41 +08:00
parent 30b2c0a0b4
commit 87a54625aa

View File

@@ -19,6 +19,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/event_bus.dart'; import 'package:island/services/event_bus.dart';
import 'package:url_launcher/url_launcher.dart';
class CommandPattleWidget extends HookConsumerWidget { class CommandPattleWidget extends HookConsumerWidget {
final VoidCallback onDismiss; final VoidCallback onDismiss;
@@ -143,23 +144,22 @@ class CommandPattleWidget extends HookConsumerWidget {
.take(5) // Limit to 5 results .take(5) // Limit to 5 results
.toList(); .toList();
// Combine results: chats first, then special actions, then routes final filteredFallbacks =
searchQuery.value.isNotEmpty &&
filteredChats.isEmpty &&
filteredSpecialActions.isEmpty &&
filteredRoutes.isEmpty
? _getFallbackActions(searchQuery.value)
: <FallbackAction>[];
// Combine results: fallbacks first, then chats, special actions, routes
final allResults = [ final allResults = [
...filteredFallbacks,
...filteredChats, ...filteredChats,
...filteredSpecialActions, ...filteredSpecialActions,
...filteredRoutes, ...filteredRoutes,
]; ];
// Update focused index when results change
useEffect(() {
if (allResults.isNotEmpty && focusedIndex.value == null) {
focusedIndex.value = 0;
} else if (allResults.isEmpty) {
focusedIndex.value = null;
}
return null;
}, [allResults]);
// Scroll to focused item // Scroll to focused item
useEffect(() { useEffect(() {
if (focusedIndex.value != null && allResults.isNotEmpty) { if (focusedIndex.value != null && allResults.isNotEmpty) {
@@ -209,6 +209,9 @@ class CommandPattleWidget extends HookConsumerWidget {
item.action(); item.action();
} else if (item is RouteItem) { } else if (item is RouteItem) {
_navigateToRoute(context, ref, item); _navigateToRoute(context, ref, item);
} else if (item is FallbackAction) {
onDismiss();
item.action();
} }
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (allResults.isNotEmpty) { if (allResults.isNotEmpty) {
@@ -239,7 +242,12 @@ class CommandPattleWidget extends HookConsumerWidget {
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withOpacity(0.5),
child: Center( child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height * 0.2,
),
child: AnimatedBuilder( child: AnimatedBuilder(
animation: animationController, animation: animationController,
builder: (context, child) => Opacity( builder: (context, child) => Opacity(
@@ -331,6 +339,16 @@ class CommandPattleWidget extends HookConsumerWidget {
item, item,
), ),
); );
} else if (item is FallbackAction) {
return _FallbackSearchResult(
action: item,
isFocused:
index == focusedIndex.value,
onTap: () {
onDismiss();
item.action();
},
);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
@@ -348,6 +366,7 @@ class CommandPattleWidget extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
@@ -369,6 +388,62 @@ class CommandPattleWidget extends HookConsumerWidget {
onDismiss(); onDismiss();
ref.read(routerProvider).go(route.path); ref.read(routerProvider).go(route.path);
} }
static List<FallbackAction> _getFallbackActions(String query) {
final List<FallbackAction> actions = [];
// Check if query is a URL
final Uri? uri = Uri.tryParse(query);
if (uri != null && (uri.scheme == 'http' || uri.scheme == 'https')) {
actions.add(
FallbackAction(
name: 'Open URL',
description: 'Open $query in browser',
icon: Symbols.open_in_new,
action: () async {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
),
);
} else {
// Search the web
actions.add(
FallbackAction(
name: 'Search the web',
description: 'Search "$query" on Google',
icon: Symbols.search,
action: () async {
final searchUri = Uri.https('www.google.com', '/search', {
'q': query,
});
if (await canLaunchUrl(searchUri)) {
await launchUrl(searchUri, mode: LaunchMode.externalApplication);
}
},
),
);
}
return actions;
}
}
class FallbackAction {
final String name;
final String description;
final IconData icon;
final VoidCallback action;
final List<String> searchableAliases;
const FallbackAction({
required this.name,
required this.description,
required this.icon,
required this.action,
this.searchableAliases = const [],
});
} }
class _RouteSearchResult extends StatelessWidget { class _RouteSearchResult extends StatelessWidget {
@@ -439,6 +514,40 @@ class _SpecialActionSearchResult extends StatelessWidget {
} }
} }
class _FallbackSearchResult extends StatelessWidget {
final FallbackAction action;
final bool isFocused;
final VoidCallback onTap;
const _FallbackSearchResult({
required this.action,
required this.isFocused,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isFocused
? Theme.of(context).colorScheme.surfaceContainerHighest
: null,
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
child: Icon(action.icon),
),
title: Text(action.name),
subtitle: Text(action.description),
onTap: onTap,
),
);
}
}
class _ChatRoomSearchResult extends HookConsumerWidget { class _ChatRoomSearchResult extends HookConsumerWidget {
final SnChatRoom room; final SnChatRoom room;
final bool isFocused; final bool isFocused;