Merge pull request #21 from SteelDynamite/remove-flutter

remove flutter
This commit is contained in:
SteelDynamite 2026-04-02 16:11:05 +01:00 committed by GitHub
commit 40142cb1ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 5 additions and 12548 deletions

View file

@ -32,8 +32,6 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility. - **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility.
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`. - **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android. - **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android.
- **apps/flutter/** — Flutter GUI. Dart frontend in `lib/src/`, Rust backend in `rust/` via flutter_rust_bridge FFI into `onyx-core`.
### Key patterns ### Key patterns
- **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. - **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files.
@ -62,7 +60,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
- **Phase 1** (Core + CLI): Complete - **Phase 1** (Core + CLI): Complete
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials) - **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
- **Phase 3** (GUI MVP): Complete — both Tauri and Flutter GUIs at feature parity - **Phase 3** (GUI MVP): Complete
- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build - **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build
### GUI features done ### GUI features done
@ -88,7 +86,6 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
- Sync status indicators (last-sync time + upload/download counts chip) - Sync status indicators (last-sync time + upload/download counts chip)
- Push/pull/full sync mode selection (session-only, in settings) - Push/pull/full sync mode selection (session-only, in settings)
- Desktop packaging (Linux: AppImage + .deb) - Desktop packaging (Linux: AppImage + .deb)
- Flutter GUI at full parity with Tauri (WebDAV UI, has_time, sync status, sync mode)
- Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation - Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation
- Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning) - Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning)
- Custom confirmation dialogs (ConfirmDialog component replaces native confirm()) - Custom confirmation dialogs (ConfirmDialog component replaces native confirm())

View file

@ -5,7 +5,6 @@ members = [
] ]
exclude = [ exclude = [
"apps/tauri/src-tauri", "apps/tauri/src-tauri",
"apps/flutter/rust",
] ]
resolver = "2" resolver = "2"

59
PLAN.md
View file

@ -735,7 +735,6 @@ WorkspaceConfig {
- [ ] Sub-300ms startup time (not yet measured/optimized) - [ ] Sub-300ms startup time (not yet measured/optimized)
- [x] Clean, minimal UI - [x] Clean, minimal UI
- [ ] Feature parity with CLI - [ ] Feature parity with CLI
- [x] Flutter GUI at feature parity with Tauri (WebDAV, has_time, sync status, sync mode)
### Build & Release ### Build & Release
@ -750,7 +749,7 @@ WorkspaceConfig {
## Phase 4: Mobile Basic Support ## Phase 4: Mobile Basic Support
**Goal**: Get both GUIs building and running on Android and iOS, validate cross-platform architecture **Goal**: Get the Tauri GUI building and running on Android and iOS, validate cross-platform architecture
### Why Early Mobile? ### Why Early Mobile?
- De-risk mobile builds early in development - De-risk mobile builds early in development
@ -769,55 +768,7 @@ All Android work can be done locally on Linux. iOS must go through CI or a Mac.
--- ---
### Flutter GUI (Priority Path) ### Known Blockers
Flutter + flutter_rust_bridge was designed for mobile from the start and is the lower-risk path.
#### Known Blockers
**`window_manager` is desktop-only** (`pubspec.yaml` line 13). This package crashes or fails to compile on mobile. Must be gated behind `Platform.isDesktop` checks before any mobile build will succeed.
**No platform directories exist yet.** `apps/flutter/android/` and `apps/flutter/ios/` have not been generated. Run `flutter create --platforms android,ios .` from `apps/flutter/` to scaffold them.
#### Android Prerequisites
1. Android Studio + NDK installed, `ANDROID_HOME` and `NDK_HOME` set
2. `cargo install cargo-ndk`
3. Rust Android targets: `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`
flutter_rust_bridge invokes cargo-ndk automatically during `flutter build apk` — no manual cross-compilation step needed.
#### Build Commands
```bash
# Android
cd apps/flutter
flutter build apk # release APK
flutter build apk --debug # debug APK for sideloading
# iOS (macOS CI only)
flutter build ios --no-codesign # unsigned build for simulator
flutter build ipa # signed IPA for TestFlight
```
#### Features
- [ ] Gate `window_manager` behind `Platform.isDesktop` checks
- [ ] Generate Android platform (`flutter create --platforms android .`)
- [ ] Generate iOS platform (`flutter create --platforms ios .`)
- [ ] Install cargo-ndk + Android Rust targets (Android prereqs)
- [ ] Confirm `flutter build apk` succeeds locally
- [ ] Set up macOS CI for iOS builds
- [ ] Confirm `flutter build ios` succeeds on CI
- [ ] Basic smoke test: app launches, workspace setup, create a task
---
### Tauri GUI
Tauri v2 has mobile support but it's newer and less mature. Requires more code surgery than Flutter.
#### Known Blockers
**`notify` crate doesn't compile for mobile.** The file-watcher subsystem (`notify` + `notify-debouncer-mini` in `Cargo.toml`) does not support Android or iOS targets. The entire file-watcher initialization path must be gated behind `#[cfg(not(mobile))]` before cross-compilation will succeed. **`notify` crate doesn't compile for mobile.** The file-watcher subsystem (`notify` + `notify-debouncer-mini` in `Cargo.toml`) does not support Android or iOS targets. The entire file-watcher initialization path must be gated behind `#[cfg(not(mobile))]` before cross-compilation will succeed.
@ -860,7 +811,7 @@ npm run tauri ios build
--- ---
### Shared Mobile Adaptation (Both GUIs) ### Mobile Adaptation
**Touch Support**: **Touch Support**:
- Larger touch targets (44pt minimum) - Larger touch targets (44pt minimum)
@ -878,11 +829,9 @@ npm run tauri ios build
### Deliverables ### Deliverables
- [ ] Flutter APK builds locally on Linux (Android)
- [ ] Tauri APK builds locally on Linux (Android) - [ ] Tauri APK builds locally on Linux (Android)
- [ ] Flutter iOS builds on macOS CI
- [ ] Tauri iOS builds on macOS CI - [ ] Tauri iOS builds on macOS CI
- [ ] Basic task CRUD works on mobile (both GUIs) - [ ] Basic task CRUD works on mobile
- [ ] Validates cross-platform architecture - [ ] Validates cross-platform architecture
### Distribution ### Distribution

View file

@ -1,54 +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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Generated plugin registrants
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# 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

View file

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

View file

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

View file

@ -1,28 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,3 +0,0 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust

View file

@ -1,227 +0,0 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'src/rust/frb_generated.dart';
import 'src/theme.dart';
import 'src/state/app_state.dart';
import 'src/screens/setup_screen.dart';
import 'src/screens/tasks_screen.dart';
import 'src/screens/settings_screen.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
await windowManager.ensureInitialized();
await windowManager.waitUntilReadyToShow(
const WindowOptions(
size: Size(400, 700),
minimumSize: Size(320, 500),
titleBarStyle: TitleBarStyle.hidden,
),
() async {
await windowManager.setBackgroundColor(Colors.transparent);
await windowManager.setResizable(true);
await windowManager.show();
await windowManager.focus();
},
);
runApp(
ChangeNotifierProvider(
create: (_) => AppState()..loadConfig(),
child: const OnyxApp(),
),
);
}
class OnyxApp extends StatelessWidget {
const OnyxApp({super.key});
@override
Widget build(BuildContext context) {
final state = context.watch<AppState>();
return MaterialApp(
title: 'Onyx',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: state.darkMode ? ThemeMode.dark : ThemeMode.light,
home: const AppShell(),
);
}
}
class AppShell extends StatefulWidget {
const AppShell({super.key});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> with SingleTickerProviderStateMixin {
static const _edge = 8.0;
late final AnimationController _settingsAnim;
late final Animation<double> _settingsFade;
late final Animation<double> _settingsScale;
bool _settingsVisible = false;
String? _prevScreen;
@override
void initState() {
super.initState();
_settingsAnim = AnimationController(vsync: this, duration: const Duration(milliseconds: 150));
_settingsFade = CurvedAnimation(parent: _settingsAnim, curve: Curves.easeOut);
_settingsScale = Tween<double>(begin: 0.95, end: 1.0)
.animate(CurvedAnimation(parent: _settingsAnim, curve: Curves.easeOut));
}
@override
void dispose() {
_settingsAnim.dispose();
super.dispose();
}
void _onScreenChanged(String screen) {
if (screen == 'settings' && _prevScreen != 'settings') {
_settingsVisible = true;
_settingsAnim.forward();
} else if (screen != 'settings' && _prevScreen == 'settings') {
_settingsAnim.reverse().then((_) {
if (mounted) setState(() => _settingsVisible = false);
});
}
_prevScreen = screen;
}
SystemMouseCursor _cursorFor(ResizeEdge? edge) => switch (edge) {
ResizeEdge.top || ResizeEdge.bottom => SystemMouseCursors.resizeUpDown,
ResizeEdge.left || ResizeEdge.right => SystemMouseCursors.resizeLeftRight,
ResizeEdge.topLeft || ResizeEdge.bottomRight => SystemMouseCursors.resizeUpLeftDownRight,
ResizeEdge.topRight || ResizeEdge.bottomLeft => SystemMouseCursors.resizeUpRightDownLeft,
_ => SystemMouseCursors.basic,
};
@override
Widget build(BuildContext context) {
final state = context.watch<AppState>();
final isDark = Theme.of(context).brightness == Brightness.dark;
final hasNativeBorder = Platform.isWindows;
_onScreenChanged(state.screen);
Widget content = Stack(
children: [
if (state.screen == 'setup')
const SetupScreen()
else
const TasksScreen(),
if (state.error != null)
Positioned(
top: 0, left: 0, right: 0,
child: Material(
color: AppTheme.danger,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Text(state.error!, style: const TextStyle(color: Colors.white, fontSize: 13)),
),
GestureDetector(
onTap: state.clearError,
child: const Text('', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
),
),
),
),
if (_settingsVisible)
FadeTransition(
opacity: _settingsFade,
child: ScaleTransition(
scale: _settingsScale,
child: const SettingsScreen(),
),
),
],
);
if (hasNativeBorder) {
// Windows provides native border + shadow, just fill with surface color
return Scaffold(
backgroundColor: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
body: ClipRect(child: content),
);
}
// Linux/macOS: custom border, shadow, and resize zones
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(builder: (context, constraints) {
return MouseRegion(
cursor: SystemMouseCursors.basic,
hitTestBehavior: HitTestBehavior.translucent,
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerHover: (event) {},
child: Stack(
children: [
..._buildResizeZones(constraints),
Padding(
padding: const EdgeInsets.all(_edge),
child: Container(
decoration: BoxDecoration(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isDark ? Colors.white.withValues(alpha: 0.15) : Colors.black.withValues(alpha: 0.15),
),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.25), blurRadius: 8, offset: const Offset(0, 2)),
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 2),
],
),
clipBehavior: Clip.antiAlias,
child: content,
),
),
],
),
),
);
}),
);
}
List<Widget> _buildResizeZones(BoxConstraints constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
Widget zone(ResizeEdge edge, {required double left, required double top, required double width, required double height}) {
return Positioned(
left: left, top: top, width: width, height: height,
child: MouseRegion(
cursor: _cursorFor(edge),
child: GestureDetector(
onPanStart: (_) => windowManager.startResizing(edge),
),
),
);
}
return [
// Corners (larger hit area)
zone(ResizeEdge.topLeft, left: 0, top: 0, width: _edge * 2, height: _edge * 2),
zone(ResizeEdge.topRight, left: w - _edge * 2, top: 0, width: _edge * 2, height: _edge * 2),
zone(ResizeEdge.bottomLeft, left: 0, top: h - _edge * 2, width: _edge * 2, height: _edge * 2),
zone(ResizeEdge.bottomRight, left: w - _edge * 2, top: h - _edge * 2, width: _edge * 2, height: _edge * 2),
// Edges
zone(ResizeEdge.top, left: _edge * 2, top: 0, width: w - _edge * 4, height: _edge),
zone(ResizeEdge.bottom, left: _edge * 2, top: h - _edge, width: w - _edge * 4, height: _edge),
zone(ResizeEdge.left, left: 0, top: _edge * 2, width: _edge, height: h - _edge * 4),
zone(ResizeEdge.right, left: w - _edge, top: _edge * 2, width: _edge, height: h - _edge * 4),
];
}
}

View file

@ -1,312 +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_dto`, `ensure_repo`, `mute_watcher`, `task_to_dto`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AppState`
Future<AppConfigDto> getConfig() => RustLib.instance.api.crateApiGetConfig();
Future<void> initWorkspace({required String path}) =>
RustLib.instance.api.crateApiInitWorkspace(path: path);
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<TaskListDto>> getLists() => RustLib.instance.api.crateApiGetLists();
Future<TaskListDto> createList({required String name}) =>
RustLib.instance.api.crateApiCreateList(name: name);
Future<void> deleteList({required String listId}) =>
RustLib.instance.api.crateApiDeleteList(listId: listId);
Future<List<TaskDto>> listTasks({required String listId}) =>
RustLib.instance.api.crateApiListTasks(listId: listId);
Future<TaskDto> createTask({
required String listId,
required String title,
required String description,
}) => RustLib.instance.api.crateApiCreateTask(
listId: listId,
title: title,
description: description,
);
Future<void> updateTask({required String listId, required TaskDto task}) =>
RustLib.instance.api.crateApiUpdateTask(listId: listId, task: task);
Future<void> deleteTask({required String listId, required String taskId}) =>
RustLib.instance.api.crateApiDeleteTask(listId: listId, taskId: taskId);
Future<TaskDto> toggleTask({required String listId, required String taskId}) =>
RustLib.instance.api.crateApiToggleTask(listId: listId, taskId: taskId);
Future<void> reorderTask({
required String listId,
required String taskId,
required int newPosition,
}) => RustLib.instance.api.crateApiReorderTask(
listId: listId,
taskId: taskId,
newPosition: newPosition,
);
Future<void> moveTask({
required String fromListId,
required String toListId,
required String taskId,
}) => RustLib.instance.api.crateApiMoveTask(
fromListId: fromListId,
toListId: toListId,
taskId: taskId,
);
Future<void> renameList({required String listId, required String newName}) =>
RustLib.instance.api.crateApiRenameList(listId: listId, newName: newName);
Future<void> setGroupByDueDate({
required String listId,
required bool enabled,
}) => RustLib.instance.api.crateApiSetGroupByDueDate(
listId: listId,
enabled: enabled,
);
Future<bool> getGroupByDueDate({required String listId}) =>
RustLib.instance.api.crateApiGetGroupByDueDate(listId: listId);
Future<void> storeCredentials({
required String domain,
required String username,
required String password,
}) => RustLib.instance.api.crateApiStoreCredentials(
domain: domain,
username: username,
password: password,
);
Future<List<String>> loadCredentials({required String domain}) =>
RustLib.instance.api.crateApiLoadCredentials(domain: domain);
Future<void> setWebdavConfig({
required String workspaceName,
required String webdavUrl,
}) => RustLib.instance.api.crateApiSetWebdavConfig(
workspaceName: workspaceName,
webdavUrl: webdavUrl,
);
Future<void> testWebdavConnection({
required String url,
required String username,
required String password,
}) => RustLib.instance.api.crateApiTestWebdavConnection(
url: url,
username: username,
password: password,
);
Future<SyncResultDto> syncWorkspaceCmd({
required String workspaceName,
required String workspacePath,
required String webdavUrl,
required String username,
required String password,
required String mode,
}) => RustLib.instance.api.crateApiSyncWorkspaceCmd(
workspaceName: workspaceName,
workspacePath: workspacePath,
webdavUrl: webdavUrl,
username: username,
password: password,
mode: mode,
);
Future<Stream<void>> watchWorkspaceChanges({required String path}) =>
RustLib.instance.api.crateApiWatchWorkspaceChanges(path: path);
Future<String> greet({required String name}) =>
RustLib.instance.api.crateApiGreet(name: name);
class AppConfigDto {
final List<WorkspaceEntry> workspaces;
final String? currentWorkspace;
const AppConfigDto({required this.workspaces, this.currentWorkspace});
@override
int get hashCode => workspaces.hashCode ^ currentWorkspace.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppConfigDto &&
runtimeType == other.runtimeType &&
workspaces == other.workspaces &&
currentWorkspace == other.currentWorkspace;
}
class SyncResultDto {
final int uploaded;
final int downloaded;
final int deletedLocal;
final int deletedRemote;
final int conflicts;
final List<String> errors;
const SyncResultDto({
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 SyncResultDto &&
runtimeType == other.runtimeType &&
uploaded == other.uploaded &&
downloaded == other.downloaded &&
deletedLocal == other.deletedLocal &&
deletedRemote == other.deletedRemote &&
conflicts == other.conflicts &&
errors == other.errors;
}
class TaskDto {
final String id;
final String title;
final String description;
final String status;
final String? dueDate;
final bool hasTime;
final String createdAt;
final String updatedAt;
final String? parentId;
const TaskDto({
required this.id,
required this.title,
required this.description,
required this.status,
this.dueDate,
required this.hasTime,
required this.createdAt,
required this.updatedAt,
this.parentId,
});
@override
int get hashCode =>
id.hashCode ^
title.hashCode ^
description.hashCode ^
status.hashCode ^
dueDate.hashCode ^
hasTime.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
parentId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TaskDto &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
description == other.description &&
status == other.status &&
dueDate == other.dueDate &&
hasTime == other.hasTime &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt &&
parentId == other.parentId;
}
class TaskListDto {
final String id;
final String title;
final String createdAt;
final String updatedAt;
final bool groupByDueDate;
const TaskListDto({
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 TaskListDto &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt &&
groupByDueDate == other.groupByDueDate;
}
class WorkspaceEntry {
final String name;
final String path;
final String? webdavUrl;
final String? lastSync;
const WorkspaceEntry({
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 WorkspaceEntry &&
runtimeType == other.runtimeType &&
name == other.name &&
path == other.path &&
webdavUrl == other.webdavUrl &&
lastSync == other.lastSync;
}

File diff suppressed because it is too large Load diff

View file

@ -1,234 +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
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
RustStreamSink<void> dco_decode_StreamSink_unit_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AppConfigDto dco_decode_app_config_dto(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<TaskDto> dco_decode_list_task_dto(dynamic raw);
@protected
List<TaskListDto> dco_decode_list_task_list_dto(dynamic raw);
@protected
List<WorkspaceEntry> dco_decode_list_workspace_entry(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
SyncResultDto dco_decode_sync_result_dto(dynamic raw);
@protected
TaskDto dco_decode_task_dto(dynamic raw);
@protected
TaskListDto dco_decode_task_list_dto(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
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
RustStreamSink<void> sse_decode_StreamSink_unit_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AppConfigDto sse_decode_app_config_dto(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<TaskDto> sse_decode_list_task_dto(SseDeserializer deserializer);
@protected
List<TaskListDto> sse_decode_list_task_list_dto(SseDeserializer deserializer);
@protected
List<WorkspaceEntry> sse_decode_list_workspace_entry(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer);
@protected
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
@protected
TaskListDto sse_decode_task_list_dto(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
WorkspaceEntry sse_decode_workspace_entry(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_unit_Sse(
RustStreamSink<void> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_app_config_dto(AppConfigDto self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_task_dto(List<TaskDto> self, SseSerializer serializer);
@protected
void sse_encode_list_task_list_dto(
List<TaskListDto> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_workspace_entry(
List<WorkspaceEntry> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer);
@protected
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
@protected
void sse_encode_task_list_dto(TaskListDto 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_workspace_entry(
WorkspaceEntry self,
SseSerializer serializer,
);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
}

View file

@ -1,234 +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
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
RustStreamSink<void> dco_decode_StreamSink_unit_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AppConfigDto dco_decode_app_config_dto(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<TaskDto> dco_decode_list_task_dto(dynamic raw);
@protected
List<TaskListDto> dco_decode_list_task_list_dto(dynamic raw);
@protected
List<WorkspaceEntry> dco_decode_list_workspace_entry(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
SyncResultDto dco_decode_sync_result_dto(dynamic raw);
@protected
TaskDto dco_decode_task_dto(dynamic raw);
@protected
TaskListDto dco_decode_task_list_dto(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
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
RustStreamSink<void> sse_decode_StreamSink_unit_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AppConfigDto sse_decode_app_config_dto(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<TaskDto> sse_decode_list_task_dto(SseDeserializer deserializer);
@protected
List<TaskListDto> sse_decode_list_task_list_dto(SseDeserializer deserializer);
@protected
List<WorkspaceEntry> sse_decode_list_workspace_entry(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer);
@protected
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
@protected
TaskListDto sse_decode_task_list_dto(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
WorkspaceEntry sse_decode_workspace_entry(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_unit_Sse(
RustStreamSink<void> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_app_config_dto(AppConfigDto self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_task_dto(List<TaskDto> self, SseSerializer serializer);
@protected
void sse_encode_list_task_list_dto(
List<TaskListDto> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_workspace_entry(
List<WorkspaceEntry> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer);
@protected
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
@protected
void sse_encode_task_list_dto(TaskListDto 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_workspace_entry(
WorkspaceEntry self,
SseSerializer serializer,
);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
}
@JS('wasm_bindgen')
external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {}

View file

@ -1,372 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../rust/api.dart' as api;
import '../state/app_state.dart';
import '../theme.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _urlController = TextEditingController();
final _userController = TextEditingController();
final _passController = TextEditingController();
String _testStatus = 'idle'; // idle | testing | ok | fail
@override
void initState() {
super.initState();
_loadCredentials();
}
@override
void dispose() {
_urlController.dispose();
_userController.dispose();
_passController.dispose();
super.dispose();
}
Future<void> _loadCredentials() async {
final state = context.read<AppState>();
final wsName = state.config?.currentWorkspace;
if (wsName == null) return;
final ws = state.config!.workspaces.cast<api.WorkspaceEntry?>().firstWhere(
(w) => w?.name == wsName, orElse: () => null);
if (ws?.webdavUrl != null) {
_urlController.text = ws!.webdavUrl!;
try {
final domain = Uri.parse(ws.webdavUrl!).host;
final creds = await api.loadCredentials(domain: domain);
if (mounted) {
setState(() {
_userController.text = creds[0];
_passController.text = creds[1];
});
}
} catch (_) {}
}
}
Future<void> _testConnection() async {
setState(() => _testStatus = 'testing');
try {
await api.testWebdavConnection(
url: _urlController.text,
username: _userController.text,
password: _passController.text,
);
if (mounted) setState(() => _testStatus = 'ok');
} catch (_) {
if (mounted) setState(() => _testStatus = 'fail');
}
}
Future<void> _save() async {
final state = context.read<AppState>();
final wsName = state.config?.currentWorkspace;
if (wsName == null || _urlController.text.trim().isEmpty) return;
await api.setWebdavConfig(
workspaceName: wsName,
webdavUrl: _urlController.text.trim(),
);
if (_userController.text.isNotEmpty && _passController.text.isNotEmpty) {
final domain = Uri.parse(_urlController.text.trim()).host;
await api.storeCredentials(
domain: domain,
username: _userController.text,
password: _passController.text,
);
}
await state.loadConfig();
}
@override
Widget build(BuildContext context) {
final state = context.watch<AppState>();
final isDark = Theme.of(context).brightness == Brightness.dark;
final borderColor = isDark ? AppTheme.borderDark : AppTheme.borderLight;
final inputDecoration = InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: borderColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: borderColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppTheme.primary),
),
);
final wsName = state.config?.currentWorkspace;
final ws = wsName == null ? null : state.config!.workspaces.cast<api.WorkspaceEntry?>()
.firstWhere((w) => w?.name == wsName, orElse: () => null);
final lastSync = ws?.lastSync;
String? relTime;
if (lastSync != null) {
final t = DateTime.tryParse(lastSync);
if (t != null) {
final secsAgo = DateTime.now().difference(t).inSeconds;
if (secsAgo < 60) {
relTime = 'just now';
} else if (secsAgo < 3600) {
relTime = '${secsAgo ~/ 60}m ago';
} else {
relTime = '${secsAgo ~/ 3600}h ago';
}
}
}
return GestureDetector(
onTap: () => state.setScreen('tasks'),
child: Container(
color: Colors.black.withValues(alpha: 0.5),
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.04,
vertical: MediaQuery.of(context).size.height * 0.04,
),
child: GestureDetector(
onTap: () {},
child: Container(
decoration: BoxDecoration(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.7), blurRadius: 60, offset: const Offset(0, 25)),
BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10)),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: borderColor, width: 0.5)),
),
child: Row(
children: [
const Text('Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const Spacer(),
GestureDetector(
onTap: () => state.setScreen('tasks'),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.close, size: 20,
color: isDark ? AppTheme.textDark : AppTheme.textLight),
),
),
],
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// WebDAV section header
Text('WEBDAV SYNC',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))),
const SizedBox(height: 12),
// Credentials card
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Server URL', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
const SizedBox(height: 4),
TextField(controller: _urlController, style: const TextStyle(fontSize: 13),
decoration: inputDecoration.copyWith(hintText: 'https://dav.example.com/tasks/')),
const SizedBox(height: 10),
Text('Username', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
const SizedBox(height: 4),
TextField(controller: _userController, style: const TextStyle(fontSize: 13),
decoration: inputDecoration),
const SizedBox(height: 10),
Text('Password', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
const SizedBox(height: 4),
TextField(controller: _passController, obscureText: true,
style: const TextStyle(fontSize: 13), decoration: inputDecoration),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _urlController.text.isEmpty ? null : _testConnection,
style: OutlinedButton.styleFrom(
side: BorderSide(color: borderColor),
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(
_testStatus == 'testing' ? 'Testing…'
: _testStatus == 'ok' ? 'Connected'
: _testStatus == 'fail' ? 'Failed — Retry'
: 'Test Connection',
style: const TextStyle(fontSize: 13),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _urlController.text.isEmpty ? null : _save,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Save', style: TextStyle(fontSize: 13)),
),
),
],
),
],
),
),
// Sync direction + Sync Now
if (wsName != null) ...[
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(8),
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
),
child: DropdownButton<String>(
value: state.syncMode,
isExpanded: true,
underline: const SizedBox.shrink(),
style: TextStyle(fontSize: 13,
color: isDark ? AppTheme.textDark : AppTheme.textLight),
dropdownColor: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
items: const [
DropdownMenuItem(value: 'full', child: Text('Sync both ways')),
DropdownMenuItem(value: 'push', child: Text('Push only')),
DropdownMenuItem(value: 'pull', child: Text('Pull only')),
],
onChanged: (v) { if (v != null) state.setSyncMode(v); },
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: state.syncing ? null : () => state.triggerSync(),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
disabledBackgroundColor: AppTheme.primary.withValues(alpha: 0.4),
),
child: Text(state.syncing ? 'Syncing…' : 'Sync Now',
style: const TextStyle(fontSize: 13)),
),
),
],
),
if (relTime != null) ...[
const SizedBox(height: 6),
Row(
children: [
Text('Last sync: $relTime',
style: TextStyle(fontSize: 11,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
if (state.lastSyncResult != null) ...[
Text(' · ↑${state.lastSyncResult!.uploaded}${state.lastSyncResult!.downloaded}',
style: TextStyle(fontSize: 11,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
],
],
),
],
],
const SizedBox(height: 24),
// Appearance
Text('APPEARANCE',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))),
const SizedBox(height: 12),
GestureDetector(
onTap: () => state.toggleDarkMode(),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor),
),
child: Row(
children: [
const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const Spacer(),
AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 44, height: 24,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 150),
alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: 20, height: 20,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
),
),
),
],
),
),
),
const SizedBox(height: 32),
Center(
child: Text('Flutter + Rust',
style: TextStyle(fontSize: 12,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3))),
),
],
),
),
),
],
),
),
),
),
);
}
}

View file

@ -1,155 +0,0 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:provider/provider.dart';
import '../state/app_state.dart';
import '../theme.dart';
class SetupScreen extends StatefulWidget {
const SetupScreen({super.key});
@override
State<SetupScreen> createState() => _SetupScreenState();
}
class _SetupScreenState extends State<SetupScreen> {
final _nameController = TextEditingController();
String? _selectedPath;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _pickFolder() async {
final result = await FilePicker.platform.getDirectoryPath();
if (result != null) setState(() => _selectedPath = result);
}
Future<void> _create() async {
final name = _nameController.text.trim();
if (name.isEmpty || _selectedPath == null) return;
await context.read<AppState>().addWorkspace(name, _selectedPath!);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Container(
constraints: const BoxConstraints(maxWidth: 384),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: isDark ? AppTheme.cardDark : AppTheme.cardLight,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.15), blurRadius: 10, offset: const Offset(0, 4)),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Onyx',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700,
color: isDark ? AppTheme.textDark : AppTheme.textLight)),
const SizedBox(height: 4),
Text('Create or open a workspace to get started.',
style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
const SizedBox(height: 24),
// Workspace name label + input
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text('Workspace name', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textDark : AppTheme.textLight)),
),
TextField(
controller: _nameController,
style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight),
decoration: InputDecoration(
hintText: 'My Tasks',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppTheme.primary),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
filled: false,
),
),
const SizedBox(height: 16),
// Folder label + picker row
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text('Folder', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textDark : AppTheme.textLight)),
),
Row(
children: [
Expanded(
child: TextField(
readOnly: true,
style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight),
controller: TextEditingController(text: _selectedPath ?? ''),
decoration: InputDecoration(
hintText: 'Select a folder\u2026',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
filled: false,
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _pickFolder,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('Browse', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
),
],
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: (_nameController.text.trim().isNotEmpty && _selectedPath != null) ? _create : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
disabledBackgroundColor: AppTheme.primary.withValues(alpha: 0.4),
disabledForegroundColor: Colors.white.withValues(alpha: 0.6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(vertical: 10),
),
child: const Text('Create Workspace', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
),
),
],
),
),
),
);
}
}

View file

@ -1,874 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../rust/api.dart' as api;
import '../state/app_state.dart';
import '../theme.dart';
import '../widgets/custom_title_bar.dart';
import '../widgets/task_item.dart';
import '../widgets/task_detail_view.dart';
import '../widgets/new_task_input.dart';
class TasksScreen extends StatefulWidget {
const TasksScreen({super.key});
@override
State<TasksScreen> createState() => _TasksScreenState();
}
class _TasksScreenState extends State<TasksScreen> with SingleTickerProviderStateMixin {
bool _drawerOpen = false;
bool _showCompleted = false;
bool _completedVisible = false;
bool _addingList = false;
bool _workspaceSwitcherOpen = false;
bool _newTaskOpen = false;
final _newListController = TextEditingController();
final _newListFocus = FocusNode();
late final AnimationController _newTaskAnim;
late final Animation<Offset> _newTaskSlide;
late final Animation<double> _newTaskFade;
@override
void initState() {
super.initState();
_newTaskAnim = AnimationController(vsync: this, duration: const Duration(milliseconds: 150));
_newTaskSlide = Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero)
.animate(CurvedAnimation(parent: _newTaskAnim, curve: Curves.easeOut));
_newTaskFade = CurvedAnimation(parent: _newTaskAnim, curve: Curves.easeOut);
}
@override
void dispose() {
_newTaskAnim.dispose();
_newListController.dispose();
_newListFocus.dispose();
super.dispose();
}
void _toggleDrawer() => setState(() {
_drawerOpen = !_drawerOpen;
if (!_drawerOpen) _workspaceSwitcherOpen = false;
});
void _closeDrawer() => setState(() {
_drawerOpen = false;
_workspaceSwitcherOpen = false;
_addingList = false;
});
void _showNewTask() {
final state = context.read<AppState>();
if (state.activeListId == null) return;
setState(() => _newTaskOpen = true);
_newTaskAnim.forward();
}
void _closeNewTask() {
_newTaskAnim.reverse().then((_) {
if (mounted) setState(() => _newTaskOpen = false);
});
}
Future<void> _handleCreateTask(String title, String desc, {String? dueDate, bool hasTime = false}) async {
final state = context.read<AppState>();
final task = await state.createTask(title, desc);
if (task != null && dueDate != null) {
await state.updateTask(api.TaskDto(
id: task.id, title: task.title, description: task.description,
status: task.status, dueDate: dueDate, hasTime: hasTime,
createdAt: task.createdAt, updatedAt: task.updatedAt, parentId: task.parentId,
));
}
_closeNewTask();
}
void _startAddingList() {
setState(() {
_addingList = true;
_newListController.clear();
});
WidgetsBinding.instance.addPostFrameCallback((_) => _newListFocus.requestFocus());
}
Future<void> _submitNewList() async {
final name = _newListController.text.trim();
if (name.isNotEmpty) await context.read<AppState>().createList(name);
setState(() => _addingList = false);
}
@override
Widget build(BuildContext context) {
final state = context.watch<AppState>();
final isDark = Theme.of(context).brightness == Brightness.dark;
return LayoutBuilder(builder: (context, constraints) {
final width = constraints.maxWidth;
final drawerWidth = width * 0.8;
final hasDetail = state.selectedTask != null;
return Stack(
clipBehavior: Clip.hardEdge,
children: [
// Sliding container: drawer + main + detail
Positioned.fill(
child: ClipRect(
child: OverflowBox(
maxWidth: drawerWidth + width,
alignment: Alignment.centerLeft,
child: AnimatedSlide(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
offset: _drawerOpen ? Offset.zero : Offset(-drawerWidth / (drawerWidth + width), 0),
child: SizedBox(
width: drawerWidth + width,
child: Row(
children: [
SizedBox(width: drawerWidth, child: _buildDrawer(state, isDark)),
SizedBox(
width: width,
child: _buildMainWithDetail(state, isDark, width),
),
],
),
),
),
),
),
),
// FAB button (centered, 56px, hidden when drawer/detail/newTask open)
if (!_drawerOpen && !hasDetail && !_newTaskOpen && state.activeListId != null)
Positioned(
bottom: 24,
left: 0,
right: 0,
child: Center(
child: SizedBox(
width: 56,
height: 56,
child: FloatingActionButton(
onPressed: _showNewTask,
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
elevation: 6,
shape: const CircleBorder(),
child: const Icon(Icons.add, size: 28),
),
),
),
),
// New task overlay (animated, inside app bounds)
if (_newTaskOpen || _newTaskAnim.isAnimating)
Positioned.fill(
child: FadeTransition(
opacity: _newTaskFade,
child: GestureDetector(
onTap: _closeNewTask,
child: Container(
color: Colors.black.withValues(alpha: 0.4),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () {},
child: SlideTransition(
position: _newTaskSlide,
child: NewTaskInput(onCreate: _handleCreateTask),
),
),
),
),
),
),
],
);
});
}
Widget _buildMainWithDetail(AppState state, bool isDark, double totalWidth) {
final hasDetail = state.selectedTask != null;
return Stack(
clipBehavior: Clip.hardEdge,
children: [
ClipRect(
child: OverflowBox(
maxWidth: totalWidth * 2,
alignment: Alignment.centerLeft,
child: AnimatedSlide(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
offset: hasDetail ? const Offset(-0.5, 0) : Offset.zero,
child: SizedBox(
width: totalWidth * 2,
child: Row(
children: [
SizedBox(width: totalWidth, child: _buildMain(state, isDark)),
SizedBox(
width: totalWidth,
child: Container(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
child: hasDetail
? TaskDetailView(task: state.selectedTask!)
: const SizedBox.shrink(),
),
),
],
),
),
),
),
),
// Drawer shadow (narrow element at left edge casting right)
Positioned(
left: 0,
top: 0,
bottom: 0,
width: 1,
child: IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
opacity: _drawerOpen ? 1.0 : 0.0,
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.4),
blurRadius: 16,
offset: const Offset(4, 0),
),
],
),
),
),
),
),
// Dim overlay when drawer is open
Positioned.fill(
child: IgnorePointer(
ignoring: !_drawerOpen,
child: GestureDetector(
onTap: _closeDrawer,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
opacity: _drawerOpen ? 1.0 : 0.0,
child: Container(
color: Colors.black.withValues(alpha: 0.4),
),
),
),
),
),
// Sync status indicator
Positioned(
bottom: 16,
right: 16,
child: IgnorePointer(
child: state.syncing
? const SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: AppTheme.primary))
: state.lastSyncResult != null
? Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.lastSyncResult!.uploaded}${state.lastSyncResult!.downloaded}',
style: TextStyle(fontSize: 11,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)),
),
)
: const SizedBox.shrink(),
),
),
],
);
}
Widget _buildDrawer(AppState state, bool isDark) {
return Container(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
child: Stack(
children: [
Column(
children: [
// Header: workspace switcher
GestureDetector(
onPanStart: (_) {},
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5),
),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _workspaceSwitcherOpen = !_workspaceSwitcherOpen),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
state.config?.currentWorkspace ?? 'Workspace',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
AnimatedRotation(
turns: _workspaceSwitcherOpen ? 0.5 : 0,
duration: const Duration(milliseconds: 150),
child: Icon(Icons.expand_more, size: 14,
color: isDark ? AppTheme.textDark : AppTheme.textLight),
),
],
),
),
),
),
],
),
),
),
// List items
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
for (final list in state.lists)
_ListTile(
list: list,
isActive: list.id == state.activeListId,
onTap: () {
state.selectList(list.id);
_closeDrawer();
},
onDelete: () => state.deleteList(list.id),
onRename: (newName) => state.renameList(list.id, newName),
onToggleGroupByDueDate: () => state.setGroupByDueDate(list.id, !list.groupByDueDate),
),
// New list button / input
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: _addingList
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: [
Expanded(
child: TextField(
controller: _newListController,
focusNode: _newListFocus,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: 'List name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppTheme.primary),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
onSubmitted: (_) => _submitNewList(),
onTapOutside: (_) => setState(() => _addingList = false),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _submitNewList,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('Add', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
),
],
),
)
: GestureDetector(
onTap: _startAddingList,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Text('+ New list', style: TextStyle(fontSize: 14, color: AppTheme.primary)),
),
),
),
],
),
),
// Footer: Settings button (matching Tauri)
GestureDetector(
onTap: () => state.setScreen('settings'),
child: Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
children: [
Icon(Icons.settings, size: 18,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)),
const SizedBox(width: 8),
Text('Settings', style: TextStyle(fontSize: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5))),
],
),
),
),
],
),
// Workspace switcher popup backdrop
if (_workspaceSwitcherOpen)
Positioned.fill(
child: GestureDetector(
onTap: () => setState(() => _workspaceSwitcherOpen = false),
behavior: HitTestBehavior.opaque,
child: const SizedBox.expand(),
),
),
// Workspace switcher popup menu
Positioned(
left: 8,
right: 8,
top: 48,
child: AnimatedScale(
scale: _workspaceSwitcherOpen ? 1.0 : 0.9,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
alignment: Alignment.topLeft,
child: AnimatedOpacity(
opacity: _workspaceSwitcherOpen ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
child: IgnorePointer(
ignoring: !_workspaceSwitcherOpen,
child: Container(
constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration(
color: isDark ? AppTheme.cardDark : AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 12, offset: const Offset(0, 4)),
],
),
clipBehavior: Clip.antiAlias,
child: state.config != null
? ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 4),
children: [
for (final ws in state.config!.workspaces)
_WorkspaceMenuItem(
name: ws.name,
path: ws.path,
isActive: ws.name == state.config?.currentWorkspace,
onTap: () {
state.switchWorkspace(ws.name);
setState(() => _workspaceSwitcherOpen = false);
},
),
// Add workspace
_WorkspaceMenuItem(
icon: null,
name: '+ Add workspace',
path: null,
isActive: false,
isAccent: true,
showDivider: true,
onTap: () {
setState(() => _workspaceSwitcherOpen = false);
state.setScreen('setup');
},
),
],
)
: const SizedBox.shrink(),
),
),
),
),
),
],
),
);
}
Widget _buildMain(AppState state, bool isDark) {
return Column(
children: [
// Title bar with menu button + centered title + close
CustomTitleBar(
leading: GestureDetector(
onTap: _toggleDrawer,
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(Icons.menu, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)),
),
),
title: state.activeList?.title ?? 'Tasks',
centerTitle: true,
),
// Task list
Expanded(
child: state.lists.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('No lists yet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6))),
const SizedBox(height: 4),
Text('Tap the list name above to create one', style: TextStyle(fontSize: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
],
),
)
: state.activeList == null
? Center(
child: Text('Select a list', style: TextStyle(
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
)
: _buildTaskList(state, isDark),
),
],
);
}
Widget _buildTaskList(AppState state, bool isDark) {
if (state.pendingTasks.isEmpty && state.completedTasks.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text('No tasks. Add one below.', style: TextStyle(fontSize: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
),
);
}
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex--;
final task = state.pendingTasks[oldIndex];
state.reorderTask(task.id, newIndex);
},
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) => Material(
elevation: 4,
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
child: child,
),
child: child,
);
},
footer: _buildCompletedSection(state, isDark),
children: [
for (var i = 0; i < state.pendingTasks.length; i++)
ReorderableDragStartListener(
key: ValueKey(state.pendingTasks[i].id),
index: i,
child: TaskItem(
task: state.pendingTasks[i],
onToggle: () => state.toggleTask(state.pendingTasks[i].id),
onTap: () => state.selectTask(state.pendingTasks[i].id),
),
),
],
);
}
Widget? _buildCompletedSection(AppState state, bool isDark) {
if (state.completedTasks.isEmpty) return null;
return Column(
children: [
const SizedBox(height: 16),
// Completed header (matching Tauri: full-width, border-top, text left, chevron right)
GestureDetector(
onTap: () {
setState(() {
if (_showCompleted) {
_showCompleted = false;
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted) setState(() => _completedVisible = false);
});
} else {
_completedVisible = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => _showCompleted = true);
});
}
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
),
child: Row(
children: [
Expanded(
child: Text(
'Completed (${state.completedTasks.length})',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight,
),
),
),
AnimatedRotation(
turns: _showCompleted ? 0.25 : 0,
duration: const Duration(milliseconds: 150),
child: Icon(Icons.chevron_right, size: 16,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight),
),
],
),
),
),
if (_completedVisible)
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: _showCompleted ? 1.0 : 0.0,
child: AnimatedSlide(
duration: const Duration(milliseconds: 150),
offset: _showCompleted ? Offset.zero : const Offset(0, -0.05),
child: Column(
children: [
for (final task in state.completedTasks)
TaskItem(
task: task,
onToggle: () => state.toggleTask(task.id),
onTap: () => state.selectTask(task.id),
),
],
),
),
),
],
);
}
}
class _ListTile extends StatefulWidget {
final dynamic list;
final bool isActive;
final VoidCallback onTap;
final VoidCallback onDelete;
final void Function(String newName) onRename;
final VoidCallback onToggleGroupByDueDate;
const _ListTile({required this.list, required this.isActive, required this.onTap, required this.onDelete, required this.onRename, required this.onToggleGroupByDueDate});
@override
State<_ListTile> createState() => _ListTileState();
}
class _ListTileState extends State<_ListTile> {
bool _hovering = false;
void _showRenameDialog(BuildContext context) {
final controller = TextEditingController(text: widget.list.title);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Rename list'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(hintText: 'List name'),
onSubmitted: (value) {
Navigator.pop(ctx);
if (value.trim().isNotEmpty && value.trim() != widget.list.title) {
widget.onRename(value.trim());
}
},
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
TextButton(
onPressed: () {
Navigator.pop(ctx);
final name = controller.text.trim();
if (name.isNotEmpty && name != widget.list.title) widget.onRename(name);
},
child: const Text('Rename'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: GestureDetector(
onTap: widget.onTap,
onSecondaryTapUp: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0),
items: [
PopupMenuItem(
onTap: () => _showRenameDialog(context),
child: const Text('Rename', style: TextStyle(fontSize: 13)),
),
PopupMenuItem(
onTap: widget.onToggleGroupByDueDate,
child: Row(
children: [
Expanded(child: Text('Group by due date', style: const TextStyle(fontSize: 13))),
if (widget.list.groupByDueDate)
const Icon(Icons.check, size: 16, color: AppTheme.primary),
],
),
),
PopupMenuItem(
onTap: widget.onDelete,
child: const Text('Delete', style: TextStyle(color: AppTheme.danger, fontSize: 13)),
),
],
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
decoration: BoxDecoration(
color: _hovering && !widget.isActive
? (isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05))
: Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
if (widget.isActive)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(Icons.check, size: 16,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)),
),
Expanded(
child: Text(
widget.list.title,
style: TextStyle(
fontSize: 14,
fontWeight: widget.isActive ? FontWeight.w700 : FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
}
class _WorkspaceMenuItem extends StatefulWidget {
final String name;
final String? path;
final bool isActive;
final bool isAccent;
final bool showDivider;
final IconData? icon;
final VoidCallback onTap;
const _WorkspaceMenuItem({
required this.name,
this.path,
required this.isActive,
this.isAccent = false,
this.showDivider = false,
this.icon,
required this.onTap,
});
@override
State<_WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
}
class _WorkspaceMenuItemState extends State<_WorkspaceMenuItem> {
bool _hovering = false;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showDivider)
Divider(height: 1, thickness: 0.5, color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: GestureDetector(
onTap: widget.onTap,
child: Container(
color: _hovering
? (isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05))
: Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
if (widget.isActive)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(Icons.check, size: 16,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.name,
style: TextStyle(
fontSize: 14,
fontWeight: widget.isActive ? FontWeight.w700 : FontWeight.normal,
color: widget.isAccent ? AppTheme.primary : null,
),
overflow: TextOverflow.ellipsis),
if (widget.path != null)
Text(widget.path!,
style: TextStyle(fontSize: 12,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
overflow: TextOverflow.ellipsis),
],
),
),
],
),
),
),
),
],
);
}
}

