Commit to Tauri GUI: remove Flutter/egui, redesign task UI

- Remove Flutter app and egui placeholder crate, commit to Tauri as sole GUI
- Update PLAN.md to replace egui with Tauri across all phases (v4.0)
- Redesign task screen: sliding drawer for list picker, floating FAB for new tasks,
  bottom sheet toast for task creation with title + description fields
- Add description support to create_task Tauri command
- Lighten dark theme to GNOME-style neutral grays, shift primary to cyan-blue
- Fix Wayland compatibility (dev port change)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tristan Michael 2026-03-29 09:49:30 -07:00
parent 9b0efa8830
commit f810cfa2a3
57 changed files with 320 additions and 6589 deletions

View file

@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
Bevy Tasks is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. Currently in Phase 1 (Core Library & CLI MVP). The GUI crate is a placeholder for Phase 3+.
Bevy Tasks is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. The GUI uses Tauri v2 (Svelte 5 + Tailwind CSS 4) in `apps/tauri/`.
## Build & Test Commands
@ -14,17 +14,22 @@ cargo build -p bevy-tasks-cli # Build CLI only
cargo test # Run all tests
cargo test -p bevy-tasks-core # Run core library tests only
cargo run -p bevy-tasks-cli -- <args> # Run CLI with arguments
# Tauri GUI
cd apps/tauri && npm install # Install frontend dependencies
npm run tauri dev # Run Tauri in dev mode
npm run tauri build # Build for production
```
The CLI binary is named `bevy-tasks` (from the `bevy-tasks-cli` crate).
## Architecture
Three-crate workspace (`resolver = "2"`, edition 2021):
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
- **bevy-tasks-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
- **bevy-tasks-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
- **bevy-tasks-gui** — Placeholder for future egui/eframe GUI.
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `bevy-tasks-core`.
### Key patterns

8
Cargo.lock generated
View file

@ -138,14 +138,6 @@ dependencies = [
"wiremock",
]
[[package]]
name = "bevy-tasks-gui"
version = "0.1.0"
dependencies = [
"anyhow",
"bevy-tasks-core",
]
[[package]]
name = "bitflags"
version = "2.11.0"

View file

@ -2,11 +2,9 @@
members = [
"crates/bevy-tasks-core",
"crates/bevy-tasks-cli",
"crates/bevy-tasks-gui",
]
exclude = [
"apps/tauri/src-tauri",
"apps/flutter/rust",
]
resolver = "2"

175
PLAN.md
View file

@ -19,8 +19,9 @@ A **local-first, cross-platform tasks application** inspired by Google Tasks. Bu
## Resources
- [Bevy Documentation](https://bevyengine.org/)
- [egui Documentation](https://docs.rs/egui/)
- [Tauri Documentation](https://v2.tauri.app/)
- [Svelte Documentation](https://svelte.dev/)
- [Tailwind CSS Documentation](https://tailwindcss.com/)
- [WebDAV RFC 4918](https://datatracker.ietf.org/doc/html/rfc4918)
- [Google Tasks API](https://developers.google.com/tasks) (for importer reference)
@ -44,10 +45,11 @@ bevy-tasks/
├── Cargo.toml # Workspace definition
├── PLAN.md
├── README.md
├── apps/
│ └── tauri/ # Tauri GUI (Svelte + Tailwind)
├── crates/
│ ├── bevy-tasks-core/ # Core library (backend)
│ ├── bevy-tasks-cli/ # CLI frontend
│ └── bevy-tasks-gui/ # GUI frontend (Phase 3+)
│ └── bevy-tasks-cli/ # CLI frontend
└── docs/
```
@ -226,7 +228,9 @@ pub trait Storage {
members = [
"crates/bevy-tasks-core",
"crates/bevy-tasks-cli",
"crates/bevy-tasks-gui",
]
exclude = [
"apps/tauri/src-tauri",
]
resolver = "2"
@ -590,43 +594,50 @@ Workspace: shared
### Architecture
#### Frontend Framework: egui
#### Frontend Framework: Tauri v2 + Svelte 5 + Tailwind CSS 4
**Decision**: Use egui (immediate mode GUI) for MVP
**Decision**: Use Tauri v2 with Svelte and Tailwind for the GUI
**Why egui?**
- Fast development with rich built-in widgets
- Excellent text editing support out of the box
- Small binary size (~2-3MB stripped)
- Fast startup time (100-200ms target)
- Mature and stable
- Simple immediate-mode API
- Cross-platform (desktop AND mobile)
- Easy integration with `bevy-tasks-core`
**Why Tauri?**
- Native Rust backend — direct integration with `bevy-tasks-core`
- Svelte 5 for reactive, performant UI with minimal boilerplate
- Tailwind CSS 4 for rapid, consistent styling
- Small binary size (~5-10MB)
- Cross-platform (Windows, Linux, macOS; mobile in Tauri v2)
- Web technologies for UI = rich ecosystem, easy to iterate
- Tauri commands expose core library directly to the frontend
#### GUI Crate Structure
#### GUI Structure
```
crates/bevy-tasks-gui/
├── Cargo.toml
├── src/
│ ├── main.rs # App entry point
│ ├── app.rs # egui app setup
│ ├── ui/
│ │ ├── mod.rs
│ │ ├── screens/
│ │ │ ├── task_list.rs
│ │ │ ├── task_detail.rs
│ │ │ └── settings.rs
│ │ └── components/
│ │ ├── task_item.rs
│ │ ├── task_input.rs
│ │ └── list_selector.rs
│ └── state.rs # UI state management
├── assets/
│ ├── fonts/
│ └── icons/
└── README.md
apps/tauri/
├── package.json
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
├── index.html
├── src/ # Svelte frontend
│ ├── main.ts
│ ├── app.css
│ ├── App.svelte
│ └── lib/
│ ├── screens/
│ │ ├── TasksScreen.svelte
│ │ ├── SettingsScreen.svelte
│ │ └── SetupScreen.svelte
│ ├── components/
│ │ ├── TaskItem.svelte
│ │ ├── NewTaskInput.svelte
│ │ └── ListSelector.svelte
│ └── stores/
│ └── app.ts
└── src-tauri/ # Rust backend (Tauri commands)
├── Cargo.toml
├── tauri.conf.json
└── src/
├── main.rs
├── commands.rs # Tauri command handlers
└── lib.rs
```
#### First Run Experience
@ -661,45 +672,27 @@ WorkspaceConfig {
}
```
### Dependencies
**bevy-tasks-gui/Cargo.toml**:
```toml
[package]
name = "bevy-tasks-gui"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy-tasks-core = { path = "../bevy-tasks-core" }
anyhow = { workspace = true }
# egui for Phase 3-6
eframe = "0.31" # egui framework with native windowing
egui = "0.31" # Core egui library
```
### Performance Strategy
**Startup Sequence**:
1. Initialize eframe window (< 50ms)
2. Load config from disk (< 20ms)
3. Render empty UI (first frame < 100ms)
1. Initialize Tauri window + load Svelte app (< 100ms)
2. Load config from disk via Tauri command (< 20ms)
3. Render UI (first paint < 150ms)
4. Load current task list in background
5. Update UI as tasks load
6. Start WebDAV sync in background (if configured)
**Target**: < 200ms cold start on desktop
**Target**: < 300ms cold start on desktop
**Optimizations**:
- Lazy data loading (load visible tasks first)
- Background operations for sync
- Background operations for sync via async Tauri commands
- Efficient file I/O (stream large files)
- Minimal dependencies
- Svelte's compiled reactivity for minimal DOM updates
### Features
- [ ] egui framework integration
- [ ] Tauri v2 + Svelte 5 + Tailwind CSS 4 framework integration
- [ ] Workspace setup dialog on first launch
- [ ] Workspace selector in toolbar
- [ ] Quick-switch between workspaces
@ -715,14 +708,14 @@ egui = "0.31" # Core egui library
### Deliverables
- [ ] Functional desktop GUI app
- [ ] Sub-200ms startup time
- [ ] Sub-300ms startup time
- [ ] Clean, minimal UI
- [ ] Feature parity with CLI
### Build & Release
**Distribution**:
- Linux: AppImage, .tar.gz
- Linux: AppImage, .deb, .tar.gz
- macOS: DMG
- Windows: MSI, portable .exe
@ -737,60 +730,48 @@ egui = "0.31" # Core egui library
### Why Early Mobile?
- De-risk mobile builds early in development
- Test cross-platform architecture sooner
- Validate egui on mobile
- Tauri v2 has first-class mobile support — same codebase as desktop
- Get mobile-specific feedback early
- Can dogfood on mobile while building desktop features
### Architecture
#### Mobile Build Setup
#### Mobile Build Setup (Tauri v2 Mobile)
Tauri v2 supports iOS and Android natively. The same Svelte frontend and Rust backend are used, with platform-specific configuration.
**iOS**:
- Use Xcode for builds
- Tauri generates Xcode project
- Bundle identifier: `com.bevytasks.app`
- Target: `aarch64-apple-ios`
- Bundle identifier: `com.bevy-tasks`
**Android**:
- Use Android SDK/NDK
- Build with `cargo-apk` or `cargo-ndk`
- Tauri generates Gradle project
- Min SDK: 26 (Android 8.0)
- NDK handles Rust compilation
#### egui Mobile Adaptation
#### Mobile Adaptation
**Touch Support**:
- egui has basic touch support
- Add larger touch targets (44pt minimum)
- Tailwind responsive utilities for mobile-friendly layouts
- Larger touch targets (44pt minimum)
- Mobile-specific Svelte components where needed
- Test on real devices
**File System Access**:
- iOS: App sandbox documents directory + file picker
- Android: Scoped storage + SAF (Storage Access Framework)
- iOS: App sandbox documents directory + Tauri file dialog plugin
- Android: Scoped storage + Tauri file dialog plugin
#### First Run on Mobile
- Show folder picker on first launch
- Suggest locations: Documents, iCloud Drive (iOS), Google Drive (Android)
- User selects folder, path stored in preferences
### Platform-Specific Code
```rust
#[cfg(target_os = "ios")]
mod ios {
// iOS-specific file picker, etc.
}
#[cfg(target_os = "android")]
mod android {
// Android-specific file picker, etc.
}
```
### Features
- [ ] iOS build pipeline setup (Xcode project)
- [ ] Android build pipeline setup (Gradle/NDK)
- [ ] Basic egui mobile adaptation
- [ ] Simple test UI (even just buttons for CRUD)
- [ ] Tauri v2 iOS build pipeline setup
- [ ] Tauri v2 Android build pipeline setup
- [ ] Mobile-responsive Svelte/Tailwind layout
- [ ] File system access on iOS
- [ ] File system access on Android
- [ ] Folder picker for mobile
@ -945,11 +926,11 @@ mod android {
### Optional: Bevy Migration
If you want game-like polish after Phase 7:
- Migrate GUI from egui to Bevy
- Migrate GUI from Tauri/Svelte to Bevy
- Full control over animations and rendering
- Unique, polished look beyond standard apps
- Backend (`bevy-tasks-core`) stays identical
- Only rewrite `bevy-tasks-gui` crate
- Only rewrite the GUI layer
### Deliverables
@ -978,6 +959,6 @@ This project is free and open-source software licensed under GPL v3.
---
**Last Updated**: 2026-03-17
**Document Version**: 3.1
**Last Updated**: 2026-03-29
**Document Version**: 4.0
**Status**: Ready to Implement - Milestone-Driven Plan

View file

@ -1,48 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Rust bridge build artifacts
rust/target/

View file

@ -1,30 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: windows
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -1,17 +0,0 @@
# bevy_tasks
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -1,6 +0,0 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
prefer_const_constructors: true
prefer_const_declarations: true

View file

@ -1,4 +0,0 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust/
c_output: windows/runner/bridge_generated.h

View file

@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/app_provider.dart';
import 'screens/setup_screen.dart';
import 'screens/tasks_screen.dart';
import 'screens/settings_screen.dart';
import 'src/rust/frb_generated.dart';
import 'theme.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
runApp(const BevyTasksApp());
}
class BevyTasksApp extends StatelessWidget {
const BevyTasksApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AppProvider()..init(),
child: Consumer<AppProvider>(
builder: (context, app, _) {
return MaterialApp(
title: 'Bevy Tasks',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: app.darkMode ? ThemeMode.dark : ThemeMode.light,
home: _buildScreen(app),
);
},
),
);
}
Widget _buildScreen(AppProvider app) {
switch (app.screen) {
case AppScreen.setup:
return const SetupScreen();
case AppScreen.tasks:
return const TasksScreen();
case AppScreen.settings:
return const SettingsScreen();
}
}
}

View file

@ -1,257 +0,0 @@
import 'package:flutter/material.dart';
import '../src/rust/api.dart' as api;
enum AppScreen { setup, tasks, settings }
class AppProvider extends ChangeNotifier {
AppScreen _screen = AppScreen.setup;
bool _darkMode = false;
bool _syncing = false;
String? _error;
List<api.BridgeWorkspace> _workspaces = [];
String? _currentWorkspace;
List<api.BridgeTaskList> _lists = [];
String? _activeListId;
List<api.BridgeTask> _tasks = [];
// Getters
AppScreen get screen => _screen;
bool get darkMode => _darkMode;
bool get syncing => _syncing;
String? get error => _error;
List<api.BridgeWorkspace> get workspaces => _workspaces;
String? get currentWorkspace => _currentWorkspace;
List<api.BridgeTaskList> get lists => _lists;
String? get activeListId => _activeListId;
api.BridgeTaskList? get activeList =>
_activeListId == null ? null : _lists.where((l) => l.id == _activeListId).firstOrNull;
List<api.BridgeTask> get tasks => _tasks;
List<api.BridgeTask> get pendingTasks => _tasks.where((t) => t.status != 'completed').toList();
List<api.BridgeTask> get completedTasks => _tasks.where((t) => t.status == 'completed').toList();
bool get hasWorkspace => _currentWorkspace != null && _workspaces.isNotEmpty;
// Init
Future<void> init() async {
try {
final config = await api.initApp();
_workspaces = config.workspaces;
_currentWorkspace = config.currentWorkspace;
if (hasWorkspace) {
_screen = AppScreen.tasks;
await loadLists();
}
} catch (e) {
_screen = AppScreen.setup;
}
notifyListeners();
}
// Navigation
void setScreen(AppScreen s) {
_screen = s;
notifyListeners();
}
void toggleDarkMode() {
_darkMode = !_darkMode;
notifyListeners();
}
void clearError() {
_error = null;
notifyListeners();
}
// Workspace operations
Future<void> addWorkspace(String name, String path) async {
try {
await api.addWorkspace(name: name, path: path);
final config = await api.getConfig();
_workspaces = config.workspaces;
_currentWorkspace = config.currentWorkspace;
_screen = AppScreen.tasks;
_error = null;
await loadLists();
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> switchWorkspace(String name) async {
try {
await api.setCurrentWorkspace(name: name);
final config = await api.getConfig();
_workspaces = config.workspaces;
_currentWorkspace = config.currentWorkspace;
_activeListId = null;
await loadLists();
_error = null;
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> removeWorkspace(String name) async {
try {
await api.removeWorkspace(name: name);
final config = await api.getConfig();
_workspaces = config.workspaces;
_currentWorkspace = config.currentWorkspace;
if (!hasWorkspace) {
_screen = AppScreen.setup;
_lists = [];
_tasks = [];
_activeListId = null;
}
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
// List operations
Future<void> loadLists() async {
try {
_lists = await api.getLists();
if (_lists.isNotEmpty && _activeListId == null) {
_activeListId = _lists.first.id;
}
if (_activeListId != null) await loadTasks();
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> selectList(String id) async {
_activeListId = id;
await loadTasks();
notifyListeners();
}
Future<void> createList(String name) async {
try {
final list = await api.createList(name: name);
_lists.add(list);
_activeListId = list.id;
_tasks = [];
_error = null;
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> deleteList(String id) async {
try {
await api.deleteList(listId: id);
_lists.removeWhere((l) => l.id == id);
if (_activeListId == id) {
_activeListId = _lists.isNotEmpty ? _lists.first.id : null;
if (_activeListId != null) await loadTasks();
else _tasks = [];
}
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
// Task operations
Future<void> loadTasks() async {
if (_activeListId == null) return;
try {
_tasks = await api.listTasks(listId: _activeListId!);
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> createTask(String title) async {
if (_activeListId == null) return;
try {
final task = await api.createTask(listId: _activeListId!, title: title);
_tasks.add(task);
_error = null;
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> toggleTask(String taskId) async {
if (_activeListId == null) return;
try {
await api.toggleTask(listId: _activeListId!, taskId: taskId);
await loadTasks();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
Future<void> updateTask(String taskId, String title, String description) async {
if (_activeListId == null) return;
try {
await api.updateTask(listId: _activeListId!, taskId: taskId, title: title, description: description);
await loadTasks();
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
Future<void> deleteTask(String taskId) async {
if (_activeListId == null) return;
try {
await api.deleteTask(listId: _activeListId!, taskId: taskId);
_tasks.removeWhere((t) => t.id == taskId);
} catch (e) {
_error = e.toString();
}
notifyListeners();
}
// Sync
Future<void> triggerSync() async {
if (_currentWorkspace == null) return;
final ws = _workspaces.where((w) => w.name == _currentWorkspace).firstOrNull;
if (ws == null || ws.webdavUrl == null) {
_error = 'No WebDAV URL configured';
notifyListeners();
return;
}
_syncing = true;
_error = null;
notifyListeners();
try {
final result = await api.syncWorkspaceBridge(
workspacePath: ws.path,
webdavUrl: ws.webdavUrl!,
username: '',
password: '',
);
if (result.errors.isNotEmpty) {
_error = result.errors.join('; ');
}
await loadLists();
} catch (e) {
_error = e.toString();
} finally {
_syncing = false;
notifyListeners();
}
}
}

View file

@ -1,236 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _webdavUrlController = TextEditingController();
final _webdavUserController = TextEditingController();
final _webdavPassController = TextEditingController();
String? _confirmRemove;
@override
void dispose() {
_webdavUrlController.dispose();
_webdavUserController.dispose();
_webdavPassController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final app = context.watch<AppProvider>();
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => app.setScreen(AppScreen.tasks),
icon: const Icon(Icons.arrow_back),
),
title: const Text('Settings'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Workspaces section
Text(
'WORKSPACES',
style: theme.textTheme.labelSmall?.copyWith(
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
),
),
const SizedBox(height: 12),
...app.workspaces.map((ws) {
final isCurrent = ws.name == app.currentWorkspace;
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ws.name,
style: theme.textTheme.titleSmall?.copyWith(
color: isCurrent ? theme.colorScheme.primary : null,
),
),
Text(
ws.path,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
),
),
],
),
),
if (!isCurrent)
TextButton(
onPressed: () => app.switchWorkspace(ws.name),
child: const Text('Switch'),
),
if (_confirmRemove == ws.name) ...[
TextButton(
onPressed: () {
app.removeWorkspace(ws.name);
setState(() => _confirmRemove = null);
},
child: Text('Confirm', style: TextStyle(color: theme.colorScheme.error)),
),
TextButton(
onPressed: () => setState(() => _confirmRemove = null),
child: const Text('Cancel'),
),
] else
TextButton(
onPressed: () => setState(() => _confirmRemove = ws.name),
child: Text(
'Remove',
style: TextStyle(color: theme.colorScheme.onSurfaceVariant.withAlpha(102)),
),
),
],
),
if (ws.webdavUrl != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Sync: ${ws.webdavUrl}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(102),
),
),
),
],
),
),
);
}),
TextButton.icon(
onPressed: () => app.setScreen(AppScreen.setup),
icon: const Icon(Icons.add, size: 18),
label: const Text('Add workspace'),
),
const SizedBox(height: 24),
// WebDAV Sync section
Text(
'WEBDAV SYNC',
style: theme.textTheme.labelSmall?.copyWith(
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
),
),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Server URL', style: theme.textTheme.labelMedium),
const SizedBox(height: 4),
TextField(
controller: _webdavUrlController,
decoration: const InputDecoration(hintText: 'https://dav.example.com/tasks/'),
keyboardType: TextInputType.url,
),
const SizedBox(height: 12),
Text('Username', style: theme.textTheme.labelMedium),
const SizedBox(height: 4),
TextField(controller: _webdavUserController),
const SizedBox(height: 12),
Text('Password', style: theme.textTheme.labelMedium),
const SizedBox(height: 4),
TextField(
controller: _webdavPassController,
obscureText: true,
),
const SizedBox(height: 16),
Row(
children: [
OutlinedButton(
onPressed: () {
// TODO: test connection
},
child: const Text('Test Connection'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () {
// TODO: save webdav config
},
child: const Text('Save'),
),
],
),
],
),
),
),
if (app.currentWorkspace != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: FilledButton(
onPressed: app.syncing ? null : app.triggerSync,
child: Text(app.syncing ? 'Syncing...' : 'Sync Now'),
),
),
const SizedBox(height: 24),
// Appearance section
Text(
'APPEARANCE',
style: theme.textTheme.labelSmall?.copyWith(
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
),
),
const SizedBox(height: 12),
Card(
child: SwitchListTile(
title: const Text('Dark mode'),
value: app.darkMode,
onChanged: (_) => app.toggleDarkMode(),
),
),
const SizedBox(height: 32),
Center(
child: Text(
'Flutter + flutter_rust_bridge',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(77),
),
),
),
],
),
);
}
}

View file

@ -1,113 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart';
import '../providers/app_provider.dart';
class SetupScreen extends StatefulWidget {
const SetupScreen({super.key});
@override
State<SetupScreen> createState() => _SetupScreenState();
}
class _SetupScreenState extends State<SetupScreen> {
final _nameController = TextEditingController();
final _pathController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_pathController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final app = context.read<AppProvider>();
final theme = Theme.of(context);
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bevy Tasks',
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'Create or open a workspace to get started.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
// Workspace name
Text('Workspace name', style: theme.textTheme.labelMedium),
const SizedBox(height: 4),
TextField(
controller: _nameController,
decoration: const InputDecoration(hintText: 'My Tasks'),
),
const SizedBox(height: 16),
// Folder path
Text('Folder', style: theme.textTheme.labelMedium),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: TextField(
controller: _pathController,
readOnly: true,
decoration: const InputDecoration(hintText: 'Select a folder…'),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () async {
final result = await FilePicker.platform.getDirectoryPath();
if (result != null) {
_pathController.text = result;
}
},
child: const Text('Browse'),
),
],
),
const SizedBox(height: 24),
// Create button
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () {
final name = _nameController.text.trim();
final path = _pathController.text.trim();
if (name.isNotEmpty && path.isNotEmpty) {
app.addWorkspace(name, path);
}
},
child: const Text('Create Workspace'),
),
),
],
),
),
),
),
),
),
);
}
}

View file

@ -1,145 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../widgets/task_item.dart';
import '../widgets/new_task_bar.dart';
import '../widgets/list_picker_sheet.dart';
class TasksScreen extends StatefulWidget {
const TasksScreen({super.key});
@override
State<TasksScreen> createState() => _TasksScreenState();
}
class _TasksScreenState extends State<TasksScreen> {
bool _showCompleted = true;
void _openListPicker() {
showModalBottomSheet(
context: context,
builder: (_) => const ListPickerSheet(),
);
}
@override
Widget build(BuildContext context) {
final app = context.watch<AppProvider>();
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: GestureDetector(
onTap: _openListPicker,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
app.activeList?.title ?? 'Tasks',
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, size: 20),
],
),
),
actions: [
if (app.syncing)
const Padding(
padding: EdgeInsets.only(right: 8),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
IconButton(
onPressed: app.toggleDarkMode,
icon: Icon(app.darkMode ? Icons.light_mode : Icons.dark_mode),
),
IconButton(
onPressed: () => app.setScreen(AppScreen.settings),
icon: const Icon(Icons.settings),
),
],
),
body: Column(
children: [
if (app.error != null)
MaterialBanner(
content: Text(app.error!, style: const TextStyle(fontSize: 13)),
backgroundColor: theme.colorScheme.errorContainer,
actions: [
TextButton(
onPressed: app.clearError,
child: const Text('Dismiss'),
),
],
),
Expanded(
child: app.lists.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('No lists yet', style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
)),
const SizedBox(height: 4),
Text('Tap the title to create one', style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
)),
],
),
)
: app.activeListId == null
? Center(
child: Text('Select a list', style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
)),
)
: ListView(
physics: const BouncingScrollPhysics(),
children: [
...app.pendingTasks.map((task) => TaskItem(task: task)),
if (app.pendingTasks.isEmpty)
Padding(
padding: const EdgeInsets.all(32),
child: Center(
child: Text(
'No tasks. Add one below.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(102),
),
),
),
),
if (app.completedTasks.isNotEmpty) ...[
const Divider(),
ListTile(
dense: true,
leading: Icon(
_showCompleted ? Icons.expand_more : Icons.chevron_right,
size: 20,
),
title: Text(
'Completed (${app.completedTasks.length})',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
onTap: () => setState(() => _showCompleted = !_showCompleted),
),
if (_showCompleted)
...app.completedTasks.map((task) => TaskItem(task: task)),
],
],
),
),
const NewTaskBar(),
],
),
);
}
}

View file

@ -1,260 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `config_to_bridge`, `ensure_repo`, `list_to_bridge`, `task_to_bridge`, `with_state`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AppState`
/// Initialize the bridge. Must be called once at app startup.
Future<BridgeConfig> initApp() => RustLib.instance.api.crateApiInitApp();
Future<BridgeConfig> getConfig() => RustLib.instance.api.crateApiGetConfig();
Future<void> addWorkspace({required String name, required String path}) =>
RustLib.instance.api.crateApiAddWorkspace(name: name, path: path);
Future<void> setCurrentWorkspace({required String name}) =>
RustLib.instance.api.crateApiSetCurrentWorkspace(name: name);
Future<void> removeWorkspace({required String name}) =>
RustLib.instance.api.crateApiRemoveWorkspace(name: name);
Future<List<BridgeTaskList>> getLists() =>
RustLib.instance.api.crateApiGetLists();
Future<BridgeTaskList> createList({required String name}) =>
RustLib.instance.api.crateApiCreateList(name: name);
Future<void> deleteList({required String listId}) =>
RustLib.instance.api.crateApiDeleteList(listId: listId);
Future<List<BridgeTask>> listTasks({required String listId}) =>
RustLib.instance.api.crateApiListTasks(listId: listId);
Future<BridgeTask> createTask(
{required String listId, required String title}) =>
RustLib.instance.api.crateApiCreateTask(listId: listId, title: title);
Future<BridgeTask> toggleTask(
{required String listId, required String taskId}) =>
RustLib.instance.api.crateApiToggleTask(listId: listId, taskId: taskId);
Future<void> updateTask(
{required String listId,
required String taskId,
required String title,
required String description}) =>
RustLib.instance.api.crateApiUpdateTask(
listId: listId, taskId: taskId, title: title, description: description);
Future<void> deleteTask({required String listId, required String taskId}) =>
RustLib.instance.api.crateApiDeleteTask(listId: listId, taskId: taskId);
Future<void> reorderTask(
{required String listId,
required String taskId,
required BigInt newPosition}) =>
RustLib.instance.api.crateApiReorderTask(
listId: listId, taskId: taskId, newPosition: newPosition);
Future<void> setWebdavConfig(
{required String workspaceName, required String webdavUrl}) =>
RustLib.instance.api.crateApiSetWebdavConfig(
workspaceName: workspaceName, webdavUrl: webdavUrl);
Future<void> storeWebdavCredentials(
{required String domain,
required String username,
required String password}) =>
RustLib.instance.api.crateApiStoreWebdavCredentials(
domain: domain, username: username, password: password);
Future<BridgeSyncResult> syncWorkspaceBridge(
{required String workspacePath,
required String webdavUrl,
required String username,
required String password}) =>
RustLib.instance.api.crateApiSyncWorkspaceBridge(
workspacePath: workspacePath,
webdavUrl: webdavUrl,
username: username,
password: password);
/// Flat app config for FFI transport.
class BridgeConfig {
final List<BridgeWorkspace> workspaces;
final String? currentWorkspace;
const BridgeConfig({
required this.workspaces,
this.currentWorkspace,
});
@override
int get hashCode => workspaces.hashCode ^ currentWorkspace.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeConfig &&
runtimeType == other.runtimeType &&
workspaces == other.workspaces &&
currentWorkspace == other.currentWorkspace;
}
/// Sync result for FFI transport.
class BridgeSyncResult {
final int uploaded;
final int downloaded;
final int deletedLocal;
final int deletedRemote;
final int conflicts;
final List<String> errors;
const BridgeSyncResult({
required this.uploaded,
required this.downloaded,
required this.deletedLocal,
required this.deletedRemote,
required this.conflicts,
required this.errors,
});
@override
int get hashCode =>
uploaded.hashCode ^
downloaded.hashCode ^
deletedLocal.hashCode ^
deletedRemote.hashCode ^
conflicts.hashCode ^
errors.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeSyncResult &&
runtimeType == other.runtimeType &&
uploaded == other.uploaded &&
downloaded == other.downloaded &&
deletedLocal == other.deletedLocal &&
deletedRemote == other.deletedRemote &&
conflicts == other.conflicts &&
errors == other.errors;
}
/// Flat task struct for FFI transport.
class BridgeTask {
final String id;
final String title;
final String description;
final String status;
final String? dueDate;
final String createdAt;
final String updatedAt;
final String? parentId;
const BridgeTask({
required this.id,
required this.title,
required this.description,
required this.status,
this.dueDate,
required this.createdAt,
required this.updatedAt,
this.parentId,
});
@override
int get hashCode =>
id.hashCode ^
title.hashCode ^
description.hashCode ^
status.hashCode ^
dueDate.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
parentId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeTask &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
description == other.description &&
status == other.status &&
dueDate == other.dueDate &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt &&
parentId == other.parentId;
}
/// Flat list struct for FFI transport.
class BridgeTaskList {
final String id;
final String title;
final String createdAt;
final String updatedAt;
final bool groupByDueDate;
const BridgeTaskList({
required this.id,
required this.title,
required this.createdAt,
required this.updatedAt,
required this.groupByDueDate,
});
@override
int get hashCode =>
id.hashCode ^
title.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
groupByDueDate.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeTaskList &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt &&
groupByDueDate == other.groupByDueDate;
}
/// Flat workspace config for FFI transport.
class BridgeWorkspace {
final String name;
final String path;
final String? webdavUrl;
final String? lastSync;
const BridgeWorkspace({
required this.name,
required this.path,
this.webdavUrl,
this.lastSync,
});
@override
int get hashCode =>
name.hashCode ^ path.hashCode ^ webdavUrl.hashCode ^ lastSync.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeWorkspace &&
runtimeType == other.runtimeType &&
name == other.name &&
path == other.path &&
webdavUrl == other.webdavUrl &&
lastSync == other.lastSync;
}

File diff suppressed because it is too large Load diff

View file

@ -1,203 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
String dco_decode_String(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
BridgeConfig dco_decode_bridge_config(dynamic raw);
@protected
BridgeSyncResult dco_decode_bridge_sync_result(dynamic raw);
@protected
BridgeTask dco_decode_bridge_task(dynamic raw);
@protected
BridgeTaskList dco_decode_bridge_task_list(dynamic raw);
@protected
BridgeWorkspace dco_decode_bridge_workspace(dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<BridgeTask> dco_decode_list_bridge_task(dynamic raw);
@protected
List<BridgeTaskList> dco_decode_list_bridge_task_list(dynamic raw);
@protected
List<BridgeWorkspace> dco_decode_list_bridge_workspace(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
BridgeConfig sse_decode_bridge_config(SseDeserializer deserializer);
@protected
BridgeSyncResult sse_decode_bridge_sync_result(SseDeserializer deserializer);
@protected
BridgeTask sse_decode_bridge_task(SseDeserializer deserializer);
@protected
BridgeTaskList sse_decode_bridge_task_list(SseDeserializer deserializer);
@protected
BridgeWorkspace sse_decode_bridge_workspace(SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<BridgeTask> sse_decode_list_bridge_task(SseDeserializer deserializer);
@protected
List<BridgeTaskList> sse_decode_list_bridge_task_list(
SseDeserializer deserializer);
@protected
List<BridgeWorkspace> sse_decode_list_bridge_workspace(
SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_bridge_config(BridgeConfig self, SseSerializer serializer);
@protected
void sse_encode_bridge_sync_result(
BridgeSyncResult self, SseSerializer serializer);
@protected
void sse_encode_bridge_task(BridgeTask self, SseSerializer serializer);
@protected
void sse_encode_bridge_task_list(
BridgeTaskList self, SseSerializer serializer);
@protected
void sse_encode_bridge_workspace(
BridgeWorkspace self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_bridge_task(
List<BridgeTask> self, SseSerializer serializer);
@protected
void sse_encode_list_bridge_task_list(
List<BridgeTaskList> self, SseSerializer serializer);
@protected
void sse_encode_list_bridge_workspace(
List<BridgeWorkspace> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self, SseSerializer serializer);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
}

View file

@ -1,150 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
// Static analysis wrongly picks the IO variant, thus ignore this
// ignore_for_file: argument_type_not_assignable
import 'api.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected String dco_decode_String(dynamic raw);
@protected bool dco_decode_bool(dynamic raw);
@protected BridgeConfig dco_decode_bridge_config(dynamic raw);
@protected BridgeSyncResult dco_decode_bridge_sync_result(dynamic raw);
@protected BridgeTask dco_decode_bridge_task(dynamic raw);
@protected BridgeTaskList dco_decode_bridge_task_list(dynamic raw);
@protected BridgeWorkspace dco_decode_bridge_workspace(dynamic raw);
@protected List<String> dco_decode_list_String(dynamic raw);
@protected List<BridgeTask> dco_decode_list_bridge_task(dynamic raw);
@protected List<BridgeTaskList> dco_decode_list_bridge_task_list(dynamic raw);
@protected List<BridgeWorkspace> dco_decode_list_bridge_workspace(dynamic raw);
@protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected String? dco_decode_opt_String(dynamic raw);
@protected int dco_decode_u_32(dynamic raw);
@protected int dco_decode_u_8(dynamic raw);
@protected void dco_decode_unit(dynamic raw);
@protected BigInt dco_decode_usize(dynamic raw);
@protected String sse_decode_String(SseDeserializer deserializer);
@protected bool sse_decode_bool(SseDeserializer deserializer);
@protected BridgeConfig sse_decode_bridge_config(SseDeserializer deserializer);
@protected BridgeSyncResult sse_decode_bridge_sync_result(SseDeserializer deserializer);
@protected BridgeTask sse_decode_bridge_task(SseDeserializer deserializer);
@protected BridgeTaskList sse_decode_bridge_task_list(SseDeserializer deserializer);
@protected BridgeWorkspace sse_decode_bridge_workspace(SseDeserializer deserializer);
@protected List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected List<BridgeTask> sse_decode_list_bridge_task(SseDeserializer deserializer);
@protected List<BridgeTaskList> sse_decode_list_bridge_task_list(SseDeserializer deserializer);
@protected List<BridgeWorkspace> sse_decode_list_bridge_workspace(SseDeserializer deserializer);
@protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected String? sse_decode_opt_String(SseDeserializer deserializer);
@protected int sse_decode_u_32(SseDeserializer deserializer);
@protected int sse_decode_u_8(SseDeserializer deserializer);
@protected void sse_decode_unit(SseDeserializer deserializer);
@protected BigInt sse_decode_usize(SseDeserializer deserializer);
@protected int sse_decode_i_32(SseDeserializer deserializer);
@protected void sse_encode_String(String self, SseSerializer serializer);
@protected void sse_encode_bool(bool self, SseSerializer serializer);
@protected void sse_encode_bridge_config(BridgeConfig self, SseSerializer serializer);
@protected void sse_encode_bridge_sync_result(BridgeSyncResult self, SseSerializer serializer);
@protected void sse_encode_bridge_task(BridgeTask self, SseSerializer serializer);
@protected void sse_encode_bridge_task_list(BridgeTaskList self, SseSerializer serializer);
@protected void sse_encode_bridge_workspace(BridgeWorkspace self, SseSerializer serializer);
@protected void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected void sse_encode_list_bridge_task(List<BridgeTask> self, SseSerializer serializer);
@protected void sse_encode_list_bridge_task_list(List<BridgeTaskList> self, SseSerializer serializer);
@protected void sse_encode_list_bridge_workspace(List<BridgeWorkspace> self, SseSerializer serializer);
@protected void sse_encode_list_prim_u_8_strict(Uint8List self, SseSerializer serializer);
@protected void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected void sse_encode_u_32(int self, SseSerializer serializer);
@protected void sse_encode_u_8(int self, SseSerializer serializer);
@protected void sse_encode_unit(void self, SseSerializer serializer);
@protected void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
}
@JS('wasm_bindgen') external RustLibWasmModule get wasmModule;
@JS() @anonymous extension type RustLibWasmModule._(JSObject _) implements JSObject {
}

View file

@ -1,116 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static const _blue = Color(0xFF2563EB);
static ThemeData light() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorSchemeSeed: _blue,
scaffoldBackgroundColor: Colors.white,
textTheme: GoogleFonts.notoSansTextTheme(),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Color(0xFF1F2937),
elevation: 0,
scrolledUnderElevation: 1,
),
cardTheme: const CardThemeData(
color: Color(0xFFF9FAFB),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: _blue,
foregroundColor: Colors.white,
elevation: 4,
shape: CircleBorder(),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
),
dividerTheme: const DividerThemeData(
color: Color(0xFFE5E7EB),
thickness: 1,
space: 0,
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _blue, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
static ThemeData dark() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: _blue,
scaffoldBackgroundColor: const Color(0xFF121212),
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF121212),
foregroundColor: Color(0xFFE5E7EB),
elevation: 0,
scrolledUnderElevation: 1,
),
cardTheme: const CardThemeData(
color: Color(0xFF1E1E1E),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: _blue,
foregroundColor: Colors.white,
elevation: 4,
shape: CircleBorder(),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
),
dividerTheme: const DividerThemeData(
color: Color(0xFF374151),
thickness: 1,
space: 0,
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF374151)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF374151)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _blue, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
}

View file

@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
class ListPickerSheet extends StatefulWidget {
const ListPickerSheet({super.key});
@override
State<ListPickerSheet> createState() => _ListPickerSheetState();
}
class _ListPickerSheetState extends State<ListPickerSheet> {
bool _showNewList = false;
final _newListController = TextEditingController();
String? _confirmDelete;
@override
void dispose() {
_newListController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final app = context.watch<AppProvider>();
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: theme.dividerColor,
),
),
),
const SizedBox(height: 16),
Text('Lists', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...app.lists.map((list) {
final isActive = list.id == app.activeListId;
return ListTile(
dense: true,
title: Text(
list.title,
style: TextStyle(
fontWeight: isActive ? FontWeight.bold : null,
color: isActive ? theme.colorScheme.primary : null,
),
),
trailing: _confirmDelete == list.id
? Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
app.deleteList(list.id);
setState(() => _confirmDelete = null);
},
child: Text('Delete', style: TextStyle(color: theme.colorScheme.error)),
),
TextButton(
onPressed: () => setState(() => _confirmDelete = null),
child: const Text('Cancel'),
),
],
)
: IconButton(
onPressed: () => setState(() => _confirmDelete = list.id),
icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.onSurfaceVariant.withAlpha(77)),
),
onTap: () {
app.selectList(list.id);
Navigator.pop(context);
},
);
}),
if (_showNewList)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _newListController,
autofocus: true,
decoration: const InputDecoration(hintText: 'List name'),
onSubmitted: (_) => _createList(),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _createList,
child: const Text('Add'),
),
],
),
)
else
TextButton.icon(
onPressed: () => setState(() => _showNewList = true),
icon: const Icon(Icons.add, size: 18),
label: const Text('New list'),
),
],
),
);
}
void _createList() {
final name = _newListController.text.trim();
if (name.isEmpty) return;
context.read<AppProvider>().createList(name);
_newListController.clear();
setState(() => _showNewList = false);
Navigator.pop(context);
}
}

