Merge pull request #5 from SteelDynamite/tauri-gui-redesign
tauri-gui-redesign
This commit is contained in:
commit
e5c78ddfde
56
CLAUDE.md
56
CLAUDE.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
Bevy Tasks is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. Currently in Phase 1 (Core Library & CLI MVP). The GUI crate is a placeholder for Phase 3+.
|
||||
Bevy Tasks is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. The GUI uses Tauri v2 (Svelte 5 + Tailwind CSS 4) in `apps/tauri/`.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
|
|
@ -14,17 +14,24 @@ cargo build -p bevy-tasks-cli # Build CLI only
|
|||
cargo test # Run all tests
|
||||
cargo test -p bevy-tasks-core # Run core library tests only
|
||||
cargo run -p bevy-tasks-cli -- <args> # Run CLI with arguments
|
||||
|
||||
# Tauri GUI
|
||||
cd apps/tauri && npm install # Install frontend dependencies
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev # Run Tauri in dev mode (Wayland)
|
||||
npm run tauri build # Build for production
|
||||
```
|
||||
|
||||
The CLI binary is named `bevy-tasks` (from the `bevy-tasks-cli` crate).
|
||||
|
||||
The Tauri dev server runs on port 1422 (`vite.config.ts` and `tauri.conf.json`).
|
||||
|
||||
## Architecture
|
||||
|
||||
Three-crate workspace (`resolver = "2"`, edition 2021):
|
||||
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
||||
|
||||
- **bevy-tasks-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
||||
- **bevy-tasks-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
||||
- **bevy-tasks-gui** — Placeholder for future egui/eframe GUI.
|
||||
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `bevy-tasks-core`.
|
||||
|
||||
### Key patterns
|
||||
|
||||
|
|
@ -36,6 +43,47 @@ Three-crate workspace (`resolver = "2"`, edition 2021):
|
|||
|
||||
Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.metadata.json` for list ordering.
|
||||
|
||||
### Tauri GUI structure
|
||||
|
||||
The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`). Key UI patterns:
|
||||
|
||||
- **Sliding drawer**: Left panel (lists) slides with main content as one piece via `translateX`. 80vw wide.
|
||||
- **Settings popup**: Floating overlay card with backdrop, not a sliding panel.
|
||||
- **Workspace switcher**: Custom drop-up menu in drawer footer (left), settings gear (right).
|
||||
- **Task animations**: Grid-rows `0fr`/`1fr` trick for smooth collapse/expand. Module-level `animateInIds` Set coordinates expand-in after toggle.
|
||||
- **Inline editing**: Click task to edit, auto-save on blur, shared `editingTaskId` across instances.
|
||||
- **Kebab menus**: Tasks, lists, and workspaces all use kebab → submenu pattern for delete.
|
||||
- **New task**: FAB button opens bottom toast sheet (outside sliding container for fixed positioning).
|
||||
|
||||
### Current state (2026-03-29)
|
||||
|
||||
- **Phase 1** (Core + CLI): Complete
|
||||
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI partially wired (empty credentials issue)
|
||||
- **Phase 3** (GUI MVP): In progress — core task CRUD working, UI polished with animations
|
||||
|
||||
### GUI features done
|
||||
|
||||
- Task CRUD (create, read, update, delete)
|
||||
- Task completion/restoration with animated transitions
|
||||
- Drag-and-drop task reordering
|
||||
- Inline task editing (auto-save on blur)
|
||||
- Sliding lists drawer with checkmark selection
|
||||
- Settings popup overlay
|
||||
- Workspace switcher drop-up with add/remove
|
||||
- Dark mode (GNOME-style neutral grays, cyan-blue accent)
|
||||
- Completed tasks section with animated show/hide
|
||||
|
||||
### GUI features NOT yet done (CLI has these)
|
||||
|
||||
- Due date editing (model supports it, not exposed in UI)
|
||||
- WebDAV setup flow (GUI passes empty credentials)
|
||||
- Push-only / pull-only sync modes
|
||||
- Sync status view
|
||||
- Workspace retarget/migrate
|
||||
- Group-by-due-date toggle
|
||||
- Subtask hierarchy (data model exists, not used anywhere)
|
||||
- List/workspace rename
|
||||
|
||||
## Roadmap
|
||||
|
||||
See `PLAN.md` for the 7-phase roadmap. Phase 1 is complete. Detailed API docs in `docs/API.md`, development practices in `docs/DEVELOPMENT.md`.
|
||||
See `PLAN.md` for the 7-phase roadmap. Detailed API docs in `docs/API.md`, development practices in `docs/DEVELOPMENT.md`.
|
||||
|
|
|
|||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -138,14 +138,6 @@ dependencies = [
|
|||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy-tasks-gui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bevy-tasks-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@
|
|||
members = [
|
||||
"crates/bevy-tasks-core",
|
||||
"crates/bevy-tasks-cli",
|
||||
"crates/bevy-tasks-gui",
|
||||
]
|
||||
exclude = [
|
||||
"apps/tauri/src-tauri",
|
||||
"apps/flutter/rust",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
221
PLAN.md
221
PLAN.md
|
|
@ -19,8 +19,9 @@ A **local-first, cross-platform tasks application** inspired by Google Tasks. Bu
|
|||
|
||||
## Resources
|
||||
|
||||
- [Bevy Documentation](https://bevyengine.org/)
|
||||
- [egui Documentation](https://docs.rs/egui/)
|
||||
- [Tauri Documentation](https://v2.tauri.app/)
|
||||
- [Svelte Documentation](https://svelte.dev/)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/)
|
||||
- [WebDAV RFC 4918](https://datatracker.ietf.org/doc/html/rfc4918)
|
||||
- [Google Tasks API](https://developers.google.com/tasks) (for importer reference)
|
||||
|
||||
|
|
@ -44,10 +45,11 @@ bevy-tasks/
|
|||
├── Cargo.toml # Workspace definition
|
||||
├── PLAN.md
|
||||
├── README.md
|
||||
├── apps/
|
||||
│ └── tauri/ # Tauri GUI (Svelte + Tailwind)
|
||||
├── crates/
|
||||
│ ├── bevy-tasks-core/ # Core library (backend)
|
||||
│ ├── bevy-tasks-cli/ # CLI frontend
|
||||
│ └── bevy-tasks-gui/ # GUI frontend (Phase 3+)
|
||||
│ └── bevy-tasks-cli/ # CLI frontend
|
||||
└── docs/
|
||||
```
|
||||
|
||||
|
|
@ -226,7 +228,9 @@ pub trait Storage {
|
|||
members = [
|
||||
"crates/bevy-tasks-core",
|
||||
"crates/bevy-tasks-cli",
|
||||
"crates/bevy-tasks-gui",
|
||||
]
|
||||
exclude = [
|
||||
"apps/tauri/src-tauri",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
@ -590,43 +594,50 @@ Workspace: shared
|
|||
|
||||
### Architecture
|
||||
|
||||
#### Frontend Framework: egui
|
||||
#### Frontend Framework: Tauri v2 + Svelte 5 + Tailwind CSS 4
|
||||
|
||||
**Decision**: Use egui (immediate mode GUI) for MVP
|
||||
**Decision**: Use Tauri v2 with Svelte and Tailwind for the GUI
|
||||
|
||||
**Why egui?**
|
||||
- Fast development with rich built-in widgets
|
||||
- Excellent text editing support out of the box
|
||||
- Small binary size (~2-3MB stripped)
|
||||
- Fast startup time (100-200ms target)
|
||||
- Mature and stable
|
||||
- Simple immediate-mode API
|
||||
- Cross-platform (desktop AND mobile)
|
||||
- Easy integration with `bevy-tasks-core`
|
||||
**Why Tauri?**
|
||||
- Native Rust backend — direct integration with `bevy-tasks-core`
|
||||
- Svelte 5 for reactive, performant UI with minimal boilerplate
|
||||
- Tailwind CSS 4 for rapid, consistent styling
|
||||
- Small binary size (~5-10MB)
|
||||
- Cross-platform (Windows, Linux, macOS; mobile in Tauri v2)
|
||||
- Web technologies for UI = rich ecosystem, easy to iterate
|
||||
- Tauri commands expose core library directly to the frontend
|
||||
|
||||
#### GUI Crate Structure
|
||||
#### GUI Structure
|
||||
|
||||
```
|
||||
crates/bevy-tasks-gui/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── main.rs # App entry point
|
||||
│ ├── app.rs # egui app setup
|
||||
│ ├── ui/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── screens/
|
||||
│ │ │ ├── task_list.rs
|
||||
│ │ │ ├── task_detail.rs
|
||||
│ │ │ └── settings.rs
|
||||
│ │ └── components/
|
||||
│ │ ├── task_item.rs
|
||||
│ │ ├── task_input.rs
|
||||
│ │ └── list_selector.rs
|
||||
│ └── state.rs # UI state management
|
||||
├── assets/
|
||||
│ ├── fonts/
|
||||
│ └── icons/
|
||||
└── README.md
|
||||
apps/tauri/
|
||||
├── package.json
|
||||
├── svelte.config.js
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
├── index.html
|
||||
├── src/ # Svelte frontend
|
||||
│ ├── main.ts
|
||||
│ ├── app.css
|
||||
│ ├── App.svelte
|
||||
│ └── lib/
|
||||
│ ├── screens/
|
||||
│ │ ├── TasksScreen.svelte
|
||||
│ │ ├── SettingsScreen.svelte
|
||||
│ │ └── SetupScreen.svelte
|
||||
│ ├── components/
|
||||
│ │ ├── TaskItem.svelte
|
||||
│ │ ├── NewTaskInput.svelte
|
||||
│ │ └── ListSelector.svelte
|
||||
│ └── stores/
|
||||
│ └── app.ts
|
||||
└── src-tauri/ # Rust backend (Tauri commands)
|
||||
├── Cargo.toml
|
||||
├── tauri.conf.json
|
||||
└── src/
|
||||
├── main.rs
|
||||
├── commands.rs # Tauri command handlers
|
||||
└── lib.rs
|
||||
```
|
||||
|
||||
#### First Run Experience
|
||||
|
|
@ -661,68 +672,62 @@ WorkspaceConfig {
|
|||
}
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
**bevy-tasks-gui/Cargo.toml**:
|
||||
```toml
|
||||
[package]
|
||||
name = "bevy-tasks-gui"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bevy-tasks-core = { path = "../bevy-tasks-core" }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# egui for Phase 3-6
|
||||
eframe = "0.31" # egui framework with native windowing
|
||||
egui = "0.31" # Core egui library
|
||||
```
|
||||
|
||||
### Performance Strategy
|
||||
|
||||
**Startup Sequence**:
|
||||
1. Initialize eframe window (< 50ms)
|
||||
2. Load config from disk (< 20ms)
|
||||
3. Render empty UI (first frame < 100ms)
|
||||
1. Initialize Tauri window + load Svelte app (< 100ms)
|
||||
2. Load config from disk via Tauri command (< 20ms)
|
||||
3. Render UI (first paint < 150ms)
|
||||
4. Load current task list in background
|
||||
5. Update UI as tasks load
|
||||
6. Start WebDAV sync in background (if configured)
|
||||
|
||||
**Target**: < 200ms cold start on desktop
|
||||
**Target**: < 300ms cold start on desktop
|
||||
|
||||
**Optimizations**:
|
||||
- Lazy data loading (load visible tasks first)
|
||||
- Background operations for sync
|
||||
- Background operations for sync via async Tauri commands
|
||||
- Efficient file I/O (stream large files)
|
||||
- Minimal dependencies
|
||||
- Svelte's compiled reactivity for minimal DOM updates
|
||||
|
||||
### Features
|
||||
|
||||
- [ ] egui framework integration
|
||||
- [ ] Workspace setup dialog on first launch
|
||||
- [ ] Workspace selector in toolbar
|
||||
- [ ] Quick-switch between workspaces
|
||||
- [ ] Basic task list view
|
||||
- [ ] Create new tasks
|
||||
- [ ] Edit existing tasks
|
||||
- [ ] Delete tasks
|
||||
- [ ] Mark tasks complete/incomplete
|
||||
- [ ] Settings screen (manage workspaces, WebDAV config)
|
||||
- [x] Tauri v2 + Svelte 5 + Tailwind CSS 4 framework integration
|
||||
- [x] Workspace setup dialog on first launch
|
||||
- [x] Workspace selector (drop-up menu in drawer footer)
|
||||
- [x] Quick-switch between workspaces
|
||||
- [x] Basic task list view with pending/completed sections
|
||||
- [x] Create new tasks (FAB + bottom toast sheet with title/description)
|
||||
- [x] Edit existing tasks (inline editing, auto-save on blur)
|
||||
- [x] Delete tasks (kebab menu → delete)
|
||||
- [x] Mark tasks complete/incomplete with animated transitions
|
||||
- [x] Drag-and-drop task reordering
|
||||
- [x] Sliding lists drawer (80vw, left side)
|
||||
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
||||
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
||||
- [x] Animated completed section show/hide
|
||||
- [ ] Due date picker/editor (backend supports it, needs date input in new task toast + inline editing)
|
||||
- [ ] WebDAV setup flow with credentials (settings panel has fields, triggerSync needs to pull creds from config)
|
||||
- [ ] List/workspace rename (needs `rename_list` added to bevy-tasks-core first)
|
||||
- [ ] Keyboard shortcuts (Escape to close drawers/menus, tab navigation, Enter behaviors)
|
||||
- [ ] Sync status indicators (per workspace)
|
||||
- [ ] Desktop support (Windows, Linux, macOS)
|
||||
- [ ] Push/pull sync mode selection
|
||||
- [ ] Group-by-due-date toggle per list
|
||||
- [ ] Subtask hierarchy (data model exists, needs UI)
|
||||
- [ ] Search/filter tasks
|
||||
- [ ] Desktop packaging (Windows, Linux, macOS)
|
||||
|
||||
### Deliverables
|
||||
|
||||
- [ ] Functional desktop GUI app
|
||||
- [ ] Sub-200ms startup time
|
||||
- [ ] Clean, minimal UI
|
||||
- [x] Functional desktop GUI app (Linux verified, Wayland native)
|
||||
- [ ] Sub-300ms startup time (not yet measured/optimized)
|
||||
- [x] Clean, minimal UI
|
||||
- [ ] Feature parity with CLI
|
||||
|
||||
### Build & Release
|
||||
|
||||
**Distribution**:
|
||||
- Linux: AppImage, .tar.gz
|
||||
- Linux: AppImage, .deb, .tar.gz
|
||||
- macOS: DMG
|
||||
- Windows: MSI, portable .exe
|
||||
|
||||
|
|
@ -737,60 +742,48 @@ egui = "0.31" # Core egui library
|
|||
### Why Early Mobile?
|
||||
- De-risk mobile builds early in development
|
||||
- Test cross-platform architecture sooner
|
||||
- Validate egui on mobile
|
||||
- Tauri v2 has first-class mobile support — same codebase as desktop
|
||||
- Get mobile-specific feedback early
|
||||
- Can dogfood on mobile while building desktop features
|
||||
|
||||
### Architecture
|
||||
|
||||
#### Mobile Build Setup
|
||||
#### Mobile Build Setup (Tauri v2 Mobile)
|
||||
|
||||
Tauri v2 supports iOS and Android natively. The same Svelte frontend and Rust backend are used, with platform-specific configuration.
|
||||
|
||||
**iOS**:
|
||||
- Use Xcode for builds
|
||||
- Tauri generates Xcode project
|
||||
- Bundle identifier: `com.bevytasks.app`
|
||||
- Target: `aarch64-apple-ios`
|
||||
- Bundle identifier: `com.bevy-tasks`
|
||||
|
||||
**Android**:
|
||||
- Use Android SDK/NDK
|
||||
- Build with `cargo-apk` or `cargo-ndk`
|
||||
- Tauri generates Gradle project
|
||||
- Min SDK: 26 (Android 8.0)
|
||||
- NDK handles Rust compilation
|
||||
|
||||
#### egui Mobile Adaptation
|
||||
#### Mobile Adaptation
|
||||
|
||||
**Touch Support**:
|
||||
- egui has basic touch support
|
||||
- Add larger touch targets (44pt minimum)
|
||||
- Tailwind responsive utilities for mobile-friendly layouts
|
||||
- Larger touch targets (44pt minimum)
|
||||
- Mobile-specific Svelte components where needed
|
||||
- Test on real devices
|
||||
|
||||
**File System Access**:
|
||||
- iOS: App sandbox documents directory + file picker
|
||||
- Android: Scoped storage + SAF (Storage Access Framework)
|
||||
- iOS: App sandbox documents directory + Tauri file dialog plugin
|
||||
- Android: Scoped storage + Tauri file dialog plugin
|
||||
|
||||
#### First Run on Mobile
|
||||
- Show folder picker on first launch
|
||||
- Suggest locations: Documents, iCloud Drive (iOS), Google Drive (Android)
|
||||
- User selects folder, path stored in preferences
|
||||
|
||||
### Platform-Specific Code
|
||||
|
||||
```rust
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios {
|
||||
// iOS-specific file picker, etc.
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android {
|
||||
// Android-specific file picker, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- [ ] iOS build pipeline setup (Xcode project)
|
||||
- [ ] Android build pipeline setup (Gradle/NDK)
|
||||
- [ ] Basic egui mobile adaptation
|
||||
- [ ] Simple test UI (even just buttons for CRUD)
|
||||
- [ ] Tauri v2 iOS build pipeline setup
|
||||
- [ ] Tauri v2 Android build pipeline setup
|
||||
- [ ] Mobile-responsive Svelte/Tailwind layout
|
||||
- [ ] File system access on iOS
|
||||
- [ ] File system access on Android
|
||||
- [ ] Folder picker for mobile
|
||||
|
|
@ -821,23 +814,23 @@ mod android {
|
|||
### Features
|
||||
|
||||
#### Desktop & Mobile
|
||||
- [ ] Multiple task lists (folders)
|
||||
- [ ] Switch between lists
|
||||
- [x] Multiple task lists (folders)
|
||||
- [x] Switch between lists
|
||||
- [ ] Subtasks support
|
||||
- [ ] Due dates with date picker
|
||||
- [ ] Rich markdown editor for task notes
|
||||
- [ ] Move tasks between lists
|
||||
- [ ] Change storage folder location in settings
|
||||
- [ ] Search functionality
|
||||
- [ ] Theme selection (light/dark mode)
|
||||
- [x] Theme selection (light/dark mode)
|
||||
|
||||
#### Desktop-Specific
|
||||
- [ ] Drag & drop reordering
|
||||
- [x] Drag & drop reordering
|
||||
- [ ] Keyboard shortcuts
|
||||
- [ ] Multiple windows (optional)
|
||||
|
||||
#### Mobile-Specific
|
||||
- [ ] Swipe gestures (swipe to complete, swipe to delete)
|
||||
- [x] Swipe gestures (swipe to complete, swipe to delete)
|
||||
- [ ] Pull-to-refresh
|
||||
- [ ] Touch-optimized UI elements
|
||||
- [ ] Larger touch targets
|
||||
|
|
@ -945,11 +938,11 @@ mod android {
|
|||
### Optional: Bevy Migration
|
||||
|
||||
If you want game-like polish after Phase 7:
|
||||
- Migrate GUI from egui to Bevy
|
||||
- Migrate GUI from Tauri/Svelte to Bevy
|
||||
- Full control over animations and rendering
|
||||
- Unique, polished look beyond standard apps
|
||||
- Backend (`bevy-tasks-core`) stays identical
|
||||
- Only rewrite `bevy-tasks-gui` crate
|
||||
- Only rewrite the GUI layer
|
||||
|
||||
### Deliverables
|
||||
|
||||
|
|
@ -978,6 +971,6 @@ This project is free and open-source software licensed under GPL v3.
|
|||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-03-17
|
||||
**Document Version**: 3.1
|
||||
**Last Updated**: 2026-03-29
|
||||
**Document Version**: 4.0
|
||||
**Status**: Ready to Implement - Milestone-Driven Plan
|
||||
|
|
|
|||
48
apps/flutter/.gitignore
vendored
48
apps/flutter/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_const_constructors: true
|
||||
prefer_const_declarations: true
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/src/rust/
|
||||
c_output: windows/runner/bridge_generated.h
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
pub mod api;
|
||||
mod frb_generated;
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
17
apps/flutter/windows/.gitignore
vendored
17
apps/flutter/windows/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
@ -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)
|
||||
|
|
@ -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}
|
||||
)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
}
|
||||
|
|
@ -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_
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
// Nothing when using full_dep=false mode
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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_
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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_
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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_
|
||||
|
|
@ -194,12 +194,16 @@ fn list_tasks(
|
|||
fn create_task(
|
||||
list_id: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
state: State<'_, Mutex<AppState>>,
|
||||
) -> Result<Task, String> {
|
||||
let mut s = state.lock().unwrap();
|
||||
ensure_repo(&mut s)?;
|
||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||
let task = Task::new(title);
|
||||
let mut task = Task::new(title);
|
||||
if let Some(desc) = description.filter(|d| !d.is_empty()) {
|
||||
task.description = desc;
|
||||
}
|
||||
s.repo
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"identifier": "com.bevytasks.app",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"devUrl": "http://localhost:1422",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { app } from "./lib/stores/app.svelte";
|
||||
import SetupScreen from "./lib/screens/SetupScreen.svelte";
|
||||
import TasksScreen from "./lib/screens/TasksScreen.svelte";
|
||||
import SettingsScreen from "./lib/screens/SettingsScreen.svelte";
|
||||
|
||||
|
||||
onMount(() => {
|
||||
app.loadConfig();
|
||||
|
|
@ -25,10 +25,8 @@
|
|||
|
||||
{#if app.screen === "setup"}
|
||||
<SetupScreen />
|
||||
{:else if app.screen === "tasks"}
|
||||
{:else}
|
||||
<TasksScreen />
|
||||
{:else if app.screen === "settings"}
|
||||
<SettingsScreen />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-hover: #1d4ed8;
|
||||
--color-primary: #2d87b8;
|
||||
--color-primary-hover: #2474a0;
|
||||
--color-surface-light: #ffffff;
|
||||
--color-surface-dark: #121212;
|
||||
--color-surface-dark: #242424;
|
||||
--color-card-light: #f9fafb;
|
||||
--color-card-dark: #1e1e1e;
|
||||
--color-card-dark: #303030;
|
||||
--color-text-light: #1f2937;
|
||||
--color-text-dark: #e5e7eb;
|
||||
--color-text-secondary-light: #6b7280;
|
||||
--color-text-secondary-dark: #9ca3af;
|
||||
--color-border-light: #e5e7eb;
|
||||
--color-border-dark: #374151;
|
||||
--color-border-dark: #3d3d3d;
|
||||
--color-danger: #ef4444;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,81 @@
|
|||
<script lang="ts" module>
|
||||
// Shared state accessible from outside
|
||||
export const newTaskState = $state({ open: false });
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { app } from "../stores/app.svelte";
|
||||
|
||||
let title = $state("");
|
||||
let description = $state("");
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!title.trim()) return;
|
||||
await app.createTask(title.trim());
|
||||
await app.createTask(title.trim(), description.trim() || undefined);
|
||||
title = "";
|
||||
description = "";
|
||||
newTaskState.open = false;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
newTaskState.open = false;
|
||||
title = "";
|
||||
description = "";
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (newTaskState.open) {
|
||||
requestAnimationFrame(() => inputEl?.focus());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="border-t border-border-light bg-surface-light px-4 py-3 dark:border-border-dark dark:bg-surface-dark"
|
||||
class="absolute inset-0 z-40 transition-opacity duration-250 ease-out {newTaskState.open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
||||
style="background: rgba(0,0,0,0.4)"
|
||||
onclick={handleClose}
|
||||
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
|
||||
></div>
|
||||
|
||||
<!-- Toast input sheet -->
|
||||
<div
|
||||
class="pointer-events-auto absolute bottom-0 left-0 right-0 z-50 rounded-t-2xl bg-surface-light shadow-xl transition-all duration-250 ease-out dark:bg-card-dark {newTaskState.open ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0 pointer-events-none'}"
|
||||
>
|
||||
<form
|
||||
onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={title}
|
||||
placeholder={app.activeListId ? "Add a task…" : "Select a list first"}
|
||||
disabled={!app.activeListId}
|
||||
class="min-w-0 flex-1 rounded-xl border border-border-light bg-card-light px-4 py-2.5 text-sm outline-none placeholder:opacity-40 focus:border-primary disabled:opacity-30 dark:border-border-dark dark:bg-card-dark"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!title.trim() || !app.activeListId}
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-transform hover:scale-105 active:scale-95 disabled:opacity-40 disabled:shadow-none"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
<div class="px-4 pb-4 pt-3">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="text"
|
||||
bind:value={title}
|
||||
placeholder="New task"
|
||||
class="w-full border-none bg-transparent text-base font-medium outline-none placeholder:opacity-40"
|
||||
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={description}
|
||||
placeholder="Add details"
|
||||
class="mt-2 w-full border-none bg-transparent text-sm outline-none placeholder:opacity-40"
|
||||
onkeydown={(e) => { if (e.key === "Escape") handleClose(); }}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<button class="opacity-40 hover:opacity-70" title="Set due date">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={!title.trim()}
|
||||
class="text-sm font-medium text-primary disabled:opacity-30"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,87 @@
|
|||
<script lang="ts" module>
|
||||
let editingTaskId = $state<string | null>(null);
|
||||
export const animateInIds = new Set<string>();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Task } from "../types";
|
||||
import { app } from "../stores/app.svelte";
|
||||
|
||||
let { task }: { task: Task } = $props();
|
||||
|
||||
let editing = $state(false);
|
||||
let editTitle = $state(task.title);
|
||||
let editDesc = $state(task.description);
|
||||
let editing = $derived(editingTaskId === task.id);
|
||||
let touchStartX = $state(0);
|
||||
let swipeX = $state(0);
|
||||
let swiping = $state(false);
|
||||
let containerEl = $state<HTMLDivElement | null>(null);
|
||||
let titleInputEl = $state<HTMLInputElement | null>(null);
|
||||
let showMenu = $state(false);
|
||||
let menuEl = $state<HTMLDivElement | null>(null);
|
||||
let transitioning = $state(false);
|
||||
let animatingIn = $state(false);
|
||||
|
||||
let isCompleted = $derived(task.status === "completed");
|
||||
|
||||
$effect(() => {
|
||||
// Check on status change whether this task should animate in
|
||||
const _ = task.status; // track reactively
|
||||
if (animateInIds.has(task.id)) {
|
||||
animateInIds.delete(task.id);
|
||||
animatingIn = true;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
animatingIn = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function handleToggle() {
|
||||
transitioning = true;
|
||||
animateInIds.add(task.id);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
await app.toggleTask(task.id);
|
||||
}
|
||||
|
||||
function handleMenuClickOutside(e: MouseEvent) {
|
||||
if (showMenu && menuEl && !menuEl.contains(e.target as Node)) {
|
||||
showMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showMenu) {
|
||||
window.addEventListener("mousedown", handleMenuClickOutside);
|
||||
return () => window.removeEventListener("mousedown", handleMenuClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
function startEditing() {
|
||||
if (editing) return;
|
||||
editingTaskId = task.id;
|
||||
editTitle = task.title;
|
||||
editDesc = task.description;
|
||||
setTimeout(() => titleInputEl?.focus(), 220);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (editingTaskId !== task.id) return;
|
||||
editingTaskId = null;
|
||||
const trimmed = editTitle.trim();
|
||||
if (!trimmed) { editTitle = task.title; return; }
|
||||
if (trimmed === task.title && editDesc === task.description) return;
|
||||
await app.updateTask({ ...task, title: trimmed, description: editDesc });
|
||||
}
|
||||
|
||||
function handleFocusOut(e: FocusEvent) {
|
||||
if (containerEl?.contains(e.relatedTarget as Node)) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (editingTaskId === task.id) save();
|
||||
});
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
swiping = true;
|
||||
|
|
@ -21,26 +90,21 @@
|
|||
function handleTouchMove(e: TouchEvent) {
|
||||
if (!swiping) return;
|
||||
const dx = e.touches[0].clientX - touchStartX;
|
||||
// Only allow left swipe for pending, right swipe for completed
|
||||
if (isCompleted) swipeX = Math.max(0, dx);
|
||||
else swipeX = Math.min(0, dx);
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
if (Math.abs(swipeX) > 100) {
|
||||
app.toggleTask(task.id);
|
||||
swipeX = 0;
|
||||
swiping = false;
|
||||
handleToggle();
|
||||
return;
|
||||
}
|
||||
swipeX = 0;
|
||||
swiping = false;
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editTitle.trim()) return;
|
||||
const updated = { ...task, title: editTitle.trim(), description: editDesc };
|
||||
await app.updateTask(updated);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const today = new Date();
|
||||
|
|
@ -53,7 +117,12 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden border-b border-border-light dark:border-border-dark"
|
||||
class="grid transition-[grid-template-rows,opacity] duration-300 ease-out {animatingIn || transitioning ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'}"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<div
|
||||
bind:this={containerEl}
|
||||
class="relative {showMenu ? 'z-40' : ''}"
|
||||
ontouchstart={handleTouchStart}
|
||||
ontouchmove={handleTouchMove}
|
||||
ontouchend={handleTouchEnd}
|
||||
|
|
@ -70,13 +139,15 @@
|
|||
{/if}
|
||||
|
||||
<!-- Task content -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative flex items-start gap-3 bg-surface-light px-4 py-3 dark:bg-surface-dark"
|
||||
class="group relative flex items-start gap-3 bg-surface-light px-4 py-3 hover:bg-black/5 dark:bg-surface-dark dark:hover:bg-white/5"
|
||||
style="transform: translateX({swipeX}px); transition: {swiping ? 'none' : 'transform 0.2s ease-out'}"
|
||||
onmousedown={startEditing}
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<button
|
||||
onclick={() => app.toggleTask(task.id)}
|
||||
onmousedown={(e) => { e.stopPropagation(); handleToggle(); }}
|
||||
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors {isCompleted
|
||||
? 'border-primary bg-primary'
|
||||
: 'border-gray-400 dark:border-gray-500'}"
|
||||
|
|
@ -92,40 +163,16 @@
|
|||
</button>
|
||||
|
||||
<!-- Content -->
|
||||
{#if editing}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="min-w-0 flex-1" onfocusout={handleFocusOut}>
|
||||
{#if editing}
|
||||
<input
|
||||
type="text"
|
||||
bind:this={titleInputEl}
|
||||
bind:value={editTitle}
|
||||
class="w-full bg-transparent text-sm font-medium outline-none"
|
||||
onkeydown={(e) => { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") editing = false; }}
|
||||
onkeydown={(e) => { if (e.key === "Enter") (e.target as HTMLElement).blur(); if (e.key === "Escape") { editTitle = task.title; editDesc = task.description; editingTaskId = null; } }}
|
||||
/>
|
||||
<textarea
|
||||
bind:value={editDesc}
|
||||
placeholder="Add description…"
|
||||
rows="2"
|
||||
class="mt-1 w-full resize-none bg-transparent text-xs opacity-60 outline-none"
|
||||
/>
|
||||
<div class="mt-1 flex gap-2">
|
||||
<button
|
||||
onclick={saveEdit}
|
||||
class="rounded px-2 py-1 text-xs font-medium text-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (editing = false)}
|
||||
class="rounded px-2 py-1 text-xs opacity-60"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => { editing = true; editTitle = task.title; editDesc = task.description; }}
|
||||
class="min-w-0 flex-1 text-left"
|
||||
>
|
||||
{:else}
|
||||
<p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
|
||||
{task.title}
|
||||
</p>
|
||||
|
|
@ -137,25 +184,49 @@
|
|||
{formatDate(task.due_date)}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Delete -->
|
||||
{#if !editing}
|
||||
<button
|
||||
onclick={() => app.deleteTask(task.id)}
|
||||
class="shrink-0 rounded p-1 opacity-0 transition-opacity hover:opacity-60 group-hover:opacity-30"
|
||||
style="opacity: 0.15"
|
||||
title="Delete"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<!-- Expandable edit description -->
|
||||
<div class="grid transition-[grid-template-rows,opacity] duration-200 ease-out {editing ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}">
|
||||
<div class="overflow-hidden">
|
||||
<textarea
|
||||
bind:value={editDesc}
|
||||
placeholder="Add description…"
|
||||
rows="2"
|
||||
class="mt-1 w-full resize-none bg-transparent text-xs opacity-60 outline-none"
|
||||
tabindex={editing ? 0 : -1}
|
||||
onkeydown={(e) => { if (e.key === "Escape") { editTitle = task.title; editDesc = task.description; editingTaskId = null; } }}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kebab menu -->
|
||||
<div class="relative shrink-0" bind:this={menuEl}>
|
||||
<button
|
||||
onmousedown={(e) => { e.stopPropagation(); showMenu = !showMenu; }}
|
||||
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {showMenu || editing ? '!opacity-40' : ''}"
|
||||
title="More"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if showMenu}
|
||||
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||
<button
|
||||
onmousedown={(e) => { e.stopPropagation(); showMenu = false; app.deleteTask(task.id); }}
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { app } from "../stores/app.svelte";
|
||||
|
||||
let { onclose }: { onclose?: () => void } = $props();
|
||||
|
||||
let webdavUrl = $state("");
|
||||
let webdavUser = $state("");
|
||||
let webdavPass = $state("");
|
||||
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
||||
let confirmRemove = $state<string | null>(null);
|
||||
|
||||
async function testConnection() {
|
||||
testStatus = "testing";
|
||||
|
|
@ -37,17 +38,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRemoveWorkspace(name: string) {
|
||||
await app.removeWorkspace(name);
|
||||
confirmRemove = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="flex items-center gap-3 border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
>
|
||||
<button
|
||||
onclick={() => app.setScreen("tasks")}
|
||||
onclick={() => onclose?.()}
|
||||
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
|
|
@ -60,76 +57,7 @@
|
|||
<h1 class="text-lg font-bold">Settings</h1>
|
||||
</header>
|
||||
|
||||
<main class="overflow-y-auto p-4" style="height: calc(100vh - 57px)">
|
||||
<!-- Workspaces -->
|
||||
<section class="mb-6">
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
||||
Workspaces
|
||||
</h2>
|
||||
{#if app.config}
|
||||
{#each Object.entries(app.config.workspaces) as [name, ws]}
|
||||
<div
|
||||
class="mb-2 rounded-xl border border-border-light p-3 dark:border-border-dark"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium {name === app.config.current_workspace ? 'text-primary' : ''}">
|
||||
{name}
|
||||
</p>
|
||||
<p class="text-xs opacity-50">{ws.path}</p>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
{#if name !== app.config.current_workspace}
|
||||
<button
|
||||
onclick={() => app.switchWorkspace(name)}
|
||||
class="rounded-lg px-3 py-1.5 text-xs font-medium text-primary hover:bg-primary/5"
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
{/if}
|
||||
{#if confirmRemove === name}
|
||||
<button
|
||||
onclick={() => handleRemoveWorkspace(name)}
|
||||
class="rounded-lg px-3 py-1.5 text-xs font-medium text-danger"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (confirmRemove = null)}
|
||||
class="rounded-lg px-3 py-1.5 text-xs opacity-60"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (confirmRemove = name)}
|
||||
class="rounded-lg px-3 py-1.5 text-xs opacity-40 hover:text-danger hover:opacity-100"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if ws.webdav_url}
|
||||
<p class="mt-1 text-xs opacity-40">Sync: {ws.webdav_url}</p>
|
||||
{/if}
|
||||
{#if ws.last_sync}
|
||||
<p class="text-xs opacity-40">
|
||||
Last synced: {new Date(ws.last_sync).toLocaleString()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={() => app.setScreen("setup")}
|
||||
class="mt-2 rounded-lg px-3 py-2 text-sm text-primary hover:bg-primary/5"
|
||||
>
|
||||
+ Add workspace
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-4">
|
||||
<!-- WebDAV Sync -->
|
||||
<section class="mb-6">
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
||||
|
|
|
|||
|
|
@ -1,207 +1,436 @@
|
|||
<script lang="ts">
|
||||
import { app } from "../stores/app.svelte";
|
||||
import TaskItem from "../components/TaskItem.svelte";
|
||||
import BottomSheet from "../components/BottomSheet.svelte";
|
||||
import NewTaskInput from "../components/NewTaskInput.svelte";
|
||||
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
||||
import SettingsScreen from "./SettingsScreen.svelte";
|
||||
|
||||
let showListSheet = $state(false);
|
||||
let showDrawer = $state(false);
|
||||
let showSettings = $state(false);
|
||||
let showNewList = $state(false);
|
||||
let showWorkspacePicker = $state(false);
|
||||
let workspacePickerEl = $state<HTMLDivElement | null>(null);
|
||||
|
||||
function handleWindowClick(e: MouseEvent) {
|
||||
if (showWorkspacePicker && workspacePickerEl && !workspacePickerEl.contains(e.target as Node)) {
|
||||
showWorkspacePicker = false;
|
||||
}
|
||||
const target = e.target as HTMLElement;
|
||||
if (listMenuId && !target.closest("[data-list-menu]")) listMenuId = null;
|
||||
if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("mousedown", handleWindowClick);
|
||||
}
|
||||
let newListName = $state("");
|
||||
let showCompleted = $state(true);
|
||||
let confirmDeleteList = $state<string | null>(null);
|
||||
let completedVisible = $state(true);
|
||||
let listMenuId = $state<string | null>(null);
|
||||
let wsMenuName = $state<string | null>(null);
|
||||
let dragId = $state<string | null>(null);
|
||||
let dragOverId = $state<string | null>(null);
|
||||
let resizing = $state(false);
|
||||
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("resize", () => {
|
||||
resizing = true;
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => (resizing = false), 150);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleNewList() {
|
||||
if (!newListName.trim()) return;
|
||||
await app.createList(newListName.trim());
|
||||
newListName = "";
|
||||
showNewList = false;
|
||||
showListSheet = false;
|
||||
}
|
||||
|
||||
async function handleDeleteList(id: string) {
|
||||
listMenuId = null;
|
||||
await app.deleteList(id);
|
||||
confirmDeleteList = null;
|
||||
showListSheet = false;
|
||||
}
|
||||
|
||||
function handleDragStart(e: DragEvent, taskId: string) {
|
||||
dragId = taskId;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", taskId);
|
||||
const el = (e.target as HTMLElement).closest("[draggable]") as HTMLElement;
|
||||
if (el) {
|
||||
const clone = el.cloneNode(true) as HTMLElement;
|
||||
clone.style.width = `${el.offsetWidth}px`;
|
||||
clone.style.position = "absolute";
|
||||
clone.style.top = "-9999px";
|
||||
clone.style.left = "-9999px";
|
||||
if (app.darkMode) {
|
||||
clone.classList.add("dark");
|
||||
clone.style.backgroundColor = "var(--color-surface-dark)";
|
||||
clone.style.color = "var(--color-text-dark)";
|
||||
}
|
||||
clone.style.opacity = "0.85";
|
||||
clone.style.borderRadius = "8px";
|
||||
clone.style.overflow = "hidden";
|
||||
clone.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
|
||||
document.body.appendChild(clone);
|
||||
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY);
|
||||
requestAnimationFrame(() => clone.remove());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent, taskId: string) {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||
dragOverId = taskId;
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
dragId = null;
|
||||
dragOverId = null;
|
||||
}
|
||||
|
||||
async function handleDrop(e: DragEvent, targetId: string) {
|
||||
e.preventDefault();
|
||||
if (!dragId || dragId === targetId) { handleDragEnd(); return; }
|
||||
const targetIndex = app.pendingTasks.findIndex((t) => t.id === targetId);
|
||||
if (targetIndex >= 0) await app.reorderTask(dragId, targetIndex);
|
||||
handleDragEnd();
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
showDrawer = false;
|
||||
showNewList = false;
|
||||
listMenuId = null;
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
showSettings = true;
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
showSettings = false;
|
||||
}
|
||||
|
||||
let workspaceNames = $derived(app.config ? Object.keys(app.config.workspaces) : []);
|
||||
let translateX = $derived(showDrawer ? '0' : '-80vw');
|
||||
</script>
|
||||
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
<!-- Viewport clip -->
|
||||
<div class="h-screen w-screen overflow-hidden">
|
||||
<!-- Sliding container: left drawer + main content -->
|
||||
<div
|
||||
class="flex h-full ease-out {resizing ? '' : 'transition-transform duration-250'}"
|
||||
style="width: calc(100vw + 80vw); transform: translateX({translateX})"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<button
|
||||
onclick={() => (showListSheet = true)}
|
||||
class="flex items-center gap-1 text-lg font-bold"
|
||||
>
|
||||
{app.activeList?.title ?? "Tasks"}
|
||||
<svg class="h-4 w-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||
{app.config?.current_workspace ?? ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{#if app.syncing}
|
||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => app.toggleDarkMode()}
|
||||
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
title="Toggle theme"
|
||||
>
|
||||
{#if app.darkMode}
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => app.setScreen("settings")}
|
||||
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
title="Settings"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Task list -->
|
||||
<main class="flex-1 overflow-y-auto" style="height: calc(100vh - 57px - 72px)">
|
||||
{#if app.lists.length === 0}
|
||||
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
|
||||
<p class="text-lg font-medium opacity-60">No lists yet</p>
|
||||
<p class="mt-1 text-sm opacity-40">Tap the list name above to create one</p>
|
||||
</div>
|
||||
{:else if !app.activeListId}
|
||||
<div class="flex h-full items-center justify-center opacity-40">
|
||||
Select a list
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Pending tasks -->
|
||||
{#each app.pendingTasks as task (task.id)}
|
||||
<TaskItem {task} />
|
||||
{/each}
|
||||
|
||||
{#if app.pendingTasks.length === 0}
|
||||
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div>
|
||||
{/if}
|
||||
|
||||
<!-- Completed section -->
|
||||
{#if app.completedTasks.length > 0}
|
||||
<button
|
||||
onclick={() => (showCompleted = !showCompleted)}
|
||||
class="flex w-full items-center gap-2 border-t border-border-light px-4 py-3 text-sm font-medium text-text-secondary-light dark:border-border-dark dark:text-text-secondary-dark"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
|
||||
/>
|
||||
</svg>
|
||||
Completed ({app.completedTasks.length})
|
||||
</button>
|
||||
{#if showCompleted}
|
||||
{#each app.completedTasks as task (task.id)}
|
||||
<TaskItem {task} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<!-- New task input (bottom bar) -->
|
||||
<NewTaskInput />
|
||||
|
||||
<!-- List picker bottom sheet -->
|
||||
{#if showListSheet}
|
||||
<BottomSheet onclose={() => { showListSheet = false; showNewList = false; }}>
|
||||
<div class="px-4 pb-2 pt-4">
|
||||
<h2 class="mb-3 text-lg font-bold">Lists</h2>
|
||||
<!-- Drawer panel -->
|
||||
<div class="flex h-full w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||
<!-- List items + new list button -->
|
||||
<div class="flex-1 overflow-y-auto py-2">
|
||||
{#each app.lists as list (list.id)}
|
||||
<div class="flex items-center">
|
||||
<div class="group relative flex items-center px-2 hover:bg-black/5 dark:hover:bg-white/10">
|
||||
<button
|
||||
onclick={() => { app.selectList(list.id); showListSheet = false; }}
|
||||
class="flex-1 rounded-lg px-3 py-2.5 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10 {list.id === app.activeListId ? 'font-bold text-primary' : ''}"
|
||||
onclick={() => { app.selectList(list.id); closeDrawer(); }}
|
||||
class="flex flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm {list.id === app.activeListId ? 'font-bold' : ''}"
|
||||
>
|
||||
{list.title}
|
||||
{#if list.id === app.activeListId}
|
||||
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span>{list.title}</span>
|
||||
</button>
|
||||
{#if confirmDeleteList === list.id}
|
||||
<div class="relative shrink-0" data-list-menu>
|
||||
<button
|
||||
onclick={() => handleDeleteList(list.id)}
|
||||
class="rounded px-2 py-1 text-xs font-medium text-danger hover:bg-danger/10"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (confirmDeleteList = null)}
|
||||
class="rounded px-2 py-1 text-xs opacity-60 hover:opacity-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (confirmDeleteList = list.id)}
|
||||
class="rounded p-1.5 opacity-30 hover:opacity-60"
|
||||
title="Delete list"
|
||||
onclick={() => (listMenuId = listMenuId === list.id ? null : list.id)}
|
||||
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {listMenuId === list.id ? '!opacity-80' : ''}"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
{#if listMenuId === list.id}
|
||||
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||
<button
|
||||
onclick={() => handleDeleteList(list.id)}
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if showNewList}
|
||||
<div class="mt-2 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newListName}
|
||||
placeholder="List name"
|
||||
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
||||
onkeydown={(e) => { if (e.key === "Enter") handleNewList(); }}
|
||||
/>
|
||||
<!-- New list inline -->
|
||||
<div class="px-2 mt-1">
|
||||
{#if showNewList}
|
||||
<div class="flex gap-2 px-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newListName}
|
||||
placeholder="List name"
|
||||
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
||||
onkeydown={(e) => { if (e.key === "Enter") handleNewList(); if (e.key === "Escape") { showNewList = false; newListName = ""; } }}
|
||||
/>
|
||||
<button
|
||||
onclick={handleNewList}
|
||||
disabled={!newListName.trim()}
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-40"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
onclick={handleNewList}
|
||||
disabled={!newListName.trim()}
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-40"
|
||||
onclick={() => (showNewList = true)}
|
||||
class="w-full rounded-lg px-3 py-2.5 text-left text-sm text-primary hover:bg-primary/5"
|
||||
>
|
||||
Add
|
||||
+ New list
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: workspace switcher (left) + settings gear (right) -->
|
||||
<div class="flex items-center justify-between border-t border-border-light px-3 py-2 dark:border-border-dark">
|
||||
<!-- Workspace switcher (custom drop-up) -->
|
||||
<div class="relative min-w-0 flex-1" bind:this={workspacePickerEl}>
|
||||
<button
|
||||
onclick={() => (showWorkspacePicker = !showWorkspacePicker)}
|
||||
class="flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm opacity-60 hover:bg-black/5 hover:opacity-100 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5 shrink-0 transition-transform {showWorkspacePicker ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" />
|
||||
</svg>
|
||||
<span class="truncate">{app.config?.current_workspace ?? "Workspace"}</span>
|
||||
</button>
|
||||
{#if showWorkspacePicker}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="absolute bottom-full left-0 mb-1 w-full rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark"
|
||||
>
|
||||
{#each workspaceNames as name}
|
||||
{@const ws = app.config?.workspaces[name]}
|
||||
<div class="group flex items-center px-1 hover:bg-black/5 dark:hover:bg-white/10">
|
||||
<button
|
||||
onclick={() => { app.switchWorkspace(name); showWorkspacePicker = false; }}
|
||||
class="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left {name === app.config?.current_workspace ? 'font-bold' : ''}"
|
||||
>
|
||||
{#if name === app.config?.current_workspace}
|
||||
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm">{name}</p>
|
||||
<p class="truncate text-xs opacity-40">{ws?.path ?? ""}</p>
|
||||
</div>
|
||||
</button>
|
||||
<div class="relative shrink-0" data-ws-menu>
|
||||
<button
|
||||
onclick={(e) => { e.stopPropagation(); wsMenuName = wsMenuName === name ? null : name; }}
|
||||
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {wsMenuName === name ? '!opacity-80' : ''}"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if wsMenuName === name}
|
||||
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||
<button
|
||||
onclick={() => { wsMenuName = null; app.removeWorkspace(name); }}
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="mt-1 border-t border-border-light px-1 pt-1 dark:border-border-dark">
|
||||
<button
|
||||
onclick={() => { showWorkspacePicker = false; app.setScreen("setup"); }}
|
||||
class="w-full rounded-md px-2 py-1.5 text-left text-sm text-primary hover:bg-primary/5"
|
||||
>
|
||||
+ Add workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Settings gear -->
|
||||
<button
|
||||
onclick={openSettings}
|
||||
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
title="Settings"
|
||||
>
|
||||
<svg class="h-5 w-5 opacity-50 hover:opacity-80" 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>
|
||||
</div>
|
||||
|
||||
<!-- Main content panel -->
|
||||
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||
<!-- Dim overlay 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="relative flex items-center border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
>
|
||||
<!-- Back arrow (left) -->
|
||||
<button
|
||||
onclick={() => (showDrawer = !showDrawer)}
|
||||
class="absolute left-2 rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-5 w-5 opacity-60" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Centered title -->
|
||||
<div class="flex-1 text-center">
|
||||
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||
{app.config?.current_workspace ?? ""}
|
||||
</p>
|
||||
<p class="text-lg font-bold">{app.activeList?.title ?? "Tasks"}</p>
|
||||
</div>
|
||||
|
||||
<!-- Sync spinner (right) -->
|
||||
{#if app.syncing}
|
||||
<div class="absolute right-4 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
{/if}
|
||||
</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}
|
||||
<button
|
||||
onclick={() => (showNewList = true)}
|
||||
class="mt-2 w-full rounded-lg px-3 py-2.5 text-left text-sm text-primary hover:bg-primary/5"
|
||||
>
|
||||
+ New list
|
||||
</button>
|
||||
{#each app.pendingTasks as task (task.id)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleDragStart(e, task.id)}
|
||||
ondragover={(e) => handleDragOver(e, task.id)}
|
||||
ondragend={handleDragEnd}
|
||||
ondrop={(e) => handleDrop(e, task.id)}
|
||||
class="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
|
||||
>
|
||||
<TaskItem {task} />
|
||||
</div>
|
||||
{/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}
|
||||
<div class="h-4"></div>
|
||||
<button
|
||||
onclick={() => {
|
||||
if (showCompleted) {
|
||||
showCompleted = false;
|
||||
setTimeout(() => (completedVisible = false), 300);
|
||||
} else {
|
||||
completedVisible = true;
|
||||
requestAnimationFrame(() => (showCompleted = true));
|
||||
}
|
||||
}}
|
||||
class="relative z-10 flex w-full items-center justify-between border-t border-border-light bg-surface-light px-4 py-3 text-sm font-medium text-text-secondary-light transition-colors hover:bg-black/5 dark:border-border-dark dark:bg-surface-dark dark:text-text-secondary-dark dark:hover:bg-white/5"
|
||||
>
|
||||
Completed ({app.completedTasks.length})
|
||||
<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>
|
||||
</button>
|
||||
{#if completedVisible}
|
||||
<div class="transition-all duration-300 ease-out {showCompleted ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}">
|
||||
{#each app.completedTasks as task (task.id)}
|
||||
<TaskItem {task} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<!-- FAB button -->
|
||||
<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>
|
||||
</BottomSheet>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings popup overlay -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex transition-opacity duration-200 {showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
||||
style="padding: 4%"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/50"
|
||||
onclick={closeSettings}
|
||||
onkeydown={(e) => { if (e.key === "Escape") closeSettings(); }}
|
||||
></div>
|
||||
<!-- Settings card -->
|
||||
<div
|
||||
class="relative flex h-full w-full flex-col overflow-hidden rounded-2xl bg-surface-light transition-transform duration-200 dark:bg-surface-dark {showSettings ? 'scale-100' : 'scale-95'}"
|
||||
style="border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 25px 60px rgba(0,0,0,0.7), 0 10px 20px rgba(0,0,0,0.5)"
|
||||
>
|
||||
<SettingsScreen onclose={closeSettings} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
||||
<div class="pointer-events-none fixed inset-0 z-50">
|
||||
<NewTaskInput />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,12 +140,13 @@ async function deleteList(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function createTask(title: string) {
|
||||
async function createTask(title: string, description?: string) {
|
||||
if (!activeListId) return;
|
||||
try {
|
||||
const task = await invoke<Task>("create_task", {
|
||||
listId: activeListId,
|
||||
title,
|
||||
description: description ?? "",
|
||||
});
|
||||
tasks = [...tasks, task];
|
||||
error = null;
|
||||
|
|
@ -161,7 +162,13 @@ async function toggleTask(taskId: string) {
|
|||
listId: activeListId,
|
||||
taskId,
|
||||
});
|
||||
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
|
||||
// Move to top of list locally, then persist order in background
|
||||
if (updated.status === "backlog") {
|
||||
tasks = [updated, ...tasks.filter((t) => t.id !== taskId)];
|
||||
invoke("reorder_task", { listId: activeListId, taskId, newPosition: 0 }).catch(() => {});
|
||||
} else {
|
||||
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
|
||||
}
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
}
|
||||
|
|
@ -177,6 +184,16 @@ async function updateTask(task: Task) {
|
|||
}
|
||||
}
|
||||
|
||||
async function reorderTask(taskId: string, newPosition: number) {
|
||||
if (!activeListId) return;
|
||||
try {
|
||||
await invoke("reorder_task", { listId: activeListId, taskId, newPosition });
|
||||
await loadTasks();
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(taskId: string) {
|
||||
if (!activeListId) return;
|
||||
try {
|
||||
|
|
@ -277,6 +294,7 @@ export const app = {
|
|||
createTask,
|
||||
toggleTask,
|
||||
updateTask,
|
||||
reorderTask,
|
||||
deleteTask,
|
||||
triggerSync,
|
||||
toggleDarkMode,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||
plugins: [svelte(), tailwindcss()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
port: 1422,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Reference in a new issue