View file

@ -1,322 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../rust/api.dart' as api;
class AppState extends ChangeNotifier {
String screen = 'setup';
api.AppConfigDto? config;
List<api.TaskListDto> lists = [];
String? activeListId;
List<api.TaskDto> tasks = [];
bool darkMode = true;
StreamSubscription? _watcherSub;
bool syncing = false;
String syncMode = 'full';
api.SyncResultDto? lastSyncResult;
String? error;
// Selected task for detail view
String? selectedTaskId;
api.TaskListDto? get activeList =>
activeListId == null ? null : lists.cast<api.TaskListDto?>().firstWhere((l) => l?.id == activeListId, orElse: () => null);
List<api.TaskDto> get pendingTasks => tasks.where((t) => t.status == 'backlog').toList();
List<api.TaskDto> get completedTasks => tasks.where((t) => t.status == 'completed').toList();
bool get hasWorkspace =>
config != null && config!.currentWorkspace != null && config!.workspaces.isNotEmpty;
api.TaskDto? get selectedTask =>
selectedTaskId == null ? null : tasks.cast<api.TaskDto?>().firstWhere((t) => t?.id == selectedTaskId, orElse: () => null);
Future<void> _startWatcher(String path) async {
_watcherSub?.cancel();
try {
final stream = await api.watchWorkspaceChanges(path: path);
_watcherSub = stream.listen((_) => loadLists());
} catch (_) {}
}
Future<void> loadConfig() async {
try {
config = await api.getConfig();
if (hasWorkspace) {
screen = 'tasks';
await loadLists();
final ws = config!.workspaces.firstWhere((w) => w.name == config!.currentWorkspace);
_startWatcher(ws.path);
} else {
screen = 'setup';
}
} catch (e) {
config = const api.AppConfigDto(workspaces: [], currentWorkspace: null);
screen = 'setup';
}
notifyListeners();
}
Future<void> addWorkspace(String name, String path) async {
try {
await api.initWorkspace(path: path);
await api.addWorkspace(name: name, path: path);
config = await api.getConfig();
await loadLists();
_startWatcher(path);
screen = 'tasks';
error = null;
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> switchWorkspace(String name) async {
try {
await api.setCurrentWorkspace(name: name);
config = await api.getConfig();
activeListId = null;
await loadLists();
final ws = config!.workspaces.firstWhere((w) => w.name == name);
_startWatcher(ws.path);
error = null;
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> removeWorkspace(String name) async {
try {
await api.removeWorkspace(name: name);
config = await api.getConfig();
if (!hasWorkspace) {
screen = 'setup';
lists = [];
tasks = [];
activeListId = null;
}
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> loadLists() async {
try {
lists = await api.getLists();
if (lists.isNotEmpty && activeListId == null) {
activeListId = lists[0].id;
}
if (activeListId != null) await loadTasks();
} catch (e) {
error = e.toString();
}
}
Future<void> loadTasks() async {
if (activeListId == null) return;
try {
tasks = await api.listTasks(listId: activeListId!);
} catch (e) {
error = e.toString();
}
}
Future<void> selectList(String id) async {
activeListId = id;
selectedTaskId = null;
await loadTasks();
notifyListeners();
}
Future<void> createList(String name) async {
try {
final list = await api.createList(name: name);
lists = [...lists, 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 = lists.where((l) => l.id != id).toList();
if (activeListId == id) {
activeListId = lists.isNotEmpty ? lists[0].id : null;
if (activeListId != null) {
await loadTasks();
} else {
tasks = [];
}
}
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<api.TaskDto?> createTask(String title, String description) async {
if (activeListId == null) return null;
try {
final task = await api.createTask(listId: activeListId!, title: title, description: description);
tasks = [...tasks, task];
error = null;
notifyListeners();
return task;
} catch (e) {
error = e.toString();
notifyListeners();
return null;
}
}
Future<void> toggleTask(String taskId) async {
if (activeListId == null) return;
try {
final updated = await api.toggleTask(listId: activeListId!, taskId: taskId);
if (updated.status == 'backlog') {
tasks = [updated, ...tasks.where((t) => t.id != taskId)];
try {
await api.reorderTask(listId: activeListId!, taskId: taskId, newPosition: 0);
} catch (_) {}
} else {
tasks = tasks.map((t) => t.id == taskId ? updated : t).toList();
}
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> updateTask(api.TaskDto task) async {
if (activeListId == null) return;
try {
await api.updateTask(listId: activeListId!, task: task);
tasks = tasks.map((t) => t.id == task.id ? task : t).toList();
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> reorderTask(String taskId, int newPosition) async {
if (activeListId == null) return;
try {
await api.reorderTask(listId: activeListId!, taskId: taskId, newPosition: newPosition);
await loadTasks();
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> moveTask(String taskId, String targetListId) async {
if (activeListId == null) return;
try {
await api.moveTask(fromListId: activeListId!, toListId: targetListId, taskId: taskId);
tasks = tasks.where((t) => t.id != taskId).toList();
if (selectedTaskId == taskId) selectedTaskId = null;
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> renameList(String listId, String newName) async {
try {
await api.renameList(listId: listId, newName: newName);
await loadLists();
} catch (e) {
error = e.toString();
}
notifyListeners();
}
Future<void> setGroupByDueDate(String listId, bool enabled) async {
try {
await api.setGroupByDueDate(listId: listId, enabled: enabled);
await loadLists();
if (listId == activeListId) 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 = tasks.where((t) => t.id != taskId).toList();
if (selectedTaskId == taskId) selectedTaskId = null;
} catch (e) {
error = e.toString();
}
notifyListeners();
}
void selectTask(String? taskId) {
selectedTaskId = taskId;
notifyListeners();
}
void setSyncMode(String mode) {
syncMode = mode;
notifyListeners();
}
Future<void> triggerSync() async {
if (config?.currentWorkspace == null) return;
final wsName = config!.currentWorkspace!;
final ws = config!.workspaces.firstWhere((w) => w.name == wsName);
if (ws.webdavUrl == null) {
error = 'No WebDAV URL configured';
notifyListeners();
return;
}
syncing = true;
error = null;
notifyListeners();
try {
final domain = Uri.parse(ws.webdavUrl!).host;
final creds = await api.loadCredentials(domain: domain);
final result = await api.syncWorkspaceCmd(
workspaceName: wsName,
workspacePath: ws.path,
webdavUrl: ws.webdavUrl!,
username: creds[0],
password: creds[1],
mode: syncMode,
);
lastSyncResult = result;
if (result.errors.isNotEmpty) error = result.errors.join('; ');
config = await api.getConfig();
await loadLists();
} catch (e) {
error = e.toString();
}
syncing = false;
notifyListeners();
}
void toggleDarkMode() {
darkMode = !darkMode;
notifyListeners();
}
void setScreen(String s) {
screen = s;
notifyListeners();
}
void clearError() {
error = null;
notifyListeners();
}
}

View file

@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static const primary = Color(0xFF2D87B8);
static const primaryHover = Color(0xFF2474A0);
static const danger = Color(0xFFEF4444);
static const surfaceLight = Color(0xFFFFFFFF);
static const cardLight = Color(0xFFF9FAFB);
static const textLight = Color(0xFF1F2937);
static const textSecondaryLight = Color(0xFF6B7280);
static const borderLight = Color(0xFFE5E7EB);
static const surfaceDark = Color(0xFF242424);
static const cardDark = Color(0xFF303030);
static const textDark = Color(0xFFE5E7EB);
static const textSecondaryDark = Color(0xFF9CA3AF);
static const borderDark = Color(0xFF3D3D3D);
static ThemeData light() => ThemeData(
brightness: Brightness.light,
colorScheme: const ColorScheme.light(
primary: primary,
surface: surfaceLight,
error: danger,
),
scaffoldBackgroundColor: surfaceLight,
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.light().textTheme),
dividerColor: borderLight,
cardColor: cardLight,
);
static ThemeData dark() => ThemeData(
brightness: Brightness.dark,
colorScheme: const ColorScheme.dark(
primary: primary,
surface: surfaceDark,
error: danger,
),
scaffoldBackgroundColor: surfaceDark,
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme),
dividerColor: borderDark,
cardColor: cardDark,
);
}

View file

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../theme.dart';
class CustomTitleBar extends StatelessWidget {
final Widget? leading;
final String? title;
final bool centerTitle;
final List<Widget>? actions;
final bool showClose;
const CustomTitleBar({super.key, this.leading, this.title, this.centerTitle = false, this.actions, this.showClose = true});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onPanStart: (_) => windowManager.startDragging(),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isDark ? AppTheme.borderDark : AppTheme.borderLight,
width: 0.5,
),
),
),
child: Row(
children: [
if (leading != null) leading!,
if (title != null)
Expanded(
child: centerTitle
? Center(
child: Text(
title!,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
)
: Text(
title!,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
)
else
const Expanded(child: SizedBox.shrink()),
if (actions != null) ...actions!,
if (showClose) ...[
const SizedBox(width: 4),
_TitleBarButton(
icon: Icons.close,
onPressed: () => windowManager.close(),
hoverColor: AppTheme.danger,
),
],
],
),
),
);
}
}
class _TitleBarButton extends StatefulWidget {
final IconData icon;
final VoidCallback onPressed;
final Color hoverColor;
const _TitleBarButton({required this.icon, required this.onPressed, required this.hoverColor});
@override
State<_TitleBarButton> createState() => _TitleBarButtonState();
}
class _TitleBarButtonState extends State<_TitleBarButton> {
bool _hovering = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: GestureDetector(
onTap: widget.onPressed,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: _hovering ? Colors.black.withValues(alpha: 0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Icon(widget.icon, size: 14,
color: _hovering ? widget.hoverColor : Theme.of(context).iconTheme.color?.withValues(alpha: 0.5)),
),
),
);
}
}

View file

@ -1,209 +0,0 @@
import 'package:flutter/material.dart';
import '../theme.dart';
class DateTimePicker extends StatefulWidget {
final DateTime? initialDate;
final bool initialHasTime;
final void Function(DateTime date, bool hasTime) onDone;
final VoidCallback onClear;
const DateTimePicker({super.key, this.initialDate, this.initialHasTime = false, required this.onDone, required this.onClear});
@override
State<DateTimePicker> createState() => _DateTimePickerState();
}
class _DateTimePickerState extends State<DateTimePicker> {
late DateTime _viewMonth;
DateTime? _selected;
bool _showTime = false;
int _hour = 12;
int _minute = 0;
@override
void initState() {
super.initState();
_selected = widget.initialDate;
_viewMonth = widget.initialDate ?? DateTime.now();
if (widget.initialDate != null) {
_hour = widget.initialDate!.hour;
_minute = widget.initialDate!.minute;
_showTime = widget.initialHasTime;
}
}
void _prevMonth() => setState(() => _viewMonth = DateTime(_viewMonth.year, _viewMonth.month - 1));
void _nextMonth() => setState(() => _viewMonth = DateTime(_viewMonth.year, _viewMonth.month + 1));
void _done() {
if (_selected == null) return;
final result = _showTime
? DateTime(_selected!.year, _selected!.month, _selected!.day, _hour, _minute)
: DateTime(_selected!.year, _selected!.month, _selected!.day);
widget.onDone(result, _showTime);
Navigator.of(context).pop();
}
void _clear() {
widget.onClear();
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final firstDay = DateTime(_viewMonth.year, _viewMonth.month, 1);
final lastDay = DateTime(_viewMonth.year, _viewMonth.month + 1, 0);
final startWeekday = firstDay.weekday; // 1=Mon
const dayNames = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
children: [
const Text('Date & Time', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
const Spacer(),
GestureDetector(
onTap: _done,
child: const Text('Done', style: TextStyle(fontSize: 14, color: AppTheme.primary, fontWeight: FontWeight.w500)),
),
],
),
const SizedBox(height: 16),
// Month navigation
Row(
children: [
GestureDetector(onTap: _prevMonth, child: const Icon(Icons.chevron_left, size: 20)),
Expanded(
child: Center(
child: Text('${months[_viewMonth.month - 1]} ${_viewMonth.year}',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
),
),
GestureDetector(onTap: _nextMonth, child: const Icon(Icons.chevron_right, size: 20)),
],
),
const SizedBox(height: 12),
// Day names
Row(
children: [
for (final name in dayNames)
Expanded(
child: Center(
child: Text(name, style: TextStyle(fontSize: 11, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
),
),
],
),
const SizedBox(height: 4),
// Calendar grid
...List.generate(6, (week) {
return Row(
children: List.generate(7, (dow) {
final dayIndex = week * 7 + dow - (startWeekday - 1);
if (dayIndex < 0 || dayIndex >= lastDay.day) return const Expanded(child: SizedBox(height: 32));
final day = dayIndex + 1;
final date = DateTime(_viewMonth.year, _viewMonth.month, day);
final isToday = date == today;
final isSelected = _selected != null && date.year == _selected!.year && date.month == _selected!.month && date.day == _selected!.day;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selected = date),
child: Container(
height: 32,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? AppTheme.primary : Colors.transparent,
),
child: Text(
'$day',
style: TextStyle(
fontSize: 13,
fontWeight: isToday ? FontWeight.w700 : FontWeight.normal,
color: isSelected ? Colors.white : (isToday ? AppTheme.primary : null),
),
),
),
),
);
}),
);
}),
const SizedBox(height: 8),
// Time section
Container(
padding: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
),
child: _showTime
? Row(
children: [
Text('Time', style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
const SizedBox(width: 12),
// Hour
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
borderRadius: BorderRadius.circular(6),
),
child: DropdownButton<int>(
value: _hour,
underline: const SizedBox.shrink(),
isDense: true,
style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight),
items: List.generate(24, (i) => DropdownMenuItem(value: i, child: Text(i.toString().padLeft(2, '0')))),
onChanged: (v) => setState(() => _hour = v!),
),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 6), child: Text(':', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
// Minute
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
borderRadius: BorderRadius.circular(6),
),
child: DropdownButton<int>(
value: _minute,
underline: const SizedBox.shrink(),
isDense: true,
style: TextStyle(fontSize: 14, color: isDark ? AppTheme.textDark : AppTheme.textLight),
items: List.generate(12, (i) => i * 5).map((m) => DropdownMenuItem(value: m, child: Text(m.toString().padLeft(2, '0')))).toList(),
onChanged: (v) => setState(() => _minute = v!),
),
),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _showTime = false),
child: Icon(Icons.close, size: 18, color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withAlpha(160)),
),
],
)
: GestureDetector(
onTap: () => setState(() { _showTime = true; if (_selected == null) _selected = DateTime.now(); }),
child: Text('Set time', style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
),
),
// Clear button
if (widget.initialDate != null) ...[
const SizedBox(height: 12),
GestureDetector(
onTap: _clear,
child: const Text('Clear date', style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
],
),
);
}
}

View file

@ -1,201 +0,0 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'date_time_picker.dart';
class NewTaskInput extends StatefulWidget {
final Future<void> Function(String title, String description, {String? dueDate, bool hasTime}) onCreate;
const NewTaskInput({super.key, required this.onCreate});
@override
State<NewTaskInput> createState() => _NewTaskInputState();
}
class _NewTaskInputState extends State<NewTaskInput> {
final _titleController = TextEditingController();
final _descController = TextEditingController();
final _titleFocus = FocusNode();
DateTime? _selectedDate;
bool _selectedHasTime = false;
@override
void initState() {
super.initState();
_titleFocus.requestFocus();
}
@override
void dispose() {
_titleController.dispose();
_descController.dispose();
_titleFocus.dispose();
super.dispose();
}
Future<void> _submit() async {
final title = _titleController.text.trim();
if (title.isEmpty) return;
await widget.onCreate(title, _descController.text.trim(), dueDate: _selectedDate?.toUtc().toIso8601String(), hasTime: _selectedHasTime);
}
void _pickDate() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => DateTimePicker(
initialDate: _selectedDate,
initialHasTime: _selectedHasTime,
onDone: (date, hasTime) => setState(() { _selectedDate = date; _selectedHasTime = hasTime; }),
onClear: () => setState(() { _selectedDate = null; _selectedHasTime = false; }),
),
);
}
String _formatDateChip(DateTime d) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final taskDate = DateTime(d.year, d.month, d.day);
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
final day = dayNames[d.weekday % 7];
final pad = (int n) => n.toString().padLeft(2, '0');
final timePart = _selectedHasTime ? ', ${pad(d.hour)}:${pad(d.minute)}' : '';
if (taskDate == today) return 'Today$timePart';
return '$day, ${pad(d.day)}/${pad(d.month)}$timePart';
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: isDark ? AppTheme.cardDark : AppTheme.surfaceLight,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: EdgeInsets.only(
left: 16, right: 16, top: 16,
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title input
TextField(
controller: _titleController,
focusNode: _titleFocus,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
decoration: InputDecoration(
hintText: 'Task title',
hintStyle: TextStyle(
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
onSubmitted: (_) => _submit(),
),
const SizedBox(height: 16),
// Description with icon (matching Tauri)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(Icons.subject, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _descController,
style: const TextStyle(fontSize: 14),
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add details',
hintStyle: TextStyle(
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
],
),
const SizedBox(height: 16),
// Date/time with icon (matching Tauri)
Row(
children: [
Icon(Icons.access_time, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
const SizedBox(width: 12),
if (_selectedDate != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: _pickDate,
child: Text(
_formatDateChip(_selectedDate!),
style: const TextStyle(fontSize: 14),
),
),
const SizedBox(width: 6),
GestureDetector(
onTap: () => setState(() => _selectedDate = null),
child: Icon(Icons.close, size: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
),
],
),
)
else
GestureDetector(
onTap: _pickDate,
child: Text('Add date/time', style: TextStyle(fontSize: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
),
],
),
const SizedBox(height: 16),
// Save button (centered, matching Tauri)
Container(
padding: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
),
child: SizedBox(
width: double.infinity,
child: GestureDetector(
onTap: _submit,
child: Text('Save',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: _titleController.text.trim().isNotEmpty
? AppTheme.primary
: AppTheme.primary.withValues(alpha: 0.3),
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
),
);
}
}

View file

@ -1,430 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import '../rust/api.dart' as api;
import '../state/app_state.dart';
import '../theme.dart';
import 'package:provider/provider.dart';
import 'date_time_picker.dart';
class TaskDetailView extends StatefulWidget {
final api.TaskDto task;
const TaskDetailView({super.key, required this.task});
@override
State<TaskDetailView> createState() => _TaskDetailViewState();
}
class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProviderStateMixin {
late TextEditingController _titleController;
late TextEditingController _descController;
Timer? _debounce;
bool _showMenu = false;
late final AnimationController _menuAnim;
late final Animation<double> _menuFade;
late final Animation<double> _menuScale;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.task.title);
_descController = TextEditingController(text: widget.task.description);
_menuAnim = AnimationController(vsync: this, duration: const Duration(milliseconds: 150));
_menuFade = CurvedAnimation(parent: _menuAnim, curve: Curves.easeOut);
_menuScale = Tween<double>(begin: 0.9, end: 1.0)
.animate(CurvedAnimation(parent: _menuAnim, curve: Curves.easeOut));
}
@override
void didUpdateWidget(TaskDetailView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.task.id != widget.task.id) {
_titleController.text = widget.task.title;
_descController.text = widget.task.description;
_showMenu = false;
}
}
@override
void dispose() {
_debounce?.cancel();
_menuAnim.dispose();
_titleController.dispose();
_descController.dispose();
super.dispose();
}
void _scheduleUpdate({String? dueDate, bool? hasTime}) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 400), () {
final state = context.read<AppState>();
state.updateTask(api.TaskDto(
id: widget.task.id,
title: _titleController.text,
description: _descController.text,
status: widget.task.status,
dueDate: dueDate ?? widget.task.dueDate,
hasTime: hasTime ?? widget.task.hasTime,
createdAt: widget.task.createdAt,
updatedAt: widget.task.updatedAt,
parentId: widget.task.parentId,
));
});
}
void _updateDueDate(String? dueDate, {bool hasTime = false}) {
final state = context.read<AppState>();
state.updateTask(api.TaskDto(
id: widget.task.id,
title: _titleController.text,
description: _descController.text,
status: widget.task.status,
dueDate: dueDate,
hasTime: hasTime,
createdAt: widget.task.createdAt,
updatedAt: widget.task.updatedAt,
parentId: widget.task.parentId,
));
}
void _editDate() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => DateTimePicker(
initialDate: widget.task.dueDate != null ? DateTime.tryParse(widget.task.dueDate!) : null,
initialHasTime: widget.task.hasTime,
onDone: (date, hasTime) => _updateDueDate(date.toUtc().toIso8601String(), hasTime: hasTime),
onClear: () => _updateDueDate(null),
),
);
}
String _formatDateChip(String iso) {
final d = DateTime.tryParse(iso);
if (d == null) return iso;
final local = d.toLocal();
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final taskDate = DateTime(local.year, local.month, local.day);
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
final day = dayNames[local.weekday % 7];
final pad = (int n) => n.toString().padLeft(2, '0');
final timePart = widget.task.hasTime ? ', ${pad(local.hour)}:${pad(local.minute)}' : '';
if (taskDate == today) return 'Today$timePart';
return '$day, ${pad(local.day)}/${pad(local.month)}$timePart';
}
void _showMoveToSheet(BuildContext context, AppState state) {
final otherLists = state.lists.where((l) => l.id != state.activeListId).toList();
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text('Move to...', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
),
for (final list in otherLists)
ListTile(
title: Text(list.title, style: const TextStyle(fontSize: 14)),
onTap: () {
Navigator.pop(context);
state.moveTask(widget.task.id, list.id);
state.selectTask(null);
},
),
const SizedBox(height: 8),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final state = context.read<AppState>();
final isCompleted = widget.task.status == 'completed';
return Column(
children: [
// Header (just back button, matching Tauri)
GestureDetector(
onPanStart: (_) => windowManager.startDragging(),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isDark ? AppTheme.borderDark : AppTheme.borderLight,
width: 0.5,
),
),
),
child: Row(
children: [
GestureDetector(
onTap: () => state.selectTask(null),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(Icons.arrow_back, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)),
),
),
],
),
),
),
// Content
Expanded(
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
TextField(
controller: _titleController,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Task title',
isDense: true,
contentPadding: EdgeInsets.zero,
),
onChanged: (_) => _scheduleUpdate(),
),
const SizedBox(height: 16),
// Description with icon (matching Tauri)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(Icons.subject, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _descController,
style: const TextStyle(fontSize: 14),
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Add details',
hintStyle: TextStyle(
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
isDense: true,
contentPadding: EdgeInsets.zero,
),
onChanged: (_) => _scheduleUpdate(),
),
),
],
),
const SizedBox(height: 16),
// Date/time with icon (matching Tauri)
Row(
children: [
Icon(Icons.access_time, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
const SizedBox(width: 12),
if (widget.task.dueDate != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: _editDate,
child: Text(
_formatDateChip(widget.task.dueDate!),
style: const TextStyle(fontSize: 14),
),
),
const SizedBox(width: 6),
GestureDetector(
onTap: () => _updateDueDate(null),
child: Icon(Icons.close, size: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4)),
),
],
),
)
else
GestureDetector(
onTap: _editDate,
child: Text('Add date/time', style: TextStyle(fontSize: 14,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
),
],
),
],
),
),
// Click-off backdrop to close kebab menu
if (_showMenu)
Positioned.fill(
child: GestureDetector(
onTap: () {
_menuAnim.reverse().then((_) {
if (mounted) setState(() => _showMenu = false);
});
},
behavior: HitTestBehavior.opaque,
child: const SizedBox.expand(),
),
),
// Kebab menu (absolute positioned in content, matching Tauri)
Positioned(
right: 12,
top: 8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
setState(() => _showMenu = !_showMenu);
if (_showMenu) _menuAnim.forward(); else _menuAnim.reverse();
},
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(Icons.more_vert, size: 20,
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)),
),
),
ScaleTransition(
scale: _menuScale,
alignment: Alignment.topRight,
child: FadeTransition(
opacity: _menuFade,
child: IgnorePointer(
ignoring: !_showMenu,
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: Container(
decoration: BoxDecoration(
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(7),
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_KebabMenuItem(
icon: isCompleted ? Icons.close : Icons.check,
label: isCompleted ? 'Restore task' : 'Mark as completed',
onTap: () {
setState(() => _showMenu = false);
state.toggleTask(widget.task.id);
state.selectTask(null);
},
),
if (state.lists.where((l) => l.id != state.activeListId).isNotEmpty)
_KebabMenuItem(
icon: Icons.drive_file_move_outline,
label: 'Move to...',
onTap: () {
setState(() => _showMenu = false);
_showMoveToSheet(context, state);
},
),
_KebabMenuItem(
icon: Icons.delete_outline,
label: 'Delete',
color: AppTheme.danger,
onTap: () {
setState(() => _showMenu = false);
state.deleteTask(widget.task.id);
},
),
],
),
),
),
),
),
),
],
),
),
],
),
),
],
);
}
}
class _KebabMenuItem extends StatefulWidget {
final IconData icon;
final String label;
final Color? color;
final VoidCallback onTap;
const _KebabMenuItem({required this.icon, required this.label, this.color, required this.onTap});
@override
State<_KebabMenuItem> createState() => _KebabMenuItemState();
}
class _KebabMenuItemState extends State<_KebabMenuItem> {
bool _hovering = false;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: GestureDetector(
onTap: widget.onTap,
child: Container(
color: _hovering
? (isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.05))
: Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Icon(widget.icon, size: 16, color: widget.color),
const SizedBox(width: 8),
Text(widget.label, style: TextStyle(color: widget.color, fontSize: 14)),
],
),
),
),
);
}
}

