feat(flutter): add screens — setup, tasks, settings
This commit is contained in:
parent
b236892203
commit
0fc5066f97
154
apps/flutter/lib/src/screens/settings_screen.dart
Normal file
154
apps/flutter/lib/src/screens/settings_screen.dart
Normal file
|
|
@ -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<AppState>();
|
||||
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))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
155
apps/flutter/lib/src/screens/setup_screen.dart
Normal file
155
apps/flutter/lib/src/screens/setup_screen.dart
Normal file
|
|
@ -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<SetupScreen> createState() => _SetupScreenState();
|
||||
}
|
||||
|
||||
class _SetupScreenState extends State<SetupScreen> {
|
||||
final _nameController = TextEditingController();
|
||||
String? _selectedPath;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickFolder() async {
|
||||
final result = await FilePicker.platform.getDirectoryPath();
|
||||
if (result != null) setState(() => _selectedPath = result);
|
||||
}
|
||||
|
||||
Future<void> _create() async {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty || _selectedPath == null) return;
|
||||
await context.read<AppState>().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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
683
apps/flutter/lib/src/screens/tasks_screen.dart
Normal file
683
apps/flutter/lib/src/screens/tasks_screen.dart
Normal file
|
|
@ -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<TasksScreen> createState() => _TasksScreenState();
|
||||
}
|
||||
|
||||
class _TasksScreenState extends State<TasksScreen> {
|
||||
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<AppState>();
|
||||
if (state.activeListId == null) return;
|
||||
setState(() => _newTaskOpen = true);
|
||||
}
|
||||
|
||||
void _closeNewTask() {
|
||||
setState(() => _newTaskOpen = false);
|
||||
}
|
||||
|
||||
Future<void> _handleCreateTask(String title, String desc, {String? dueDate}) async {
|
||||
final state = context.read<AppState>();
|
||||
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<void> _submitNewList() async {
|
||||
final name = _newListController.text.trim();
|
||||
if (name.isNotEmpty) await context.read<AppState>().createList(name);
|
||||
setState(() => _addingList = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue