diff --git a/Cargo.toml b/Cargo.toml index c75200f..5834190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ members = [ "crates/bevy-tasks-cli", "crates/bevy-tasks-gui", ] +exclude = [ + "apps/tauri/src-tauri", + "apps/flutter/rust", +] resolver = "2" [workspace.dependencies] diff --git a/apps/flutter/.gitignore b/apps/flutter/.gitignore new file mode 100644 index 0000000..ec745b2 --- /dev/null +++ b/apps/flutter/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Rust bridge build artifacts +rust/target/ diff --git a/apps/flutter/.metadata b/apps/flutter/.metadata new file mode 100644 index 0000000..9b9642c --- /dev/null +++ b/apps/flutter/.metadata @@ -0,0 +1,30 @@ +# 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' diff --git a/apps/flutter/README.md b/apps/flutter/README.md new file mode 100644 index 0000000..d72b258 --- /dev/null +++ b/apps/flutter/README.md @@ -0,0 +1,17 @@ +# bevy_tasks + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/apps/flutter/analysis_options.yaml b/apps/flutter/analysis_options.yaml new file mode 100644 index 0000000..fbd24ec --- /dev/null +++ b/apps/flutter/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_const_constructors: true + prefer_const_declarations: true diff --git a/apps/flutter/flutter_rust_bridge.yaml b/apps/flutter/flutter_rust_bridge.yaml new file mode 100644 index 0000000..433952f --- /dev/null +++ b/apps/flutter/flutter_rust_bridge.yaml @@ -0,0 +1,4 @@ +rust_input: crate::api +rust_root: rust/ +dart_output: lib/src/rust/ +c_output: windows/runner/bridge_generated.h diff --git a/apps/flutter/lib/main.dart b/apps/flutter/lib/main.dart new file mode 100644 index 0000000..8fa8487 --- /dev/null +++ b/apps/flutter/lib/main.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'providers/app_provider.dart'; +import 'screens/setup_screen.dart'; +import 'screens/tasks_screen.dart'; +import 'screens/settings_screen.dart'; +import 'src/rust/frb_generated.dart'; +import 'theme.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await RustLib.init(); + runApp(const BevyTasksApp()); +} + +class BevyTasksApp extends StatelessWidget { + const BevyTasksApp({super.key}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => AppProvider()..init(), + child: Consumer( + builder: (context, app, _) { + return MaterialApp( + title: 'Bevy Tasks', + debugShowCheckedModeBanner: false, + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), + themeMode: app.darkMode ? ThemeMode.dark : ThemeMode.light, + home: _buildScreen(app), + ); + }, + ), + ); + } + + Widget _buildScreen(AppProvider app) { + switch (app.screen) { + case AppScreen.setup: + return const SetupScreen(); + case AppScreen.tasks: + return const TasksScreen(); + case AppScreen.settings: + return const SettingsScreen(); + } + } +} diff --git a/apps/flutter/lib/providers/app_provider.dart b/apps/flutter/lib/providers/app_provider.dart new file mode 100644 index 0000000..8a3f3ba --- /dev/null +++ b/apps/flutter/lib/providers/app_provider.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import '../src/rust/api.dart' as api; + +enum AppScreen { setup, tasks, settings } + +class AppProvider extends ChangeNotifier { + AppScreen _screen = AppScreen.setup; + bool _darkMode = false; + bool _syncing = false; + String? _error; + + List _workspaces = []; + String? _currentWorkspace; + + List _lists = []; + String? _activeListId; + List _tasks = []; + + // ── Getters ────────────────────────────────────────────────────── + + AppScreen get screen => _screen; + bool get darkMode => _darkMode; + bool get syncing => _syncing; + String? get error => _error; + List get workspaces => _workspaces; + String? get currentWorkspace => _currentWorkspace; + List get lists => _lists; + String? get activeListId => _activeListId; + api.BridgeTaskList? get activeList => + _activeListId == null ? null : _lists.where((l) => l.id == _activeListId).firstOrNull; + List get tasks => _tasks; + List get pendingTasks => _tasks.where((t) => t.status != 'completed').toList(); + List get completedTasks => _tasks.where((t) => t.status == 'completed').toList(); + bool get hasWorkspace => _currentWorkspace != null && _workspaces.isNotEmpty; + + // ── Init ───────────────────────────────────────────────────────── + + Future init() async { + try { + final config = await api.initApp(); + _workspaces = config.workspaces; + _currentWorkspace = config.currentWorkspace; + if (hasWorkspace) { + _screen = AppScreen.tasks; + await loadLists(); + } + } catch (e) { + _screen = AppScreen.setup; + } + notifyListeners(); + } + + // ── Navigation ─────────────────────────────────────────────────── + + void setScreen(AppScreen s) { + _screen = s; + notifyListeners(); + } + + void toggleDarkMode() { + _darkMode = !_darkMode; + notifyListeners(); + } + + void clearError() { + _error = null; + notifyListeners(); + } + + // ── Workspace operations ───────────────────────────────────────── + + Future addWorkspace(String name, String path) async { + try { + await api.addWorkspace(name: name, path: path); + final config = await api.getConfig(); + _workspaces = config.workspaces; + _currentWorkspace = config.currentWorkspace; + _screen = AppScreen.tasks; + _error = null; + await loadLists(); + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future switchWorkspace(String name) async { + try { + await api.setCurrentWorkspace(name: name); + final config = await api.getConfig(); + _workspaces = config.workspaces; + _currentWorkspace = config.currentWorkspace; + _activeListId = null; + await loadLists(); + _error = null; + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future removeWorkspace(String name) async { + try { + await api.removeWorkspace(name: name); + final config = await api.getConfig(); + _workspaces = config.workspaces; + _currentWorkspace = config.currentWorkspace; + if (!hasWorkspace) { + _screen = AppScreen.setup; + _lists = []; + _tasks = []; + _activeListId = null; + } + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + // ── List operations ────────────────────────────────────────────── + + Future loadLists() async { + try { + _lists = await api.getLists(); + if (_lists.isNotEmpty && _activeListId == null) { + _activeListId = _lists.first.id; + } + if (_activeListId != null) await loadTasks(); + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future selectList(String id) async { + _activeListId = id; + await loadTasks(); + notifyListeners(); + } + + Future createList(String name) async { + try { + final list = await api.createList(name: name); + _lists.add(list); + _activeListId = list.id; + _tasks = []; + _error = null; + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future deleteList(String id) async { + try { + await api.deleteList(listId: id); + _lists.removeWhere((l) => l.id == id); + if (_activeListId == id) { + _activeListId = _lists.isNotEmpty ? _lists.first.id : null; + if (_activeListId != null) await loadTasks(); + else _tasks = []; + } + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + // ── Task operations ────────────────────────────────────────────── + + Future loadTasks() async { + if (_activeListId == null) return; + try { + _tasks = await api.listTasks(listId: _activeListId!); + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future createTask(String title) async { + if (_activeListId == null) return; + try { + final task = await api.createTask(listId: _activeListId!, title: title); + _tasks.add(task); + _error = null; + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future toggleTask(String taskId) async { + if (_activeListId == null) return; + try { + await api.toggleTask(listId: _activeListId!, taskId: taskId); + await loadTasks(); + } catch (e) { + _error = e.toString(); + notifyListeners(); + } + } + + Future updateTask(String taskId, String title, String description) async { + if (_activeListId == null) return; + try { + await api.updateTask(listId: _activeListId!, taskId: taskId, title: title, description: description); + await loadTasks(); + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + Future deleteTask(String taskId) async { + if (_activeListId == null) return; + try { + await api.deleteTask(listId: _activeListId!, taskId: taskId); + _tasks.removeWhere((t) => t.id == taskId); + } catch (e) { + _error = e.toString(); + } + notifyListeners(); + } + + // ── Sync ───────────────────────────────────────────────────────── + + Future triggerSync() async { + if (_currentWorkspace == null) return; + final ws = _workspaces.where((w) => w.name == _currentWorkspace).firstOrNull; + if (ws == null || ws.webdavUrl == null) { + _error = 'No WebDAV URL configured'; + notifyListeners(); + return; + } + _syncing = true; + _error = null; + notifyListeners(); + try { + final result = await api.syncWorkspaceBridge( + workspacePath: ws.path, + webdavUrl: ws.webdavUrl!, + username: '', + password: '', + ); + if (result.errors.isNotEmpty) { + _error = result.errors.join('; '); + } + await loadLists(); + } catch (e) { + _error = e.toString(); + } finally { + _syncing = false; + notifyListeners(); + } + } +} diff --git a/apps/flutter/lib/screens/settings_screen.dart b/apps/flutter/lib/screens/settings_screen.dart new file mode 100644 index 0000000..78b8427 --- /dev/null +++ b/apps/flutter/lib/screens/settings_screen.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/app_provider.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + final _webdavUrlController = TextEditingController(); + final _webdavUserController = TextEditingController(); + final _webdavPassController = TextEditingController(); + String? _confirmRemove; + + @override + void dispose() { + _webdavUrlController.dispose(); + _webdavUserController.dispose(); + _webdavPassController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final app = context.watch(); + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () => app.setScreen(AppScreen.tasks), + icon: const Icon(Icons.arrow_back), + ), + title: const Text('Settings'), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // ── Workspaces section ── + Text( + 'WORKSPACES', + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 1.2, + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + ), + ), + const SizedBox(height: 12), + + ...app.workspaces.map((ws) { + final isCurrent = ws.name == app.currentWorkspace; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ws.name, + style: theme.textTheme.titleSmall?.copyWith( + color: isCurrent ? theme.colorScheme.primary : null, + ), + ), + Text( + ws.path, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + ), + ), + ], + ), + ), + if (!isCurrent) + TextButton( + onPressed: () => app.switchWorkspace(ws.name), + child: const Text('Switch'), + ), + if (_confirmRemove == ws.name) ...[ + TextButton( + onPressed: () { + app.removeWorkspace(ws.name); + setState(() => _confirmRemove = null); + }, + child: Text('Confirm', style: TextStyle(color: theme.colorScheme.error)), + ), + TextButton( + onPressed: () => setState(() => _confirmRemove = null), + child: const Text('Cancel'), + ), + ] else + TextButton( + onPressed: () => setState(() => _confirmRemove = ws.name), + child: Text( + 'Remove', + style: TextStyle(color: theme.colorScheme.onSurfaceVariant.withAlpha(102)), + ), + ), + ], + ), + if (ws.webdavUrl != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Sync: ${ws.webdavUrl}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(102), + ), + ), + ), + ], + ), + ), + ); + }), + + TextButton.icon( + onPressed: () => app.setScreen(AppScreen.setup), + icon: const Icon(Icons.add, size: 18), + label: const Text('Add workspace'), + ), + + const SizedBox(height: 24), + + // ── WebDAV Sync section ── + Text( + 'WEBDAV SYNC', + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 1.2, + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + ), + ), + const SizedBox(height: 12), + + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Server URL', style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + TextField( + controller: _webdavUrlController, + decoration: const InputDecoration(hintText: 'https://dav.example.com/tasks/'), + keyboardType: TextInputType.url, + ), + const SizedBox(height: 12), + + Text('Username', style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + TextField(controller: _webdavUserController), + const SizedBox(height: 12), + + Text('Password', style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + TextField( + controller: _webdavPassController, + obscureText: true, + ), + const SizedBox(height: 16), + + Row( + children: [ + OutlinedButton( + onPressed: () { + // TODO: test connection + }, + child: const Text('Test Connection'), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: () { + // TODO: save webdav config + }, + child: const Text('Save'), + ), + ], + ), + ], + ), + ), + ), + + if (app.currentWorkspace != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FilledButton( + onPressed: app.syncing ? null : app.triggerSync, + child: Text(app.syncing ? 'Syncing...' : 'Sync Now'), + ), + ), + + const SizedBox(height: 24), + + // ── Appearance section ── + Text( + 'APPEARANCE', + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 1.2, + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + ), + ), + const SizedBox(height: 12), + + Card( + child: SwitchListTile( + title: const Text('Dark mode'), + value: app.darkMode, + onChanged: (_) => app.toggleDarkMode(), + ), + ), + ], + ), + ); + } +} diff --git a/apps/flutter/lib/screens/setup_screen.dart b/apps/flutter/lib/screens/setup_screen.dart new file mode 100644 index 0000000..86f473b --- /dev/null +++ b/apps/flutter/lib/screens/setup_screen.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:file_picker/file_picker.dart'; +import '../providers/app_provider.dart'; + +class SetupScreen extends StatefulWidget { + const SetupScreen({super.key}); + + @override + State createState() => _SetupScreenState(); +} + +class _SetupScreenState extends State { + final _nameController = TextEditingController(); + final _pathController = TextEditingController(); + + @override + void dispose() { + _nameController.dispose(); + _pathController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final app = context.read(); + final theme = Theme.of(context); + + return Scaffold( + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Card( + child: Padding( + padding: const EdgeInsets.all(32), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Bevy Tasks', + style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + 'Create or open a workspace to get started.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 24), + + // Workspace name + Text('Workspace name', style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + TextField( + controller: _nameController, + decoration: const InputDecoration(hintText: 'My Tasks'), + ), + const SizedBox(height: 16), + + // Folder path + Text('Folder', style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: TextField( + controller: _pathController, + readOnly: true, + decoration: const InputDecoration(hintText: 'Select a folder…'), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: () async { + final result = await FilePicker.platform.getDirectoryPath(); + if (result != null) { + _pathController.text = result; + } + }, + child: const Text('Browse'), + ), + ], + ), + const SizedBox(height: 24), + + // Create button + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + final name = _nameController.text.trim(); + final path = _pathController.text.trim(); + if (name.isNotEmpty && path.isNotEmpty) { + app.addWorkspace(name, path); + } + }, + child: const Text('Create Workspace'), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/apps/flutter/lib/screens/tasks_screen.dart b/apps/flutter/lib/screens/tasks_screen.dart new file mode 100644 index 0000000..b1c40cd --- /dev/null +++ b/apps/flutter/lib/screens/tasks_screen.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/app_provider.dart'; +import '../widgets/task_item.dart'; +import '../widgets/new_task_bar.dart'; +import '../widgets/list_picker_sheet.dart'; + +class TasksScreen extends StatefulWidget { + const TasksScreen({super.key}); + + @override + State createState() => _TasksScreenState(); +} + +class _TasksScreenState extends State { + bool _showCompleted = true; + + void _openListPicker() { + showModalBottomSheet( + context: context, + builder: (_) => const ListPickerSheet(), + ); + } + + @override + Widget build(BuildContext context) { + final app = context.watch(); + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: GestureDetector( + onTap: _openListPicker, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + app.activeList?.title ?? 'Tasks', + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, size: 20), + ], + ), + ), + actions: [ + if (app.syncing) + const Padding( + padding: EdgeInsets.only(right: 8), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + IconButton( + onPressed: app.toggleDarkMode, + icon: Icon(app.darkMode ? Icons.light_mode : Icons.dark_mode), + ), + IconButton( + onPressed: () => app.setScreen(AppScreen.settings), + icon: const Icon(Icons.settings), + ), + ], + ), + body: Column( + children: [ + if (app.error != null) + MaterialBanner( + content: Text(app.error!, style: const TextStyle(fontSize: 13)), + backgroundColor: theme.colorScheme.errorContainer, + actions: [ + TextButton( + onPressed: app.clearError, + child: const Text('Dismiss'), + ), + ], + ), + Expanded( + child: app.lists.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('No lists yet', style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + )), + const SizedBox(height: 4), + Text('Tap the title to create one', style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + )), + ], + ), + ) + : app.activeListId == null + ? Center( + child: Text('Select a list', style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + )), + ) + : ListView( + physics: const BouncingScrollPhysics(), + children: [ + ...app.pendingTasks.map((task) => TaskItem(task: task)), + if (app.pendingTasks.isEmpty) + Padding( + padding: const EdgeInsets.all(32), + child: Center( + child: Text( + 'No tasks. Add one below.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(102), + ), + ), + ), + ), + if (app.completedTasks.isNotEmpty) ...[ + const Divider(), + ListTile( + dense: true, + leading: Icon( + _showCompleted ? Icons.expand_more : Icons.chevron_right, + size: 20, + ), + title: Text( + 'Completed (${app.completedTasks.length})', + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + onTap: () => setState(() => _showCompleted = !_showCompleted), + ), + if (_showCompleted) + ...app.completedTasks.map((task) => TaskItem(task: task)), + ], + ], + ), + ), + const NewTaskBar(), + ], + ), + ); + } +} diff --git a/apps/flutter/lib/src/rust/api.dart b/apps/flutter/lib/src/rust/api.dart new file mode 100644 index 0000000..9511229 --- /dev/null +++ b/apps/flutter/lib/src/rust/api.dart @@ -0,0 +1,260 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `config_to_bridge`, `ensure_repo`, `list_to_bridge`, `task_to_bridge`, `with_state` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AppState` + +/// Initialize the bridge. Must be called once at app startup. +Future initApp() => RustLib.instance.api.crateApiInitApp(); + +Future getConfig() => RustLib.instance.api.crateApiGetConfig(); + +Future addWorkspace({required String name, required String path}) => + RustLib.instance.api.crateApiAddWorkspace(name: name, path: path); + +Future setCurrentWorkspace({required String name}) => + RustLib.instance.api.crateApiSetCurrentWorkspace(name: name); + +Future removeWorkspace({required String name}) => + RustLib.instance.api.crateApiRemoveWorkspace(name: name); + +Future> getLists() => + RustLib.instance.api.crateApiGetLists(); + +Future createList({required String name}) => + RustLib.instance.api.crateApiCreateList(name: name); + +Future deleteList({required String listId}) => + RustLib.instance.api.crateApiDeleteList(listId: listId); + +Future> listTasks({required String listId}) => + RustLib.instance.api.crateApiListTasks(listId: listId); + +Future createTask( + {required String listId, required String title}) => + RustLib.instance.api.crateApiCreateTask(listId: listId, title: title); + +Future toggleTask( + {required String listId, required String taskId}) => + RustLib.instance.api.crateApiToggleTask(listId: listId, taskId: taskId); + +Future updateTask( + {required String listId, + required String taskId, + required String title, + required String description}) => + RustLib.instance.api.crateApiUpdateTask( + listId: listId, taskId: taskId, title: title, description: description); + +Future deleteTask({required String listId, required String taskId}) => + RustLib.instance.api.crateApiDeleteTask(listId: listId, taskId: taskId); + +Future reorderTask( + {required String listId, + required String taskId, + required BigInt newPosition}) => + RustLib.instance.api.crateApiReorderTask( + listId: listId, taskId: taskId, newPosition: newPosition); + +Future setWebdavConfig( + {required String workspaceName, required String webdavUrl}) => + RustLib.instance.api.crateApiSetWebdavConfig( + workspaceName: workspaceName, webdavUrl: webdavUrl); + +Future storeWebdavCredentials( + {required String domain, + required String username, + required String password}) => + RustLib.instance.api.crateApiStoreWebdavCredentials( + domain: domain, username: username, password: password); + +Future syncWorkspaceBridge( + {required String workspacePath, + required String webdavUrl, + required String username, + required String password}) => + RustLib.instance.api.crateApiSyncWorkspaceBridge( + workspacePath: workspacePath, + webdavUrl: webdavUrl, + username: username, + password: password); + +/// Flat app config for FFI transport. +class BridgeConfig { + final List workspaces; + final String? currentWorkspace; + + const BridgeConfig({ + required this.workspaces, + this.currentWorkspace, + }); + + @override + int get hashCode => workspaces.hashCode ^ currentWorkspace.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeConfig && + runtimeType == other.runtimeType && + workspaces == other.workspaces && + currentWorkspace == other.currentWorkspace; +} + +/// Sync result for FFI transport. +class BridgeSyncResult { + final int uploaded; + final int downloaded; + final int deletedLocal; + final int deletedRemote; + final int conflicts; + final List errors; + + const BridgeSyncResult({ + required this.uploaded, + required this.downloaded, + required this.deletedLocal, + required this.deletedRemote, + required this.conflicts, + required this.errors, + }); + + @override + int get hashCode => + uploaded.hashCode ^ + downloaded.hashCode ^ + deletedLocal.hashCode ^ + deletedRemote.hashCode ^ + conflicts.hashCode ^ + errors.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeSyncResult && + runtimeType == other.runtimeType && + uploaded == other.uploaded && + downloaded == other.downloaded && + deletedLocal == other.deletedLocal && + deletedRemote == other.deletedRemote && + conflicts == other.conflicts && + errors == other.errors; +} + +/// Flat task struct for FFI transport. +class BridgeTask { + final String id; + final String title; + final String description; + final String status; + final String? dueDate; + final String createdAt; + final String updatedAt; + final String? parentId; + + const BridgeTask({ + required this.id, + required this.title, + required this.description, + required this.status, + this.dueDate, + required this.createdAt, + required this.updatedAt, + this.parentId, + }); + + @override + int get hashCode => + id.hashCode ^ + title.hashCode ^ + description.hashCode ^ + status.hashCode ^ + dueDate.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + parentId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeTask && + runtimeType == other.runtimeType && + id == other.id && + title == other.title && + description == other.description && + status == other.status && + dueDate == other.dueDate && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + parentId == other.parentId; +} + +/// Flat list struct for FFI transport. +class BridgeTaskList { + final String id; + final String title; + final String createdAt; + final String updatedAt; + final bool groupByDueDate; + + const BridgeTaskList({ + required this.id, + required this.title, + required this.createdAt, + required this.updatedAt, + required this.groupByDueDate, + }); + + @override + int get hashCode => + id.hashCode ^ + title.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + groupByDueDate.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeTaskList && + runtimeType == other.runtimeType && + id == other.id && + title == other.title && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + groupByDueDate == other.groupByDueDate; +} + +/// Flat workspace config for FFI transport. +class BridgeWorkspace { + final String name; + final String path; + final String? webdavUrl; + final String? lastSync; + + const BridgeWorkspace({ + required this.name, + required this.path, + this.webdavUrl, + this.lastSync, + }); + + @override + int get hashCode => + name.hashCode ^ path.hashCode ^ webdavUrl.hashCode ^ lastSync.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BridgeWorkspace && + runtimeType == other.runtimeType && + name == other.name && + path == other.path && + webdavUrl == other.webdavUrl && + lastSync == other.lastSync; +} diff --git a/apps/flutter/lib/src/rust/frb_generated.dart b/apps/flutter/lib/src/rust/frb_generated.dart new file mode 100644 index 0000000..3eb10e1 --- /dev/null +++ b/apps/flutter/lib/src/rust/frb_generated.dart @@ -0,0 +1,1072 @@ +// 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 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + bool forceSameCodegenVersion = true, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + forceSameCodegenVersion: forceSameCodegenVersion, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({ + required RustLibApi api, + }) { + instance.initMockImpl( + api: api, + ); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async {} + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.11.1'; + + @override + int get rustContentHash => -1505334582; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'bevy_tasks_flutter_bridge', + ioDirectory: 'rust/target/release/', + webPrefix: 'pkg/', + ); +} + +abstract class RustLibApi extends BaseApi { + Future crateApiAddWorkspace( + {required String name, required String path}); + + Future crateApiCreateList({required String name}); + + Future crateApiCreateTask( + {required String listId, required String title}); + + Future crateApiDeleteList({required String listId}); + + Future crateApiDeleteTask( + {required String listId, required String taskId}); + + Future crateApiGetConfig(); + + Future> crateApiGetLists(); + + Future crateApiInitApp(); + + Future> crateApiListTasks({required String listId}); + + Future crateApiRemoveWorkspace({required String name}); + + Future crateApiReorderTask( + {required String listId, + required String taskId, + required BigInt newPosition}); + + Future crateApiSetCurrentWorkspace({required String name}); + + Future crateApiSetWebdavConfig( + {required String workspaceName, required String webdavUrl}); + + Future crateApiStoreWebdavCredentials( + {required String domain, + required String username, + required String password}); + + Future crateApiSyncWorkspaceBridge( + {required String workspacePath, + required String webdavUrl, + required String username, + required String password}); + + Future crateApiToggleTask( + {required String listId, required String taskId}); + + Future crateApiUpdateTask( + {required String listId, + required String taskId, + required String title, + required String description}); +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + Future crateApiAddWorkspace( + {required String name, required String path}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(name, serializer); + sse_encode_String(path, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 1, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiAddWorkspaceConstMeta, + argValues: [name, path], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiAddWorkspaceConstMeta => const TaskConstMeta( + debugName: "add_workspace", + argNames: ["name", "path"], + ); + + @override + Future crateApiCreateList({required String name}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(name, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 2, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_task_list, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiCreateListConstMeta, + argValues: [name], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiCreateListConstMeta => const TaskConstMeta( + debugName: "create_list", + argNames: ["name"], + ); + + @override + Future crateApiCreateTask( + {required String listId, required String title}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + sse_encode_String(title, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 3, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_task, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiCreateTaskConstMeta, + argValues: [listId, title], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiCreateTaskConstMeta => const TaskConstMeta( + debugName: "create_task", + argNames: ["listId", "title"], + ); + + @override + Future crateApiDeleteList({required String listId}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 4, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiDeleteListConstMeta, + argValues: [listId], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiDeleteListConstMeta => const TaskConstMeta( + debugName: "delete_list", + argNames: ["listId"], + ); + + @override + Future crateApiDeleteTask( + {required String listId, required String taskId}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + sse_encode_String(taskId, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 5, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiDeleteTaskConstMeta, + argValues: [listId, taskId], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiDeleteTaskConstMeta => const TaskConstMeta( + debugName: "delete_task", + argNames: ["listId", "taskId"], + ); + + @override + Future crateApiGetConfig() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 6, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_config, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiGetConfigConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiGetConfigConstMeta => const TaskConstMeta( + debugName: "get_config", + argNames: [], + ); + + @override + Future> crateApiGetLists() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 7, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_bridge_task_list, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiGetListsConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiGetListsConstMeta => const TaskConstMeta( + debugName: "get_lists", + argNames: [], + ); + + @override + Future crateApiInitApp() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 8, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_config, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiInitAppConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiInitAppConstMeta => const TaskConstMeta( + debugName: "init_app", + argNames: [], + ); + + @override + Future> crateApiListTasks({required String listId}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 9, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_bridge_task, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiListTasksConstMeta, + argValues: [listId], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiListTasksConstMeta => const TaskConstMeta( + debugName: "list_tasks", + argNames: ["listId"], + ); + + @override + Future crateApiRemoveWorkspace({required String name}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(name, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 10, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiRemoveWorkspaceConstMeta, + argValues: [name], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiRemoveWorkspaceConstMeta => const TaskConstMeta( + debugName: "remove_workspace", + argNames: ["name"], + ); + + @override + Future crateApiReorderTask( + {required String listId, + required String taskId, + required BigInt newPosition}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + sse_encode_String(taskId, serializer); + sse_encode_usize(newPosition, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 11, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiReorderTaskConstMeta, + argValues: [listId, taskId, newPosition], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiReorderTaskConstMeta => const TaskConstMeta( + debugName: "reorder_task", + argNames: ["listId", "taskId", "newPosition"], + ); + + @override + Future crateApiSetCurrentWorkspace({required String name}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(name, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 12, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSetCurrentWorkspaceConstMeta, + argValues: [name], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSetCurrentWorkspaceConstMeta => + const TaskConstMeta( + debugName: "set_current_workspace", + argNames: ["name"], + ); + + @override + Future crateApiSetWebdavConfig( + {required String workspaceName, required String webdavUrl}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(workspaceName, serializer); + sse_encode_String(webdavUrl, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 13, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSetWebdavConfigConstMeta, + argValues: [workspaceName, webdavUrl], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSetWebdavConfigConstMeta => const TaskConstMeta( + debugName: "set_webdav_config", + argNames: ["workspaceName", "webdavUrl"], + ); + + @override + Future crateApiStoreWebdavCredentials( + {required String domain, + required String username, + required String password}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(domain, serializer); + sse_encode_String(username, serializer); + sse_encode_String(password, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 14, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiStoreWebdavCredentialsConstMeta, + argValues: [domain, username, password], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiStoreWebdavCredentialsConstMeta => + const TaskConstMeta( + debugName: "store_webdav_credentials", + argNames: ["domain", "username", "password"], + ); + + @override + Future crateApiSyncWorkspaceBridge( + {required String workspacePath, + required String webdavUrl, + required String username, + required String password}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(workspacePath, serializer); + sse_encode_String(webdavUrl, serializer); + sse_encode_String(username, serializer); + sse_encode_String(password, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 15, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_sync_result, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSyncWorkspaceBridgeConstMeta, + argValues: [workspacePath, webdavUrl, username, password], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSyncWorkspaceBridgeConstMeta => + const TaskConstMeta( + debugName: "sync_workspace_bridge", + argNames: ["workspacePath", "webdavUrl", "username", "password"], + ); + + @override + Future crateApiToggleTask( + {required String listId, required String taskId}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + sse_encode_String(taskId, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 16, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bridge_task, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiToggleTaskConstMeta, + argValues: [listId, taskId], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiToggleTaskConstMeta => const TaskConstMeta( + debugName: "toggle_task", + argNames: ["listId", "taskId"], + ); + + @override + Future crateApiUpdateTask( + {required String listId, + required String taskId, + required String title, + required String description}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(listId, serializer); + sse_encode_String(taskId, serializer); + sse_encode_String(title, serializer); + sse_encode_String(description, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 17, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiUpdateTaskConstMeta, + argValues: [listId, taskId, title, description], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiUpdateTaskConstMeta => const TaskConstMeta( + debugName: "update_task", + argNames: ["listId", "taskId", "title", "description"], + ); + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + bool dco_decode_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + + @protected + BridgeConfig dco_decode_bridge_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return BridgeConfig( + workspaces: dco_decode_list_bridge_workspace(arr[0]), + currentWorkspace: dco_decode_opt_String(arr[1]), + ); + } + + @protected + BridgeSyncResult dco_decode_bridge_sync_result(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 6) + throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + return BridgeSyncResult( + uploaded: dco_decode_u_32(arr[0]), + downloaded: dco_decode_u_32(arr[1]), + deletedLocal: dco_decode_u_32(arr[2]), + deletedRemote: dco_decode_u_32(arr[3]), + conflicts: dco_decode_u_32(arr[4]), + errors: dco_decode_list_String(arr[5]), + ); + } + + @protected + BridgeTask dco_decode_bridge_task(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 8) + throw Exception('unexpected arr length: expect 8 but see ${arr.length}'); + return BridgeTask( + id: dco_decode_String(arr[0]), + title: dco_decode_String(arr[1]), + description: dco_decode_String(arr[2]), + status: dco_decode_String(arr[3]), + dueDate: dco_decode_opt_String(arr[4]), + createdAt: dco_decode_String(arr[5]), + updatedAt: dco_decode_String(arr[6]), + parentId: dco_decode_opt_String(arr[7]), + ); + } + + @protected + BridgeTaskList dco_decode_bridge_task_list(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return BridgeTaskList( + id: dco_decode_String(arr[0]), + title: dco_decode_String(arr[1]), + createdAt: dco_decode_String(arr[2]), + updatedAt: dco_decode_String(arr[3]), + groupByDueDate: dco_decode_bool(arr[4]), + ); + } + + @protected + BridgeWorkspace dco_decode_bridge_workspace(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return BridgeWorkspace( + name: dco_decode_String(arr[0]), + path: dco_decode_String(arr[1]), + webdavUrl: dco_decode_opt_String(arr[2]), + lastSync: dco_decode_opt_String(arr[3]), + ); + } + + @protected + List dco_decode_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_String).toList(); + } + + @protected + List dco_decode_list_bridge_task(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_bridge_task).toList(); + } + + @protected + List dco_decode_list_bridge_task_list(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_bridge_task_list).toList(); + } + + @protected + List dco_decode_list_bridge_workspace(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_bridge_workspace).toList(); + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + + @protected + int dco_decode_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + BridgeConfig sse_decode_bridge_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_workspaces = sse_decode_list_bridge_workspace(deserializer); + var var_currentWorkspace = sse_decode_opt_String(deserializer); + return BridgeConfig( + workspaces: var_workspaces, currentWorkspace: var_currentWorkspace); + } + + @protected + BridgeSyncResult sse_decode_bridge_sync_result(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_uploaded = sse_decode_u_32(deserializer); + var var_downloaded = sse_decode_u_32(deserializer); + var var_deletedLocal = sse_decode_u_32(deserializer); + var var_deletedRemote = sse_decode_u_32(deserializer); + var var_conflicts = sse_decode_u_32(deserializer); + var var_errors = sse_decode_list_String(deserializer); + return BridgeSyncResult( + uploaded: var_uploaded, + downloaded: var_downloaded, + deletedLocal: var_deletedLocal, + deletedRemote: var_deletedRemote, + conflicts: var_conflicts, + errors: var_errors); + } + + @protected + BridgeTask sse_decode_bridge_task(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_String(deserializer); + var var_title = sse_decode_String(deserializer); + var var_description = sse_decode_String(deserializer); + var var_status = sse_decode_String(deserializer); + var var_dueDate = sse_decode_opt_String(deserializer); + var var_createdAt = sse_decode_String(deserializer); + var var_updatedAt = sse_decode_String(deserializer); + var var_parentId = sse_decode_opt_String(deserializer); + return BridgeTask( + id: var_id, + title: var_title, + description: var_description, + status: var_status, + dueDate: var_dueDate, + createdAt: var_createdAt, + updatedAt: var_updatedAt, + parentId: var_parentId); + } + + @protected + BridgeTaskList sse_decode_bridge_task_list(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_String(deserializer); + var var_title = sse_decode_String(deserializer); + var var_createdAt = sse_decode_String(deserializer); + var var_updatedAt = sse_decode_String(deserializer); + var var_groupByDueDate = sse_decode_bool(deserializer); + return BridgeTaskList( + id: var_id, + title: var_title, + createdAt: var_createdAt, + updatedAt: var_updatedAt, + groupByDueDate: var_groupByDueDate); + } + + @protected + BridgeWorkspace sse_decode_bridge_workspace(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_name = sse_decode_String(deserializer); + var var_path = sse_decode_String(deserializer); + var var_webdavUrl = sse_decode_opt_String(deserializer); + var var_lastSync = sse_decode_opt_String(deserializer); + return BridgeWorkspace( + name: var_name, + path: var_path, + webdavUrl: var_webdavUrl, + lastSync: var_lastSync); + } + + @protected + List sse_decode_list_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_String(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_bridge_task(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_bridge_task(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_bridge_task_list( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_bridge_task_list(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_bridge_workspace( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_bridge_workspace(deserializer)); + } + return ans_; + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + + @protected + int sse_decode_u_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint32(); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } + + @protected + void sse_encode_bridge_config(BridgeConfig self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_bridge_workspace(self.workspaces, serializer); + sse_encode_opt_String(self.currentWorkspace, serializer); + } + + @protected + void sse_encode_bridge_sync_result( + BridgeSyncResult self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self.uploaded, serializer); + sse_encode_u_32(self.downloaded, serializer); + sse_encode_u_32(self.deletedLocal, serializer); + sse_encode_u_32(self.deletedRemote, serializer); + sse_encode_u_32(self.conflicts, serializer); + sse_encode_list_String(self.errors, serializer); + } + + @protected + void sse_encode_bridge_task(BridgeTask self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.id, serializer); + sse_encode_String(self.title, serializer); + sse_encode_String(self.description, serializer); + sse_encode_String(self.status, serializer); + sse_encode_opt_String(self.dueDate, serializer); + sse_encode_String(self.createdAt, serializer); + sse_encode_String(self.updatedAt, serializer); + sse_encode_opt_String(self.parentId, serializer); + } + + @protected + void sse_encode_bridge_task_list( + BridgeTaskList self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.id, serializer); + sse_encode_String(self.title, serializer); + sse_encode_String(self.createdAt, serializer); + sse_encode_String(self.updatedAt, serializer); + sse_encode_bool(self.groupByDueDate, serializer); + } + + @protected + void sse_encode_bridge_workspace( + BridgeWorkspace self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.name, serializer); + sse_encode_String(self.path, serializer); + sse_encode_opt_String(self.webdavUrl, serializer); + sse_encode_opt_String(self.lastSync, serializer); + } + + @protected + void sse_encode_list_String(List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_String(item, serializer); + } + } + + @protected + void sse_encode_list_bridge_task( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_bridge_task(item, serializer); + } + } + + @protected + void sse_encode_list_bridge_task_list( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_bridge_task_list(item, serializer); + } + } + + @protected + void sse_encode_list_bridge_workspace( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_bridge_workspace(item, serializer); + } + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + + @protected + void sse_encode_u_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint32(self); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } +} diff --git a/apps/flutter/lib/src/rust/frb_generated.io.dart b/apps/flutter/lib/src/rust/frb_generated.io.dart new file mode 100644 index 0000000..b1b055b --- /dev/null +++ b/apps/flutter/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,203 @@ +// 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 { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @protected + String dco_decode_String(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + BridgeConfig dco_decode_bridge_config(dynamic raw); + + @protected + BridgeSyncResult dco_decode_bridge_sync_result(dynamic raw); + + @protected + BridgeTask dco_decode_bridge_task(dynamic raw); + + @protected + BridgeTaskList dco_decode_bridge_task_list(dynamic raw); + + @protected + BridgeWorkspace dco_decode_bridge_workspace(dynamic raw); + + @protected + List dco_decode_list_String(dynamic raw); + + @protected + List dco_decode_list_bridge_task(dynamic raw); + + @protected + List dco_decode_list_bridge_task_list(dynamic raw); + + @protected + List dco_decode_list_bridge_workspace(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + int dco_decode_u_32(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + BridgeConfig sse_decode_bridge_config(SseDeserializer deserializer); + + @protected + BridgeSyncResult sse_decode_bridge_sync_result(SseDeserializer deserializer); + + @protected + BridgeTask sse_decode_bridge_task(SseDeserializer deserializer); + + @protected + BridgeTaskList sse_decode_bridge_task_list(SseDeserializer deserializer); + + @protected + BridgeWorkspace sse_decode_bridge_workspace(SseDeserializer deserializer); + + @protected + List sse_decode_list_String(SseDeserializer deserializer); + + @protected + List sse_decode_list_bridge_task(SseDeserializer deserializer); + + @protected + List sse_decode_list_bridge_task_list( + SseDeserializer deserializer); + + @protected + List sse_decode_list_bridge_workspace( + SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + int sse_decode_u_32(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_bridge_config(BridgeConfig self, SseSerializer serializer); + + @protected + void sse_encode_bridge_sync_result( + BridgeSyncResult self, SseSerializer serializer); + + @protected + void sse_encode_bridge_task(BridgeTask self, SseSerializer serializer); + + @protected + void sse_encode_bridge_task_list( + BridgeTaskList self, SseSerializer serializer); + + @protected + void sse_encode_bridge_workspace( + BridgeWorkspace self, SseSerializer serializer); + + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + + @protected + void sse_encode_list_bridge_task( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_bridge_task_list( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_bridge_workspace( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_u_32(int self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; +} diff --git a/apps/flutter/lib/src/rust/frb_generated.web.dart b/apps/flutter/lib/src/rust/frb_generated.web.dart new file mode 100644 index 0000000..e54d802 --- /dev/null +++ b/apps/flutter/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,150 @@ +// 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 { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + + + @protected String dco_decode_String(dynamic raw); + +@protected bool dco_decode_bool(dynamic raw); + +@protected BridgeConfig dco_decode_bridge_config(dynamic raw); + +@protected BridgeSyncResult dco_decode_bridge_sync_result(dynamic raw); + +@protected BridgeTask dco_decode_bridge_task(dynamic raw); + +@protected BridgeTaskList dco_decode_bridge_task_list(dynamic raw); + +@protected BridgeWorkspace dco_decode_bridge_workspace(dynamic raw); + +@protected List dco_decode_list_String(dynamic raw); + +@protected List dco_decode_list_bridge_task(dynamic raw); + +@protected List dco_decode_list_bridge_task_list(dynamic raw); + +@protected List dco_decode_list_bridge_workspace(dynamic raw); + +@protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + +@protected String? dco_decode_opt_String(dynamic raw); + +@protected int dco_decode_u_32(dynamic raw); + +@protected int dco_decode_u_8(dynamic raw); + +@protected void dco_decode_unit(dynamic raw); + +@protected BigInt dco_decode_usize(dynamic raw); + +@protected String sse_decode_String(SseDeserializer deserializer); + +@protected bool sse_decode_bool(SseDeserializer deserializer); + +@protected BridgeConfig sse_decode_bridge_config(SseDeserializer deserializer); + +@protected BridgeSyncResult sse_decode_bridge_sync_result(SseDeserializer deserializer); + +@protected BridgeTask sse_decode_bridge_task(SseDeserializer deserializer); + +@protected BridgeTaskList sse_decode_bridge_task_list(SseDeserializer deserializer); + +@protected BridgeWorkspace sse_decode_bridge_workspace(SseDeserializer deserializer); + +@protected List sse_decode_list_String(SseDeserializer deserializer); + +@protected List sse_decode_list_bridge_task(SseDeserializer deserializer); + +@protected List sse_decode_list_bridge_task_list(SseDeserializer deserializer); + +@protected List sse_decode_list_bridge_workspace(SseDeserializer deserializer); + +@protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + +@protected String? sse_decode_opt_String(SseDeserializer deserializer); + +@protected int sse_decode_u_32(SseDeserializer deserializer); + +@protected int sse_decode_u_8(SseDeserializer deserializer); + +@protected void sse_decode_unit(SseDeserializer deserializer); + +@protected BigInt sse_decode_usize(SseDeserializer deserializer); + +@protected int sse_decode_i_32(SseDeserializer deserializer); + +@protected void sse_encode_String(String self, SseSerializer serializer); + +@protected void sse_encode_bool(bool self, SseSerializer serializer); + +@protected void sse_encode_bridge_config(BridgeConfig self, SseSerializer serializer); + +@protected void sse_encode_bridge_sync_result(BridgeSyncResult self, SseSerializer serializer); + +@protected void sse_encode_bridge_task(BridgeTask self, SseSerializer serializer); + +@protected void sse_encode_bridge_task_list(BridgeTaskList self, SseSerializer serializer); + +@protected void sse_encode_bridge_workspace(BridgeWorkspace self, SseSerializer serializer); + +@protected void sse_encode_list_String(List self, SseSerializer serializer); + +@protected void sse_encode_list_bridge_task(List self, SseSerializer serializer); + +@protected void sse_encode_list_bridge_task_list(List self, SseSerializer serializer); + +@protected void sse_encode_list_bridge_workspace(List self, SseSerializer serializer); + +@protected void sse_encode_list_prim_u_8_strict(Uint8List self, SseSerializer serializer); + +@protected void sse_encode_opt_String(String? self, SseSerializer serializer); + +@protected void sse_encode_u_32(int self, SseSerializer serializer); + +@protected void sse_encode_u_8(int self, SseSerializer serializer); + +@protected void sse_encode_unit(void self, SseSerializer serializer); + +@protected void sse_encode_usize(BigInt self, SseSerializer serializer); + +@protected void sse_encode_i_32(int self, SseSerializer serializer); + } + + + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + + } + @JS('wasm_bindgen') external RustLibWasmModule get wasmModule; + + @JS() @anonymous extension type RustLibWasmModule._(JSObject _) implements JSObject { + + } + \ No newline at end of file diff --git a/apps/flutter/lib/theme.dart b/apps/flutter/lib/theme.dart new file mode 100644 index 0000000..6371330 --- /dev/null +++ b/apps/flutter/lib/theme.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + static const _blue = Color(0xFF2563EB); + + static ThemeData light() { + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + colorSchemeSeed: _blue, + scaffoldBackgroundColor: Colors.white, + textTheme: GoogleFonts.notoSansTextTheme(), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Color(0xFF1F2937), + elevation: 0, + scrolledUnderElevation: 1, + ), + cardTheme: const CardThemeData( + color: Color(0xFFF9FAFB), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: _blue, + foregroundColor: Colors.white, + elevation: 4, + shape: CircleBorder(), + ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + ), + dividerTheme: const DividerThemeData( + color: Color(0xFFE5E7EB), + thickness: 1, + space: 0, + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE5E7EB)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE5E7EB)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: _blue, width: 2), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ); + } + + static ThemeData dark() { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorSchemeSeed: _blue, + scaffoldBackgroundColor: const Color(0xFF121212), + textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF121212), + foregroundColor: Color(0xFFE5E7EB), + elevation: 0, + scrolledUnderElevation: 1, + ), + cardTheme: const CardThemeData( + color: Color(0xFF1E1E1E), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: _blue, + foregroundColor: Colors.white, + elevation: 4, + shape: CircleBorder(), + ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: Color(0xFF1E1E1E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + ), + dividerTheme: const DividerThemeData( + color: Color(0xFF374151), + thickness: 1, + space: 0, + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF374151)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF374151)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: _blue, width: 2), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ); + } +} diff --git a/apps/flutter/lib/widgets/list_picker_sheet.dart b/apps/flutter/lib/widgets/list_picker_sheet.dart new file mode 100644 index 0000000..ec5684f --- /dev/null +++ b/apps/flutter/lib/widgets/list_picker_sheet.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/app_provider.dart'; + +class ListPickerSheet extends StatefulWidget { + const ListPickerSheet({super.key}); + + @override + State createState() => _ListPickerSheetState(); +} + +class _ListPickerSheetState extends State { + bool _showNewList = false; + final _newListController = TextEditingController(); + String? _confirmDelete; + + @override + void dispose() { + _newListController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final app = context.watch(); + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 32, + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: theme.dividerColor, + ), + ), + ), + const SizedBox(height: 16), + + Text('Lists', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + + ...app.lists.map((list) { + final isActive = list.id == app.activeListId; + return ListTile( + dense: true, + title: Text( + list.title, + style: TextStyle( + fontWeight: isActive ? FontWeight.bold : null, + color: isActive ? theme.colorScheme.primary : null, + ), + ), + trailing: _confirmDelete == list.id + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () { + app.deleteList(list.id); + setState(() => _confirmDelete = null); + }, + child: Text('Delete', style: TextStyle(color: theme.colorScheme.error)), + ), + TextButton( + onPressed: () => setState(() => _confirmDelete = null), + child: const Text('Cancel'), + ), + ], + ) + : IconButton( + onPressed: () => setState(() => _confirmDelete = list.id), + icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.onSurfaceVariant.withAlpha(77)), + ), + onTap: () { + app.selectList(list.id); + Navigator.pop(context); + }, + ); + }), + + if (_showNewList) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _newListController, + autofocus: true, + decoration: const InputDecoration(hintText: 'List name'), + onSubmitted: (_) => _createList(), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _createList, + child: const Text('Add'), + ), + ], + ), + ) + else + TextButton.icon( + onPressed: () => setState(() => _showNewList = true), + icon: const Icon(Icons.add, size: 18), + label: const Text('New list'), + ), + ], + ), + ); + } + + void _createList() { + final name = _newListController.text.trim(); + if (name.isEmpty) return; + context.read().createList(name); + _newListController.clear(); + setState(() => _showNewList = false); + Navigator.pop(context); + } +} diff --git a/apps/flutter/lib/widgets/new_task_bar.dart b/apps/flutter/lib/widgets/new_task_bar.dart new file mode 100644 index 0000000..3d86b07 --- /dev/null +++ b/apps/flutter/lib/widgets/new_task_bar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/app_provider.dart'; + +class NewTaskBar extends StatefulWidget { + const NewTaskBar({super.key}); + + @override + State createState() => _NewTaskBarState(); +} + +class _NewTaskBarState extends State { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _submit() { + final title = _controller.text.trim(); + if (title.isEmpty) return; + context.read().createTask(title); + _controller.clear(); + } + + @override + Widget build(BuildContext context) { + final app = context.watch(); + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: theme.dividerTheme.color ?? theme.dividerColor)), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + enabled: app.activeListId != null, + decoration: InputDecoration( + hintText: app.activeListId != null ? 'Add a task...' : 'Select a list first', + ), + onSubmitted: (_) => _submit(), + ), + ), + const SizedBox(width: 8), + FloatingActionButton.small( + onPressed: _submit, + child: const Icon(Icons.add), + ), + ], + ), + ); + } +} diff --git a/apps/flutter/lib/widgets/task_item.dart b/apps/flutter/lib/widgets/task_item.dart new file mode 100644 index 0000000..b0a7bf9 --- /dev/null +++ b/apps/flutter/lib/widgets/task_item.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../src/rust/api.dart' as api; +import '../providers/app_provider.dart'; + +class TaskItem extends StatelessWidget { + final api.BridgeTask task; + + const TaskItem({super.key, required this.task}); + + bool get _isCompleted => task.status == 'completed'; + + String _formatDate(String iso) { + final d = DateTime.tryParse(iso); + if (d == null) return iso; + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final taskDate = DateTime(d.year, d.month, d.day); + + if (taskDate == today) return 'Today'; + if (taskDate == today.add(const Duration(days: 1))) return 'Tomorrow'; + const months = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return '${months[d.month]} ${d.day}'; + } + + @override + Widget build(BuildContext context) { + final app = context.read(); + final theme = Theme.of(context); + + return Dismissible( + key: ValueKey(task.id), + direction: _isCompleted ? DismissDirection.startToEnd : DismissDirection.endToStart, + background: Container( + color: theme.colorScheme.primary, + alignment: _isCompleted ? Alignment.centerLeft : Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + _isCompleted ? 'Undo' : 'Complete', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + ), + ), + confirmDismiss: (_) async { + await app.toggleTask(task.id); + return false; // Don't remove widget — loadTasks handles the rebuild + }, + child: InkWell( + onTap: () => _showEditSheet(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Checkbox + GestureDetector( + onTap: () => app.toggleTask(task.id), + child: Container( + width: 22, + height: 22, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: _isCompleted + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant.withAlpha(102), + width: 2, + ), + color: _isCompleted ? theme.colorScheme.primary : null, + ), + child: _isCompleted + ? const Icon(Icons.check, size: 14, color: Colors.white) + : null, + ), + ), + const SizedBox(width: 12), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + task.title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: _isCompleted ? null : FontWeight.w500, + decoration: _isCompleted ? TextDecoration.lineThrough : null, + color: _isCompleted + ? theme.colorScheme.onSurfaceVariant.withAlpha(128) + : null, + ), + ), + if (task.description.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + task.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(102), + ), + ), + ), + if (task.dueDate != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: theme.dividerTheme.color ?? theme.dividerColor, + ), + ), + child: Text( + _formatDate(task.dueDate!), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withAlpha(128), + ), + ), + ), + ), + ], + ), + ), + + // Delete button + IconButton( + onPressed: () => app.deleteTask(task.id), + icon: Icon( + Icons.close, + size: 16, + color: theme.colorScheme.onSurfaceVariant.withAlpha(60), + ), + visualDensity: VisualDensity.compact, + ), + ], + ), + ), + ), + ); + } + + void _showEditSheet(BuildContext context) { + final app = context.read(); + final titleController = TextEditingController(text: task.title); + final descController = TextEditingController(text: task.description); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(ctx).viewInsets.bottom, + left: 16, + right: 16, + top: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 32, + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: Theme.of(ctx).dividerColor, + ), + ), + const SizedBox(height: 16), + TextField( + controller: titleController, + decoration: const InputDecoration(labelText: 'Title'), + enabled: !_isCompleted, + ), + const SizedBox(height: 12), + TextField( + controller: descController, + decoration: const InputDecoration(labelText: 'Description'), + maxLines: 3, + enabled: !_isCompleted, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _isCompleted + ? null + : () { + app.updateTask(task.id, titleController.text.trim(), descController.text); + Navigator.pop(ctx); + }, + child: const Text('Save'), + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } +} diff --git a/apps/flutter/pubspec.lock b/apps/flutter/pubspec.lock new file mode 100644 index 0000000..6f42c2f --- /dev/null +++ b/apps/flutter/pubspec.lock @@ -0,0 +1,490 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/apps/flutter/pubspec.yaml b/apps/flutter/pubspec.yaml new file mode 100644 index 0000000..057a9c3 --- /dev/null +++ b/apps/flutter/pubspec.yaml @@ -0,0 +1,26 @@ +name: bevy_tasks +description: A cross-platform task management app built with Flutter and Rust. +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_rust_bridge: ^2.0.0 + google_fonts: ^6.1.0 + provider: ^6.1.0 + path_provider: ^2.1.0 + file_picker: ^8.0.0 + intl: ^0.19.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true diff --git a/apps/flutter/rust/Cargo.toml b/apps/flutter/rust/Cargo.toml new file mode 100644 index 0000000..846a8fb --- /dev/null +++ b/apps/flutter/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bevy-tasks-flutter-bridge" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +flutter_rust_bridge = "=2.11.1" +bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["serde", "v4"] } +chrono = { version = "0.4", features = ["serde"] } +tokio = { version = "1", features = ["full"] } diff --git a/apps/flutter/rust/src/api.rs b/apps/flutter/rust/src/api.rs new file mode 100644 index 0000000..3de59e7 --- /dev/null +++ b/apps/flutter/rust/src/api.rs @@ -0,0 +1,323 @@ +use std::path::PathBuf; +use std::sync::Mutex; + +use bevy_tasks_core::{ + config::{AppConfig, WorkspaceConfig}, + models::{Task, TaskList, TaskStatus}, + repository::TaskRepository, + sync::{self, SyncMode, SyncResult as CoreSyncResult}, + webdav, +}; + +// ── Bridge types ───────────────────────────────────────────────────── + +/// Flat task struct for FFI transport. +pub struct BridgeTask { + pub id: String, + pub title: String, + pub description: String, + pub status: String, + pub due_date: Option, + pub created_at: String, + pub updated_at: String, + pub parent_id: Option, +} + +/// Flat list struct for FFI transport. +pub struct BridgeTaskList { + pub id: String, + pub title: String, + pub created_at: String, + pub updated_at: String, + pub group_by_due_date: bool, +} + +/// Flat workspace config for FFI transport. +pub struct BridgeWorkspace { + pub name: String, + pub path: String, + pub webdav_url: Option, + pub last_sync: Option, +} + +/// Flat app config for FFI transport. +pub struct BridgeConfig { + pub workspaces: Vec, + pub current_workspace: Option, +} + +/// Sync result for FFI transport. +pub struct BridgeSyncResult { + pub uploaded: u32, + pub downloaded: u32, + pub deleted_local: u32, + pub deleted_remote: u32, + pub conflicts: u32, + pub errors: Vec, +} + +// ── Conversion helpers ─────────────────────────────────────────────── + +fn task_to_bridge(t: &Task) -> BridgeTask { + BridgeTask { + id: t.id.to_string(), + title: t.title.clone(), + description: t.description.clone(), + status: match t.status { + TaskStatus::Backlog => "backlog".into(), + TaskStatus::Completed => "completed".into(), + }, + due_date: t.due_date.map(|d| d.to_rfc3339()), + created_at: t.created_at.to_rfc3339(), + updated_at: t.updated_at.to_rfc3339(), + parent_id: t.parent_id.map(|id| id.to_string()), + } +} + +fn list_to_bridge(l: &TaskList) -> BridgeTaskList { + BridgeTaskList { + id: l.id.to_string(), + title: l.title.clone(), + created_at: l.created_at.to_rfc3339(), + updated_at: l.updated_at.to_rfc3339(), + group_by_due_date: l.group_by_due_date, + } +} + +fn config_to_bridge(c: &AppConfig) -> BridgeConfig { + BridgeConfig { + workspaces: c + .workspaces + .iter() + .map(|(name, ws)| BridgeWorkspace { + name: name.clone(), + path: ws.path.to_string_lossy().into_owned(), + webdav_url: ws.webdav_url.clone(), + last_sync: ws.last_sync.map(|d| d.to_rfc3339()), + }) + .collect(), + current_workspace: c.current_workspace.clone(), + } +} + +// ── Global state ───────────────────────────────────────────────────── + +static STATE: Mutex> = Mutex::new(None); + +struct AppState { + config: AppConfig, + repo: Option, +} + +fn with_state(f: impl FnOnce(&mut AppState) -> Result) -> Result { + let mut guard = STATE.lock().map_err(|e| e.to_string())?; + let state = guard.as_mut().ok_or("App not initialized")?; + f(state) +} + +fn ensure_repo(state: &mut AppState) -> Result<(), String> { + if state.repo.is_some() { + return Ok(()); + } + let (_name, ws) = state.config.get_current_workspace().map_err(|e| e.to_string())?; + let repo = TaskRepository::new(ws.path.clone()).map_err(|e| e.to_string())?; + state.repo = Some(repo); + Ok(()) +} + +// ── Public API (flutter_rust_bridge will generate Dart bindings) ───── + +/// Initialize the bridge. Must be called once at app startup. +pub fn init_app() -> Result { + let config_path = AppConfig::get_config_path(); + let config = AppConfig::load_from_file(&config_path).unwrap_or_default(); + let bridge_config = config_to_bridge(&config); + let mut guard = STATE.lock().map_err(|e| e.to_string())?; + *guard = Some(AppState { config, repo: None }); + Ok(bridge_config) +} + +pub fn get_config() -> Result { + with_state(|s| Ok(config_to_bridge(&s.config))) +} + +pub fn add_workspace(name: String, path: String) -> Result<(), String> { + // Init workspace on disk + TaskRepository::init(PathBuf::from(&path)) + .map(|_| ()) + .map_err(|e| e.to_string())?; + + with_state(|s| { + let ws = WorkspaceConfig::new(PathBuf::from(&path)); + s.config.add_workspace(name.clone(), ws); + s.config.set_current_workspace(name).map_err(|e| e.to_string())?; + s.repo = None; + let config_path = AppConfig::get_config_path(); + s.config.save_to_file(&config_path).map_err(|e| e.to_string()) + }) +} + +pub fn set_current_workspace(name: String) -> Result<(), String> { + with_state(|s| { + s.config.set_current_workspace(name).map_err(|e| e.to_string())?; + s.repo = None; + let config_path = AppConfig::get_config_path(); + s.config.save_to_file(&config_path).map_err(|e| e.to_string()) + }) +} + +pub fn remove_workspace(name: String) -> Result<(), String> { + with_state(|s| { + s.config.remove_workspace(&name); + s.repo = None; + let config_path = AppConfig::get_config_path(); + s.config.save_to_file(&config_path).map_err(|e| e.to_string()) + }) +} + +pub fn get_lists() -> Result, String> { + with_state(|s| { + ensure_repo(s)?; + s.repo + .as_ref() + .unwrap() + .get_lists() + .map(|lists| lists.iter().map(|l| list_to_bridge(l)).collect()) + .map_err(|e| e.to_string()) + }) +} + +pub fn create_list(name: String) -> Result { + with_state(|s| { + ensure_repo(s)?; + s.repo + .as_mut() + .unwrap() + .create_list(name) + .map(|l| list_to_bridge(&l)) + .map_err(|e| e.to_string()) + }) +} + +pub fn delete_list(list_id: String) -> Result<(), String> { + with_state(|s| { + ensure_repo(s)?; + let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + s.repo.as_mut().unwrap().delete_list(id).map_err(|e| e.to_string()) + }) +} + +pub fn list_tasks(list_id: String) -> Result, String> { + with_state(|s| { + ensure_repo(s)?; + let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + s.repo + .as_ref() + .unwrap() + .list_tasks(id) + .map(|tasks| tasks.iter().map(|t| task_to_bridge(t)).collect()) + .map_err(|e| e.to_string()) + }) +} + +pub fn create_task(list_id: String, title: String) -> Result { + with_state(|s| { + ensure_repo(s)?; + let id = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let task = Task::new(title); + s.repo + .as_mut() + .unwrap() + .create_task(id, task) + .map(|t| task_to_bridge(&t)) + .map_err(|e| e.to_string()) + }) +} + +pub fn toggle_task(list_id: String, task_id: String) -> Result { + with_state(|s| { + ensure_repo(s)?; + let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; + let repo = s.repo.as_mut().unwrap(); + let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?; + match task.status { + TaskStatus::Backlog => task.complete(), + TaskStatus::Completed => task.uncomplete(), + } + repo.update_task(lid, task.clone()).map_err(|e| e.to_string())?; + Ok(task_to_bridge(&task)) + }) +} + +pub fn update_task(list_id: String, task_id: String, title: String, description: String) -> Result<(), String> { + with_state(|s| { + ensure_repo(s)?; + let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; + let repo = s.repo.as_mut().unwrap(); + let mut task = repo.get_task(lid, tid).map_err(|e| e.to_string())?; + task.title = title; + task.description = description; + repo.update_task(lid, task).map_err(|e| e.to_string()) + }) +} + +pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> { + with_state(|s| { + ensure_repo(s)?; + let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; + s.repo.as_mut().unwrap().delete_task(lid, tid).map_err(|e| e.to_string()) + }) +} + +pub fn reorder_task(list_id: String, task_id: String, new_position: usize) -> Result<(), String> { + with_state(|s| { + ensure_repo(s)?; + let lid = uuid::Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let tid = uuid::Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; + s.repo.as_mut().unwrap().reorder_task(lid, tid, new_position).map_err(|e| e.to_string()) + }) +} + +pub fn set_webdav_config(workspace_name: String, webdav_url: String) -> Result<(), String> { + with_state(|s| { + if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) { + ws.webdav_url = Some(webdav_url); + } + let config_path = AppConfig::get_config_path(); + s.config.save_to_file(&config_path).map_err(|e| e.to_string()) + }) +} + +pub fn store_webdav_credentials(domain: String, username: String, password: String) -> Result<(), String> { + webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string()) +} + +pub async fn sync_workspace_bridge( + workspace_path: String, + webdav_url: String, + username: String, + password: String, +) -> Result { + let result = sync::sync_workspace( + &PathBuf::from(workspace_path), + &webdav_url, + &username, + &password, + SyncMode::Full, + None, + ) + .await + .map_err(|e| e.to_string())?; + + Ok(BridgeSyncResult { + uploaded: result.uploaded, + downloaded: result.downloaded, + deleted_local: result.deleted_local, + deleted_remote: result.deleted_remote, + conflicts: result.conflicts, + errors: result.errors, + }) +} diff --git a/apps/flutter/rust/src/frb_generated.rs b/apps/flutter/rust/src/frb_generated.rs new file mode 100644 index 0000000..c7e9d68 --- /dev/null +++ b/apps/flutter/rust/src/frb_generated.rs @@ -0,0 +1,1205 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::needless_borrow +)] + +// Section: imports + +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1505334582; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__api__add_workspace_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "add_workspace", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_name = ::sse_decode(&mut deserializer); + let api_path = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::add_workspace(api_name, api_path)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__create_list_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_list", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::create_list(api_name)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__create_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + let api_title = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::create_task(api_list_id, api_title)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__delete_list_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "delete_list", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::delete_list(api_list_id)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__delete_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "delete_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + let api_task_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::delete_task(api_list_id, api_task_id)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__get_config_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_config", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::get_config()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__get_lists_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_lists", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::get_lists()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__init_app_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init_app", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::init_app()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__list_tasks_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "list_tasks", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::list_tasks(api_list_id)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__remove_workspace_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "remove_workspace", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::remove_workspace(api_name)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__reorder_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "reorder_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + let api_task_id = ::sse_decode(&mut deserializer); + let api_new_position = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = + crate::api::reorder_task(api_list_id, api_task_id, api_new_position)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__set_current_workspace_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_current_workspace", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::set_current_workspace(api_name)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__set_webdav_config_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_webdav_config", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_workspace_name = ::sse_decode(&mut deserializer); + let api_webdav_url = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = + crate::api::set_webdav_config(api_workspace_name, api_webdav_url)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__store_webdav_credentials_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "store_webdav_credentials", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_domain = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::store_webdav_credentials( + api_domain, + api_username, + api_password, + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__sync_workspace_bridge_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sync_workspace_bridge", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_workspace_path = ::sse_decode(&mut deserializer); + let api_webdav_url = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, String>( + (move || async move { + let output_ok = crate::api::sync_workspace_bridge( + api_workspace_path, + api_webdav_url, + api_username, + api_password, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__toggle_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "toggle_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + let api_task_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::toggle_task(api_list_id, api_task_id)?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__update_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "update_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_list_id = ::sse_decode(&mut deserializer); + let api_task_id = ::sse_decode(&mut deserializer); + let api_title = ::sse_decode(&mut deserializer); + let api_description = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::update_task( + api_list_id, + api_task_id, + api_title, + api_description, + )?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: dart2rust + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for crate::api::BridgeConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_workspaces = >::sse_decode(deserializer); + let mut var_currentWorkspace = >::sse_decode(deserializer); + return crate::api::BridgeConfig { + workspaces: var_workspaces, + current_workspace: var_currentWorkspace, + }; + } +} + +impl SseDecode for crate::api::BridgeSyncResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_uploaded = ::sse_decode(deserializer); + let mut var_downloaded = ::sse_decode(deserializer); + let mut var_deletedLocal = ::sse_decode(deserializer); + let mut var_deletedRemote = ::sse_decode(deserializer); + let mut var_conflicts = ::sse_decode(deserializer); + let mut var_errors = >::sse_decode(deserializer); + return crate::api::BridgeSyncResult { + uploaded: var_uploaded, + downloaded: var_downloaded, + deleted_local: var_deletedLocal, + deleted_remote: var_deletedRemote, + conflicts: var_conflicts, + errors: var_errors, + }; + } +} + +impl SseDecode for crate::api::BridgeTask { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_title = ::sse_decode(deserializer); + let mut var_description = ::sse_decode(deserializer); + let mut var_status = ::sse_decode(deserializer); + let mut var_dueDate = >::sse_decode(deserializer); + let mut var_createdAt = ::sse_decode(deserializer); + let mut var_updatedAt = ::sse_decode(deserializer); + let mut var_parentId = >::sse_decode(deserializer); + return crate::api::BridgeTask { + id: var_id, + title: var_title, + description: var_description, + status: var_status, + due_date: var_dueDate, + created_at: var_createdAt, + updated_at: var_updatedAt, + parent_id: var_parentId, + }; + } +} + +impl SseDecode for crate::api::BridgeTaskList { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_title = ::sse_decode(deserializer); + let mut var_createdAt = ::sse_decode(deserializer); + let mut var_updatedAt = ::sse_decode(deserializer); + let mut var_groupByDueDate = ::sse_decode(deserializer); + return crate::api::BridgeTaskList { + id: var_id, + title: var_title, + created_at: var_createdAt, + updated_at: var_updatedAt, + group_by_due_date: var_groupByDueDate, + }; + } +} + +impl SseDecode for crate::api::BridgeWorkspace { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_name = ::sse_decode(deserializer); + let mut var_path = ::sse_decode(deserializer); + let mut var_webdavUrl = >::sse_decode(deserializer); + let mut var_lastSync = >::sse_decode(deserializer); + return crate::api::BridgeWorkspace { + name: var_name, + path: var_path, + webdav_url: var_webdavUrl, + last_sync: var_lastSync, + }; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u32::().unwrap() + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__add_workspace_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__create_list_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__create_task_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__delete_list_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__delete_task_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__get_config_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__get_lists_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__init_app_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__list_tasks_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len), + 11 => wire__crate__api__reorder_task_impl(port, ptr, rust_vec_len, data_len), + 12 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__api__set_webdav_config_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__store_webdav_credentials_impl(port, ptr, rust_vec_len, data_len), + 15 => wire__crate__api__sync_workspace_bridge_impl(port, ptr, rust_vec_len, data_len), + 16 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len), + 17 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeConfig { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.workspaces.into_into_dart().into_dart(), + self.current_workspace.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeConfig {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::BridgeConfig { + fn into_into_dart(self) -> crate::api::BridgeConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeSyncResult { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.uploaded.into_into_dart().into_dart(), + self.downloaded.into_into_dart().into_dart(), + self.deleted_local.into_into_dart().into_dart(), + self.deleted_remote.into_into_dart().into_dart(), + self.conflicts.into_into_dart().into_dart(), + self.errors.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeSyncResult {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::BridgeSyncResult +{ + fn into_into_dart(self) -> crate::api::BridgeSyncResult { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeTask { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.title.into_into_dart().into_dart(), + self.description.into_into_dart().into_dart(), + self.status.into_into_dart().into_dart(), + self.due_date.into_into_dart().into_dart(), + self.created_at.into_into_dart().into_dart(), + self.updated_at.into_into_dart().into_dart(), + self.parent_id.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeTask {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::BridgeTask { + fn into_into_dart(self) -> crate::api::BridgeTask { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeTaskList { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.title.into_into_dart().into_dart(), + self.created_at.into_into_dart().into_dart(), + self.updated_at.into_into_dart().into_dart(), + self.group_by_due_date.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeTaskList {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::BridgeTaskList { + fn into_into_dart(self) -> crate::api::BridgeTaskList { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::BridgeWorkspace { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), + self.path.into_into_dart().into_dart(), + self.webdav_url.into_into_dart().into_dart(), + self.last_sync.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeWorkspace {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::BridgeWorkspace +{ + fn into_into_dart(self) -> crate::api::BridgeWorkspace { + self + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +impl SseEncode for crate::api::BridgeConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.workspaces, serializer); + >::sse_encode(self.current_workspace, serializer); + } +} + +impl SseEncode for crate::api::BridgeSyncResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.uploaded, serializer); + ::sse_encode(self.downloaded, serializer); + ::sse_encode(self.deleted_local, serializer); + ::sse_encode(self.deleted_remote, serializer); + ::sse_encode(self.conflicts, serializer); + >::sse_encode(self.errors, serializer); + } +} + +impl SseEncode for crate::api::BridgeTask { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.title, serializer); + ::sse_encode(self.description, serializer); + ::sse_encode(self.status, serializer); + >::sse_encode(self.due_date, serializer); + ::sse_encode(self.created_at, serializer); + ::sse_encode(self.updated_at, serializer); + >::sse_encode(self.parent_id, serializer); + } +} + +impl SseEncode for crate::api::BridgeTaskList { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.title, serializer); + ::sse_encode(self.created_at, serializer); + ::sse_encode(self.updated_at, serializer); + ::sse_encode(self.group_by_due_date, serializer); + } +} + +impl SseEncode for crate::api::BridgeWorkspace { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.name, serializer); + ::sse_encode(self.path, serializer); + >::sse_encode(self.webdav_url, serializer); + >::sse_encode(self.last_sync, serializer); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u32::(self).unwrap(); + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/apps/flutter/rust/src/lib.rs b/apps/flutter/rust/src/lib.rs new file mode 100644 index 0000000..cbb071f --- /dev/null +++ b/apps/flutter/rust/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +mod frb_generated; diff --git a/apps/flutter/test/widget_test.dart b/apps/flutter/test/widget_test.dart new file mode 100644 index 0000000..470d712 --- /dev/null +++ b/apps/flutter/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:bevy_tasks/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/apps/flutter/windows/.gitignore b/apps/flutter/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/apps/flutter/windows/.gitignore @@ -0,0 +1,17 @@ +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/ diff --git a/apps/flutter/windows/CMakeLists.txt b/apps/flutter/windows/CMakeLists.txt new file mode 100644 index 0000000..265edcd --- /dev/null +++ b/apps/flutter/windows/CMakeLists.txt @@ -0,0 +1,122 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(bevy_tasks LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "bevy_tasks") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Rust bridge library === +# Build the Rust bridge crate and install the DLL alongside the app. +set(RUST_BRIDGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust") +set(RUST_LIB_NAME "bevy_tasks_flutter_bridge") + +if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "Profile") + set(RUST_TARGET_DIR "${RUST_BRIDGE_DIR}/target/release") +else() + set(RUST_TARGET_DIR "${RUST_BRIDGE_DIR}/target/debug") +endif() + +# Add the Rust DLL to the bundled libraries list so it gets installed +list(APPEND PLUGIN_BUNDLED_LIBRARIES "${RUST_TARGET_DIR}/${RUST_LIB_NAME}.dll") + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# 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) diff --git a/apps/flutter/windows/flutter/CMakeLists.txt b/apps/flutter/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/apps/flutter/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# 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} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/apps/flutter/windows/flutter/generated_plugin_registrant.cc b/apps/flutter/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/apps/flutter/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/apps/flutter/windows/flutter/generated_plugin_registrant.h b/apps/flutter/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/apps/flutter/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/flutter/windows/flutter/generated_plugins.cmake b/apps/flutter/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/apps/flutter/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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) diff --git a/apps/flutter/windows/runner/CMakeLists.txt b/apps/flutter/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/apps/flutter/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +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) diff --git a/apps/flutter/windows/runner/Runner.rc b/apps/flutter/windows/runner/Runner.rc new file mode 100644 index 0000000..161fcdc --- /dev/null +++ b/apps/flutter/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.bevytasks" "\0" + VALUE "FileDescription", "bevy_tasks" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "bevy_tasks" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.bevytasks. All rights reserved." "\0" + VALUE "OriginalFilename", "bevy_tasks.exe" "\0" + VALUE "ProductName", "bevy_tasks" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/apps/flutter/windows/runner/bridge_generated.h b/apps/flutter/windows/runner/bridge_generated.h new file mode 100644 index 0000000..ad87ade --- /dev/null +++ b/apps/flutter/windows/runner/bridge_generated.h @@ -0,0 +1 @@ +// Nothing when using full_dep=false mode \ No newline at end of file diff --git a/apps/flutter/windows/runner/flutter_window.cpp b/apps/flutter/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/apps/flutter/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#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( + 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 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); +} diff --git a/apps/flutter/windows/runner/flutter_window.h b/apps/flutter/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/apps/flutter/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#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_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/apps/flutter/windows/runner/main.cpp b/apps/flutter/windows/runner/main.cpp new file mode 100644 index 0000000..c0613b9 --- /dev/null +++ b/apps/flutter/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#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 command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"bevy_tasks", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/apps/flutter/windows/runner/resource.h b/apps/flutter/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/apps/flutter/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{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 diff --git a/apps/flutter/windows/runner/resources/app_icon.ico b/apps/flutter/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/apps/flutter/windows/runner/resources/app_icon.ico differ diff --git a/apps/flutter/windows/runner/runner.exe.manifest b/apps/flutter/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/apps/flutter/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/apps/flutter/windows/runner/utils.cpp b/apps/flutter/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/apps/flutter/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +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 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::vector 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; +} diff --git a/apps/flutter/windows/runner/utils.h b/apps/flutter/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/apps/flutter/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// 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, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/apps/flutter/windows/runner/win32_window.cpp b/apps/flutter/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/apps/flutter/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#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(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( + 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(origin.x), + static_cast(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(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(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(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( + 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)); + } +} diff --git a/apps/flutter/windows/runner/win32_window.h b/apps/flutter/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/apps/flutter/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// 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_ diff --git a/apps/tauri/.gitignore b/apps/tauri/.gitignore new file mode 100644 index 0000000..8224285 --- /dev/null +++ b/apps/tauri/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ diff --git a/apps/tauri/index.html b/apps/tauri/index.html new file mode 100644 index 0000000..53b3460 --- /dev/null +++ b/apps/tauri/index.html @@ -0,0 +1,12 @@ + + + + + + Bevy Tasks + + +
+ + + diff --git a/apps/tauri/package-lock.json b/apps/tauri/package-lock.json new file mode 100644 index 0000000..794946d --- /dev/null +++ b/apps/tauri/package-lock.json @@ -0,0 +1,2275 @@ +{ + "name": "bevy-tasks", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bevy-tasks", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.0.0", + "@tauri-apps/cli": "^2.0.0", + "svelte": "^5.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", + "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.53.13", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.13.tgz", + "integrity": "sha512-9P6I/jGcQMzAMb76Uyd6L6RELAC7qt53GOSBLCke9lubh9iJjmjCo+EffRH4gOPnTB/x4RR2Tmt6s3o9ywQO3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/tauri/package.json b/apps/tauri/package.json new file mode 100644 index 0000000..c3290d5 --- /dev/null +++ b/apps/tauri/package.json @@ -0,0 +1,25 @@ +{ + "name": "bevy-tasks", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.0.0", + "@tauri-apps/cli": "^2.0.0", + "svelte": "^5.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0" + } +} diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml new file mode 100644 index 0000000..bc8a859 --- /dev/null +++ b/apps/tauri/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bevy-tasks-tauri" +version = "0.1.0" +edition = "2021" + +[lib] +name = "bevy_tasks_tauri_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" } +tokio = { version = "1", features = ["full"] } +uuid = { version = "1", features = ["serde", "v4"] } +chrono = { version = "0.4", features = ["serde"] } + +[package.metadata.tauri] + +[features] +custom-protocol = ["tauri/custom-protocol"] diff --git a/apps/tauri/src-tauri/build.rs b/apps/tauri/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/tauri/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/tauri/src-tauri/icons/128x128.png b/apps/tauri/src-tauri/icons/128x128.png new file mode 100644 index 0000000..ad54e8c Binary files /dev/null and b/apps/tauri/src-tauri/icons/128x128.png differ diff --git a/apps/tauri/src-tauri/icons/128x128@2x.png b/apps/tauri/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..b21c2fd Binary files /dev/null and b/apps/tauri/src-tauri/icons/128x128@2x.png differ diff --git a/apps/tauri/src-tauri/icons/32x32.png b/apps/tauri/src-tauri/icons/32x32.png new file mode 100644 index 0000000..d16eb52 Binary files /dev/null and b/apps/tauri/src-tauri/icons/32x32.png differ diff --git a/apps/tauri/src-tauri/icons/icon.icns b/apps/tauri/src-tauri/icons/icon.icns new file mode 100644 index 0000000..f839fe0 Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon.icns differ diff --git a/apps/tauri/src-tauri/icons/icon.ico b/apps/tauri/src-tauri/icons/icon.ico new file mode 100644 index 0000000..65f8268 Binary files /dev/null and b/apps/tauri/src-tauri/icons/icon.ico differ diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs new file mode 100644 index 0000000..9c1d3dc --- /dev/null +++ b/apps/tauri/src-tauri/src/lib.rs @@ -0,0 +1,376 @@ +use std::path::PathBuf; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use tauri::State; +use uuid::Uuid; + +use bevy_tasks_core::{ + config::{AppConfig, WorkspaceConfig}, + models::{Task, TaskList, TaskStatus}, + repository::TaskRepository, + sync::{self, SyncMode, SyncResult as CoreSyncResult}, + webdav, +}; + +/// Shared application state behind a mutex. +struct AppState { + config: AppConfig, + repo: Option, +} + +/// Serializable sync result for the frontend. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct SyncResult { + uploaded: u32, + downloaded: u32, + deleted_local: u32, + deleted_remote: u32, + conflicts: u32, + errors: Vec, +} + +impl From for SyncResult { + fn from(r: CoreSyncResult) -> Self { + Self { + uploaded: r.uploaded, + downloaded: r.downloaded, + deleted_local: r.deleted_local, + deleted_remote: r.deleted_remote, + conflicts: r.conflicts, + errors: r.errors, + } + } +} + +/// Helper: get or open a TaskRepository for the current workspace. +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(()) +} + +// ── Config commands ────────────────────────────────────────────────── + +#[tauri::command] +fn get_config(state: State<'_, Mutex>) -> Result { + let s = state.lock().unwrap(); + Ok(s.config.clone()) +} + +#[tauri::command] +fn save_config(state: State<'_, Mutex>) -> Result<(), String> { + let s = state.lock().unwrap(); + let path = AppConfig::get_config_path(); + s.config.save_to_file(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn add_workspace( + name: String, + path: String, + state: State<'_, Mutex>, +) -> 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())?; + // Reset repo so it reopens on next access + s.repo = None; + let config_path = AppConfig::get_config_path(); + s.config + .save_to_file(&config_path) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn set_current_workspace( + name: String, + state: State<'_, Mutex>, +) -> 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()) +} + +#[tauri::command] +fn remove_workspace( + name: String, + state: State<'_, Mutex>, +) -> 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()) +} + +// ── Workspace init ─────────────────────────────────────────────────── + +#[tauri::command] +fn init_workspace(path: String) -> Result<(), String> { + TaskRepository::init(PathBuf::from(path)) + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +// ── List commands ──────────────────────────────────────────────────── + +#[tauri::command] +fn get_lists(state: State<'_, Mutex>) -> Result, String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + s.repo + .as_ref() + .unwrap() + .get_lists() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn create_list( + name: String, + state: State<'_, Mutex>, +) -> Result { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + s.repo + .as_mut() + .unwrap() + .create_list(name) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn delete_list( + list_id: String, + state: State<'_, Mutex>, +) -> Result<(), 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_mut() + .unwrap() + .delete_list(id) + .map_err(|e| e.to_string()) +} + +// ── Task commands ──────────────────────────────────────────────────── + +#[tauri::command] +fn list_tasks( + list_id: String, + state: State<'_, Mutex>, +) -> Result, 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() + .list_tasks(id) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn create_task( + list_id: String, + title: String, + state: State<'_, Mutex>, +) -> Result { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + let task = Task::new(title); + s.repo + .as_mut() + .unwrap() + .create_task(id, task) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn update_task( + list_id: String, + task: Task, + state: State<'_, Mutex>, +) -> Result<(), 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_mut() + .unwrap() + .update_task(id, task) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn delete_task( + list_id: String, + task_id: String, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + 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()) +} + +#[tauri::command] +fn toggle_task( + list_id: String, + task_id: String, + state: State<'_, Mutex>, +) -> Result { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + 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) +} + +#[tauri::command] +fn reorder_task( + list_id: String, + task_id: String, + new_position: usize, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + 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) + .map_err(|e| e.to_string()) +} + +// ── Sync commands ──────────────────────────────────────────────────── + +#[tauri::command] +fn set_webdav_config( + workspace_name: String, + webdav_url: String, + state: State<'_, Mutex>, +) -> 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()) +} + +#[tauri::command] +fn store_credentials( + domain: String, + username: String, + password: String, +) -> Result<(), String> { + webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn test_webdav_connection( + url: String, + username: String, + password: String, +) -> Result<(), String> { + let client = bevy_tasks_core::webdav::WebDavClient::new(&url, &username, &password); + client + .test_connection() + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn sync_workspace( + workspace_path: String, + webdav_url: String, + username: String, + password: String, +) -> Result { + let result = sync::sync_workspace( + &PathBuf::from(workspace_path), + &webdav_url, + &username, + &password, + SyncMode::Full, + None, + ) + .await + .map_err(|e| e.to_string())?; + Ok(result.into()) +} + +// ── App entry ──────────────────────────────────────────────────────── + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + // Load or create config + let config_path = AppConfig::get_config_path(); + let config = AppConfig::load_from_file(&config_path).unwrap_or_default(); + + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .manage(Mutex::new(AppState { config, repo: None })) + .invoke_handler(tauri::generate_handler![ + get_config, + save_config, + add_workspace, + set_current_workspace, + remove_workspace, + init_workspace, + get_lists, + create_list, + delete_list, + list_tasks, + create_task, + update_task, + delete_task, + toggle_task, + reorder_task, + set_webdav_config, + store_credentials, + test_webdav_connection, + sync_workspace, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs new file mode 100644 index 0000000..27e0a06 --- /dev/null +++ b/apps/tauri/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + bevy_tasks_tauri_lib::run() +} diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json new file mode 100644 index 0000000..5dcaf75 --- /dev/null +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://raw.githubusercontent.com/nicegui-org/nicegui/v2/tauri-conf-schema.json", + "productName": "Bevy Tasks", + "version": "0.1.0", + "identifier": "com.bevytasks.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "withGlobalTauri": false, + "windows": [ + { + "title": "Bevy Tasks", + "width": 400, + "height": 700, + "minWidth": 320, + "minHeight": 500, + "resizable": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "plugins": {} +} diff --git a/apps/tauri/src/App.svelte b/apps/tauri/src/App.svelte new file mode 100644 index 0000000..ee168d4 --- /dev/null +++ b/apps/tauri/src/App.svelte @@ -0,0 +1,34 @@ + + +
+
+ {#if app.error} +
+ {app.error} + +
+ {/if} + + {#if app.screen === "setup"} + + {:else if app.screen === "tasks"} + + {:else if app.screen === "settings"} + + {/if} +
+
diff --git a/apps/tauri/src/app.css b/apps/tauri/src/app.css new file mode 100644 index 0000000..0a4ea4f --- /dev/null +++ b/apps/tauri/src/app.css @@ -0,0 +1,44 @@ +@import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-surface-light: #ffffff; + --color-surface-dark: #121212; + --color-card-light: #f9fafb; + --color-card-dark: #1e1e1e; + --color-text-light: #1f2937; + --color-text-dark: #e5e7eb; + --color-text-secondary-light: #6b7280; + --color-text-secondary-dark: #9ca3af; + --color-border-light: #e5e7eb; + --color-border-dark: #374151; + --color-danger: #ef4444; +} + +:root { + font-family: "Noto Sans", system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + overflow: hidden; + user-select: none; + -webkit-user-select: none; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 4px; +} +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 2px; +} +.dark ::-webkit-scrollbar-thumb { + background: #4b5563; +} diff --git a/apps/tauri/src/lib/components/BottomSheet.svelte b/apps/tauri/src/lib/components/BottomSheet.svelte new file mode 100644 index 0000000..69ab754 --- /dev/null +++ b/apps/tauri/src/lib/components/BottomSheet.svelte @@ -0,0 +1,37 @@ + + + + +
{ if (e.key === "Escape") onclose(); }} +>
+ + +
+ +
+
+
+ {@render children()} +
+
+ + diff --git a/apps/tauri/src/lib/components/NewTaskInput.svelte b/apps/tauri/src/lib/components/NewTaskInput.svelte new file mode 100644 index 0000000..fcd701b --- /dev/null +++ b/apps/tauri/src/lib/components/NewTaskInput.svelte @@ -0,0 +1,37 @@ + + +
+
{ e.preventDefault(); handleSubmit(); }} + class="flex items-center gap-2" + > + + +
+
diff --git a/apps/tauri/src/lib/components/TaskItem.svelte b/apps/tauri/src/lib/components/TaskItem.svelte new file mode 100644 index 0000000..3a25cd1 --- /dev/null +++ b/apps/tauri/src/lib/components/TaskItem.svelte @@ -0,0 +1,161 @@ + + +
+ + {#if swipeX !== 0} +
+ + {isCompleted ? "Undo" : "Complete"} + +
+ {/if} + + +
+ + + + + {#if editing} +
+ { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") editing = false; }} + /> +