View file

@ -1,179 +0,0 @@
import 'package:flutter/material.dart';
import '../rust/api.dart' as api;
import '../theme.dart';
class TaskItem extends StatefulWidget {
final api.TaskDto task;
final VoidCallback onToggle;
final VoidCallback onTap;
const TaskItem({super.key, required this.task, required this.onToggle, required this.onTap});
@override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
bool _hovering = false;
double _swipeOffset = 0;
String _formatDueDate(String isoDate) {
final date = DateTime.tryParse(isoDate);
if (date == null) return '';
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final taskDate = DateTime(date.year, date.month, date.day);
final diff = taskDate.difference(today).inDays;
if (diff == 0) return 'Today';
if (diff == 1) return 'Tomorrow';
return date.toLocal().toIso8601String().substring(5, 10).replaceAll('-', '/');
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final isCompleted = widget.task.status == 'completed';
final canSwipeLeft = !isCompleted;
final canSwipeRight = isCompleted;
return MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
child: GestureDetector(
onTap: () {
setState(() => _hovering = false);
widget.onTap();
},
onHorizontalDragUpdate: (details) {
setState(() {
_swipeOffset += details.delta.dx;
if (canSwipeLeft) _swipeOffset = _swipeOffset.clamp(-150.0, 0.0);
else if (canSwipeRight) _swipeOffset = _swipeOffset.clamp(0.0, 150.0);
else _swipeOffset = 0;
});
},
onHorizontalDragEnd: (details) {
if (_swipeOffset.abs() > 100) widget.onToggle();
setState(() => _swipeOffset = 0);
},
child: Stack(
children: [
// Swipe background
if (_swipeOffset != 0)
Positioned.fill(
child: Container(
color: AppTheme.primary,
alignment: _swipeOffset < 0 ? Alignment.centerRight : Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_swipeOffset < 0 ? 'Complete' : 'Undo',
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
),
),
// Task content
Container(
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
color: _hovering
? (isDark ? Colors.white.withValues(alpha: 0.05) : Colors.black.withValues(alpha: 0.05))
: (isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Checkbox with expanded touch target
GestureDetector(
onTap: widget.onToggle,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.all(2),
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted ? AppTheme.primary : Colors.transparent,
border: Border.all(
color: isCompleted ? AppTheme.primary : (isDark ? const Color(0xFF6B7280) : const Color(0xFF9CA3AF)),
width: 2,
),
),
child: isCompleted
? const Icon(Icons.check, size: 12, color: Colors.white)
: null,
),
),
),
const SizedBox(width: 12),
// Content column (title, description, due date below)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.task.title,
style: TextStyle(
fontSize: 14,
fontWeight: isCompleted ? FontWeight.normal : FontWeight.w500,
decoration: isCompleted ? TextDecoration.lineThrough : null,
color: isCompleted
? (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.5)
: null,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.task.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
widget.task.description,
style: TextStyle(
fontSize: 12,
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.4),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// Due date badge (below title/description, matching Tauri)
if (widget.task.dueDate != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
borderRadius: BorderRadius.circular(100),
),
child: Text(
_formatDueDate(widget.task.dueDate!),
style: TextStyle(
fontSize: 12,
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5),
),
),
),
),
],
),
),
// Chevron (show on hover only)
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: _hovering ? 0.3 : 0,
child: Padding(
padding: const EdgeInsets.only(left: 4, top: 4),
child: Icon(Icons.chevron_right, size: 16,
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight),
),
),
],
),
),
],
),
),
);
}
}