View file

@ -1,59 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
class NewTaskBar extends StatefulWidget {
const NewTaskBar({super.key});
@override
State<NewTaskBar> createState() => _NewTaskBarState();
}
class _NewTaskBarState extends State<NewTaskBar> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _submit() {
final title = _controller.text.trim();
if (title.isEmpty) return;
context.read<AppProvider>().createTask(title);
_controller.clear();
}
@override
Widget build(BuildContext context) {
final app = context.watch<AppProvider>();
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: theme.dividerTheme.color ?? theme.dividerColor)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
enabled: app.activeListId != null,
decoration: InputDecoration(
hintText: app.activeListId != null ? 'Add a task...' : 'Select a list first',
),
onSubmitted: (_) => _submit(),
),
),
const SizedBox(width: 8),
FloatingActionButton.small(
onPressed: _submit,
child: const Icon(Icons.add),
),
],
),
);
}
}

View file

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../src/rust/api.dart' as api;
import '../providers/app_provider.dart';
class TaskItem extends StatelessWidget {
final api.BridgeTask task;
const TaskItem({super.key, required this.task});
bool get _isCompleted => task.status == 'completed';
String _formatDate(String iso) {
final d = DateTime.tryParse(iso);
if (d == null) return iso;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final taskDate = DateTime(d.year, d.month, d.day);
if (taskDate == today) return 'Today';
if (taskDate == today.add(const Duration(days: 1))) return 'Tomorrow';
const months = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return '${months[d.month]} ${d.day}';
}
@override
Widget build(BuildContext context) {
final app = context.read<AppProvider>();
final theme = Theme.of(context);
return Dismissible(
key: ValueKey(task.id),
direction: _isCompleted ? DismissDirection.startToEnd : DismissDirection.endToStart,
background: Container(
color: theme.colorScheme.primary,
alignment: _isCompleted ? Alignment.centerLeft : Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
_isCompleted ? 'Undo' : 'Complete',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
),
),
confirmDismiss: (_) async {
await app.toggleTask(task.id);
return false; // Don't remove widget — loadTasks handles the rebuild
},
child: InkWell(
onTap: () => _showEditSheet(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Checkbox
GestureDetector(
onTap: () => app.toggleTask(task.id),
child: Container(
width: 22,
height: 22,
margin: const EdgeInsets.only(top: 1),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: _isCompleted
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant.withAlpha(102),
width: 2,
),
color: _isCompleted ? theme.colorScheme.primary : null,
),
child: _isCompleted
? const Icon(Icons.check, size: 14, color: Colors.white)
: null,
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: _isCompleted ? null : FontWeight.w500,
decoration: _isCompleted ? TextDecoration.lineThrough : null,
color: _isCompleted
? theme.colorScheme.onSurfaceVariant.withAlpha(128)
: null,
),
),
if (task.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
task.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(102),
),
),
),
if (task.dueDate != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: theme.dividerTheme.color ?? theme.dividerColor,
),
),
child: Text(
_formatDate(task.dueDate!),
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withAlpha(128),
),
),
),
),
],
),
),
// Delete button
IconButton(
onPressed: () => app.deleteTask(task.id),
icon: Icon(
Icons.close,
size: 16,
color: theme.colorScheme.onSurfaceVariant.withAlpha(60),
),
visualDensity: VisualDensity.compact,
),
],
),
),
),
);
}
void _showEditSheet(BuildContext context) {
final app = context.read<AppProvider>();
final titleController = TextEditingController(text: task.title);
final descController = TextEditingController(text: task.description);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (ctx) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(ctx).viewInsets.bottom,
left: 16,
right: 16,
top: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Theme.of(ctx).dividerColor,
),
),
const SizedBox(height: 16),
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
enabled: !_isCompleted,
),
const SizedBox(height: 12),
TextField(
controller: descController,
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 3,
enabled: !_isCompleted,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _isCompleted
? null
: () {
app.updateTask(task.id, titleController.text.trim(), descController.text);
Navigator.pop(ctx);
},
child: const Text('Save'),
),
],
),
const SizedBox(height: 16),
],
),
);
},
);
}
}

