From 94fdfe51ebcde06120335e4ba9df7d6da0abf355 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 13 May 2026 22:57:23 +0200 Subject: [PATCH] Implement local vault service with encryption key management and integrate it into the app. Add settings screen for data management and enhance home screen with new features. Update database connection for encryption support and modify repository to use the new database structure. Improve UI elements across the application for better user experience. --- lib/app.dart | 201 ++++++++++++++++- lib/data/app_database.dart | 16 +- lib/data/local_vault_service.dart | 56 +++++ lib/data/note_repository.dart | 4 +- lib/screens/home_screen.dart | 50 ++++- lib/screens/settings_screen.dart | 204 +++++++++++++++++ lib/screens/vault_access_screen.dart | 206 ++++++++++++++++++ lib/widgets/menu_drawer.dart | 2 +- lib/widgets/search_app_bar.dart | 139 ++++++------ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 66 +++++- pubspec.yaml | 7 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 16 files changed, 875 insertions(+), 87 deletions(-) create mode 100644 lib/data/local_vault_service.dart create mode 100644 lib/screens/settings_screen.dart create mode 100644 lib/screens/vault_access_screen.dart diff --git a/lib/app.dart b/lib/app.dart index 7bffd76..e7507c9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,8 +1,17 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:notas/data/app_database.dart'; +import 'package:notas/data/local_vault_service.dart'; +import 'package:notas/data/note_repository.dart'; import 'package:notas/platform/app_platform.dart'; -import 'package:notas/screens/home_screen.dart'; -import 'package:notas/theme/app_theme.dart'; import 'package:notas/platform/window_state.dart'; +import 'package:notas/screens/home_screen.dart'; +import 'package:notas/screens/vault_access_screen.dart'; +import 'package:notas/theme/app_theme.dart'; +import 'package:notas/widgets/app_title_bar.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:window_manager/window_manager.dart'; class NotesApp extends StatefulWidget { @@ -13,12 +22,22 @@ class NotesApp extends StatefulWidget { } class _NotesAppState extends State with WindowListener { + final LocalVaultService _vaultService = LocalVaultService.instance; + final GlobalKey _scaffoldMessengerKey = + GlobalKey(); + + AppDatabase? _database; + NoteRepository? _repository; + bool _isBootstrapping = true; + bool _isUnlocking = false; + @override void initState() { super.initState(); if (isDesktop) { windowManager.addListener(this); } + _bootstrapVault(); } @override @@ -26,9 +45,116 @@ class _NotesAppState extends State with WindowListener { if (isDesktop) { windowManager.removeListener(this); } + _database?.close(); super.dispose(); } + Future _bootstrapVault() async { + try { + final String? encryptionKey = await _vaultService.readEncryptionKey(); + + if (encryptionKey != null) { + await _openVault(encryptionKey); + } + } finally { + if (mounted) { + setState(() { + _isBootstrapping = false; + }); + } + } + } + + Future _openVault(String encryptionKey) async { + await _database?.close(); + + final AppDatabase database = AppDatabase(encryptionKey: encryptionKey); + if (!mounted) { + await database.close(); + return; + } + + setState(() { + _database = database; + _repository = NoteRepository(database: database); + }); + } + + Future _resetLocalVaultData() async { + final AppDatabase? database = _database; + + setState(() { + _repository = null; + _database = null; + _isBootstrapping = true; + }); + + await database?.close(); + + await _vaultService.clearEncryptionKey(); + + final Directory supportDir = await getApplicationSupportDirectory(); + final String dbPath = p.join(supportDir.path, 'notes.sqlite'); + final List filesToDelete = [ + dbPath, + '$dbPath-wal', + '$dbPath-shm', + '$dbPath-journal', + ]; + + for (final String filePath in filesToDelete) { + final File file = File(filePath); + if (await file.exists()) { + await file.delete(); + } + } + + if (!mounted) { + return; + } + + setState(() { + _isBootstrapping = false; + _isUnlocking = false; + }); + } + + Future _enterWithoutAccount() async { + if (_isUnlocking) { + return; + } + + setState(() { + _isUnlocking = true; + }); + + try { + final String encryptionKey = await _vaultService.createEncryptionKey(); + await _openVault(encryptionKey); + } catch (error) { + _scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar(content: Text('No se pudo crear el vault local: $error')), + ); + } finally { + if (mounted) { + setState(() { + _isUnlocking = false; + _isBootstrapping = false; + }); + } + } + } + + void _showAccountPlaceholder(String actionLabel) { + _scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text( + '$actionLabel todavía no está conectado con la API. Usa "Entrar sin cuenta" para empezar en local.', + ), + ), + ); + } + Future _saveWindowSize() async { if (await windowManager.isFullScreen()) { return; @@ -42,6 +168,45 @@ class _NotesAppState extends State with WindowListener { await WindowStateStore.instance.saveWindowSize(currentSize); } + Widget _buildLoadingScreen() { + return MaterialApp( + title: 'Mis Notas', + debugShowCheckedModeBanner: false, + theme: AppTheme.theme, + home: const Scaffold( + body: SafeArea( + child: Column( + children: [ + AppTitleBar(), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Preparando el vault local...'), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAppShell({required Widget home}) { + return MaterialApp( + title: 'Mis Notas', + debugShowCheckedModeBanner: false, + scaffoldMessengerKey: _scaffoldMessengerKey, + theme: AppTheme.theme, + home: home, + ); + } + @override void onWindowResize() { _saveWindowSize(); @@ -52,12 +217,34 @@ class _NotesAppState extends State with WindowListener { _saveWindowSize(); } + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Mis Notas', - debugShowCheckedModeBanner: false, - theme: AppTheme.theme, - home: const HomeScreen(), + if (_isBootstrapping) { + return _buildLoadingScreen(); + } + + final NoteRepository? repository = _repository; + + if (repository != null) { + return _buildAppShell( + home: HomeScreen( + repository: repository, + onDeleteAllData: _resetLocalVaultData, + ), + ); + } + + return _buildAppShell( + home: VaultAccessScreen( + isBusy: _isUnlocking, + onCreateAccountPressed: (String email, String password) async { + _showAccountPlaceholder('Crear cuenta'); + }, + onSignInPressed: (String email, String password) async { + _showAccountPlaceholder('Iniciar sesión'); + }, + onContinueWithoutAccount: _enterWithoutAccount, + ), ); } } \ No newline at end of file diff --git a/lib/data/app_database.dart b/lib/data/app_database.dart index dbeea60..0e6e7f6 100644 --- a/lib/data/app_database.dart +++ b/lib/data/app_database.dart @@ -19,7 +19,7 @@ class Notes extends Table { @DriftDatabase(tables: [Notes]) class AppDatabase extends _$AppDatabase { - AppDatabase() : super(_openConnection()); + AppDatabase({required String encryptionKey}) : super(_openConnection(encryptionKey)); @override int get schemaVersion => 1; @@ -94,11 +94,21 @@ class AppDatabase extends _$AppDatabase { } } -LazyDatabase _openConnection() { +LazyDatabase _openConnection(String encryptionKey) { return LazyDatabase(() async { final Directory supportDir = await getApplicationSupportDirectory(); final File file = File(p.join(supportDir.path, 'notes.sqlite')); - return NativeDatabase(file); + return NativeDatabase( + file, + setup: (database) { + final String escapedKey = encryptionKey.replaceAll("'", "''"); + + // sqlite3mc can emulate SQLCipher file format for compatibility. + database.execute("PRAGMA cipher = 'sqlcipher'"); + database.execute('PRAGMA legacy = 4'); + database.execute("PRAGMA key = '$escapedKey'"); + }, + ); }); } \ No newline at end of file diff --git a/lib/data/local_vault_service.dart b/lib/data/local_vault_service.dart new file mode 100644 index 0000000..e48e94e --- /dev/null +++ b/lib/data/local_vault_service.dart @@ -0,0 +1,56 @@ +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class LocalVaultService { + LocalVaultService._(); + + static final LocalVaultService instance = LocalVaultService._(); + + static const String _encryptionKeyStorageKey = + 'notes_local_encryption_key_v1'; + + final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + + String? _cachedEncryptionKey; + + Future readEncryptionKey() async { + final String? cachedKey = _cachedEncryptionKey; + if (cachedKey != null) { + return cachedKey; + } + + final String? storedKey = await _secureStorage.read( + key: _encryptionKeyStorageKey, + ); + + _cachedEncryptionKey = storedKey; + return storedKey; + } + + Future createEncryptionKey() async { + final String encryptionKey = _generateEncryptionKey(); + + await _secureStorage.write( + key: _encryptionKeyStorageKey, + value: encryptionKey, + ); + + _cachedEncryptionKey = encryptionKey; + return encryptionKey; + } + + Future clearEncryptionKey() async { + await _secureStorage.delete(key: _encryptionKeyStorageKey); + _cachedEncryptionKey = null; + } + + String _generateEncryptionKey() { + final Random random = Random.secure(); + final List bytes = List.generate(32, (_) => random.nextInt(256)); + + return bytes + .map((int byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(); + } +} \ No newline at end of file diff --git a/lib/data/note_repository.dart b/lib/data/note_repository.dart index 4ebec7b..e0a3a1d 100644 --- a/lib/data/note_repository.dart +++ b/lib/data/note_repository.dart @@ -2,9 +2,7 @@ import 'package:notas/data/app_database.dart'; import 'package:notas/models/note.dart'; class NoteRepository { - NoteRepository({AppDatabase? database}) : _database = database ?? _sharedDatabase; - - static final AppDatabase _sharedDatabase = AppDatabase(); + NoteRepository({required AppDatabase database}) : _database = database; final AppDatabase _database; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index e0d1aef..3e5c276 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -8,6 +8,7 @@ import 'package:notas/models/note.dart'; import 'package:notas/screens/note_editor_screen.dart'; import 'package:notas/widgets/app_title_bar.dart'; import 'package:notas/widgets/menu_drawer.dart'; +import 'package:notas/screens/settings_screen.dart'; import 'package:notas/widgets/note_card.dart'; import 'package:notas/widgets/search_app_bar.dart'; @@ -18,14 +19,20 @@ import 'package:notas/widgets/search_app_bar.dart'; // - Drag & drop reordering (updates `index` in the database) class HomeScreen extends StatefulWidget { - const HomeScreen({super.key}); + const HomeScreen({ + super.key, + required this.repository, + required this.onDeleteAllData, + }); + + final NoteRepository repository; + final Future Function() onDeleteAllData; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { - final NoteRepository _repository = NoteRepository(); List _notes = []; String _searchQuery = ''; bool _isLoading = true; @@ -39,7 +46,7 @@ class _HomeScreenState extends State { } Future _loadNotes() async { - final List storedNotes = await _repository.loadNotes(); + final List storedNotes = await widget.repository.loadNotes(); if (!mounted) { return; @@ -59,7 +66,7 @@ class _HomeScreenState extends State { } if (result is Note) { - final Note createdNote = await _repository.createNote(result); + final Note createdNote = await widget.repository.createNote(result); final List updatedNotes = _normalizeNotes([createdNote, ..._notes]); if (!mounted) { @@ -73,7 +80,7 @@ class _HomeScreenState extends State { } Future _deleteNote(Note note) async { - await _repository.deleteNote(note); + await widget.repository.deleteNote(note); final List updatedNotes = _normalizeNotes( _notes.where((Note item) => item.id != note.id).toList(), @@ -97,7 +104,7 @@ class _HomeScreenState extends State { final Note movedNote = updatedNotes.removeAt(oldIndex); updatedNotes.insert(newIndex, movedNote); - await _repository.moveNote(movedNote, newIndex); + await widget.repository.moveNote(movedNote, newIndex); if (!mounted) { return; @@ -123,7 +130,7 @@ class _HomeScreenState extends State { if (result is Note) { final int noteIndex = _notes.indexWhere((Note item) => item == note); if (noteIndex != -1) { - final Note savedNote = await _repository.updateNote(result); + final Note savedNote = await widget.repository.updateNote(result); final List updatedNotes = [..._notes]; updatedNotes[noteIndex] = savedNote; @@ -293,8 +300,20 @@ class _HomeScreenState extends State { ); return Scaffold( - body: SafeArea( - child: Column( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF191A1D), + Color(0xFF222326), + Color(0xFF101114), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: Column( children: [ const AppTitleBar(), SearchAppBar( @@ -353,6 +372,16 @@ class _HomeScreenState extends State { setState(() { _isMenuOpen = false; }); + + if (item == 'settings') { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => SettingsScreen( + onDeleteAllData: widget.onDeleteAllData, + ), + ), + ); + } }, ), ), @@ -361,7 +390,8 @@ class _HomeScreenState extends State { ), ), ], - ), + ), + ), ), floatingActionButton: FloatingActionButton( onPressed: _openNoteComposer, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..51c2c43 --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:notas/widgets/app_title_bar.dart'; +import 'package:notas/widgets/menu_drawer.dart'; +import 'package:notas/widgets/search_app_bar.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({ + super.key, + required this.onDeleteAllData, + }); + + final Future Function() onDeleteAllData; + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + bool _isBusy = false; + bool _isMenuOpen = false; + final GlobalKey _headerKey = GlobalKey(); + double _menuTopInset = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateMenuTopInset(); + }); + } + + void _updateMenuTopInset() { + final BuildContext? headerContext = _headerKey.currentContext; + if (headerContext == null) { + return; + } + + final RenderObject? renderObject = headerContext.findRenderObject(); + if (renderObject is! RenderBox) { + return; + } + + final double newInset = renderObject.size.height; + if ((newInset - _menuTopInset).abs() < 0.5) { + return; + } + + setState(() { + _menuTopInset = newInset; + }); + } + + Future _confirmAndDeleteAll() async { + final bool? confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Borrar todos los datos'), + content: const Text('¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. No se podrá recuperar.'), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancelar')), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Borrar', style: TextStyle(color: Colors.red))), + ], + ), + ); + + if (confirmed != true) return; + + setState(() { + _isBusy = true; + }); + + try { + await widget.onDeleteAllData(); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Todos los datos locales han sido eliminados.')), + ); + + Navigator.of(context).pop(); + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error al borrar los datos: $error')), + ); + } finally { + if (!mounted) return; + setState(() { + _isBusy = false; + }); + } + } + + void _handleMenuItemTapped(String item) { + setState(() { + _isMenuOpen = false; + }); + + if (item == 'all_notes') { + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateMenuTopInset(); + }); + + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF191A1D), + Color(0xFF222326), + Color(0xFF101114), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: Stack( + children: [ + Column( + children: [ + Column( + key: _headerKey, + mainAxisSize: MainAxisSize.min, + children: [ + const AppTitleBar(), + SearchAppBar( + onMenuPressed: () { + setState(() { + _isMenuOpen = !_isMenuOpen; + }); + }, + showSearch: false, + titleText: 'Configuración', + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), + onPressed: _isBusy ? null : _confirmAndDeleteAll, + icon: _isBusy ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.delete_forever), + label: const Text('Borrar todos los datos'), + ), + const SizedBox(height: 16), + const Text('Esto cerrará el vault actual y eliminará la base de datos local junto con la clave de cifrado.'), + ], + ), + ), + ), + ], + ), + Positioned.fill( + top: _menuTopInset, + child: IgnorePointer( + ignoring: !_isMenuOpen, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _isMenuOpen ? 0.5 : 0.0, + curve: Curves.easeOutCubic, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + _isMenuOpen = false; + }); + }, + child: Container(color: Colors.black), + ), + ), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + left: _isMenuOpen ? 0 : -280, + top: _menuTopInset, + bottom: 0, + width: 280, + child: Material( + color: const Color.fromRGBO(24, 25, 26, 1), + elevation: 8, + child: MenuDrawer(onMenuItemTapped: _handleMenuItemTapped), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/vault_access_screen.dart b/lib/screens/vault_access_screen.dart new file mode 100644 index 0000000..fd37862 --- /dev/null +++ b/lib/screens/vault_access_screen.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:notas/widgets/app_title_bar.dart'; + +class VaultAccessScreen extends StatefulWidget { + const VaultAccessScreen({ + super.key, + required this.isBusy, + required this.onCreateAccountPressed, + required this.onSignInPressed, + required this.onContinueWithoutAccount, + }); + + final bool isBusy; + final Future Function(String email, String password) onCreateAccountPressed; + final Future Function(String email, String password) onSignInPressed; + final Future Function() onContinueWithoutAccount; + + @override + State createState() => _VaultAccessScreenState(); +} + +class _VaultAccessScreenState extends State { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _handleCreateAccount() async { + await widget.onCreateAccountPressed( + _emailController.text.trim(), + _passwordController.text, + ); + } + + Future _handleSignIn() async { + await widget.onSignInPressed( + _emailController.text.trim(), + _passwordController.text, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF191A1D), + Color(0xFF222326), + Color(0xFF101114), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: Column( + children: [ + const AppTitleBar(), + Expanded( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 460), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFF1D1E20), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.35), + blurRadius: 30, + offset: const Offset(0, 18), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon( + Icons.lock_outline, + color: Colors.amber, + size: 44, + ), + const SizedBox(height: 16), + const Text( + 'Mis Notas', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 30, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + Text( + 'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.72), + height: 1.4, + ), + ), + const SizedBox(height: 28), + TextField( + controller: _emailController, + enabled: !widget.isBusy, + keyboardType: TextInputType.emailAddress, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Email', + labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.05), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Colors.amber, width: 1.2), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + enabled: !widget.isBusy, + obscureText: true, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Contraseña', + labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.05), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Colors.amber, width: 1.2), + ), + ), + ), + const SizedBox(height: 22), + FilledButton( + onPressed: widget.isBusy ? null : _handleCreateAccount, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: widget.isBusy + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Crear cuenta'), + ), + const SizedBox(height: 10), + OutlinedButton( + onPressed: widget.isBusy ? null : _handleSignIn, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + side: const BorderSide(color: Colors.white24), + foregroundColor: Colors.white, + ), + child: const Text('Iniciar sesión'), + ), + const SizedBox(height: 18), + TextButton( + onPressed: widget.isBusy ? null : widget.onContinueWithoutAccount, + child: const Text('Entrar sin cuenta'), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/menu_drawer.dart b/lib/widgets/menu_drawer.dart index 6ce4e63..7e070a3 100644 --- a/lib/widgets/menu_drawer.dart +++ b/lib/widgets/menu_drawer.dart @@ -12,7 +12,7 @@ class MenuDrawer extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: const BoxDecoration( - color: Color.fromRGBO(31, 32, 33, 1), + color: Color.fromARGB(255, 30, 31, 35), border: Border( right: BorderSide(color: Colors.white12, width: 0.5), ), diff --git a/lib/widgets/search_app_bar.dart b/lib/widgets/search_app_bar.dart index 0e646aa..e70b19f 100644 --- a/lib/widgets/search_app_bar.dart +++ b/lib/widgets/search_app_bar.dart @@ -6,11 +6,15 @@ class SearchAppBar extends StatefulWidget { this.onMenuPressed, this.onSearchChanged, this.searchHint = 'Buscar notas...', + this.showSearch = true, + this.titleText, }); final VoidCallback? onMenuPressed; final ValueChanged? onSearchChanged; final String searchHint; + final bool showSearch; + final String? titleText; @override State createState() => _SearchAppBarState(); @@ -35,7 +39,7 @@ class _SearchAppBarState extends State { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: const Color.fromRGBO(31, 32, 33, 1), + color: Colors.transparent, border: Border( bottom: BorderSide( color: Colors.white.withValues(alpha: 0.12), @@ -55,75 +59,86 @@ class _SearchAppBarState extends State { constraints: const BoxConstraints(minWidth: 40, minHeight: 40), ), const SizedBox(width: 8), - // Search input (centered with max width) Expanded( - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: TextField( - controller: _searchController, - onChanged: widget.onSearchChanged, - style: const TextStyle(color: Colors.white, fontSize: 13), - cursorColor: Colors.white70, - decoration: InputDecoration( - hintText: widget.searchHint, - hintStyle: TextStyle( - color: Colors.white.withValues(alpha: 0.5), - ), - prefixIcon: const Icon( - Icons.search, - color: Colors.white70, - size: 18, - ), - suffixIcon: _searchController.text.isNotEmpty - ? IconButton( - icon: const Icon( - Icons.clear, - color: Colors.white70, - size: 18, + child: widget.showSearch + ? Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: TextField( + controller: _searchController, + onChanged: widget.onSearchChanged, + style: const TextStyle(color: Colors.white, fontSize: 13), + cursorColor: Colors.white70, + decoration: InputDecoration( + hintText: widget.searchHint, + hintStyle: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + ), + prefixIcon: const Icon( + Icons.search, + color: Colors.white70, + size: 18, + ), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon( + Icons.clear, + color: Colors.white70, + size: 18, + ), + onPressed: () { + _searchController.clear(); + widget.onSearchChanged?.call(''); + }, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + width: 0.5, ), - onPressed: () { - _searchController.clear(); - widget.onSearchChanged?.call(''); - }, - constraints: const BoxConstraints( - minWidth: 36, - minHeight: 36, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.white.withValues(alpha: 0.2), + width: 0.5, ), - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: Colors.white.withValues(alpha: 0.2), - width: 0.5, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.white.withValues(alpha: 0.4), + width: 0.5, + ), + ), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.05), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + isDense: true, + ), ), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: Colors.white.withValues(alpha: 0.2), - width: 0.5, + ) + : Align( + alignment: Alignment.centerLeft, + child: Text( + widget.titleText ?? '', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, ), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: Colors.white.withValues(alpha: 0.4), - width: 0.5, - ), - ), - filled: true, - fillColor: Colors.white.withValues(alpha: 0.05), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - isDense: true, ), - ), - ), - ), ), ], ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c8f3dcc..1bf5c50 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4ce8cfc..ae20184 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux screen_retriever_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 869e2c3..f658d77 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_darwin import screen_retriever_macos import shared_preferences_foundation import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 58f9b78..e48d488 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -238,6 +246,54 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8" + url: "https://pub.dev" + source: hosted + version: "4.2.1" flutter_staggered_grid_view: dependency: "direct main" description: @@ -678,7 +734,7 @@ packages: source: hosted version: "1.10.2" sqlite3: - dependency: transitive + dependency: "direct main" description: name: sqlite3 sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" @@ -797,6 +853,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9 + url: "https://pub.dev" + source: hosted + version: "6.2.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ac7908c..b3c0e0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: shared_preferences: ^2.3.2 window_manager: ^0.5.1 intl: ^0.19.0 + flutter_secure_storage: ^10.2.0 + sqlite3: ^3.3.1 dev_dependencies: flutter_test: @@ -66,6 +68,11 @@ flutter: # the material Icons class. uses-material-design: true +hooks: + user_defines: + sqlite3: + source: sqlite3mc + # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c6fe39a..e3176cf 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e1de489..8827481 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows screen_retriever_windows window_manager )