💄 Optimized CMP
✨ CMP now available to search the web
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user