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 ## 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 ## Build & Test Commands
@ -14,17 +14,22 @@ cargo build -p bevy-tasks-cli # Build CLI only
cargo test # Run all tests cargo test # Run all tests
cargo test -p bevy-tasks-core # Run core library tests only cargo test -p bevy-tasks-core # Run core library tests only
cargo run -p bevy-tasks-cli -- <args> # Run CLI with arguments 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). The CLI binary is named `bevy-tasks` (from the `bevy-tasks-cli` crate).
## Architecture ## 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-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-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 ### Key patterns

8
Cargo.lock generated
View file

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

View file

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

175
PLAN.md
View file

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

View file

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

View file

@ -3,18 +3,18 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--color-primary: #2563eb; --color-primary: #2d87b8;
--color-primary-hover: #1d4ed8; --color-primary-hover: #2474a0;
--color-surface-light: #ffffff; --color-surface-light: #ffffff;
--color-surface-dark: #121212; --color-surface-dark: #242424;
--color-card-light: #f9fafb; --color-card-light: #f9fafb;
--color-card-dark: #1e1e1e; --color-card-dark: #303030;
--color-text-light: #1f2937; --color-text-light: #1f2937;
--color-text-dark: #e5e7eb; --color-text-dark: #e5e7eb;
--color-text-secondary-light: #6b7280; --color-text-secondary-light: #6b7280;
--color-text-secondary-dark: #9ca3af; --color-text-secondary-dark: #9ca3af;
--color-border-light: #e5e7eb; --color-border-light: #e5e7eb;
--color-border-dark: #374151; --color-border-dark: #3d3d3d;
--color-danger: #ef4444; --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"> <script lang="ts">
import { app } from "../stores/app.svelte"; import { app } from "../stores/app.svelte";
let title = $state(""); let title = $state("");
let description = $state("");
let inputEl = $state<HTMLInputElement | null>(null);
async function handleSubmit() { async function handleSubmit() {
if (!title.trim()) return; if (!title.trim()) return;
await app.createTask(title.trim()); await app.createTask(title.trim(), description.trim() || undefined);
title = ""; title = "";
description = "";
newTaskState.open = false;
} }
function handleClose() {
newTaskState.open = false;
title = "";
description = "";
}
$effect(() => {
if (newTaskState.open) {
requestAnimationFrame(() => inputEl?.focus());
}
});
</script> </script>
<!-- Backdrop -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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 <div class="px-4 pb-4 pt-3">
onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
class="flex items-center gap-2" <input
> bind:this={inputEl}
<input type="text"
type="text" bind:value={title}
bind:value={title} placeholder="New task"
placeholder={app.activeListId ? "Add a task…" : "Select a list first"} class="w-full border-none bg-transparent text-base font-medium outline-none placeholder:opacity-40"
disabled={!app.activeListId} onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
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" />
/> <input
<button type="text"
type="submit" bind:value={description}
disabled={!title.trim() || !app.activeListId} placeholder="Add details"
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" class="mt-2 w-full border-none bg-transparent text-sm outline-none placeholder:opacity-40"
> onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
<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" /> </form>
</svg>
</button> <div class="mt-3 flex items-center justify-between">
</form> <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> </div>

View file

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { app } from "../stores/app.svelte"; import { app } from "../stores/app.svelte";
import TaskItem from "../components/TaskItem.svelte"; import TaskItem from "../components/TaskItem.svelte";
import BottomSheet from "../components/BottomSheet.svelte"; import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
import NewTaskInput from "../components/NewTaskInput.svelte";
let showListSheet = $state(false); let showDrawer = $state(false);
let showNewList = $state(false); let showNewList = $state(false);
let newListName = $state(""); let newListName = $state("");
let showCompleted = $state(true); let showCompleted = $state(true);
@ -15,134 +14,44 @@
await app.createList(newListName.trim()); await app.createList(newListName.trim());
newListName = ""; newListName = "";
showNewList = false; showNewList = false;
showListSheet = false; showDrawer = false;
} }
async function handleDeleteList(id: string) { async function handleDeleteList(id: string) {
await app.deleteList(id); await app.deleteList(id);
confirmDeleteList = null; confirmDeleteList = null;
showListSheet = false; showDrawer = false;
}
function closeDrawer() {
showDrawer = false;
showNewList = false;
confirmDeleteList = null;
} }
</script> </script>
<!-- Header --> <!-- Sliding container: drawer + main content move as one piece -->
<header <div
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark" 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"> <!-- Drawer panel (always rendered, sits to the left) -->
<button <div class="flex h-full w-72 shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
onclick={() => (showListSheet = true)} <!-- Drawer header -->
class="flex items-center gap-1 text-lg font-bold" <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.activeList?.title ?? "Tasks"} {app.config?.current_workspace ?? ""}
<svg class="h-4 w-4 opacity-50" viewBox="0 0 20 20" fill="currentColor"> </p>
<path <h2 class="text-lg font-bold">Lists</h2>
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>
</div> </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} <!-- List items -->
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div> <div class="flex-1 overflow-y-auto py-2">
{/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>
{#each app.lists as list (list.id)} {#each app.lists as list (list.id)}
<div class="flex items-center"> <div class="flex items-center px-2">
<button <button
onclick={() => { app.selectList(list.id); showListSheet = false; }} 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' : ''}" 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} {list.title}
</button> </button>
@ -176,9 +85,12 @@
{/if} {/if}
</div> </div>
{/each} {/each}
</div>
<!-- New list button at bottom -->
<div class="border-t border-border-light px-2 py-2 dark:border-border-dark">
{#if showNewList} {#if showNewList}
<div class="mt-2 flex gap-2"> <div class="flex gap-2 px-2">
<input <input
type="text" type="text"
bind:value={newListName} bind:value={newListName}
@ -197,11 +109,130 @@
{:else} {:else}
<button <button
onclick={() => (showNewList = true)} 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 + New list
</button> </button>
{/if} {/if}
</div> </div>
</BottomSheet> </div>
{/if}
<!-- 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; if (!activeListId) return;
try { try {
const task = await invoke<Task>("create_task", { const task = await invoke<Task>("create_task", {
listId: activeListId, listId: activeListId,
title, title,
description: description ?? "",
}); });
tasks = [...tasks, task]; tasks = [...tasks, task];
error = null; error = null;

View file

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