View file

@ -1,490 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
version: "8.3.7"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_rust_bridge:
dependency: "direct main"
description:
name: flutter_rust_bridge
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
url: "https://pub.dev"
source: hosted
version: "1.0.2"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

View file

@ -1,26 +0,0 @@
name: bevy_tasks
description: A cross-platform task management app built with Flutter and Rust.
publish_to: "none"
version: 0.1.0
environment:
sdk: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
flutter_rust_bridge: ^2.0.0
google_fonts: ^6.1.0
provider: ^6.1.0
path_provider: ^2.1.0
file_picker: ^8.0.0
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true

View file

@ -1,16 +0,0 @@
[package]
name = "bevy-tasks-flutter-bridge"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.11.1"
bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["full"] }

View file

@ -1,323 +0,0 @@
use std::path::PathBuf;
use std::sync::Mutex;
use bevy_tasks_core::{
config::{AppConfig, WorkspaceConfig},
models::{Task, TaskList, TaskStatus},
repository::TaskRepository,
sync::{self, SyncMode, SyncResult as CoreSyncResult},
webdav,
};
// ── Bridge types ─────────────────────────────────────────────────────
/// Flat task struct for FFI transport.
pub struct BridgeTask {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub due_date: Option<String>,
pub created_at: String,
pub updated_at: String,
pub parent_id: Option<String>,
}
/// Flat list struct for FFI transport.
pub struct BridgeTaskList {
pub id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub group_by_due_date: bool,
}
/// Flat workspace config for FFI transport.
pub struct BridgeWorkspace {
pub name: String,
pub path: String,
pub webdav_url: Option<String>,
pub last_sync: Option<String>,
}
/// Flat app config for FFI transport.
pub struct BridgeConfig {
pub workspaces: Vec<BridgeWorkspace>,
pub current_workspace: Option<String>,
}
/// Sync result for FFI transport.
pub struct BridgeSyncResult {
pub uploaded: u32,
pub downloaded: u32,
pub deleted_local: u32,
pub deleted_remote: u32,
pub conflicts: u32,
pub errors: Vec<String>,
}
// ── Conversion helpers ───────────────────────────────────────────────
fn task_to_bridge(t: &Task) -> BridgeTask {
BridgeTask {
id: t.id.to_string(),
title: t.title.clone(),
description: t.description.clone(),
status: match t.status {
TaskStatus::Backlog => "backlog".into(),
TaskStatus::Completed => "completed".into(),
},
due_date: t.due_date.map(|d| d.to_rfc3339()),
created_at: t.created_at.to_rfc3339(),
updated_at: t.updated_at.to_rfc3339(),
parent_id: t.parent_id.map(|id| id.to_string()),
}
}
fn list_to_bridge(l: &TaskList) -> BridgeTaskList {
BridgeTaskList {
id: l.id.to_string(),
title: l.title.clone(),
created_at: l.created_at.to_rfc3339(),
updated_at: l.updated_at.to_rfc3339(),
group_by_due_date: l.group_by_due_date,
}
}
fn config_to_bridge(c: &AppConfig) -> BridgeConfig {
BridgeConfig {
workspaces: c
.workspaces
.iter()
.map(|(name, ws)| BridgeWorkspace {
name: name.clone(),
path: ws.path.to_string_lossy().into_owned(),
webdav_url: ws.webdav_url.clone(),
last_sync: ws.last_sync.map(|d| d.to_rfc3339()),
})
.collect(),
current_workspace: c.current_workspace.clone(),
}
}
// ── Global state ─────────────────────────────────────────────────────
static STATE: Mutex<Option<AppState>> = Mutex::new(None);
struct AppState {
config: AppConfig,
repo: Option<TaskRepository>,
}
fn with_state<T>(f: impl FnOnce(&mut AppState) -> Result<T, String>) -> Result<T, String> {
let mut guard = STATE.lock().map_err(|e| e.to_string())?;
let state = guard.as_mut().ok_or("App not initialized")?;
f(state)
}
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
if state.repo.is_some() {
return Ok(());
}
let (_name, ws) = state.config.get_current_workspace().map_err(|e| e.to_string())?;
let repo = TaskRepository::new(ws.path.clone()).map_err(|e| e.to_string())?;
state.repo = Some(repo);
Ok(())
}
// ── Public API (flutter_rust_bridge will generate Dart bindings) ─────
/// Initialize the bridge. Must be called once at app startup.
pub fn init_app() -> Result<BridgeConfig, String> {
let config_path = AppConfig::get_config_path();
let config = AppConfig::load_from_file(&config_path).unwrap_or_default();
let bridge_config = config_to_bridge(&config);
let mut guard = STATE.lock().map_err(|e| e.to_string())?;
*guard = Some(AppState { config, repo: None });
Ok(bridge_config)
}
pub fn get_config() -> Result<BridgeConfig, String> {
with_state(|s| Ok(config_to_bridge(&s.config)))
}
pub fn add_workspace(name: String, path: String) -> Result<(), String> {
// Init workspace on disk
TaskRepository::init(PathBuf::from(&path))
.map(|_| ())
.map_err(|e| e.to_string())?;
with_state(|s| {
let ws = WorkspaceConfig::new(PathBuf::from(&path));
s.config.add_workspace(name.clone(), ws);
s.config.set_current_workspace(name).map_err(|e| e.to_string())?;
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
})
}
pub fn set_current_workspace(name: String) -> Result<(), String> {
with_state(|s| {
s.config.set_current_workspace(name).map_err(|e| e.to_string())?;
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
})
}
pub fn remove_workspace(name: String) -> Result<(), String> {
with_state(|s| {
s.config.remove_workspace(&name);
s.repo = None;
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
})
}
pub fn get_lists() -> Result<Vec<BridgeTaskList>, String> {
with_state(|s| {
ensure_repo(s)?;
s.repo
.as_ref()
.unwrap()
.get_lists()
.map(|lists| lists.iter().map(|l| list_to_bridge(l)).collect())
.map_err(|e| e.to_string())
})
}
pub fn create_list(name: String) -> Result<BridgeTaskList, String> {
with_state(|s| {
ensure_repo(s)?;
s.repo
.as_mut()
.unwrap()
.create_list(name)
.map(|l| list_to_bridge(&l))
.map_err(|e| e.to_string())
})
}
pub fn delete_list(list_id: String) -> Result<(), String> {
with_state(|s| {
ensure_repo(s)?;
let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().delete_list(id).map_err(|e| e.to_string())
})
}
pub fn list_tasks(list_id: String) -> Result<Vec<BridgeTask>, String> {
with_state(|s| {
ensure_repo(s)?;
let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo
.as_ref()
.unwrap()
.list_tasks(id)
.map(|tasks| tasks.iter().map(|t| task_to_bridge(t)).collect())
.map_err(|e| e.to_string())
})
}
pub fn create_task(list_id: String, title: String) -> Result<BridgeTask, String> {
with_state(|s| {
ensure_repo(s)?;
let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let task = Task::new(title);
s.repo
.as_mut()
.unwrap()
.create_task(id, task)
.map(|t| task_to_bridge(&t))
.map_err(|e| e.to_string())
})
}
pub fn toggle_task(list_id: String, task_id: String) -> Result<BridgeTask, String> {
with_state(|s| {
ensure_repo(s)?;
let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
let repo = s.repo.as_mut().unwrap();
let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?;
match task.status {
TaskStatus::Backlog => task.complete(),
TaskStatus::Completed => task.uncomplete(),
}
repo.update_task(lid, task.clone()).map_err(|e| e.to_string())?;
Ok(task_to_bridge(&task))
})
}
pub fn update_task(list_id: String, task_id: String, title: String, description: String) -> Result<(), String> {
with_state(|s| {
ensure_repo(s)?;
let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
let repo = s.repo.as_mut().unwrap();
let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?;
task.title = title;
task.description = description;
repo.update_task(lid, task).map_err(|e| e.to_string())
})
}
pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
with_state(|s| {
ensure_repo(s)?;
let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().delete_task(lid, tid).map_err(|e| e.to_string())
})
}
pub fn reorder_task(list_id: String, task_id: String, new_position: usize) -> Result<(), String> {
with_state(|s| {
ensure_repo(s)?;
let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().reorder_task(lid, tid, new_position).map_err(|e| e.to_string())
})
}
pub fn set_webdav_config(workspace_name: String, webdav_url: String) -> Result<(), String> {
with_state(|s| {
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
ws.webdav_url = Some(webdav_url);
}
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
})
}
pub fn store_webdav_credentials(domain: String, username: String, password: String) -> Result<(), String> {
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
}
pub async fn sync_workspace_bridge(
workspace_path: String,
webdav_url: String,
username: String,
password: String,
) -> Result<BridgeSyncResult, String> {
let result = sync::sync_workspace(
&PathBuf::from(workspace_path),
&webdav_url,
&username,
&password,
SyncMode::Full,
None,
)
.await
.map_err(|e| e.to_string())?;
Ok(BridgeSyncResult {
uploaded: result.uploaded,
downloaded: result.downloaded,
deleted_local: result.deleted_local,
deleted_remote: result.deleted_remote,
conflicts: result.conflicts,
errors: result.errors,
})
}

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
pub mod api;
mod frb_generated;