View file

@ -1 +0,0 @@
flutter/ephemeral

View file

@ -1,155 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner 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 "onyx")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.onyx.onyx")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
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()
# 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_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Build the Rust FFI library for flutter_rust_bridge
set(RUST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust")
set(RUST_LIB_NAME "libonyx_flutter.so")
if(CMAKE_BUILD_TYPE MATCHES "Debug")
set(RUST_TARGET_DIR "${RUST_DIR}/target/debug")
add_custom_command(
OUTPUT "${RUST_TARGET_DIR}/${RUST_LIB_NAME}"
COMMAND cargo build
WORKING_DIRECTORY "${RUST_DIR}"
COMMENT "Building Rust FFI library (debug)"
)
else()
set(RUST_TARGET_DIR "${RUST_DIR}/target/release")
add_custom_command(
OUTPUT "${RUST_TARGET_DIR}/${RUST_LIB_NAME}"
COMMAND cargo build --release
WORKING_DIRECTORY "${RUST_DIR}"
COMMENT "Building Rust FFI library (release)"
)
endif()
add_custom_target(rust_lib DEPENDS "${RUST_TARGET_DIR}/${RUST_LIB_NAME}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble rust_lib)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
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)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Install the Rust FFI library into the bundle
install(FILES "${RUST_TARGET_DIR}/${RUST_LIB_NAME}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
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.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -1,88 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
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.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# 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/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter 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.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -1,19 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View file

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

View file

@ -1,25 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux 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}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -1,26 +0,0 @@
cmake_minimum_required(VERSION 3.13)
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}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# 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 application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View file

