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 )