View file

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bevy_tasks/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View file

@ -1,17 +0,0 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View file

@ -1,122 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(bevy_tasks LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "bevy_tasks")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Rust bridge library ===
# Build the Rust bridge crate and install the DLL alongside the app.
set(RUST_BRIDGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust")
set(RUST_LIB_NAME "bevy_tasks_flutter_bridge")
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "Profile")
set(RUST_TARGET_DIR "${RUST_BRIDGE_DIR}/target/release")
else()
set(RUST_TARGET_DIR "${RUST_BRIDGE_DIR}/target/debug")
endif()
# Add the Rust DLL to the bundled libraries list so it gets installed
list(APPEND PLUGIN_BUNDLED_LIBRARIES "${RUST_TARGET_DIR}/${RUST_LIB_NAME}.dll")
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View file

@ -1,109 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View file

@ -1,11 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void RegisterPlugins(flutter::PluginRegistry* registry) {
}

View file

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -1,23 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -1,40 +0,0 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

View file

@ -1,121 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.bevytasks" "\0"
VALUE "FileDescription", "bevy_tasks" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "bevy_tasks" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.bevytasks. All rights reserved." "\0"
VALUE "OriginalFilename", "bevy_tasks.exe" "\0"
VALUE "ProductName", "bevy_tasks" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View file