@ -1,6 +0,0 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -1,130 +0,0 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Frameless transparent window
gtk_window_set_decorated(window, FALSE);
gtk_window_set_title(window, "onyx");
gtk_window_set_default_size(window, 400, 700);
// Enable transparency
GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(window));
GdkVisual* visual = gdk_screen_get_rgba_visual(screen);
if (visual != nullptr) {
gtk_widget_set_visual(GTK_WIDGET(window), visual);
}
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
gdk_rgba_parse(&background_color, "#00000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View file

@ -1,21 +0,0 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View file

@ -1,538 +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: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
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: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.34"
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"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.dev"
source: hosted
version: "4.11.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: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.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: transitive
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: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
url: "https://pub.dev"
source: hosted
version: "2.2.23"
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"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.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"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
url: "https://pub.dev"
source: hosted
version: "0.4.3"
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.11.1 <4.0.0"
flutter: ">=3.38.4"

View file

@ -1,24 +0,0 @@
name: onyx
description: "Onyx - local-first task management"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.11.1
dependencies:
flutter:
sdk: flutter
flutter_rust_bridge: 2.11.1
provider: ^6.1.0
window_manager: ^0.4.0
file_picker: ^8.0.0
google_fonts: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
[package]
name = "onyx-flutter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.11.1"
onyx-core = { path = "../../../crates/onyx-core" }
uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
once_cell = "1"
notify = "7"
notify-debouncer-mini = "0.5"
tokio = { version = "1", features = ["full"] }

View file

@ -1,422 +0,0 @@
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use flutter_rust_bridge::frb;
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
use once_cell::sync::Lazy;
use uuid::Uuid;
use onyx_core::{
config::{AppConfig, WorkspaceConfig},
models::{Task, TaskStatus},
repository::TaskRepository,
sync::{self, SyncMode},
webdav,
};
// ── State ───────────────────────────────────────────────────────────
struct AppState {
config: AppConfig,
repo: Option<TaskRepository>,
}
static STATE: Lazy<Mutex<AppState>> = Lazy::new(|| {
let config_path = AppConfig::get_config_path();
let config = AppConfig::load_from_file(&config_path).unwrap_or_default();
Mutex::new(AppState { config, repo: None })
});
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(())
}
// ── DTOs ────────────────────────────────────────────────────────────
pub struct TaskDto {
pub id: String,
pub title: String,
pub description: String,
pub status: String,
pub due_date: Option<String>,
pub has_time: bool,
pub created_at: String,
pub updated_at: String,
pub parent_id: Option<String>,
}
pub struct SyncResultDto {
pub uploaded: u32,
pub downloaded: u32,
pub deleted_local: u32,
pub deleted_remote: u32,
pub conflicts: u32,
pub errors: Vec<String>,
}
pub struct TaskListDto {
pub id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub group_by_due_date: bool,
}
pub struct WorkspaceEntry {
pub name: String,
pub path: String,
pub webdav_url: Option<String>,
pub last_sync: Option<String>,
}
pub struct AppConfigDto {
pub workspaces: Vec<WorkspaceEntry>,
pub current_workspace: Option<String>,
}
fn task_to_dto(t: &Task) -> TaskDto {
TaskDto {
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()),
has_time: t.has_time,
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 config_to_dto(c: &AppConfig) -> AppConfigDto {
AppConfigDto {
workspaces: c
.workspaces
.iter()
.map(|(name, ws)| WorkspaceEntry {
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(),
}
}
// ── Config commands ─────────────────────────────────────────────────
pub fn get_config() -> Result<AppConfigDto, String> {
let s = STATE.lock().unwrap();
Ok(config_to_dto(&s.config))
}
pub fn init_workspace(path: String) -> Result<(), String> {
TaskRepository::init(PathBuf::from(path))
.map(|_| ())
.map_err(|e| e.to_string())
}
pub fn add_workspace(name: String, path: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
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> {
let mut s = STATE.lock().unwrap();
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> {
let mut s = STATE.lock().unwrap();
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())
}
// ── List commands ───────────────────────────────────────────────────
pub fn get_lists() -> Result<Vec<TaskListDto>, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let lists = s.repo.as_ref().unwrap().get_lists().map_err(|e| e.to_string())?;
Ok(lists
.iter()
.map(|l| TaskListDto {
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,
})
.collect())
}
pub fn create_list(name: String) -> Result<TaskListDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let list = s.repo.as_mut().unwrap().create_list(name).map_err(|e| e.to_string())?;
Ok(TaskListDto {
id: list.id.to_string(),
title: list.title.clone(),
created_at: list.created_at.to_rfc3339(),
updated_at: list.updated_at.to_rfc3339(),
group_by_due_date: list.group_by_due_date,
})
}
pub fn delete_list(list_id: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let id = 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())
}
// ── Task commands ───────────────────────────────────────────────────
pub fn list_tasks(list_id: String) -> Result<Vec<TaskDto>, 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 tasks = s.repo.as_ref().unwrap().list_tasks(id).map_err(|e| e.to_string())?;
Ok(tasks.iter().map(|t| task_to_dto(t)).collect())
}
pub fn create_task(list_id: String, title: String, description: String) -> Result<TaskDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let mut task = Task::new(title);
if !description.is_empty() {
task.description = description;
}
let created = s.repo.as_mut().unwrap().create_task(id, task).map_err(|e| e.to_string())?;
Ok(task_to_dto(&created))
}
pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task.id).map_err(|e| e.to_string())?;
let mut existing = s.repo.as_ref().unwrap().get_task(lid, tid).map_err(|e| e.to_string())?;
existing.title = task.title;
existing.description = task.description;
existing.due_date = task
.due_date
.as_deref()
.and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok())
.map(|d| d.with_timezone(&chrono::Utc));
existing.has_time = task.has_time;
s.repo.as_mut().unwrap().update_task(lid, existing).map_err(|e| e.to_string())
}
pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = 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 toggle_task(list_id: String, task_id: String) -> Result<TaskDto, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = 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_dto(&task))
}
pub fn reorder_task(list_id: String, task_id: String, new_position: u32) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo
.as_mut()
.unwrap()
.reorder_task(lid, tid, new_position as usize)
.map_err(|e| e.to_string())
}
// ── Move / rename / grouping ───────────────────────────────────────
pub fn move_task(from_list_id: String, to_list_id: String, task_id: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?;
let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?;
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().move_task(from, to, tid).map_err(|e| e.to_string())
}
pub fn rename_list(list_id: String, new_name: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().rename_list(id, new_name).map_err(|e| e.to_string())
}
pub fn set_group_by_due_date(list_id: String, enabled: bool) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
mute_watcher();
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo.as_mut().unwrap().set_group_by_due_date(id, enabled).map_err(|e| e.to_string())
}
pub fn get_group_by_due_date(list_id: String) -> Result<bool, String> {
let mut s = STATE.lock().unwrap();
ensure_repo(&mut s)?;
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
s.repo.as_ref().unwrap().get_group_by_due_date(id).map_err(|e| e.to_string())
}
// ── Sync commands ──────────────────────────────────────────────────
pub fn store_credentials(domain: String, username: String, password: String) -> Result<(), String> {
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
}
pub fn load_credentials(domain: String) -> Result<Vec<String>, String> {
let (u, p) = webdav::load_credentials(&domain).map_err(|e| e.to_string())?;
Ok(vec![u, p])
}
pub fn set_webdav_config(workspace_name: String, webdav_url: String) -> Result<(), String> {
let mut s = STATE.lock().unwrap();
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 async fn test_webdav_connection(url: String, username: String, password: String) -> Result<(), String> {
let client = webdav::WebDavClient::new(&url, &username, &password);
client.test_connection().await.map_err(|e| e.to_string())
}
pub async fn sync_workspace_cmd(
workspace_name: String,
workspace_path: String,
webdav_url: String,
username: String,
password: String,
mode: String,
) -> Result<SyncResultDto, String> {
let sync_mode = match mode.as_str() {
"push" => SyncMode::Push,
"pull" => SyncMode::Pull,
_ => SyncMode::Full,
};
let result = sync::sync_workspace(
&PathBuf::from(&workspace_path),
&webdav_url,
&username,
&password,
sync_mode,
None,
)
.await
.map_err(|e| e.to_string())?;
{
let mut s = STATE.lock().unwrap();
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
ws.last_sync = Some(chrono::Utc::now());
}
let config_path = AppConfig::get_config_path();
s.config.save_to_file(&config_path).map_err(|e| e.to_string())?;
}
Ok(SyncResultDto {
uploaded: result.uploaded,
downloaded: result.downloaded,
deleted_local: result.deleted_local,
deleted_remote: result.deleted_remote,
conflicts: result.conflicts,
errors: result.errors,
})
}
// ── File watcher ───────────────────────────────────────────────────
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
Mutex::new(None);
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
fn mute_watcher() {
*LAST_WRITE.lock().unwrap() = Some(Instant::now());
}
#[frb(stream_dart_await)]
pub fn watch_workspace_changes(path: String, sink: crate::frb_generated::StreamSink<()>) {
let debouncer = new_debouncer(
Duration::from_millis(500),
move |events: Result<Vec<notify_debouncer_mini::DebouncedEvent>, notify::Error>| {
let Ok(events) = events else { return };
let has_data_change = events.iter().any(|e| {
if e.kind != DebouncedEventKind::Any { return false; }
let p = e.path.to_string_lossy();
p.ends_with(".md") || p.ends_with(".json")
});
if !has_data_change { return; }
if let Some(t) = *LAST_WRITE.lock().unwrap() {
if t.elapsed() < Duration::from_secs(1) { return; }
}
let _ = sink.add(());
},
);
match debouncer {
Ok(mut d) => {
let _ = d.watcher().watch(&PathBuf::from(&path), notify::RecursiveMode::Recursive);
*WATCHER.lock().unwrap() = Some(d);
}
Err(e) => eprintln!("Failed to start file watcher: {e}"),
}
}
// ── Test function ───────────────────────────────────────────────────
pub fn greet(name: String) -> String {
format!("Hello, {name}! From Rust via flutter_rust_bridge.")
}

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
mod frb_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */
pub mod api;

View file

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

View file

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

View file

@ -1,108 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(onyx 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 "onyx")
# 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)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View file

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

View file

@ -1,17 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

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

View file

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

View file

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

View file

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

View file

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