6232c7c99a
Plans 1-3 implemented: Plan 1 — Foundation - Add google_fonts ^6.2.1 - lib/core/theme.dart: slate-indigo ColorScheme (dark/light), Fraunces headings, GradientButton widget - lib/providers/capture_work_queue_provider.dart: in-memory sequential work queue; CaptureWorkQueueNotifier drains one item at a time; captureResultProvider feeds snackbars to UI - lib/app.dart: wire fabledDarkTheme/fabledLightTheme; replace blocking _QuickCaptureBar with queue-based implementation (progress bar, badge) Plan 2 — Navigation - 3-tab shell: Briefing · Library · Chat (was Notes · Tasks · Projects · Chat) - lib/screens/library/library_screen.dart: unified notes+tasks+projects list with filter pills, status sub-filter for tasks, live search, FAB - lib/widgets/library_item_card.dart: NoteLibraryCard, TaskLibraryCard (status cycle), ProjectLibraryCard - lib/screens/chat/conversations_tab_screen.dart: focused replacement for ConversationsListScreen - Delete 5 dead screens: notes_list, tasks_list, project_list, conversations_list, quick_capture Plan 3 — Briefing - lib/data/models/briefing_conversation.dart - lib/data/api/briefing_api.dart: getToday, getHistory, getMessages, triggerSlot - lib/providers/briefing_provider.dart: BriefingNotifier with sendReply (optimistic + SSE + poll, same pattern as MessagesNotifier) and refresh - lib/widgets/chat_message_bubble.dart: extracted + redesigned shared bubble (ghost user bubbles, left-accent assistant bubbles) - lib/widgets/briefing_digest_card.dart: expandable first-message card - lib/screens/briefing/briefing_screen.dart: digest card, conversation list, streaming reply bar, refresh button, history overflow menu - lib/screens/briefing/briefing_history_screen.dart: read-only past dates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
295 lines
10 KiB
Dart
295 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../core/constants.dart';
|
|
import '../../data/models/note.dart';
|
|
import '../../data/models/task.dart';
|
|
import '../../providers/notes_provider.dart';
|
|
import '../../providers/projects_provider.dart';
|
|
import '../../providers/tasks_provider.dart';
|
|
import '../../widgets/library_item_card.dart';
|
|
|
|
enum _LibraryFilter { all, notes, tasks, projects }
|
|
|
|
enum _TaskStatusFilter { all, todo, inProgress, done }
|
|
|
|
class LibraryScreen extends ConsumerStatefulWidget {
|
|
const LibraryScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<LibraryScreen> createState() => _LibraryScreenState();
|
|
}
|
|
|
|
class _LibraryScreenState extends ConsumerState<LibraryScreen> {
|
|
_LibraryFilter _filter = _LibraryFilter.all;
|
|
_TaskStatusFilter _taskStatus = _TaskStatusFilter.all;
|
|
bool _searchActive = false;
|
|
String _searchQuery = '';
|
|
final _searchController = TextEditingController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
bool _matchesSearch(String text) {
|
|
if (_searchQuery.isEmpty) return true;
|
|
return text.toLowerCase().contains(_searchQuery.toLowerCase());
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final notesAsync = ref.watch(notesProvider);
|
|
final tasksAsync = ref.watch(tasksProvider);
|
|
final projectsAsync = ref.watch(projectsProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: _searchActive
|
|
? TextField(
|
|
controller: _searchController,
|
|
autofocus: true,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Search…',
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
),
|
|
onChanged: (q) => setState(() => _searchQuery = q),
|
|
)
|
|
: Text('Library', style: theme.textTheme.titleLarge),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(_searchActive ? Icons.close : Icons.search),
|
|
onPressed: () => setState(() {
|
|
_searchActive = !_searchActive;
|
|
if (!_searchActive) {
|
|
_searchQuery = '';
|
|
_searchController.clear();
|
|
}
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// ── Filter pills ──────────────────────────────────────────────────
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
|
child: Row(
|
|
children: _LibraryFilter.values.map((f) {
|
|
final label = switch (f) {
|
|
_LibraryFilter.all => 'All',
|
|
_LibraryFilter.notes => 'Notes',
|
|
_LibraryFilter.tasks => 'Tasks',
|
|
_LibraryFilter.projects => 'Projects',
|
|
};
|
|
final selected = _filter == f;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: FilterChip(
|
|
label: Text(label),
|
|
selected: selected,
|
|
onSelected: (_) => setState(() {
|
|
_filter = f;
|
|
_taskStatus = _TaskStatusFilter.all;
|
|
}),
|
|
selectedColor:
|
|
theme.colorScheme.primary.withValues(alpha: 0.18),
|
|
checkmarkColor: theme.colorScheme.primary,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
|
|
// ── Task status sub-filter (Tasks pill only) ───────────────────
|
|
if (_filter == _LibraryFilter.tasks)
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 4),
|
|
child: Row(
|
|
children: _TaskStatusFilter.values.map((s) {
|
|
final label = switch (s) {
|
|
_TaskStatusFilter.all => 'All',
|
|
_TaskStatusFilter.todo => 'To Do',
|
|
_TaskStatusFilter.inProgress => 'In Progress',
|
|
_TaskStatusFilter.done => 'Done',
|
|
};
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: ChoiceChip(
|
|
label: Text(label),
|
|
selected: _taskStatus == s,
|
|
onSelected: (_) => setState(() => _taskStatus = s),
|
|
selectedColor:
|
|
theme.colorScheme.secondary.withValues(alpha: 0.15),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
|
|
const Divider(height: 1),
|
|
|
|
// ── Content ───────────────────────────────────────────────────────
|
|
Expanded(
|
|
child: switch (_filter) {
|
|
_LibraryFilter.notes => _buildNotesList(notesAsync),
|
|
_LibraryFilter.tasks => _buildTasksList(tasksAsync),
|
|
_LibraryFilter.projects => _buildProjectsList(projectsAsync),
|
|
_LibraryFilter.all => _buildAllList(notesAsync, tasksAsync),
|
|
},
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () => _showCreateSheet(context),
|
|
child: const Icon(Icons.add),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNotesList(AsyncValue<List<Note>> notesAsync) {
|
|
return notesAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
data: (notes) {
|
|
final filtered = notes
|
|
.where((n) => _matchesSearch(n.title) || _matchesSearch(n.body))
|
|
.toList();
|
|
if (filtered.isEmpty) {
|
|
return const Center(child: Text('No notes found'));
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async => ref.invalidate(notesProvider),
|
|
child: ListView.builder(
|
|
itemCount: filtered.length,
|
|
itemBuilder: (_, i) => NoteLibraryCard(note: filtered[i]),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildTasksList(AsyncValue<List<Task>> tasksAsync) {
|
|
return tasksAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
data: (tasks) {
|
|
var filtered = tasks.where((t) => _matchesSearch(t.title)).toList();
|
|
if (_taskStatus != _TaskStatusFilter.all) {
|
|
final status = switch (_taskStatus) {
|
|
_TaskStatusFilter.todo => TaskStatus.todo,
|
|
_TaskStatusFilter.inProgress => TaskStatus.inProgress,
|
|
_TaskStatusFilter.done => TaskStatus.done,
|
|
_ => TaskStatus.todo,
|
|
};
|
|
filtered = filtered.where((t) => t.status == status).toList();
|
|
}
|
|
if (filtered.isEmpty) {
|
|
return const Center(child: Text('No tasks found'));
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async => ref.invalidate(tasksProvider),
|
|
child: ListView.builder(
|
|
itemCount: filtered.length,
|
|
itemBuilder: (_, i) => TaskLibraryCard(task: filtered[i]),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildProjectsList(AsyncValue<List<dynamic>> projectsAsync) {
|
|
return projectsAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
data: (projects) {
|
|
if (projects.isEmpty) {
|
|
return const Center(child: Text('No projects'));
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async => ref.invalidate(projectsProvider),
|
|
child: ListView.builder(
|
|
itemCount: projects.length,
|
|
itemBuilder: (_, i) =>
|
|
ProjectLibraryCard(project: projects[i]),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAllList(
|
|
AsyncValue<List<Note>> notesAsync,
|
|
AsyncValue<List<Task>> tasksAsync,
|
|
) {
|
|
final notes = notesAsync.valueOrNull ?? [];
|
|
final tasks = tasksAsync.valueOrNull ?? [];
|
|
|
|
// Merge and sort by updatedAt desc
|
|
final items = <(DateTime, Widget)>[];
|
|
for (final n in notes) {
|
|
if (_matchesSearch(n.title) || _matchesSearch(n.body)) {
|
|
items.add((n.updatedAt, NoteLibraryCard(note: n)));
|
|
}
|
|
}
|
|
for (final t in tasks) {
|
|
if (_matchesSearch(t.title)) {
|
|
items.add((t.updatedAt, TaskLibraryCard(task: t)));
|
|
}
|
|
}
|
|
items.sort((a, b) => b.$1.compareTo(a.$1));
|
|
|
|
if (notesAsync.isLoading || tasksAsync.isLoading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (items.isEmpty) {
|
|
return const Center(child: Text('Nothing here yet'));
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
ref.invalidate(notesProvider);
|
|
ref.invalidate(tasksProvider);
|
|
},
|
|
child: ListView.builder(
|
|
itemCount: items.length,
|
|
itemBuilder: (_, i) => items[i].$2,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCreateSheet(BuildContext context) {
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
builder: (_) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.article_outlined),
|
|
title: const Text('New note'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
context.push(Routes.noteNew);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.check_box_outlined),
|
|
title: const Text('New task'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
context.push(Routes.taskNew);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|