@ -1 +0,0 @@
// Nothing when using full_dep=false mode

View file

@ -1,71 +0,0 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

View file

@ -1,33 +0,0 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View file

@ -1,43 +0,0 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"bevy_tasks", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}

View file

@ -1,16 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View file

@ -1,65 +0,0 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}

View file

@ -1,19 +0,0 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View file

@ -1,288 +0,0 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

View file

@ -1,102 +0,0 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_

View file

@ -194,12 +194,16 @@ fn list_tasks(
fn create_task(
list_id: String,
title: String,
description: Option<String>,
state: State<'_, Mutex<AppState>>,
) -> Result<Task, String> {
let mut s = state.lock().unwrap();
ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let task = Task::new(title);
let mut task = Task::new(title);
if let Some(desc) = description.filter(|d| !d.is_empty()) {
task.description = desc;
}
s.repo
.as_mut()
.unwrap()

View file

@ -5,7 +5,7 @@
"identifier": "com.bevytasks.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:1422",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},

View file

@ -3,18 +3,18 @@
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary: #2d87b8;
--color-primary-hover: #2474a0;
--color-surface-light: #ffffff;
--color-surface-dark: #121212;
--color-surface-dark: #242424;
--color-card-light: #f9fafb;
--color-card-dark: #1e1e1e;
--color-card-dark: #303030;
--color-text-light: #1f2937;
--color-text-dark: #e5e7eb;
--color-text-secondary-light: #6b7280;
--color-text-secondary-dark: #9ca3af;
--color-border-light: #e5e7eb;
--color-border-dark: #374151;
--color-border-dark: #3d3d3d;
--color-danger: #ef4444;
}

