Add search, offline queue, app icon, and UI polish
- Collapsible search in notes and tasks: magnifying glass in AppBar expands to a text field inline; close button resets the filter - Offline capture queue: failed quick-captures (NetworkException) are persisted to SharedPreferences and retried automatically on next successful submit or app start; badge shows pending count - App icon: book-with-sparkle logo from FabledAssistant SVG rendered at all Android densities with adaptive icon (indigo #6366f1 bg) - Dark mode subtitle fix: use colorScheme.onSurfaceVariant for note preview and conversation timestamp text - Remove swipe-to-delete (accidental deletions); long-press remains - Chat: SSE streaming reliability, polling fallback, title patching - Settings: theme toggle (system/light/dark) persisted to prefs - Notes: delete button in edit screen; body preview in list - Tasks: fix delete dialog context; description search support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,12 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.message)));
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to send message.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +69,26 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
_scrollToBottom();
|
||||
});
|
||||
|
||||
final convTitle = ref
|
||||
.watch(conversationsProvider)
|
||||
.valueOrNull
|
||||
?.where((c) => c.id == widget.conversationId)
|
||||
.firstOrNull
|
||||
?.title;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Chat')),
|
||||
appBar: AppBar(
|
||||
title: Text(convTitle?.isNotEmpty == true ? convTitle! : 'Chat'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: messagesAsync.when(
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error: $e')),
|
||||
error: (_, _) => const Center(
|
||||
child: Text('Could not load messages.'),
|
||||
),
|
||||
data: (messages) {
|
||||
if (messages.isEmpty) {
|
||||
return const Center(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/constants.dart';
|
||||
import '../../core/exceptions.dart';
|
||||
import '../../providers/chat_provider.dart';
|
||||
|
||||
class ConversationsListScreen extends ConsumerWidget {
|
||||
@@ -13,14 +14,16 @@ class ConversationsListScreen extends ConsumerWidget {
|
||||
final convsAsync = ref.watch(conversationsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Chat')),
|
||||
body: convsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
error: (_, _) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Error: $e'),
|
||||
const Icon(Icons.cloud_off, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Could not load conversations.'),
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => ref.invalidate(conversationsProvider),
|
||||
child: const Text('Retry'),
|
||||
@@ -42,10 +45,14 @@ class ConversationsListScreen extends ConsumerWidget {
|
||||
final conv = convs[i];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.chat_bubble_outline),
|
||||
title: Text(conv.title),
|
||||
title: Text(
|
||||
conv.title.isNotEmpty ? conv.title : 'New conversation',
|
||||
),
|
||||
subtitle: Text(
|
||||
conv.updatedAt.toLocal().toString().substring(0, 16),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
onTap: () => context.push(
|
||||
Routes.chat.replaceFirst(':id', '${conv.id}'),
|
||||
@@ -53,16 +60,21 @@ class ConversationsListScreen extends ConsumerWidget {
|
||||
onLongPress: () async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Delete conversation?'),
|
||||
content: Text(conv.title),
|
||||
content: Text(
|
||||
conv.title.isNotEmpty
|
||||
? conv.title
|
||||
: 'New conversation',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
onPressed: () =>
|
||||
Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
@@ -89,34 +101,16 @@ class ConversationsListScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _newConversation(BuildContext context, WidgetRef ref) async {
|
||||
final controller = TextEditingController();
|
||||
final title = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('New Conversation'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(hintText: 'Title'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
||||
child: const Text('Create'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (title != null && title.isNotEmpty) {
|
||||
final conv =
|
||||
await ref.read(conversationsProvider.notifier).create(title);
|
||||
try {
|
||||
final conv = await ref.read(conversationsProvider.notifier).create('');
|
||||
if (context.mounted) {
|
||||
context.push(Routes.chat.replaceFirst(':id', '${conv.id}'));
|
||||
}
|
||||
} on AppException catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.message)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user