feat(knowledge): wire 4-tab shell, retire LibraryScreen, complete Knowledge View feature
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
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.value ?? [];
|
||||
final tasks = tasksAsync.value ?? [];
|
||||
|
||||
// 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user