View file

@ -1,37 +1,81 @@
<script lang="ts" module>
// Shared state accessible from outside
export const newTaskState = $state({ open: false });
</script>
<script lang="ts">
import { app } from "../stores/app.svelte";
let title = $state("");
let description = $state("");
let inputEl = $state<HTMLInputElement | null>(null);
async function handleSubmit() {
if (!title.trim()) return;
await app.createTask(title.trim());
await app.createTask(title.trim(), description.trim() || undefined);
title = "";
description = "";
newTaskState.open = false;
}
function handleClose() {
newTaskState.open = false;
title = "";
description = "";
}
$effect(() => {
if (newTaskState.open) {
requestAnimationFrame(() => inputEl?.focus());
}
});
</script>
<!-- Backdrop -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="border-t border-border-light bg-surface-light px-4 py-3 dark:border-border-dark dark:bg-surface-dark"
class="absolute inset-0 z-40 transition-opacity duration-250 ease-out {newTaskState.open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
style="background: rgba(0,0,0,0.4)"
onclick={handleClose}
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
></div>
<!-- Toast input sheet -->
<div
class="pointer-events-auto absolute bottom-0 left-0 right-0 z-50 rounded-t-2xl bg-surface-light shadow-xl transition-all duration-250 ease-out dark:bg-card-dark {newTaskState.open ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0 pointer-events-none'}"
>
<form
onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}
class="flex items-center gap-2"
>
<input
type="text"
bind:value={title}
placeholder={app.activeListId ? "Add a task…" : "Select a list first"}
disabled={!app.activeListId}
class="min-w-0 flex-1 rounded-xl border border-border-light bg-card-light px-4 py-2.5 text-sm outline-none placeholder:opacity-40 focus:border-primary disabled:opacity-30 dark:border-border-dark dark:bg-card-dark"
/>
<button
type="submit"
disabled={!title.trim() || !app.activeListId}
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-transform hover:scale-105 active:scale-95 disabled:opacity-40 disabled:shadow-none"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
</svg>
</button>
</form>
<div class="px-4 pb-4 pt-3">
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input
bind:this={inputEl}
type="text"
bind:value={title}
placeholder="New task"
class="w-full border-none bg-transparent text-base font-medium outline-none placeholder:opacity-40"
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
/>
<input
type="text"
bind:value={description}
placeholder="Add details"
class="mt-2 w-full border-none bg-transparent text-sm outline-none placeholder:opacity-40"
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
/>
</form>
<div class="mt-3 flex items-center justify-between">
<button class="opacity-40 hover:opacity-70" title="Set due date">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
</button>
<button
onclick={handleSubmit}
disabled={!title.trim()}
class="text-sm font-medium text-primary disabled:opacity-30"
>
Save
</button>
</div>
</div>
</div>

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { app } from "../stores/app.svelte";
import TaskItem from "../components/TaskItem.svelte";
import BottomSheet from "../components/BottomSheet.svelte";
import NewTaskInput from "../components/NewTaskInput.svelte";
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
let showListSheet = $state(false);
let showDrawer = $state(false);
let showNewList = $state(false);
let newListName = $state("");
let showCompleted = $state(true);
@ -15,134 +14,44 @@
await app.createList(newListName.trim());
newListName = "";
showNewList = false;
showListSheet = false;
showDrawer = false;
}
async function handleDeleteList(id: string) {
await app.deleteList(id);
confirmDeleteList = null;
showListSheet = false;
showDrawer = false;
}
function closeDrawer() {
showDrawer = false;
showNewList = false;
confirmDeleteList = null;
}
</script>
<!-- Header -->
<header
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
<!-- Sliding container: drawer + main content move as one piece -->
<div
class="flex h-screen transition-transform duration-250 ease-out"
style="width: calc(100vw + 18rem); transform: translateX({showDrawer ? '0' : '-18rem'})"
>
<div class="min-w-0 flex-1">
<button
onclick={() => (showListSheet = true)}
class="flex items-center gap-1 text-lg font-bold"
>
{app.activeList?.title ?? "Tasks"}
<svg class="h-4 w-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
/>
</svg>
</button>
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
{app.config?.current_workspace ?? ""}
</p>
</div>
<div class="flex items-center gap-2">
{#if app.syncing}
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
{/if}
<button
onclick={() => app.toggleDarkMode()}
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
title="Toggle theme"
>
{#if app.darkMode}
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
</svg>
{:else}
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
{/if}
</button>
<button
onclick={() => app.setScreen("settings")}
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
title="Settings"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</header>
<!-- Task list -->
<main class="flex-1 overflow-y-auto" style="height: calc(100vh - 57px - 72px)">
{#if app.lists.length === 0}
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
<p class="text-lg font-medium opacity-60">No lists yet</p>
<p class="mt-1 text-sm opacity-40">Tap the list name above to create one</p>
<!-- Drawer panel (always rendered, sits to the left) -->
<div class="flex h-full w-72 shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
<!-- Drawer header -->
<div class="border-b border-border-light px-4 py-4 dark:border-border-dark">
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
{app.config?.current_workspace ?? ""}
</p>
<h2 class="text-lg font-bold">Lists</h2>
</div>
{:else if !app.activeListId}
<div class="flex h-full items-center justify-center opacity-40">
Select a list
</div>
{:else}
<!-- Pending tasks -->
{#each app.pendingTasks as task (task.id)}
<TaskItem {task} />
{/each}
{#if app.pendingTasks.length === 0}
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div>
{/if}
<!-- Completed section -->
{#if app.completedTasks.length > 0}
<button
onclick={() => (showCompleted = !showCompleted)}
class="flex w-full items-center gap-2 border-t border-border-light px-4 py-3 text-sm font-medium text-text-secondary-light dark:border-border-dark dark:text-text-secondary-dark"
>
<svg
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
/>
</svg>
Completed ({app.completedTasks.length})
</button>
{#if showCompleted}
{#each app.completedTasks as task (task.id)}
<TaskItem {task} />
{/each}
{/if}
{/if}
{/if}
</main>
<!-- New task input (bottom bar) -->
<NewTaskInput />
<!-- List picker bottom sheet -->
{#if showListSheet}
<BottomSheet onclose={() => { showListSheet = false; showNewList = false; }}>
<div class="px-4 pb-2 pt-4">
<h2 class="mb-3 text-lg font-bold">Lists</h2>
<!-- List items -->
<div class="flex-1 overflow-y-auto py-2">
{#each app.lists as list (list.id)}
<div class="flex items-center">
<div class="flex items-center px-2">
<button
onclick={() => { app.selectList(list.id); showListSheet = false; }}
class="flex-1 rounded-lg px-3 py-2.5 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10 {list.id === app.activeListId ? 'font-bold text-primary' : ''}"
onclick={() => { app.selectList(list.id); closeDrawer(); }}
class="flex-1 rounded-lg px-3 py-2.5 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10 {list.id === app.activeListId ? 'font-bold text-primary bg-primary/5' : ''}"
>
{list.title}
</button>
@ -176,9 +85,12 @@
{/if}
</div>
{/each}
</div>
<!-- New list button at bottom -->
<div class="border-t border-border-light px-2 py-2 dark:border-border-dark">
{#if showNewList}
<div class="mt-2 flex gap-2">
<div class="flex gap-2 px-2">
<input
type="text"
bind:value={newListName}
@ -197,11 +109,130 @@
{:else}
<button
onclick={() => (showNewList = true)}
class="mt-2 w-full rounded-lg px-3 py-2.5 text-left text-sm text-primary hover:bg-primary/5"
class="w-full rounded-lg px-3 py-2.5 text-left text-sm text-primary hover:bg-primary/5"
>
+ New list
</button>
{/if}
</div>
</BottomSheet>
{/if}
</div>
<!-- Main content panel -->
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
<!-- Dim overlay + shadow when drawer is open -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute inset-0 z-30 transition-opacity duration-250 ease-out {showDrawer ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
style="box-shadow: inset 8px 0 24px rgba(0,0,0,0.4); background: rgba(0,0,0,0.4)"
onclick={closeDrawer}
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
></div>
<!-- Header -->
<header
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
>
<div class="min-w-0 flex-1">
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
{app.config?.current_workspace ?? ""}
</p>
<button
onclick={() => (showDrawer = !showDrawer)}
class="flex items-center gap-1 text-lg font-bold"
>
{app.activeList?.title ?? "Tasks"}
<svg class="h-4 w-4 opacity-50 transition-transform {showDrawer ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
/>
</svg>
</button>
</div>
<div class="flex items-center gap-2">
{#if app.syncing}
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
{/if}
<button
onclick={() => app.setScreen("settings")}
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
title="Settings"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</header>
<!-- Task list -->
<main class="flex-1 overflow-y-auto">
{#if app.lists.length === 0}
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
<p class="text-lg font-medium opacity-60">No lists yet</p>
<p class="mt-1 text-sm opacity-40">Tap the list name above to create one</p>
</div>
{:else if !app.activeListId}
<div class="flex h-full items-center justify-center opacity-40">
Select a list
</div>
{:else}
{#each app.pendingTasks as task (task.id)}
<TaskItem {task} />
{/each}
{#if app.pendingTasks.length === 0}
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div>
{/if}
{#if app.completedTasks.length > 0}
<button
onclick={() => (showCompleted = !showCompleted)}
class="flex w-full items-center gap-2 border-t border-border-light px-4 py-3 text-sm font-medium text-text-secondary-light dark:border-border-dark dark:text-text-secondary-dark"
>
<svg
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
/>
</svg>
Completed ({app.completedTasks.length})
</button>
{#if showCompleted}
{#each app.completedTasks as task (task.id)}
<TaskItem {task} />
{/each}
{/if}
{/if}
{/if}
</main>
<!-- FAB button, slides with main content -->
<div
class="pointer-events-none absolute bottom-6 left-0 right-0 z-30 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
>
<button
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
disabled={!app.activeListId}
class="pointer-events-auto flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-transform hover:scale-105 active:scale-95 disabled:opacity-40 disabled:shadow-none"
>
<svg class="h-7 w-7" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
</svg>
</button>
</div>
</div>
</div>
<!-- Toast overlay (outside sliding container so it stays centered) -->
<div class="pointer-events-none fixed inset-0 z-50">
<NewTaskInput />
</div>

View file

@ -140,12 +140,13 @@ async function deleteList(id: string) {
}
}
async function createTask(title: string) {
async function createTask(title: string, description?: string) {
if (!activeListId) return;
try {
const task = await invoke<Task>("create_task", {
listId: activeListId,
title,
description: description ?? "",
});
tasks = [...tasks, task];
error = null;

View file

@ -8,7 +8,7 @@ export default defineConfig({
plugins: [svelte(), tailwindcss()],
clearScreen: false,
server: {
port: 1420,
port: 1422,
strictPort: true,
host: host || false,
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,

View file

@ -1,12 +0,0 @@
[package]
name = "bevy-tasks-gui"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy-tasks-core = { path = "../bevy-tasks-core" }
anyhow = { workspace = true }
# GUI dependencies (Phase 3+)
# eframe = "0.31"
# egui = "0.31"

View file

@ -1,7 +0,0 @@
// GUI implementation (Phase 3+)
// This is a placeholder for future development
fn main() {
println!("GUI is not yet implemented. Use the CLI for now:");
println!(" bevy-tasks --help");
}