remove flutter
This commit is contained in:
parent
0294711d1d
commit
ddf3829cda
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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
59
PLAN.md
|
|
@ -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
|
||||||
|
|
|
||||||
54
apps/flutter/.gitignore
vendored
54
apps/flutter/.gitignore
vendored
|
|
@ -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
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# This file tracks properties of this Flutter project.
|
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
|
||||||
#
|
|
||||||
# This file should be version controlled and should not be manually edited.
|
|
||||||
|
|
||||||
version:
|
|
||||||
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
project_type: app
|
|
||||||
|
|
||||||
# Tracks metadata for the flutter migrate command
|
|
||||||
migration:
|
|
||||||
platforms:
|
|
||||||
- platform: root
|
|
||||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
|
||||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
|
||||||
- platform: windows
|
|
||||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
|
||||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
|
||||||
|
|
||||||
# User provided section
|
|
||||||
|
|
||||||
# List of Local paths (relative to this file) that should be
|
|
||||||
# ignored by the migrate tool.
|
|
||||||
#
|
|
||||||
# Files that are not part of the templates will be ignored by default.
|
|
||||||
unmanaged_files:
|
|
||||||
- 'lib/main.dart'
|
|
||||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
rust_input: crate::api
|
|
||||||
rust_root: rust/
|
|
||||||
dart_output: lib/src/rust
|
|
||||||
|
|
@ -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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
apps/flutter/linux/.gitignore
vendored
1
apps/flutter/linux/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
flutter/ephemeral
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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}
|
|
||||||
)
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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_
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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}")
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
@ -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_
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
2767
apps/flutter/rust/Cargo.lock
generated
2767
apps/flutter/rust/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"] }
|
|
||||||
|
|
@ -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
|
|
@ -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;
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
17
apps/flutter/windows/.gitignore
vendored
17
apps/flutter/windows/.gitignore
vendored
|
|
@ -1,17 +0,0 @@
|
||||||
flutter/ephemeral/
|
|
||||||
|
|
||||||
# Visual Studio user-specific files.
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# Visual Studio build-related files.
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!*.[Cc]ache/
|
|
||||||
|
|
@ -1,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)
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
# This file controls Flutter-level build steps. It should not be edited.
|
|
||||||
cmake_minimum_required(VERSION 3.14)
|
|
||||||
|
|
||||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
|
||||||
|
|
||||||
# Configuration provided via flutter tool.
|
|
||||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
|
||||||
|
|
||||||
# TODO: Move the rest of this into files in ephemeral. See
|
|
||||||
# https://github.com/flutter/flutter/issues/57146.
|
|
||||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
|
||||||
|
|
||||||
# Set fallback configurations for older versions of the flutter tool.
|
|
||||||
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
|
||||||
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# === Flutter Library ===
|
|
||||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
|
||||||
|
|
||||||
# Published to parent scope for install step.
|
|
||||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
|
||||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
|
||||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
|
||||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
|
||||||
"flutter_export.h"
|
|
||||||
"flutter_windows.h"
|
|
||||||
"flutter_messenger.h"
|
|
||||||
"flutter_plugin_registrar.h"
|
|
||||||
"flutter_texture_registrar.h"
|
|
||||||
)
|
|
||||||
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
|
||||||
add_library(flutter INTERFACE)
|
|
||||||
target_include_directories(flutter INTERFACE
|
|
||||||
"${EPHEMERAL_DIR}"
|
|
||||||
)
|
|
||||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
|
||||||
add_dependencies(flutter flutter_assemble)
|
|
||||||
|
|
||||||
# === Wrapper ===
|
|
||||||
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
|
||||||
"core_implementations.cc"
|
|
||||||
"standard_codec.cc"
|
|
||||||
)
|
|
||||||
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
|
||||||
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
|
||||||
"plugin_registrar.cc"
|
|
||||||
)
|
|
||||||
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
|
||||||
list(APPEND CPP_WRAPPER_SOURCES_APP
|
|
||||||
"flutter_engine.cc"
|
|
||||||
"flutter_view_controller.cc"
|
|
||||||
)
|
|
||||||
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
|
||||||
|
|
||||||
# Wrapper sources needed for a plugin.
|
|
||||||
add_library(flutter_wrapper_plugin STATIC
|
|
||||||
${CPP_WRAPPER_SOURCES_CORE}
|
|
||||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
|
||||||
)
|
|
||||||
apply_standard_settings(flutter_wrapper_plugin)
|
|
||||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
|
||||||
POSITION_INDEPENDENT_CODE ON)
|
|
||||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
|
||||||
CXX_VISIBILITY_PRESET hidden)
|
|
||||||
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
|
||||||
target_include_directories(flutter_wrapper_plugin PUBLIC
|
|
||||||
"${WRAPPER_ROOT}/include"
|
|
||||||
)
|
|
||||||
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
|
||||||
|
|
||||||
# Wrapper sources needed for the runner.
|
|
||||||
add_library(flutter_wrapper_app STATIC
|
|
||||||
${CPP_WRAPPER_SOURCES_CORE}
|
|
||||||
${CPP_WRAPPER_SOURCES_APP}
|
|
||||||
)
|
|
||||||
apply_standard_settings(flutter_wrapper_app)
|
|
||||||
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
|
||||||
target_include_directories(flutter_wrapper_app PUBLIC
|
|
||||||
"${WRAPPER_ROOT}/include"
|
|
||||||
)
|
|
||||||
add_dependencies(flutter_wrapper_app flutter_assemble)
|
|
||||||
|
|
||||||
# === Flutter tool backend ===
|
|
||||||
# _phony_ is a non-existent file to force this command to run every time,
|
|
||||||
# since currently there's no way to get a full input/output list from the
|
|
||||||
# flutter tool.
|
|
||||||
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
|
||||||
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
|
||||||
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
|
||||||
${CPP_WRAPPER_SOURCES_APP}
|
|
||||||
${PHONY_OUTPUT}
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E env
|
|
||||||
${FLUTTER_TOOL_ENVIRONMENT}
|
|
||||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
|
||||||
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
add_custom_target(flutter_assemble DEPENDS
|
|
||||||
"${FLUTTER_LIBRARY}"
|
|
||||||
${FLUTTER_LIBRARY_HEADERS}
|
|
||||||
${CPP_WRAPPER_SOURCES_CORE}
|
|
||||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
|
||||||
${CPP_WRAPPER_SOURCES_APP}
|
|
||||||
)
|
|
||||||
|
|
@ -1,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"));
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
#define GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
||||||
#include <flutter/plugin_registry.h>
|
|
||||||
|
|
||||||
// Registers Flutter plugins.
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
|
||||||
|
|
||||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
@ -1,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)
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
|
||||||
project(runner LANGUAGES CXX)
|
|
||||||
|
|
||||||
# Define the application target. To change its name, change BINARY_NAME in the
|
|
||||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
|
||||||
# work.
|
|
||||||
#
|
|
||||||
# Any new source files that you add to the application should be added here.
|
|
||||||
add_executable(${BINARY_NAME} WIN32
|
|
||||||
"flutter_window.cpp"
|
|
||||||
"main.cpp"
|
|
||||||
"utils.cpp"
|
|
||||||
"win32_window.cpp"
|
|
||||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
|
||||||
"Runner.rc"
|
|
||||||
"runner.exe.manifest"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply the standard set of build settings. This can be removed for applications
|
|
||||||
# that need different build settings.
|
|
||||||
apply_standard_settings(${BINARY_NAME})
|
|
||||||
|
|
||||||
# Add preprocessor definitions for the build version.
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
|
||||||
|
|
||||||
# Disable Windows macros that collide with C++ standard library functions.
|
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
|
||||||
|
|
||||||
# Add dependency libraries and include directories. Add any application-specific
|
|
||||||
# dependencies here.
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
|
||||||
|
|
||||||
# Run the Flutter tool portions of the build. This must not be removed.
|
|
||||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
// Microsoft Visual C++ generated resource script.
|
|
||||||
//
|
|
||||||
#pragma code_page(65001)
|
|
||||||
#include "resource.h"
|
|
||||||
|
|
||||||
#define APSTUDIO_READONLY_SYMBOLS
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Generated from the TEXTINCLUDE 2 resource.
|
|
||||||
//
|
|
||||||
#include "winres.h"
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
#undef APSTUDIO_READONLY_SYMBOLS
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
// English (United States) resources
|
|
||||||
|
|
||||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
|
||||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|
||||||
|
|
||||||
#ifdef APSTUDIO_INVOKED
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// TEXTINCLUDE
|
|
||||||
//
|
|
||||||
|
|
||||||
1 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"resource.h\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
2 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"#include ""winres.h""\r\n"
|
|
||||||
"\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
3 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"\r\n"
|
|
||||||
"\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
#endif // APSTUDIO_INVOKED
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Icon
|
|
||||||
//
|
|
||||||
|
|
||||||
// Icon with lowest ID value placed first to ensure application icon
|
|
||||||
// remains consistent on all systems.
|
|
||||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Version
|
|
||||||
//
|
|
||||||
|
|
||||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
|
||||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
|
||||||
#else
|
|
||||||
#define VERSION_AS_NUMBER 1,0,0,0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(FLUTTER_VERSION)
|
|
||||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
|
||||||
#else
|
|
||||||
#define VERSION_AS_STRING "1.0.0"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
|
||||||
FILEVERSION VERSION_AS_NUMBER
|
|
||||||
PRODUCTVERSION VERSION_AS_NUMBER
|
|
||||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
|
||||||
#ifdef _DEBUG
|
|
||||||
FILEFLAGS VS_FF_DEBUG
|
|
||||||
#else
|
|
||||||
FILEFLAGS 0x0L
|
|
||||||
#endif
|
|
||||||
FILEOS VOS__WINDOWS32
|
|
||||||
FILETYPE VFT_APP
|
|
||||||
FILESUBTYPE 0x0L
|
|
||||||
BEGIN
|
|
||||||
BLOCK "StringFileInfo"
|
|
||||||
BEGIN
|
|
||||||
BLOCK "040904e4"
|
|
||||||
BEGIN
|
|
||||||
VALUE "CompanyName", "com.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
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
#include "flutter_window.h"
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
|
||||||
: project_(project) {}
|
|
||||||
|
|
||||||
FlutterWindow::~FlutterWindow() {}
|
|
||||||
|
|
||||||
bool FlutterWindow::OnCreate() {
|
|
||||||
if (!Win32Window::OnCreate()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RECT frame = GetClientArea();
|
|
||||||
|
|
||||||
// The size here must match the window dimensions to avoid unnecessary surface
|
|
||||||
// creation / destruction in the startup path.
|
|
||||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
|
||||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
|
||||||
// Ensure that basic setup of the controller was successful.
|
|
||||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
RegisterPlugins(flutter_controller_->engine());
|
|
||||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
|
||||||
|
|
||||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
|
||||||
this->Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Flutter can complete the first frame before the "show window" callback is
|
|
||||||
// registered. The following call ensures a frame is pending to ensure the
|
|
||||||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
|
||||||
flutter_controller_->ForceRedraw();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlutterWindow::OnDestroy() {
|
|
||||||
if (flutter_controller_) {
|
|
||||||
flutter_controller_ = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Win32Window::OnDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
LRESULT
|
|
||||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
|
||||||
WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept {
|
|
||||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
|
||||||
if (flutter_controller_) {
|
|
||||||
std::optional<LRESULT> result =
|
|
||||||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
|
||||||
lparam);
|
|
||||||
if (result) {
|
|
||||||
return *result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (message) {
|
|
||||||
case WM_FONTCHANGE:
|
|
||||||
flutter_controller_->engine()->ReloadSystemFonts();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
|
||||||
#define RUNNER_FLUTTER_WINDOW_H_
|
|
||||||
|
|
||||||
#include <flutter/dart_project.h>
|
|
||||||
#include <flutter/flutter_view_controller.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "win32_window.h"
|
|
||||||
|
|
||||||
// A window that does nothing but host a Flutter view.
|
|
||||||
class FlutterWindow : public Win32Window {
|
|
||||||
public:
|
|
||||||
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
|
||||||
explicit FlutterWindow(const flutter::DartProject& project);
|
|
||||||
virtual ~FlutterWindow();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// Win32Window:
|
|
||||||
bool OnCreate() override;
|
|
||||||
void OnDestroy() override;
|
|
||||||
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
// The project to run.
|
|
||||||
flutter::DartProject project_;
|
|
||||||
|
|
||||||
// The Flutter instance hosted by this window.
|
|
||||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
#include <flutter/dart_project.h>
|
|
||||||
#include <flutter/flutter_view_controller.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include "flutter_window.h"
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
|
||||||
// Attach to console when present (e.g., 'flutter run') or create a
|
|
||||||
// new console when running with a debugger.
|
|
||||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
|
||||||
CreateAndAttachConsole();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize COM, so that it is available for use in the library and/or
|
|
||||||
// plugins.
|
|
||||||
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
||||||
|
|
||||||
flutter::DartProject project(L"data");
|
|
||||||
|
|
||||||
std::vector<std::string> command_line_arguments =
|
|
||||||
GetCommandLineArguments();
|
|
||||||
|
|
||||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
|
||||||
|
|
||||||
FlutterWindow window(project);
|
|
||||||
Win32Window::Point origin(10, 10);
|
|
||||||
Win32Window::Size size(1280, 720);
|
|
||||||
if (!window.Create(L"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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//{{NO_DEPENDENCIES}}
|
|
||||||
// Microsoft Visual C++ generated include file.
|
|
||||||
// Used by Runner.rc
|
|
||||||
//
|
|
||||||
#define IDI_APP_ICON 101
|
|
||||||
|
|
||||||
// Next default values for new objects
|
|
||||||
//
|
|
||||||
#ifdef APSTUDIO_INVOKED
|
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
|
||||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
|
||||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
|
||||||
#define _APS_NEXT_SYMED_VALUE 101
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
||||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<windowsSettings>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</windowsSettings>
|
|
||||||
</application>
|
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
|
||||||
<application>
|
|
||||||
<!-- Windows 10 and Windows 11 -->
|
|
||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
|
||||||
</application>
|
|
||||||
</compatibility>
|
|
||||||
</assembly>
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
#include <flutter_windows.h>
|
|
||||||
#include <io.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
void CreateAndAttachConsole() {
|
|
||||||
if (::AllocConsole()) {
|
|
||||||
FILE *unused;
|
|
||||||
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
|
||||||
_dup2(_fileno(stdout), 1);
|
|
||||||
}
|
|
||||||
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
|
||||||
_dup2(_fileno(stdout), 2);
|
|
||||||
}
|
|
||||||
std::ios::sync_with_stdio();
|
|
||||||
FlutterDesktopResyncOutputStreams();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> GetCommandLineArguments() {
|
|
||||||
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
|
||||||
int argc;
|
|
||||||
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
|
||||||
if (argv == nullptr) {
|
|
||||||
return std::vector<std::string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> command_line_arguments;
|
|
||||||
|
|
||||||
// Skip the first argument as it's the binary name.
|
|
||||||
for (int i = 1; i < argc; i++) {
|
|
||||||
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
::LocalFree(argv);
|
|
||||||
|
|
||||||
return command_line_arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
|
||||||
if (utf16_string == nullptr) {
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
unsigned int target_length = ::WideCharToMultiByte(
|
|
||||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
|
||||||
-1, nullptr, 0, nullptr, nullptr)
|
|
||||||
-1; // remove the trailing null character
|
|
||||||
int input_length = (int)wcslen(utf16_string);
|
|
||||||
std::string utf8_string;
|
|
||||||
if (target_length == 0 || target_length > utf8_string.max_size()) {
|
|
||||||
return utf8_string;
|
|
||||||
}
|
|
||||||
utf8_string.resize(target_length);
|
|
||||||
int converted_length = ::WideCharToMultiByte(
|
|
||||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
|
||||||
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
|
||||||
if (converted_length == 0) {
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
return utf8_string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#ifndef RUNNER_UTILS_H_
|
|
||||||
#define RUNNER_UTILS_H_
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
// Creates a console for the process, and redirects stdout and stderr to
|
|
||||||
// it for both the runner and the Flutter library.
|
|
||||||
void CreateAndAttachConsole();
|
|
||||||
|
|
||||||
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
|
||||||
// encoded in UTF-8. Returns an empty std::string on failure.
|
|
||||||
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
|
||||||
|
|
||||||
// Gets the command line arguments passed in as a std::vector<std::string>,
|
|
||||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
|
||||||
std::vector<std::string> GetCommandLineArguments();
|
|
||||||
|
|
||||||
#endif // RUNNER_UTILS_H_
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
#include "win32_window.h"
|
|
||||||
|
|
||||||
#include <dwmapi.h>
|
|
||||||
#include <flutter_windows.h>
|
|
||||||
|
|
||||||
#include "resource.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// Window attribute that enables dark mode window decorations.
|
|
||||||
///
|
|
||||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
|
||||||
/// version 10.0.22000.0.
|
|
||||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
|
||||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
|
||||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
|
||||||
#endif
|
|
||||||
|
|
||||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
|
||||||
|
|
||||||
/// Registry key for app theme preference.
|
|
||||||
///
|
|
||||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
|
||||||
/// value indicates apps should use light mode.
|
|
||||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
|
||||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
|
||||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
|
||||||
|
|
||||||
// The number of Win32Window objects that currently exist.
|
|
||||||
static int g_active_window_count = 0;
|
|
||||||
|
|
||||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
|
||||||
|
|
||||||
// Scale helper to convert logical scaler values to physical using passed in
|
|
||||||
// scale factor
|
|
||||||
int Scale(int source, double scale_factor) {
|
|
||||||
return static_cast<int>(source * scale_factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
|
||||||
// This API is only needed for PerMonitor V1 awareness mode.
|
|
||||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
|
||||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
|
||||||
if (!user32_module) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto enable_non_client_dpi_scaling =
|
|
||||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
|
||||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
|
||||||
if (enable_non_client_dpi_scaling != nullptr) {
|
|
||||||
enable_non_client_dpi_scaling(hwnd);
|
|
||||||
}
|
|
||||||
FreeLibrary(user32_module);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// Manages the Win32Window's window class registration.
|
|
||||||
class WindowClassRegistrar {
|
|
||||||
public:
|
|
||||||
~WindowClassRegistrar() = default;
|
|
||||||
|
|
||||||
// Returns the singleton registrar instance.
|
|
||||||
static WindowClassRegistrar* GetInstance() {
|
|
||||||
if (!instance_) {
|
|
||||||
instance_ = new WindowClassRegistrar();
|
|
||||||
}
|
|
||||||
return instance_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the name of the window class, registering the class if it hasn't
|
|
||||||
// previously been registered.
|
|
||||||
const wchar_t* GetWindowClass();
|
|
||||||
|
|
||||||
// Unregisters the window class. Should only be called if there are no
|
|
||||||
// instances of the window.
|
|
||||||
void UnregisterWindowClass();
|
|
||||||
|
|
||||||
private:
|
|
||||||
WindowClassRegistrar() = default;
|
|
||||||
|
|
||||||
static WindowClassRegistrar* instance_;
|
|
||||||
|
|
||||||
bool class_registered_ = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
|
||||||
|
|
||||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
|
||||||
if (!class_registered_) {
|
|
||||||
WNDCLASS window_class{};
|
|
||||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
||||||
window_class.lpszClassName = kWindowClassName;
|
|
||||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
|
||||||
window_class.cbClsExtra = 0;
|
|
||||||
window_class.cbWndExtra = 0;
|
|
||||||
window_class.hInstance = GetModuleHandle(nullptr);
|
|
||||||
window_class.hIcon =
|
|
||||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
|
||||||
window_class.hbrBackground = 0;
|
|
||||||
window_class.lpszMenuName = nullptr;
|
|
||||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
|
||||||
RegisterClass(&window_class);
|
|
||||||
class_registered_ = true;
|
|
||||||
}
|
|
||||||
return kWindowClassName;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
|
||||||
UnregisterClass(kWindowClassName, nullptr);
|
|
||||||
class_registered_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Win32Window::Win32Window() {
|
|
||||||
++g_active_window_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
Win32Window::~Win32Window() {
|
|
||||||
--g_active_window_count;
|
|
||||||
Destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Win32Window::Create(const std::wstring& title,
|
|
||||||
const Point& origin,
|
|
||||||
const Size& size) {
|
|
||||||
Destroy();
|
|
||||||
|
|
||||||
const wchar_t* window_class =
|
|
||||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
|
||||||
|
|
||||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
|
||||||
static_cast<LONG>(origin.y)};
|
|
||||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
|
||||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
|
||||||
double scale_factor = dpi / 96.0;
|
|
||||||
|
|
||||||
HWND window = CreateWindow(
|
|
||||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
|
||||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
|
||||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
|
||||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
|
||||||
|
|
||||||
if (!window) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTheme(window);
|
|
||||||
|
|
||||||
return OnCreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Win32Window::Show() {
|
|
||||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
|
||||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
|
||||||
UINT const message,
|
|
||||||
WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept {
|
|
||||||
if (message == WM_NCCREATE) {
|
|
||||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
|
||||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
|
||||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
|
||||||
|
|
||||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
|
||||||
EnableFullDpiSupportIfAvailable(window);
|
|
||||||
that->window_handle_ = window;
|
|
||||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
|
||||||
return that->MessageHandler(window, message, wparam, lparam);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefWindowProc(window, message, wparam, lparam);
|
|
||||||
}
|
|
||||||
|
|
||||||
LRESULT
|
|
||||||
Win32Window::MessageHandler(HWND hwnd,
|
|
||||||
UINT const message,
|
|
||||||
WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept {
|
|
||||||
switch (message) {
|
|
||||||
case WM_DESTROY:
|
|
||||||
window_handle_ = nullptr;
|
|
||||||
Destroy();
|
|
||||||
if (quit_on_close_) {
|
|
||||||
PostQuitMessage(0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
case WM_DPICHANGED: {
|
|
||||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
|
||||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
|
||||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
|
||||||
|
|
||||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
|
||||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case WM_SIZE: {
|
|
||||||
RECT rect = GetClientArea();
|
|
||||||
if (child_content_ != nullptr) {
|
|
||||||
// Size and position the child window.
|
|
||||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
|
||||||
rect.bottom - rect.top, TRUE);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
case WM_ACTIVATE:
|
|
||||||
if (child_content_ != nullptr) {
|
|
||||||
SetFocus(child_content_);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
|
||||||
UpdateTheme(hwnd);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Win32Window::Destroy() {
|
|
||||||
OnDestroy();
|
|
||||||
|
|
||||||
if (window_handle_) {
|
|
||||||
DestroyWindow(window_handle_);
|
|
||||||
window_handle_ = nullptr;
|
|
||||||
}
|
|
||||||
if (g_active_window_count == 0) {
|
|
||||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
|
||||||
return reinterpret_cast<Win32Window*>(
|
|
||||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Win32Window::SetChildContent(HWND content) {
|
|
||||||
child_content_ = content;
|
|
||||||
SetParent(content, window_handle_);
|
|
||||||
RECT frame = GetClientArea();
|
|
||||||
|
|
||||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
|
||||||
frame.bottom - frame.top, true);
|
|
||||||
|
|
||||||
SetFocus(child_content_);
|
|
||||||
}
|
|
||||||
|
|
||||||
RECT Win32Window::GetClientArea() {
|
|
||||||
RECT frame;
|
|
||||||
GetClientRect(window_handle_, &frame);
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
HWND Win32Window::GetHandle() {
|
|
||||||
return window_handle_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
|
||||||
quit_on_close_ = quit_on_close;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Win32Window::OnCreate() {
|
|
||||||
// No-op; provided for subclasses.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Win32Window::OnDestroy() {
|
|
||||||
// No-op; provided for subclasses.
|
|
||||||
}
|
|
||||||
|
|
||||||
void Win32Window::UpdateTheme(HWND const window) {
|
|
||||||
DWORD light_mode;
|
|
||||||
DWORD light_mode_size = sizeof(light_mode);
|
|
||||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
|
||||||
kGetPreferredBrightnessRegValue,
|
|
||||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
|
||||||
&light_mode_size);
|
|
||||||
|
|
||||||
if (result == ERROR_SUCCESS) {
|
|
||||||
BOOL enable_dark_mode = light_mode == 0;
|
|
||||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
|
||||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
#ifndef RUNNER_WIN32_WINDOW_H_
|
|
||||||
#define RUNNER_WIN32_WINDOW_H_
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
|
||||||
// inherited from by classes that wish to specialize with custom
|
|
||||||
// rendering and input handling
|
|
||||||
class Win32Window {
|
|
||||||
public:
|
|
||||||
struct Point {
|
|
||||||
unsigned int x;
|
|
||||||
unsigned int y;
|
|
||||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Size {
|
|
||||||
unsigned int width;
|
|
||||||
unsigned int height;
|
|
||||||
Size(unsigned int width, unsigned int height)
|
|
||||||
: width(width), height(height) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
Win32Window();
|
|
||||||
virtual ~Win32Window();
|
|
||||||
|
|
||||||
// Creates a win32 window with |title| that is positioned and sized using
|
|
||||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
|
||||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
|
||||||
// consistent size this function will scale the inputted width and height as
|
|
||||||
// as appropriate for the default monitor. The window is invisible until
|
|
||||||
// |Show| is called. Returns true if the window was created successfully.
|
|
||||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
|
||||||
|
|
||||||
// Show the current window. Returns true if the window was successfully shown.
|
|
||||||
bool Show();
|
|
||||||
|
|
||||||
// Release OS resources associated with window.
|
|
||||||
void Destroy();
|
|
||||||
|
|
||||||
// Inserts |content| into the window tree.
|
|
||||||
void SetChildContent(HWND content);
|
|
||||||
|
|
||||||
// Returns the backing Window handle to enable clients to set icon and other
|
|
||||||
// window properties. Returns nullptr if the window has been destroyed.
|
|
||||||
HWND GetHandle();
|
|
||||||
|
|
||||||
// If true, closing this window will quit the application.
|
|
||||||
void SetQuitOnClose(bool quit_on_close);
|
|
||||||
|
|
||||||
// Return a RECT representing the bounds of the current client area.
|
|
||||||
RECT GetClientArea();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// Processes and route salient window messages for mouse handling,
|
|
||||||
// size change and DPI. Delegates handling of these to member overloads that
|
|
||||||
// inheriting classes can handle.
|
|
||||||
virtual LRESULT MessageHandler(HWND window,
|
|
||||||
UINT const message,
|
|
||||||
WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept;
|
|
||||||
|
|
||||||
// Called when CreateAndShow is called, allowing subclass window-related
|
|
||||||
// setup. Subclasses should return false if setup fails.
|
|
||||||
virtual bool OnCreate();
|
|
||||||
|
|
||||||
// Called when Destroy is called.
|
|
||||||
virtual void OnDestroy();
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class WindowClassRegistrar;
|
|
||||||
|
|
||||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
|
||||||
// is passed when the non-client area is being created and enables automatic
|
|
||||||
// non-client DPI scaling so that the non-client area automatically
|
|
||||||
// responds to changes in DPI. All other messages are handled by
|
|
||||||
// MessageHandler.
|
|
||||||
static LRESULT CALLBACK WndProc(HWND const window,
|
|
||||||
UINT const message,
|
|
||||||
WPARAM const wparam,
|
|
||||||
LPARAM const lparam) noexcept;
|
|
||||||
|
|
||||||
// Retrieves a class instance pointer for |window|
|
|
||||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
|
||||||
|
|
||||||
// Update the window frame's theme to match the system theme.
|
|
||||||
static void UpdateTheme(HWND const window);
|
|
||||||
|
|
||||||
bool quit_on_close_ = false;
|
|
||||||
|
|
||||||
// window handle for top level window.
|
|
||||||
HWND window_handle_ = nullptr;
|
|
||||||
|
|
||||||
// window handle for hosted content.
|
|
||||||
HWND child_content_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // RUNNER_WIN32_WINDOW_H_
|
|
||||||
Loading…
Reference in a new issue