From 0fc5066f97028faee7841d570e13a32133c6002b Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 07:03:07 -0700 Subject: [PATCH] =?UTF-8?q?feat(flutter):=20add=20screens=20=E2=80=94=20se?= =?UTF-8?q?tup,=20tasks,=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/screens/settings_screen.dart | 154 ++++ .../flutter/lib/src/screens/setup_screen.dart | 155 ++++ .../flutter/lib/src/screens/tasks_screen.dart | 683 ++++++++++++++++++ 3 files changed, 992 insertions(+) create mode 100644 apps/flutter/lib/src/screens/settings_screen.dart create mode 100644 apps/flutter/lib/src/screens/setup_screen.dart create mode 100644 apps/flutter/lib/src/screens/tasks_screen.dart diff --git a/apps/flutter/lib/src/screens/settings_screen.dart b/apps/flutter/lib/src/screens/settings_screen.dart new file mode 100644 index 0000000..20572c5 --- /dev/null +++ b/apps/flutter/lib/src/screens/settings_screen.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../state/app_state.dart'; +import '../theme.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final isDark = Theme.of(context).brightness == Brightness.dark; + return GestureDetector( + onTap: () => state.setScreen('tasks'), + child: Container( + color: Colors.black.withValues(alpha: 0.5), + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.04, + vertical: MediaQuery.of(context).size.height * 0.04, + ), + child: GestureDetector( + onTap: () {}, + child: AnimatedScale( + scale: 1.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + child: Container( + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.7), blurRadius: 60, offset: const Offset(0, 25)), + BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10)), + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + // Header (matching Tauri: text-lg font-bold, border-b, px-4 py-3) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)), + ), + child: Row( + children: [ + const Text('Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), + const Spacer(), + GestureDetector( + onTap: () => state.setScreen('tasks'), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.close, size: 20, + color: isDark ? AppTheme.textDark : AppTheme.textLight), + ), + ), + ], + ), + ), + // Scrollable content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // WebDAV Sync section (matching Tauri order: sync first) + Text('WEBDAV SYNC', + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5), + )), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + child: Text( + 'WebDAV sync not yet available in Flutter build', + style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight), + ), + ), + const SizedBox(height: 24), + // Appearance section + Text('APPEARANCE', + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5), + )), + const SizedBox(height: 12), + // Dark mode toggle in bordered card (matching Tauri) + GestureDetector( + onTap: () => state.toggleDarkMode(), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + child: Row( + children: [ + const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const Spacer(), + // Toggle switch (matching Tauri: h-6 w-11) + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 44, + height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 20, + height: 20, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 32), + Center( + child: Text('Flutter + Rust', style: TextStyle(fontSize: 12, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3))), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/apps/flutter/lib/src/screens/setup_screen.dart b/apps/flutter/lib/src/screens/setup_screen.dart new file mode 100644 index 0000000..9da106b --- /dev/null +++ b/apps/flutter/lib/src/screens/setup_screen.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:provider/provider.dart'; +import '../state/app_state.dart'; +import '../theme.dart'; + +class SetupScreen extends StatefulWidget { + const SetupScreen({super.key}); + + @override + State createState() => _SetupScreenState(); +} + +class _SetupScreenState extends State { + final _nameController = TextEditingController(); + String? _selectedPath; + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _pickFolder() async { + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) setState(() => _selectedPath = result); + } + + Future _create() async { + final name = _nameController.text.trim(); + if (name.isEmpty || _selectedPath == null) return; + await context.read().addWorkspace(name, _selectedPath!); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Container( + constraints: const BoxConstraints(maxWidth: 384), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: isDark ? AppTheme.cardDark : AppTheme.cardLight, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.15), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Bevy Tasks', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, + color: isDark ? AppTheme.textDark : AppTheme.textLight)), + const SizedBox(height: 4), + Text('Create or open a workspace to get started.', + style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)), + const SizedBox(height: 24), + // Workspace name label + input + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text('Workspace name', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? AppTheme.textDark : AppTheme.textLight)), + ), + TextField( + controller: _nameController, + style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight), + decoration: InputDecoration( + hintText: 'My Tasks', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppTheme.primary), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + filled: false, + ), + ), + const SizedBox(height: 16), + // Folder label + picker row + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text('Folder', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? AppTheme.textDark : AppTheme.textLight)), + ), + Row( + children: [ + Expanded( + child: TextField( + readOnly: true, + style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight), + controller: TextEditingController(text: _selectedPath ?? ''), + decoration: InputDecoration( + hintText: 'Select a folder\u2026', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + filled: false, + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _pickFolder, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('Browse', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: (_nameController.text.trim().isNotEmpty && _selectedPath != null) ? _create : null, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primary, + foregroundColor: Colors.white, + disabledBackgroundColor: AppTheme.primary.withValues(alpha: 0.4), + disabledForegroundColor: Colors.white.withValues(alpha: 0.6), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + child: const Text('Create Workspace', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/flutter/lib/src/screens/tasks_screen.dart b/apps/flutter/lib/src/screens/tasks_screen.dart new file mode 100644 index 0000000..56dbe43 --- /dev/null +++ b/apps/flutter/lib/src/screens/tasks_screen.dart @@ -0,0 +1,683 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../rust/api.dart' as api; +import '../state/app_state.dart'; +import '../theme.dart'; +import '../widgets/custom_title_bar.dart'; +import '../widgets/task_item.dart'; +import '../widgets/task_detail_view.dart'; +import '../widgets/new_task_input.dart'; + +class TasksScreen extends StatefulWidget { + const TasksScreen({super.key}); + + @override + State createState() => _TasksScreenState(); +} + +class _TasksScreenState extends State { + bool _drawerOpen = false; + bool _showCompleted = false; + bool _completedVisible = false; + bool _addingList = false; + bool _workspaceSwitcherOpen = false; + bool _newTaskOpen = false; + final _newListController = TextEditingController(); + final _newListFocus = FocusNode(); + + @override + void dispose() { + _newListController.dispose(); + _newListFocus.dispose(); + super.dispose(); + } + + void _toggleDrawer() => setState(() { + _drawerOpen = !_drawerOpen; + if (!_drawerOpen) _workspaceSwitcherOpen = false; + }); + + void _closeDrawer() => setState(() { + _drawerOpen = false; + _workspaceSwitcherOpen = false; + _addingList = false; + }); + + void _showNewTask() { + final state = context.read(); + if (state.activeListId == null) return; + setState(() => _newTaskOpen = true); + } + + void _closeNewTask() { + setState(() => _newTaskOpen = false); + } + + Future _handleCreateTask(String title, String desc, {String? dueDate}) async { + final state = context.read(); + final task = await state.createTask(title, desc); + if (task != null && dueDate != null) { + await state.updateTask(api.TaskDto( + id: task.id, title: task.title, description: task.description, + status: task.status, dueDate: dueDate, + createdAt: task.createdAt, updatedAt: task.updatedAt, parentId: task.parentId, + )); + } + _closeNewTask(); + } + + void _startAddingList() { + setState(() { + _addingList = true; + _newListController.clear(); + }); + WidgetsBinding.instance.addPostFrameCallback((_) => _newListFocus.requestFocus()); + } + + Future _submitNewList() async { + final name = _newListController.text.trim(); + if (name.isNotEmpty) await context.read().createList(name); + setState(() => _addingList = false); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return LayoutBuilder(builder: (context, constraints) { + final width = constraints.maxWidth; + final drawerWidth = width * 0.8; + final hasDetail = state.selectedTask != null; + + return Stack( + clipBehavior: Clip.hardEdge, + children: [ + // Sliding container: drawer + main + detail + AnimatedPositioned( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + left: _drawerOpen ? 0.0 : -drawerWidth, + top: 0, + bottom: 0, + width: drawerWidth + width, + child: Row( + children: [ + SizedBox(width: drawerWidth, child: _buildDrawer(state, isDark)), + SizedBox( + width: width, + child: _buildMainWithDetail(state, isDark, width), + ), + ], + ), + ), + // FAB button (centered, 56px, hidden when drawer/detail/newTask open) + if (!_drawerOpen && !hasDetail && !_newTaskOpen && state.activeListId != null) + Positioned( + bottom: 24, + left: 0, + right: 0, + child: Center( + child: SizedBox( + width: 56, + height: 56, + child: FloatingActionButton( + onPressed: _showNewTask, + backgroundColor: AppTheme.primary, + foregroundColor: Colors.white, + elevation: 6, + shape: const CircleBorder(), + child: const Icon(Icons.add, size: 28), + ), + ), + ), + ), + // New task overlay (animated, inside app bounds) + Positioned.fill( + child: IgnorePointer( + ignoring: !_newTaskOpen, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + opacity: _newTaskOpen ? 1.0 : 0.0, + child: GestureDetector( + onTap: _closeNewTask, + child: Container( + color: Colors.black.withValues(alpha: 0.4), + alignment: Alignment.bottomCenter, + child: GestureDetector( + onTap: () {}, + child: _newTaskOpen + ? NewTaskInput(onCreate: _handleCreateTask) + : const SizedBox.shrink(), + ), + ), + ), + ), + ), + ), + ], + ); + }); + } + + Widget _buildMainWithDetail(AppState state, bool isDark, double totalWidth) { + final hasDetail = state.selectedTask != null; + return Stack( + clipBehavior: Clip.hardEdge, + children: [ + ClipRect( + child: OverflowBox( + maxWidth: totalWidth * 2, + alignment: Alignment.centerLeft, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + transform: Matrix4.translationValues(hasDetail ? -totalWidth : 0, 0, 0), + width: totalWidth * 2, + child: Row( + children: [ + SizedBox(width: totalWidth, child: _buildMain(state, isDark)), + SizedBox( + width: totalWidth, + child: Container( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + child: hasDetail + ? TaskDetailView(task: state.selectedTask!) + : const SizedBox.shrink(), + ), + ), + ], + ), + ), + ), + ), + // Dim overlay when drawer is open (animated fade) + Positioned.fill( + child: IgnorePointer( + ignoring: !_drawerOpen, + child: GestureDetector( + onTap: _closeDrawer, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + opacity: _drawerOpen ? 1.0 : 0.0, + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.4), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.4), + blurRadius: 24, + offset: const Offset(8, 0), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildDrawer(AppState state, bool isDark) { + return Container( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + child: Column( + children: [ + // Header: workspace switcher (matching Tauri) + GestureDetector( + onPanStart: (_) {}, + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5), + ), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _workspaceSwitcherOpen = !_workspaceSwitcherOpen), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + state.config?.currentWorkspace ?? 'Workspace', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 6), + AnimatedRotation( + turns: _workspaceSwitcherOpen ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: Icon(Icons.expand_more, size: 14, + color: isDark ? AppTheme.textDark : AppTheme.textLight), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + // Workspace dropdown (appears below header) + if (_workspaceSwitcherOpen && state.config != null) + Container( + constraints: const BoxConstraints(maxHeight: 200), + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + border: Border( + bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5), + ), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 4), + children: [ + for (final ws in state.config!.workspaces) + GestureDetector( + onTap: () { + state.switchWorkspace(ws.name); + setState(() => _workspaceSwitcherOpen = false); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + if (ws.name == state.config?.currentWorkspace) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.check, size: 16, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(ws.name, + style: TextStyle(fontSize: 14, + fontWeight: ws.name == state.config?.currentWorkspace ? FontWeight.w700 : FontWeight.normal), + overflow: TextOverflow.ellipsis), + Text(ws.path, + style: TextStyle(fontSize: 12, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)), + overflow: TextOverflow.ellipsis), + ], + ), + ), + ], + ), + ), + ), + ), + // Add workspace + Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)), + ), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: GestureDetector( + onTap: () { + setState(() => _workspaceSwitcherOpen = false); + state.setScreen('setup'); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Text('+ Add workspace', + style: TextStyle(fontSize: 14, color: AppTheme.primary)), + ), + ), + ), + ], + ), + ), + // List items + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + for (final list in state.lists) + _ListTile( + list: list, + isActive: list.id == state.activeListId, + onTap: () { + state.selectList(list.id); + _closeDrawer(); + }, + onDelete: () => state.deleteList(list.id), + ), + // New list button / input + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: _addingList + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _newListController, + focusNode: _newListFocus, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: 'List name', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppTheme.primary), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + ), + onSubmitted: (_) => _submitNewList(), + onTapOutside: (_) => setState(() => _addingList = false), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _submitNewList, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('Add', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + ), + ], + ), + ) + : GestureDetector( + onTap: _startAddingList, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Text('+ New list', style: TextStyle(fontSize: 14, color: AppTheme.primary)), + ), + ), + ), + ], + ), + ), + // Footer: Settings button (matching Tauri) + GestureDetector( + onTap: () => state.setScreen('settings'), + child: Container( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Icon(Icons.settings, size: 18, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)), + const SizedBox(width: 8), + Text('Settings', style: TextStyle(fontSize: 14, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5))), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildMain(AppState state, bool isDark) { + return Column( + children: [ + // Title bar with menu button + centered title + close + CustomTitleBar( + leading: GestureDetector( + onTap: _toggleDrawer, + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon(Icons.menu, size: 20, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)), + ), + ), + title: state.activeList?.title ?? 'Tasks', + centerTitle: true, + ), + // Task list + Expanded( + child: state.lists.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('No lists yet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6))), + const SizedBox(height: 4), + Text('Tap the list name above to create one', style: TextStyle(fontSize: 14, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))), + ], + ), + ) + : state.activeList == null + ? Center( + child: Text('Select a list', style: TextStyle( + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))), + ) + : _buildTaskList(state, isDark), + ), + ], + ); + } + + Widget _buildTaskList(AppState state, bool isDark) { + if (state.pendingTasks.isEmpty && state.completedTasks.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text('No tasks. Add one below.', style: TextStyle(fontSize: 14, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))), + ), + ); + } + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + if (newIndex > oldIndex) newIndex--; + final task = state.pendingTasks[oldIndex]; + state.reorderTask(task.id, newIndex); + }, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) => Material( + elevation: 4, + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + child: child, + ), + child: child, + ); + }, + footer: _buildCompletedSection(state, isDark), + children: [ + for (var i = 0; i < state.pendingTasks.length; i++) + ReorderableDragStartListener( + key: ValueKey(state.pendingTasks[i].id), + index: i, + child: TaskItem( + task: state.pendingTasks[i], + onToggle: () => state.toggleTask(state.pendingTasks[i].id), + onTap: () => state.selectTask(state.pendingTasks[i].id), + ), + ), + ], + ); + } + + Widget? _buildCompletedSection(AppState state, bool isDark) { + if (state.completedTasks.isEmpty) return null; + return Column( + children: [ + const SizedBox(height: 16), + // Completed header (matching Tauri: full-width, border-top, text left, chevron right) + GestureDetector( + onTap: () { + setState(() { + if (_showCompleted) { + _showCompleted = false; + Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) setState(() => _completedVisible = false); + }); + } else { + _completedVisible = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() => _showCompleted = true); + }); + } + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)), + ), + child: Row( + children: [ + Expanded( + child: Text( + 'Completed (${state.completedTasks.length})', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight, + ), + ), + ), + AnimatedRotation( + turns: _showCompleted ? 0.25 : 0, + duration: const Duration(milliseconds: 200), + child: Icon(Icons.chevron_right, size: 16, + color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight), + ), + ], + ), + ), + ), + if (_completedVisible) + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _showCompleted ? 1.0 : 0.0, + child: AnimatedSlide( + duration: const Duration(milliseconds: 300), + offset: _showCompleted ? Offset.zero : const Offset(0, -0.05), + child: Column( + children: [ + for (final task in state.completedTasks) + TaskItem( + task: task, + onToggle: () => state.toggleTask(task.id), + onTap: () => state.selectTask(task.id), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ListTile extends StatefulWidget { + final dynamic list; + final bool isActive; + final VoidCallback onTap; + final VoidCallback onDelete; + + const _ListTile({required this.list, required this.isActive, required this.onTap, required this.onDelete}); + + @override + State<_ListTile> createState() => _ListTileState(); +} + +class _ListTileState extends State<_ListTile> { + bool _hovering = false; + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return MouseRegion( + onEnter: (_) => setState(() => _hovering = true), + onExit: (_) => setState(() => _hovering = false), + child: GestureDetector( + onTap: widget.onTap, + onSecondaryTapUp: (details) { + showMenu( + context: context, + position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0), + items: [ + PopupMenuItem( + onTap: widget.onDelete, + child: const Text('Delete', style: TextStyle(color: AppTheme.danger, fontSize: 13)), + ), + ], + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), + decoration: BoxDecoration( + color: _hovering && !widget.isActive + ? (isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05)) + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + if (widget.isActive) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.check, size: 16, + color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)), + ), + Expanded( + child: Text( + widget.list.title, + style: TextStyle( + fontSize: 14, + fontWeight: widget.isActive ? FontWeight.w700 : FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +}