Compare commits

..

14 Commits

Author SHA1 Message Date
Marcos b201da0552 refactor: Improve code formatting and replace print with debugPrint for error logging 2026-07-02 23:47:52 +02:00
Marcos 9b6d92c372 refactor: Enhance sync status handling and improve UI feedback in NotesApp 2026-07-02 20:59:22 +02:00
Marcos f662e59547 refactor: Enhance category handling in note editor and card components 2026-07-02 12:52:41 +02:00
Marcos 78dddd571a refactor: Remove category handling from note editor and simplify note card options 2026-07-02 11:20:28 +02:00
Marcos c2db704155 refactor: Improve code readability by formatting and simplifying widget structures 2026-07-02 10:52:33 +02:00
Marcos b00da9ae88 update 2026-07-01 18:47:15 +02:00
Marcos 972006c29f refactor: Update color handling to use ARGB format and improve theme consistency 2026-07-01 10:41:59 +02:00
Marcos 82515960f6 Reestructuracion de la app 2026-06-29 20:32:47 +02:00
Marcos 710be805ee feat: integrate Flutter Quill for rich text editing and update note handling
- Added Flutter Quill package for rich text editing capabilities.
- Refactored note body handling to support Quill's Document format.
- Updated note editor screen to use QuillEditor and QuillController.
- Enhanced note search functionality to convert note body to plain text.
- Modified note card display to show plain text from note body.
- Updated localization support for Quill in the app.
- Registered necessary plugins for URL launching and file selection on all platforms.
- Updated app icons and other assets for consistency across platforms.
- Updated pubspec.yaml and pubspec.lock to include new dependencies and versions.
2026-05-24 17:52:11 +02:00
Marcos d849c25ed6 refactor: Simplify AppPalette color definitions and gradients for improved readability 2026-05-23 18:28:43 +02:00
Marcos 7c1d4e5fd8 feat: Update application icons for various resolutions and platforms 2026-05-23 17:51:43 +02:00
Marcos 1dede9eb78 Refactor theme management: Replace AppColors with AppPalette
- Removed AppColors class and migrated all references to AppPalette.
- Updated VaultAccessScreen, MenuDrawer, NoteCard, SearchAppBar, and other widgets to use AppPalette for color management.
- Introduced AppPalette to handle light and dark themes with appropriate color schemes.
- Adjusted theme application in AppTheme to utilize AppPalette extensions.
- Updated tests to reflect changes in theme structure and color references.
2026-05-23 13:55:40 +02:00
Marcos 29881183ed feat: Update dialog styles and background colors for consistency across screens 2026-05-23 11:27:26 +02:00
Marcos f4bb5104e2 Refactor theme colors and styles across the application
- Introduced AppColors class to centralize color definitions for better maintainability and consistency.
- Updated various screens (Settings, Vault Access, Note Card, etc.) to use AppColors for styling instead of hardcoded colors.
- Enhanced UI elements with improved color contrast and accessibility.
- Replaced gradient backgrounds with defined color schemes for a cohesive look.
- Refactored button styles and text colors to align with the new theme structure.
2026-05-23 09:38:26 +02:00
40 changed files with 2968 additions and 2336 deletions
+2
View File
@@ -43,3 +43,5 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
problems-report.html
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+257 -42
View File
@@ -3,6 +3,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:notas/data/app_database.dart'; import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart'; import 'package:notas/data/api_client.dart';
import 'package:notas/data/local_vault_service.dart'; import 'package:notas/data/local_vault_service.dart';
@@ -14,6 +16,7 @@ import 'package:notas/screens/biometric_gate_screen.dart';
import 'package:notas/screens/home_screen.dart'; import 'package:notas/screens/home_screen.dart';
import 'package:notas/screens/settings_screen.dart'; import 'package:notas/screens/settings_screen.dart';
import 'package:notas/screens/vault_access_screen.dart'; import 'package:notas/screens/vault_access_screen.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/theme/app_theme.dart'; import 'package:notas/theme/app_theme.dart';
import 'package:notas/widgets/sync_status.dart'; import 'package:notas/widgets/sync_status.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -43,6 +46,7 @@ class _NotesAppState extends State<NotesApp>
static const Duration _syncInterval = Duration(minutes: 5); static const Duration _syncInterval = Duration(minutes: 5);
static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350); static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350);
static const String _themeSeedColorKey = 'theme_seed_color_v1'; static const String _themeSeedColorKey = 'theme_seed_color_v1';
static const String _themeModeKey = 'theme_mode_v1';
final LocalVaultService _vaultService = LocalVaultService.instance; final LocalVaultService _vaultService = LocalVaultService.instance;
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
@@ -69,6 +73,138 @@ class _NotesAppState extends State<NotesApp>
int _syncOperationId = 0; int _syncOperationId = 0;
int _homeRefreshToken = 0; int _homeRefreshToken = 0;
Color _themeSeedColor = Colors.amber; Color _themeSeedColor = Colors.amber;
ThemeMode _themeMode = ThemeMode.system;
// Cached ThemeData for light and dark variants.
ThemeData? _lightTheme;
ThemeData? _darkTheme;
bool _isSyncBannerVisible() {
switch (_syncStatus) {
case SyncStatus.preparing:
case SyncStatus.encrypting:
case SyncStatus.uploading:
case SyncStatus.waitingResponse:
case SyncStatus.decrypting:
case SyncStatus.syncing:
case SyncStatus.synced:
case SyncStatus.error:
return true;
case SyncStatus.idle:
return false;
}
}
Widget _buildSyncBanner(BuildContext context) {
if (!_isSyncBannerVisible()) {
return const SizedBox.shrink();
}
final AppPalette palette = _activePalette();
final String message =
_syncErrorMessage ?? _syncDetailMessage ?? 'Sincronizando...';
final double? progress = _syncProgress;
final IconData icon;
final Color accentColor;
switch (_syncStatus) {
case SyncStatus.preparing:
case SyncStatus.encrypting:
case SyncStatus.uploading:
case SyncStatus.waitingResponse:
case SyncStatus.decrypting:
case SyncStatus.syncing:
icon = Icons.cloud_sync_outlined;
accentColor = palette.textSecondary;
break;
case SyncStatus.synced:
icon = Icons.check_circle;
accentColor = palette.success;
break;
case SyncStatus.error:
icon = Icons.error;
accentColor = palette.destructiveAccent;
break;
case SyncStatus.idle:
icon = Icons.cloud_sync_outlined;
accentColor = palette.textSecondary;
break;
}
return Material(
color: palette.surfaceElevated,
elevation: 12,
child: SafeArea(
top: false,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: accentColor.withValues(alpha: 0.45)),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(icon, color: accentColor, size: 18),
const SizedBox(width: 10),
Expanded(
child: Text(
message,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
if (_syncStatus == SyncStatus.preparing ||
_syncStatus == SyncStatus.encrypting ||
_syncStatus == SyncStatus.uploading ||
_syncStatus == SyncStatus.waitingResponse ||
_syncStatus == SyncStatus.decrypting ||
_syncStatus == SyncStatus.syncing) ...[
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
minHeight: 4,
value: progress,
backgroundColor: palette.borderMuted,
),
),
],
],
),
),
),
);
}
Brightness _effectiveBrightness() {
switch (_themeMode) {
case ThemeMode.dark:
return Brightness.dark;
case ThemeMode.light:
return Brightness.light;
case ThemeMode.system:
return WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
}
AppPalette _activePalette() {
return AppPalette.fromBrightness(
_effectiveBrightness(),
seedColor: _themeSeedColor,
);
}
@override @override
void initState() { void initState() {
@@ -79,6 +215,7 @@ class _NotesAppState extends State<NotesApp>
windowManager.setPreventClose(true); windowManager.setPreventClose(true);
} }
_loadThemeSeedColor(); _loadThemeSeedColor();
_loadThemeMode();
_bootstrapVault(); _bootstrapVault();
} }
@@ -105,13 +242,13 @@ class _NotesAppState extends State<NotesApp>
setState(() { setState(() {
_themeSeedColor = Color(storedColorValue); _themeSeedColor = Color(storedColorValue);
_themeData = AppTheme.theme(seedColor: _themeSeedColor); _updateThemeData();
}); });
} }
Future<void> _setThemeSeedColor(Color color) async { Future<void> _setThemeSeedColor(Color color) async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_themeSeedColorKey, color.value); await prefs.setInt(_themeSeedColorKey, color.toARGB32());
if (!mounted) { if (!mounted) {
return; return;
@@ -119,15 +256,54 @@ class _NotesAppState extends State<NotesApp>
setState(() { setState(() {
_themeSeedColor = color; _themeSeedColor = color;
_themeData = AppTheme.theme(seedColor: _themeSeedColor); _updateThemeData();
}); });
} }
// Cached ThemeData to avoid recomputing on every build. Future<void> _loadThemeMode() async {
ThemeData? _themeData; final SharedPreferences prefs = await SharedPreferences.getInstance();
final int? stored = prefs.getInt(_themeModeKey);
if (!mounted) return;
ThemeData get _theme => setState(() {
_themeData ??= AppTheme.theme(seedColor: _themeSeedColor); if (stored == null) {
_themeMode = ThemeMode.system;
} else if (stored == 1) {
_themeMode = ThemeMode.light;
} else if (stored == 2) {
_themeMode = ThemeMode.dark;
} else {
_themeMode = ThemeMode.system;
}
_updateThemeData();
});
}
Future<void> _setThemeMode(ThemeMode mode) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int stored = mode == ThemeMode.system
? 0
: (mode == ThemeMode.light ? 1 : 2);
await prefs.setInt(_themeModeKey, stored);
if (!mounted) return;
setState(() {
_themeMode = mode;
});
}
void _updateThemeData() {
_lightTheme = AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.light,
);
_darkTheme = AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.dark,
);
// Updated light/dark themes regenerated
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
@@ -418,32 +594,38 @@ class _NotesAppState extends State<NotesApp>
if (available) { if (available) {
bool activated = await _vaultService.enableBiometricProtection(); bool activated = await _vaultService.enableBiometricProtection();
while (!activated) { while (!activated) {
// Ask the user to retry or skip // Ask the user to retry or skip - pass currentContext directly in builder
final BuildContext? dialogCtx = _navigatorKey.currentContext; final NavigatorState? navigator = _navigatorKey.currentState;
if (dialogCtx == null) {
break;
}
final NavigatorState navigator = Navigator.of(dialogCtx); if (navigator == null) break;
if (!mounted) return;
final bool? retry = await showDialog<bool>( final bool? retry = await showDialog<bool>(
context: dialogCtx, context: context,
builder: (BuildContext context) => AlertDialog( builder: (BuildContext dialogContext) {
title: const Text('No se pudo activar la biometría'), final AppPalette palette = _activePalette();
content: const Text( return AlertDialog(
'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?', backgroundColor: palette.cardBackground,
), shape: RoundedRectangleBorder(
actions: [ borderRadius: BorderRadius.circular(12),
TextButton( side: BorderSide(color: palette.border),
onPressed: () => navigator.pop(false),
child: const Text('Entrar sin huella'),
), ),
FilledButton( title: const Text('No se pudo activar la biometría'),
onPressed: () => navigator.pop(true), content: const Text(
child: const Text('Reintentar'), 'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?',
), ),
], actions: [
), TextButton(
onPressed: () => navigator.pop(false),
child: const Text('Entrar sin huella'),
),
FilledButton(
onPressed: () => navigator.pop(true),
child: const Text('Reintentar'),
),
],
);
},
); );
if (retry != true) { if (retry != true) {
@@ -766,8 +948,8 @@ class _NotesAppState extends State<NotesApp>
_homeRefreshToken += 1; _homeRefreshToken += 1;
}); });
// Reset to idle after 3 seconds // Keep the completion state visible briefly so it can be read.
Future<void>.delayed(const Duration(seconds: 3), () { Future<void>.delayed(const Duration(seconds: 1), () {
if (mounted && syncOperationId == _syncOperationId) { if (mounted && syncOperationId == _syncOperationId) {
setState(() { setState(() {
_syncStatus = SyncStatus.idle; _syncStatus = SyncStatus.idle;
@@ -819,6 +1001,7 @@ class _NotesAppState extends State<NotesApp>
} }
Widget _buildMainShell(NoteRepository repository) { Widget _buildMainShell(NoteRepository repository) {
final AppPalette palette = _activePalette();
final Widget activeScreen = _currentSection == _AppSection.home final Widget activeScreen = _currentSection == _AppSection.home
? HomeScreen( ? HomeScreen(
key: const ValueKey<String>('home-screen'), key: const ValueKey<String>('home-screen'),
@@ -839,6 +1022,8 @@ class _NotesAppState extends State<NotesApp>
onForceSync: () => _performSync(forceFull: true), onForceSync: () => _performSync(forceFull: true),
currentSeedColor: _themeSeedColor, currentSeedColor: _themeSeedColor,
onThemeColorSelected: _setThemeSeedColor, onThemeColorSelected: _setThemeSeedColor,
currentThemeMode: _themeMode,
onThemeModeSelected: _setThemeMode,
); );
return Shortcuts( return Shortcuts(
@@ -858,17 +1043,7 @@ class _NotesAppState extends State<NotesApp>
autofocus: true, autofocus: true,
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(gradient: palette.backdropGradient),
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
@@ -896,6 +1071,28 @@ class _NotesAppState extends State<NotesApp>
child: activeScreen, child: activeScreen,
), ),
), ),
AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder:
(Widget child, Animation<double> animation) {
final Animation<Offset> slideAnimation =
Tween<Offset>(
begin: const Offset(0.0, 0.35),
end: Offset.zero,
).animate(animation);
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: slideAnimation,
child: child,
),
);
},
child: _buildSyncBanner(context),
),
], ],
), ),
), ),
@@ -1028,7 +1225,25 @@ class _NotesAppState extends State<NotesApp>
title: 'Mis Notas', title: 'Mis Notas',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
scaffoldMessengerKey: _scaffoldMessengerKey, scaffoldMessengerKey: _scaffoldMessengerKey,
theme: _theme, localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
],
theme:
_lightTheme ??
AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.light,
),
darkTheme:
_darkTheme ??
AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.dark,
),
themeMode: _themeMode,
home: homeWidget, home: homeWidget,
); );
} }
+38
View File
@@ -0,0 +1,38 @@
import 'dart:convert';
import 'package:flutter_quill/flutter_quill.dart';
Document noteBodyToDocument(String storedBody) {
final String trimmedBody = storedBody.trimLeft();
if (trimmedBody.startsWith('[')) {
try {
final dynamic decoded = jsonDecode(storedBody);
if (decoded is List) {
return Document.fromJson(decoded);
}
} catch (_) {
// Fall back to plain text for legacy notes or malformed JSON.
}
}
if (storedBody.isEmpty) {
return Document();
}
final String plainText = storedBody.endsWith('\n')
? storedBody
: '$storedBody\n';
return Document.fromJson(<dynamic>[
<String, String>{'insert': plainText},
]);
}
String noteBodyToPlainText(String storedBody) {
return noteBodyToDocument(storedBody).toPlainText();
}
String noteDocumentToStorageJson(Document document) {
return jsonEncode(document.toDelta().toJson());
}
+5 -1
View File
@@ -56,6 +56,10 @@ class NoteRepository {
return categories; return categories;
} }
Future<DateTime?> getLastSyncAt() async {
return _authApi.getLastSyncAt();
}
Future<void> createCategory(Category category) async { Future<void> createCategory(Category category) async {
debugPrint('createCategory called with: ${category.name}'); debugPrint('createCategory called with: ${category.name}');
@@ -672,7 +676,7 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
masterKey, masterKey,
); );
} catch (e) { } catch (e) {
print('Failed to decrypt note ${note['id']}: $e'); debugPrint('Failed to decrypt note ${note['id']}: $e');
} }
} else { } else {
decryptedTitle = ''; decryptedTitle = '';
+19 -20
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class BiometricChoiceScreen extends StatelessWidget { class BiometricChoiceScreen extends StatelessWidget {
const BiometricChoiceScreen({ const BiometricChoiceScreen({
@@ -14,19 +15,11 @@ class BiometricChoiceScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(gradient: palette.backdropGradient),
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
@@ -39,12 +32,12 @@ class BiometricChoiceScreen extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1D1E20), color: palette.cardBackground,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)), border: Border.all(color: palette.border),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.35), color: palette.shadowSoft,
blurRadius: 30, blurRadius: 30,
offset: const Offset(0, 18), offset: const Offset(0, 18),
), ),
@@ -74,7 +67,7 @@ class BiometricChoiceScreen extends StatelessWidget {
'¿Quieres que la app te pida huella o cara antes de entrar a tus notas?', '¿Quieres que la app te pida huella o cara antes de entrar a tus notas?',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.72), color: palette.textSecondary,
height: 1.4, height: 1.4,
), ),
), ),
@@ -82,13 +75,17 @@ class BiometricChoiceScreen extends StatelessWidget {
FilledButton( FilledButton(
onPressed: isBusy ? null : onEnableBiometrics, onPressed: isBusy ? null : onEnableBiometrics,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(
vertical: 14,
),
), ),
child: isBusy child: isBusy
? const SizedBox( ? const SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(
strokeWidth: 2,
),
) )
: const Text('Sí, activar huella'), : const Text('Sí, activar huella'),
), ),
@@ -96,9 +93,11 @@ class BiometricChoiceScreen extends StatelessWidget {
OutlinedButton( OutlinedButton(
onPressed: isBusy ? null : onSkipBiometrics, onPressed: isBusy ? null : onSkipBiometrics,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(
side: const BorderSide(color: Colors.white24), vertical: 14,
foregroundColor: Colors.white, ),
side: BorderSide(color: palette.border),
foregroundColor: palette.textPrimary,
), ),
child: const Text('No, entrar sin huella'), child: const Text('No, entrar sin huella'),
), ),
+17 -18
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class BiometricGateScreen extends StatefulWidget { class BiometricGateScreen extends StatefulWidget {
const BiometricGateScreen({ const BiometricGateScreen({
@@ -38,19 +39,11 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(gradient: palette.backdropGradient),
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
@@ -63,12 +56,12 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
child: Container( child: Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1D1E20), color: palette.cardBackground,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)), border: Border.all(color: palette.border),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.35), color: palette.shadowSoft,
blurRadius: 30, blurRadius: 30,
offset: const Offset(0, 18), offset: const Offset(0, 18),
), ),
@@ -98,21 +91,27 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
'Pon tu huella o cara para entrar a tus notas.', 'Pon tu huella o cara para entrar a tus notas.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.72), color: palette.textSecondary,
height: 1.4, height: 1.4,
), ),
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
FilledButton( FilledButton(
onPressed: widget.isBusy ? null : widget.onUnlockRequested, onPressed: widget.isBusy
? null
: widget.onUnlockRequested,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(
vertical: 14,
),
), ),
child: widget.isBusy child: widget.isBusy
? const SizedBox( ? const SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(
strokeWidth: 2,
),
) )
: const Text('Desbloquear'), : const Text('Desbloquear'),
), ),
+1078 -1030
View File
File diff suppressed because it is too large Load Diff
+177 -652
View File
@@ -1,679 +1,217 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'package:notas/models/category.dart'; import 'package:notas/data/note_body.dart';
import 'package:notas/data/note_repository.dart';
import 'package:notas/models/note.dart'; import 'package:notas/models/note.dart';
import 'package:notas/platform/app_platform.dart'; import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart';
// NoteEditorScreen: unified UI for creating and editing notes.
// - Use `NoteEditorScreen.showDialog(context, note: existing)` to edit.
// - Use `NoteEditorScreen.showDialog(context)` to create a new note.
// The screen returns either a `Note` (saved) or the string `'delete'` when
// the user confirmed deletion. `null` indicates the user closed without saving.
class NoteEditorScreen extends StatefulWidget { class NoteEditorScreen extends StatefulWidget {
const NoteEditorScreen({ const NoteEditorScreen({
super.key, super.key,
this.repository,
this.saveNote,
required this.note, required this.note,
this.categoryId, this.embedded = false,
this.categories = const <Category>[], this.onSaved,
this.onComplete,
}); });
final Note? note; final NoteRepository? repository;
final String? categoryId; final Future<Note> Function(Note note)? saveNote;
final List<Category> categories; final Note note;
final ValueChanged<dynamic>? onComplete; final bool embedded;
final ValueChanged<Note>? onSaved;
@override @override
State<NoteEditorScreen> createState() => _NoteEditorScreenState(); State<NoteEditorScreen> createState() => _NoteEditorScreenState();
static Future<dynamic> _showGeneralEditorDialog(
BuildContext context, {
Note? note,
String? categoryId,
List<Category> categories = const <Category>[],
}) {
return showGeneralDialog<dynamic>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, animation, secondaryAnimation) {
return NoteEditorScreen(
note: note,
categoryId: categoryId,
categories: categories,
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(scale: animation, child: child);
},
);
}
static Future<dynamic> showDialog(
BuildContext context, {
Note? note,
String? categoryId,
List<Category> categories = const <Category>[],
}) {
if (isAndroid || isIOS) {
return _showGeneralEditorDialog(
context,
note: note,
categoryId: categoryId,
categories: categories,
);
}
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
if (overlayState == null) {
return _showGeneralEditorDialog(
context,
note: note,
categoryId: categoryId,
categories: categories,
);
}
final Completer<dynamic> completer = Completer<dynamic>();
late final OverlayEntry entry;
entry = OverlayEntry(
builder: (BuildContext overlayContext) {
return NoteEditorScreen(
note: note,
categoryId: categoryId,
categories: categories,
onComplete: (dynamic result) {
if (!completer.isCompleted) {
completer.complete(result);
}
if (entry.mounted) {
entry.remove();
}
},
);
},
);
overlayState.insert(entry);
return completer.future;
}
} }
class _NoteEditorScreenState extends State<NoteEditorScreen> { class _NoteEditorScreenState extends State<NoteEditorScreen> {
late TextEditingController _titleController; static const Duration _debounceDuration = Duration(seconds: 1);
late TextEditingController _bodyController;
late Note _currentNote;
late bool _isNewNote;
String? _selectedCategoryId;
final GlobalKey _categorySelectorKey = GlobalKey();
OverlayEntry? _categoryMenuEntry;
bool _didComplete = false;
bool get _isMobileLayout => isAndroid || isIOS; late final TextEditingController _titleController;
late final QuillController _bodyController;
late final FocusNode _bodyFocusNode;
late final ScrollController _bodyScrollController;
Timer? _debounceTimer;
bool _isSaving = false;
bool _saveQueued = false;
late Note _baselineNote;
AppPalette _paletteOf(BuildContext context) {
return Theme.of(context).extension<AppPalette>() ??
AppPalette.fromBrightness(Theme.of(context).brightness);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isNewNote = widget.note == null; _baselineNote = widget.note;
_titleController = TextEditingController(text: widget.note.title)
if (_isNewNote) { ..addListener(_scheduleSave);
final DateTime now = DateTime.now(); _bodyController = QuillController(
_currentNote = Note( document: noteBodyToDocument(widget.note.body),
title: '', selection: const TextSelection.collapsed(offset: 0),
body: '', )..addListener(_scheduleSave);
createdAt: now, _bodyFocusNode = FocusNode();
updatedAt: now, _bodyScrollController = ScrollController();
position: 0,
categoryId: widget.categoryId,
);
} else {
_currentNote = widget.note!;
}
_selectedCategoryId = _currentNote.categoryId ?? widget.categoryId;
_titleController = TextEditingController(text: _currentNote.title);
_bodyController = TextEditingController(text: _currentNote.body);
} }
@override @override
void dispose() { void dispose() {
_closeCategoryMenu(); _debounceTimer?.cancel();
_titleController.dispose(); _titleController.dispose();
_bodyController.dispose(); _bodyController.dispose();
_bodyFocusNode.dispose();
_bodyScrollController.dispose();
super.dispose(); super.dispose();
} }
void _complete(dynamic result) { String _bodyAsJson() {
if (_didComplete) { return noteDocumentToStorageJson(_bodyController.document);
}
void _scheduleSave() {
_debounceTimer?.cancel();
_debounceTimer = Timer(_debounceDuration, () {
unawaited(_saveNow());
});
}
Future<void> _saveNow() async {
if (!mounted) {
return; return;
} }
_didComplete = true;
_closeCategoryMenu();
final ValueChanged<dynamic>? callback = widget.onComplete;
if (callback != null) {
callback(result);
return;
}
Navigator.of(context).pop(result);
}
void _closeWithoutSaving() {
_complete(null);
}
void _saveNote() {
final String title = _titleController.text.trim(); final String title = _titleController.text.trim();
final String body = _bodyController.text.trim(); final String body = _bodyAsJson();
final bool categoryChanged = _selectedCategoryId != _currentNote.categoryId; final Note draft = _baselineNote.copyWith(
if (title.isEmpty && body.isEmpty && !categoryChanged) {
_complete(null);
return;
}
final Note updatedNote = _currentNote.copyWith(
title: title.isEmpty ? 'Sin título' : title, title: title.isEmpty ? 'Sin título' : title,
body: body, body: body,
categoryId: _selectedCategoryId, categoryId: _baselineNote.categoryId,
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
isDirty: true, isDirty: true,
); );
_complete(updatedNote); final bool hasChanges =
} draft.title != _baselineNote.title ||
draft.body != _baselineNote.body ||
draft.categoryId != _baselineNote.categoryId;
Widget _buildDeleteConfirmationDialog({ if (!hasChanges) {
required ValueChanged<bool> onConfirmed, return;
}) {
final bool isDeletedNote = _currentNote.isDeleted;
return AlertDialog(
backgroundColor: const Color(0xFF303134),
title: Text(
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar nota',
style: const TextStyle(color: Colors.white),
),
content: Text(
isDeletedNote
? 'Esta nota ya está borrada. Si la eliminas ahora, se borrará permanentemente.'
: '¿Estás seguro de que deseas eliminar esta nota?',
style: const TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => onConfirmed(false),
child: const Text(
'Cancelar',
style: TextStyle(color: Colors.white70),
),
),
TextButton(
onPressed: () => onConfirmed(true),
child: Text(
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
style: const TextStyle(color: Colors.red),
),
),
],
);
}
Future<bool> _showDeleteConfirmation() async {
if (_isMobileLayout) {
final bool? confirmed = await showDialog<bool>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext dialogContext) {
return _buildDeleteConfirmationDialog(
onConfirmed: (bool confirmed) =>
Navigator.of(dialogContext).pop(confirmed),
);
},
);
return confirmed ?? false;
} }
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true); if (_isSaving) {
if (overlayState == null) { _saveQueued = true;
final bool? confirmed = await showDialog<bool>( return;
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext dialogContext) {
return _buildDeleteConfirmationDialog(
onConfirmed: (bool confirmed) =>
Navigator.of(dialogContext).pop(confirmed),
);
},
);
return confirmed ?? false;
} }
final Completer<bool> completer = Completer<bool>(); _isSaving = true;
late final OverlayEntry entry; try {
bool didRemove = false; final Note saved = widget.saveNote != null
? await widget.saveNote!(draft)
entry = OverlayEntry( : await widget.repository!.updateNote(draft);
builder: (BuildContext overlayContext) { _baselineNote = saved;
final ValueChanged<bool> close = (bool confirmed) { widget.onSaved?.call(saved);
if (!completer.isCompleted) { } catch (error) {
completer.complete(confirmed); if (mounted) {
} ScaffoldMessenger.of(context).showSnackBar(
if (!didRemove && entry.mounted) { SnackBar(content: Text('No se pudo guardar la nota: $error')),
didRemove = true;
entry.remove();
}
};
return Material(
color: Colors.transparent,
child: Stack(
children: [
const Positioned.fill(
child: ModalBarrier(
dismissible: false,
color: Color.fromARGB(140, 0, 0, 0),
),
),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: _buildDeleteConfirmationDialog(onConfirmed: close),
),
),
],
),
); );
}, }
); } finally {
_isSaving = false;
overlayState.insert(entry); if (_saveQueued) {
return completer.future; _saveQueued = false;
} unawaited(_saveNow());
Category? _categoryById(String? id) {
for (final Category category in widget.categories) {
if (category.id == id) {
return category;
} }
} }
return null;
} }
Color _categoryBackgroundColor(Category? category) { Widget _buildEditorBody() {
if (category?.colorValue == null) { final AppPalette palette = _paletteOf(context);
return Colors.white.withValues(alpha: 0.08);
}
return Color(category!.colorValue!);
}
Color _categoryForegroundColor(Category? category) {
if (category == null || category.colorValue == null) {
return Colors.white;
}
final Color background = Color(category.colorValue!);
return background.computeLuminance() > 0.55 ? Colors.black87 : Colors.white;
}
Widget _buildCategorySelectorBox({Category? category}) {
final String label = category?.name ?? 'Sin categoría';
final IconData icon = CategoryStyle.iconForCodePoint(
category?.iconCodePoint,
);
final Color backgroundColor = _categoryBackgroundColor(category);
final Color foregroundColor = _categoryForegroundColor(category);
return Container(
key: _categorySelectorKey,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: category?.colorValue != null
? backgroundColor.withValues(alpha: 0.85)
: Colors.white24,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: foregroundColor, size: 15),
const SizedBox(width: 6),
Expanded(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: foregroundColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 6),
Icon(
Icons.arrow_drop_down,
color: foregroundColor.withValues(alpha: 0.9),
size: 16,
),
],
),
);
}
void _closeCategoryMenu() {
final OverlayEntry? entry = _categoryMenuEntry;
if (entry != null && entry.mounted) {
entry.remove();
}
_categoryMenuEntry = null;
}
void _toggleCategoryMenu() {
if (_categoryMenuEntry != null) {
_closeCategoryMenu();
return;
}
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
if (overlayState == null) {
return;
}
_categoryMenuEntry = OverlayEntry(
builder: (BuildContext overlayContext) {
final Size screenSize = MediaQuery.of(overlayContext).size;
final double menuWidth = math.min(screenSize.width - 32, 320);
final double menuHeight = math.min(screenSize.height - 32, 360);
return Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _closeCategoryMenu,
child: const SizedBox.expand(),
),
),
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: menuWidth,
maxHeight: menuHeight,
),
child: Material(
elevation: 10,
color: const Color(0xFF303134),
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
children: [
_buildCategoryMenuItem(
category: null,
label: 'Sin categoría',
isSelected: _selectedCategoryId == null,
onTap: () {
setState(() {
_selectedCategoryId = null;
});
_closeCategoryMenu();
},
),
for (final Category category in widget.categories)
_buildCategoryMenuItem(
category: category,
label: category.name,
isSelected: _selectedCategoryId == category.id,
onTap: () {
setState(() {
_selectedCategoryId = category.id;
});
_closeCategoryMenu();
},
),
],
),
),
),
),
],
),
);
},
);
overlayState.insert(_categoryMenuEntry!);
}
Widget _buildCategoryMenuItem({
required Category? category,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
final Color backgroundColor = _categoryBackgroundColor(category);
final Color foregroundColor = _categoryForegroundColor(category);
final IconData icon = CategoryStyle.iconForCodePoint(
category?.iconCodePoint,
);
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
color: isSelected ? Colors.white.withValues(alpha: 0.08) : null,
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: category?.colorValue != null
? backgroundColor.withValues(alpha: 0.85)
: Colors.white24,
),
),
child: Icon(icon, size: 16, color: foregroundColor),
),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: foregroundColor,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
if (isSelected) Icon(Icons.check, color: foregroundColor, size: 18),
],
),
),
);
}
Future<void> _deleteNote() async {
final bool confirmed = await _showDeleteConfirmation();
if (!mounted || !confirmed) {
return;
}
_complete('delete');
}
String _formatDate(DateTime date) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime yesterday = today.subtract(const Duration(days: 1));
final DateTime noteDate = DateTime(date.year, date.month, date.day);
if (noteDate == today) {
return 'Hoy ${DateFormat('HH:mm').format(date)}';
} else if (noteDate == yesterday) {
return 'Ayer ${DateFormat('HH:mm').format(date)}';
} else {
return DateFormat('dd/MM/yyyy HH:mm').format(date);
}
}
Widget _buildEditorContent({required bool isMobile}) {
final double titleSpacing = isMobile ? 16.0 : 8.0;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Container( Row(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [
decoration: BoxDecoration( Expanded(
border: Border(bottom: BorderSide(color: Colors.white12, width: 1)), child: Padding(
), padding: const EdgeInsets.only(left: 8),
child: Row( child: TextField(
children: [
IconButton(
onPressed: _closeWithoutSaving,
icon: const Icon(Icons.close, color: Colors.white70),
tooltip: 'Cerrar sin guardar',
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Posicion: ${_currentNote.position}',
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
Text(
'Creado: ${_formatDate(_currentNote.createdAt)}',
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
if (_currentNote.updatedAt != _currentNote.createdAt)
Text(
'Modificado: ${_formatDate(_currentNote.updatedAt)}',
style: const TextStyle(
color: Colors.white54,
fontSize: 12,
),
),
],
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 150),
child: SizedBox(
width: 150,
child: KeyedSubtree(
key: const ValueKey<String>('category_selector'),
child: InkWell(
onTap: _toggleCategoryMenu,
borderRadius: BorderRadius.circular(12),
child: _buildCategorySelectorBox(
category: _categoryById(_selectedCategoryId),
),
),
),
),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _titleController, controller: _titleController,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: palette.textPrimary,
fontSize: 28, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
), ),
decoration: const InputDecoration( decoration: InputDecoration(
hintText: 'Título', hintText: 'Título',
hintStyle: TextStyle(color: Colors.white30), hintStyle: TextStyle(color: palette.textHint),
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.zero,
), ),
), ),
SizedBox(height: titleSpacing), ),
Expanded( ),
child: TextField( ],
controller: _bodyController, ),
keyboardType: TextInputType.multiline, const SizedBox(height: 10),
maxLines: null, Expanded(
expands: true, child: Container(
style: const TextStyle( padding: const EdgeInsets.all(8),
color: Colors.white, child: QuillEditor.basic(
fontSize: 16, controller: _bodyController,
height: 1.6, focusNode: _bodyFocusNode,
), scrollController: _bodyScrollController,
decoration: const InputDecoration( config: QuillEditorConfig(
hintText: 'Escribe tu nota...', scrollable: true,
hintStyle: TextStyle(color: Colors.white30), padding: EdgeInsets.zero,
border: InputBorder.none, autoFocus: false,
contentPadding: EdgeInsets.zero, expands: true,
), placeholder: 'Escribe tu nota...',
), keyboardAppearance: Theme.of(context).brightness,
), ),
],
), ),
), ),
), ),
Container( const SizedBox(height: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), QuillSimpleToolbar(
decoration: BoxDecoration( controller: _bodyController,
border: Border(top: BorderSide(color: Colors.white12, width: 1)), config: const QuillSimpleToolbarConfig(
), color: Colors.transparent,
child: Row( showBoldButton: true,
mainAxisAlignment: MainAxisAlignment.spaceBetween, showItalicButton: true,
children: [ showUnderLineButton: true,
if (!_isNewNote) showStrikeThrough: false,
IconButton( showInlineCode: false,
onPressed: _deleteNote, showColorButton: false,
icon: const Icon(Icons.delete_outline, color: Colors.red), showBackgroundColorButton: false,
tooltip: 'Eliminar nota', showClearFormat: false,
) showAlignmentButtons: false,
else showHeaderStyle: false,
const SizedBox(width: 48), showListNumbers: true,
FilledButton(onPressed: _saveNote, child: const Text('Guardar')), showListBullets: true,
], showListCheck: true,
showCodeBlock: false,
showQuote: false,
showIndent: false,
showLink: false,
showUndo: false,
showRedo: false,
showDividers: false,
showFontFamily: false,
showFontSize: false,
showDirection: false,
showSearchButton: false,
showSubscript: false,
showSuperscript: false,
multiRowsDisplay: false,
axis: Axis.horizontal,
), ),
), ),
], ],
@@ -682,51 +220,38 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isMobileLayout) { final AppPalette palette = _paletteOf(context);
return Material(
color: Colors.transparent, final Widget editor = Padding(
child: SafeArea( padding: const EdgeInsets.all(8),
child: Container( child: _buildEditorBody(),
color: const Color.fromARGB(255, 24, 25, 26), );
child: _buildEditorContent(isMobile: true),
), if (widget.embedded) {
), return editor;
);
} }
return LayoutBuilder( return Container(
builder: (BuildContext context, BoxConstraints constraints) { decoration: BoxDecoration(gradient: palette.backdropGradient),
final double maxWidth = math.min(constraints.maxWidth - 32, 600); child: Scaffold(
final double maxHeight = math.min(constraints.maxHeight - 32, 720); backgroundColor: Colors.transparent,
appBar: AppBar(
return Stack( title: const Text('Editar nota'),
children: [ backgroundColor: Colors.transparent,
Positioned.fill( elevation: 0,
child: ModalBarrier( bottom: PreferredSize(
dismissible: false, preferredSize: const Size.fromHeight(1),
color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5), child: Container(
), decoration: BoxDecoration(
), border: Border(
Positioned.fill( bottom: BorderSide(color: palette.border, width: 0.5),
child: Center(
child: SizedBox(
width: maxWidth,
height: maxHeight,
child: Material(
color: const Color.fromRGBO(24, 25, 26, 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.white24, width: 1),
),
clipBehavior: Clip.antiAlias,
child: _buildEditorContent(isMobile: false),
),
), ),
), ),
), ),
], ),
); ),
}, body: SafeArea(child: editor),
),
); );
} }
} }
+359 -228
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/data/local_vault_service.dart'; import 'package:notas/data/local_vault_service.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/search_app_bar.dart'; import 'package:notas/widgets/search_app_bar.dart';
import 'package:notas/data/api_client.dart'; import 'package:notas/data/api_client.dart';
@@ -11,6 +12,8 @@ class SettingsScreen extends StatefulWidget {
required this.onForceSync, required this.onForceSync,
required this.currentSeedColor, required this.currentSeedColor,
required this.onThemeColorSelected, required this.onThemeColorSelected,
required this.currentThemeMode,
required this.onThemeModeSelected,
}); });
final Future<void> Function() onDeleteAllData; final Future<void> Function() onDeleteAllData;
@@ -18,6 +21,8 @@ class SettingsScreen extends StatefulWidget {
final Future<void> Function() onForceSync; final Future<void> Function() onForceSync;
final Color currentSeedColor; final Color currentSeedColor;
final Future<void> Function(Color color) onThemeColorSelected; final Future<void> Function(Color color) onThemeColorSelected;
final ThemeMode currentThemeMode;
final Future<void> Function(ThemeMode mode) onThemeModeSelected;
@override @override
State<SettingsScreen> createState() => _SettingsScreenState(); State<SettingsScreen> createState() => _SettingsScreenState();
@@ -29,32 +34,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _isServerDeleting = false; bool _isServerDeleting = false;
bool _isThemeSaving = false; bool _isThemeSaving = false;
final TextEditingController _endpointController = TextEditingController(); final TextEditingController _endpointController = TextEditingController();
final TextEditingController _encryptionKeyController = TextEditingController(); final TextEditingController _encryptionKeyController =
TextEditingController();
bool _endpointLoading = true; bool _endpointLoading = true;
bool _encryptionKeyLoading = false; bool _encryptionKeyLoading = false;
bool _encryptionKeyVisible = false; bool _encryptionKeyVisible = false;
late Color _selectedSeedColor; late Color _selectedSeedColor;
late ThemeMode _selectedThemeMode;
static const List<Color> _themeColorOptions = <Color>[ static const List<Color> _themeColorOptions = AppPalette.themeSeedColors;
Colors.amber,
Colors.blue,
Colors.teal,
Colors.green,
Colors.pink,
Colors.purple,
];
Future<void> _confirmAndDeleteAll() async { Future<void> _confirmAndDeleteAll() async {
final bool? confirmed = await showDialog<bool>( final bool? confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) {
title: const Text('Borrar todos los datos'), final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
content: const Text('¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.'), return AlertDialog(
actions: [ backgroundColor: palette.cardBackground,
TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancelar')), shape: RoundedRectangleBorder(
TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Borrar', style: TextStyle(color: Colors.red))), borderRadius: BorderRadius.circular(12),
], side: BorderSide(color: palette.border),
), ),
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. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'Borrar',
style: TextStyle(color: palette.destructiveAccent),
),
),
],
);
},
); );
if (confirmed != true) return; if (confirmed != true) return;
@@ -69,7 +88,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Todos los datos locales han sido eliminados.')), const SnackBar(
content: Text('Todos los datos locales han sido eliminados.'),
),
); );
} catch (error) { } catch (error) {
if (!mounted) return; if (!mounted) return;
@@ -77,35 +98,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al borrar los datos: $error')), SnackBar(content: Text('Error al borrar los datos: $error')),
); );
} finally { } finally {
if (!mounted) return; if (mounted) {
setState(() { setState(() {
_isBusy = false; _isBusy = false;
}); });
}
} }
} }
Future<void> _confirmAndDeleteServerData() async { Future<void> _confirmAndDeleteServerData() async {
final bool? confirmed = await showDialog<bool>( final bool? confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) {
title: const Text('Borrar toda la info del servidor'), final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
content: const Text( return AlertDialog(
'¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.', backgroundColor: palette.cardBackground,
), shape: RoundedRectangleBorder(
actions: [ borderRadius: BorderRadius.circular(12),
TextButton( side: BorderSide(color: palette.border),
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
), ),
TextButton( title: const Text('Borrar toda la info del servidor'),
onPressed: () => Navigator.of(context).pop(true), content: const Text(
child: const Text( '¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.',
'Borrar', ),
style: TextStyle(color: Colors.red), actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
), ),
), TextButton(
], onPressed: () => Navigator.of(context).pop(true),
), child: Text(
'Borrar',
style: TextStyle(color: palette.destructiveAccent),
),
),
],
);
},
); );
if (confirmed != true) return; if (confirmed != true) return;
@@ -115,11 +145,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
}); });
try { try {
final Map<String, dynamic> response = final Map<String, dynamic> response = await AuthApi.instance
await AuthApi.instance.deleteAllServerData(); .deleteAllServerData();
if (response['error'] == true) { if (response['error'] == true) {
throw Exception(response['body'] ?? response['message'] ?? 'Error desconocido'); throw Exception(
response['body'] ?? response['message'] ?? 'Error desconocido',
);
} }
await AuthApi.instance.clearTokens(); await AuthApi.instance.clearTokens();
@@ -127,7 +159,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Toda la información del servidor ha sido eliminada.')), const SnackBar(
content: Text('Toda la información del servidor ha sido eliminada.'),
),
); );
} catch (error) { } catch (error) {
if (!mounted) return; if (!mounted) return;
@@ -135,10 +169,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al borrar la info del servidor: $error')), SnackBar(content: Text('Error al borrar la info del servidor: $error')),
); );
} finally { } finally {
if (!mounted) return; if (mounted) {
setState(() { setState(() {
_isServerDeleting = false; _isServerDeleting = false;
}); });
}
} }
} }
@@ -166,11 +201,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al forzar la sincronización: $error')), SnackBar(content: Text('Error al forzar la sincronización: $error')),
); );
} finally { } finally {
if (!mounted) return; if (mounted) {
setState(() {
setState(() { _isSyncing = false;
_isSyncing = false; });
}); }
} }
} }
@@ -212,6 +247,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_selectedSeedColor = widget.currentSeedColor; _selectedSeedColor = widget.currentSeedColor;
_selectedThemeMode = widget.currentThemeMode;
_loadEndpoint(); _loadEndpoint();
} }
@@ -222,6 +258,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.currentSeedColor != _selectedSeedColor) { widget.currentSeedColor != _selectedSeedColor) {
_selectedSeedColor = widget.currentSeedColor; _selectedSeedColor = widget.currentSeedColor;
} }
if (oldWidget.currentThemeMode != widget.currentThemeMode) {
_selectedThemeMode = widget.currentThemeMode;
}
}
Future<void> _selectThemeMode(ThemeMode mode) async {
if (_selectedThemeMode == mode) return;
setState(() {
_selectedThemeMode = mode;
});
try {
await widget.onThemeModeSelected(mode);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('No se pudo guardar la preferencia de tema: $e'),
),
);
}
} }
Future<void> _loadEndpoint() async { Future<void> _loadEndpoint() async {
@@ -239,7 +295,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
}); });
try { try {
final String? encryptionKey = await LocalVaultService.instance.readEncryptionKey(); final String? encryptionKey = await LocalVaultService.instance
.readEncryptionKey();
if (!mounted) return; if (!mounted) return;
@@ -284,10 +341,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
required bool isLoading, required bool isLoading,
required IconData icon, required IconData icon,
}) { }) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return ElevatedButton.icon( return ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, backgroundColor: palette.destructiveAccent,
foregroundColor: Colors.white, foregroundColor: palette.textPrimary,
textStyle: const TextStyle(fontWeight: FontWeight.w600), textStyle: const TextStyle(fontWeight: FontWeight.w600),
), ),
onPressed: onPressed, onPressed: onPressed,
@@ -303,16 +362,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
Widget _buildThemeColorButton(Color color) { Widget _buildThemeColorButton(Color color) {
final bool isSelected = _selectedSeedColor.value == color.value; final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
final bool isSelected = _selectedSeedColor.toARGB32() == color.toARGB32();
final Color foregroundColor = final Color foregroundColor =
ThemeData.estimateBrightnessForColor(color) == Brightness.dark ThemeData.estimateBrightnessForColor(color) == Brightness.dark
? Colors.white ? palette.textPrimary
: Colors.black; : palette.textOnAccent;
return Semantics( return Semantics(
button: true, button: true,
selected: isSelected, selected: isSelected,
label: 'Color ${color.value.toRadixString(16)}', label: 'Color ${color.toARGB32().toRadixString(16)}',
child: Tooltip( child: Tooltip(
message: isSelected ? 'Color actual' : 'Usar este color', message: isSelected ? 'Color actual' : 'Usar este color',
child: InkWell( child: InkWell(
@@ -326,12 +386,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
color: color, color: color,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: isSelected ? Colors.white : Colors.white24, color: isSelected ? palette.textPrimary : palette.textSecondary,
width: isSelected ? 2.5 : 1.2, width: isSelected ? 2.5 : 1.2,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.25), color: palette.shadowSoft,
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 3), offset: const Offset(0, 3),
), ),
@@ -341,11 +401,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
if (isSelected) if (isSelected)
Icon( Icon(Icons.check, size: 22, color: foregroundColor),
Icons.check,
size: 22,
color: foregroundColor,
),
], ],
), ),
), ),
@@ -366,10 +422,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
try { try {
await ApiConfig.setEndpoint(value); await ApiConfig.setEndpoint(value);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Endpoint guardado'))); ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Endpoint guardado')));
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error guardando endpoint: $e'))); ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error guardando endpoint: $e')));
} }
} }
@@ -378,7 +438,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final String endpoint = await ApiConfig.getEndpoint(); final String endpoint = await ApiConfig.getEndpoint();
if (!mounted) return; if (!mounted) return;
_endpointController.text = endpoint; _endpointController.text = endpoint;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Endpoint restaurado al valor por defecto'))); ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Endpoint restaurado al valor por defecto')),
);
} }
Widget _buildResponsiveInputActionsRow({ Widget _buildResponsiveInputActionsRow({
@@ -430,20 +492,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(gradient: palette.backdropGradient),
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
@@ -455,162 +510,238 @@ class _SettingsScreenState extends State<SettingsScreen> {
titleText: 'Configuración', titleText: 'Configuración',
), ),
Expanded( Expanded(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), physics: const BouncingScrollPhysics(),
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16.0),
children: [ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Expanded( Row(
child: Text('Borrar datos locales:'), children: [
), const Expanded(
_buildDestructiveButton( child: Text('Borrar datos locales:'),
label: 'Borrar', ),
onPressed: (_isBusy || _isServerDeleting) _buildDestructiveButton(
? null label: 'Borrar',
: _confirmAndDeleteAll, onPressed: (_isBusy || _isServerDeleting)
isLoading: _isBusy, ? null
icon: Icons.delete_forever, : _confirmAndDeleteAll,
), isLoading: _isBusy,
], icon: Icons.delete_forever,
), ),
const SizedBox(height: 16), ],
Row( ),
children: [ const SizedBox(height: 16),
const Expanded( Row(
child: Text('Borrar info del servidor:'), children: [
), const Expanded(
_buildDestructiveButton( child: Text('Borrar info del servidor:'),
label: 'Borrar', ),
onPressed: (_isBusy || _isSyncing || _isServerDeleting) _buildDestructiveButton(
? null label: 'Borrar',
: _confirmAndDeleteServerData, onPressed:
isLoading: _isServerDeleting, (_isBusy || _isSyncing || _isServerDeleting)
icon: Icons.cloud_off, ? null
), : _confirmAndDeleteServerData,
], isLoading: _isServerDeleting,
), icon: Icons.cloud_off,
const SizedBox(height: 16), ),
Row( ],
children: [ ),
const Expanded( const SizedBox(height: 16),
child: Text('Forzar sincronizacion total:'), Row(
), children: [
ElevatedButton.icon( const Expanded(
onPressed: (_isBusy || _isSyncing || _isServerDeleting) child: Text('Forzar sincronizacion total:'),
? null ),
: _forceSync, ElevatedButton.icon(
icon: _isSyncing onPressed:
? const SizedBox( (_isBusy || _isSyncing || _isServerDeleting)
width: 16, ? null
height: 16, : _forceSync,
child: CircularProgressIndicator(strokeWidth: 2), icon: _isSyncing
) ? const SizedBox(
: const Icon(Icons.sync), width: 16,
label: const Text('Sincronizar'), height: 16,
), child: CircularProgressIndicator(
], strokeWidth: 2,
), ),
const SizedBox(height: 24), )
const Text('Color del esquema'), : const Icon(Icons.sync),
const SizedBox(height: 8), label: const Text('Sincronizar'),
Wrap( ),
spacing: 12, ],
runSpacing: 12, ),
children: [ const SizedBox(height: 24),
for (final Color color in _themeColorOptions) const Text('Apariencia'),
_buildThemeColorButton(color), const SizedBox(height: 8),
], Column(
), children: [
const SizedBox(height: 24), RadioGroup<ThemeMode>(
const Text('API endpoint (ej: https://notas-api.lpncnd.es/api)'), groupValue: _selectedThemeMode,
const SizedBox(height: 8), onChanged: (ThemeMode? v) {
_buildResponsiveInputActionsRow( if (v != null) {
input: _endpointLoading _selectThemeMode(v);
? const SizedBox(height: 48, child: Center(child: CircularProgressIndicator())) }
: TextField( },
controller: _endpointController, child: Column(
style: const TextStyle(color: Colors.white), children: [
keyboardType: TextInputType.url, RadioListTile<ThemeMode>(
decoration: InputDecoration( title: const Text(
labelText: 'API endpoint', 'Seguir modo del sistema',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), ),
filled: true, value: ThemeMode.system,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
), ),
enabledBorder: OutlineInputBorder( RadioListTile<ThemeMode>(
borderRadius: BorderRadius.circular(14), title: const Text('Modo claro'),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), value: ThemeMode.light,
), ),
focusedBorder: OutlineInputBorder( RadioListTile<ThemeMode>(
borderRadius: BorderRadius.circular(14), title: const Text('Modo oscuro'),
borderSide: const BorderSide(color: Colors.amber, width: 1.2), value: ThemeMode.dark,
), ),
), ],
), ),
actions: [
ElevatedButton(
onPressed: _endpointLoading ? null : _saveEndpoint,
child: const Text('Guardar'),
),
OutlinedButton(
onPressed: _endpointLoading ? null : _resetEndpoint,
child: const Text('Restaurar'),
),
],
),
const SizedBox(height: 24),
const Text('Clave de cifrado local'),
const SizedBox(height: 8),
_buildResponsiveInputActionsRow(
input: TextField(
controller: _encryptionKeyController,
readOnly: true,
obscureText: !_encryptionKeyVisible,
enableSuggestions: false,
autocorrect: false,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: _encryptionKeyVisible ? 'Clave de cifrado' : 'Oculta hasta pulsar mostrar',
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),
const Text('Color del esquema'),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Wrap(
spacing: 12,
runSpacing: 12,
children: [
for (final Color color in _themeColorOptions)
_buildThemeColorButton(color),
],
), ),
), ),
actions: [ const SizedBox(height: 24),
ElevatedButton( const Text(
onPressed: _encryptionKeyLoading ? null : _loadEncryptionKey, 'API endpoint (ej: https://notas-api.lpncnd.es/api)',
child: _encryptionKeyLoading ),
? const SizedBox( const SizedBox(height: 8),
width: 16, _buildResponsiveInputActionsRow(
height: 16, input: _endpointLoading
child: CircularProgressIndicator(strokeWidth: 2), ? const SizedBox(
) height: 48,
: const Text('Mostrar'), child: Center(
child: CircularProgressIndicator(),
),
)
: TextField(
controller: _endpointController,
style: TextStyle(color: palette.textPrimary),
keyboardType: TextInputType.url,
decoration: InputDecoration(
labelText: 'API endpoint',
labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true,
fillColor: palette.fill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: palette.border,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: palette.border,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: palette.accent,
width: 1.2,
),
),
),
),
actions: [
ElevatedButton(
onPressed: _endpointLoading
? null
: _saveEndpoint,
child: const Text('Guardar'),
),
OutlinedButton(
onPressed: _endpointLoading
? null
: _resetEndpoint,
child: const Text('Restaurar'),
),
],
),
const SizedBox(height: 24),
const Text('Clave de cifrado local'),
const SizedBox(height: 8),
_buildResponsiveInputActionsRow(
input: TextField(
controller: _encryptionKeyController,
readOnly: true,
obscureText: !_encryptionKeyVisible,
enableSuggestions: false,
autocorrect: false,
style: TextStyle(color: palette.textPrimary),
decoration: InputDecoration(
labelText: _encryptionKeyVisible
? 'Clave de cifrado'
: 'Oculta hasta pulsar mostrar',
labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true,
fillColor: palette.fill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: palette.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: palette.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: palette.accent,
width: 1.2,
),
),
),
), ),
OutlinedButton( actions: [
onPressed: _encryptionKeyVisible ? _hideEncryptionKey : null, ElevatedButton(
child: const Text('Ocultar'), onPressed: _encryptionKeyLoading
), ? null
], : _loadEncryptionKey,
), child: _encryptionKeyLoading
], ? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('Mostrar'),
),
OutlinedButton(
onPressed: _encryptionKeyVisible
? _hideEncryptionKey
: null,
child: const Text('Ocultar'),
),
],
),
],
),
), ),
), ),
), ),
+66 -41
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/data/api_client.dart'; import 'package:notas/data/api_client.dart';
import 'package:notas/theme/app_palette.dart';
class VaultAccessScreen extends StatefulWidget { class VaultAccessScreen extends StatefulWidget {
const VaultAccessScreen({ const VaultAccessScreen({
@@ -11,7 +12,8 @@ class VaultAccessScreen extends StatefulWidget {
}); });
final bool isBusy; final bool isBusy;
final Future<void> Function(String email, String password) onCreateAccountPressed; final Future<void> Function(String email, String password)
onCreateAccountPressed;
final Future<void> Function(String email, String password) onSignInPressed; final Future<void> Function(String email, String password) onSignInPressed;
final Future<void> Function() onContinueWithoutAccount; final Future<void> Function() onContinueWithoutAccount;
@@ -73,19 +75,11 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(gradient: palette.backdropGradient),
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
@@ -98,12 +92,12 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
child: Container( child: Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1D1E20), color: palette.cardBackground,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)), border: Border.all(color: palette.border),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.35), color: palette.shadowSoft,
blurRadius: 30, blurRadius: 30,
offset: const Offset(0, 18), offset: const Offset(0, 18),
), ),
@@ -115,7 +109,7 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
children: [ children: [
const Icon( const Icon(
Icons.lock_outline, Icons.lock_outline,
color: Colors.amber, color: Colors.white,
size: 44, size: 44,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -133,7 +127,7 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.', 'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.72), color: palette.textSecondary,
height: 1.4, height: 1.4,
), ),
), ),
@@ -141,7 +135,9 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
_endpointLoading _endpointLoading
? const SizedBox( ? const SizedBox(
height: 48, height: 48,
child: Center(child: CircularProgressIndicator()), child: Center(
child: CircularProgressIndicator(),
),
) )
: TextField( : TextField(
controller: _endpointController, controller: _endpointController,
@@ -150,20 +146,29 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'API endpoint', labelText: 'API endpoint',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true, filled: true,
fillColor: Colors.white.withValues(alpha: 0.05), fillColor: palette.fill,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(
color: palette.border,
),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(
color: palette.border,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.amber, width: 1.2), borderSide: BorderSide(
color: palette.accent,
width: 1.2,
),
), ),
), ),
), ),
@@ -175,20 +180,25 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Usuario', labelText: 'Usuario',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true, filled: true,
fillColor: Colors.white.withValues(alpha: 0.05), fillColor: palette.fill,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(color: palette.border),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(color: palette.border),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.amber, width: 1.2), borderSide: BorderSide(
color: palette.accent,
width: 1.2,
),
), ),
), ),
), ),
@@ -200,34 +210,45 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Contraseña', labelText: 'Contraseña',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true, filled: true,
fillColor: Colors.white.withValues(alpha: 0.05), fillColor: palette.fill,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(color: palette.border),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)), borderSide: BorderSide(color: palette.border),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.amber, width: 1.2), borderSide: BorderSide(
color: palette.accent,
width: 1.2,
),
), ),
), ),
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
FilledButton( FilledButton(
onPressed: widget.isBusy ? null : _handleCreateAccount, onPressed: widget.isBusy
? null
: _handleCreateAccount,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(
vertical: 14,
),
), ),
child: widget.isBusy child: widget.isBusy
? const SizedBox( ? const SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(
strokeWidth: 2,
),
) )
: const Text('Crear cuenta'), : const Text('Crear cuenta'),
), ),
@@ -235,15 +256,19 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
OutlinedButton( OutlinedButton(
onPressed: widget.isBusy ? null : _handleSignIn, onPressed: widget.isBusy ? null : _handleSignIn,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(
side: const BorderSide(color: Colors.white24), vertical: 14,
foregroundColor: Colors.white, ),
side: BorderSide(color: palette.border),
foregroundColor: palette.textPrimary,
), ),
child: const Text('Iniciar sesión'), child: const Text('Iniciar sesión'),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
TextButton( TextButton(
onPressed: widget.isBusy ? null : widget.onContinueWithoutAccount, onPressed: widget.isBusy
? null
: widget.onContinueWithoutAccount,
child: const Text('Entrar sin cuenta'), child: const Text('Entrar sin cuenta'),
), ),
], ],
@@ -259,4 +284,4 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
), ),
); );
} }
} }
+303
View File
@@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
@immutable
class AppPalette extends ThemeExtension<AppPalette> {
static const Color darkDefaultThemeSeedColor = Colors.amber;
static const Color lightDefaultThemeSeedColor = Colors.blue;
static const List<Color> defaultCategoryColors = <Color>[
Colors.amber,
Colors.blue,
Colors.green,
Colors.purple,
Colors.red,
Colors.teal,
Colors.orange,
Colors.grey,
];
static const List<Color> themeSeedColors = <Color>[
Colors.blue,
Colors.teal,
Colors.green,
Colors.pink,
Colors.purple,
Colors.orange,
];
const AppPalette({
required this.backdropGradient,
required this.drawerBackground,
required this.cardBackground,
required this.categoryColors,
required this.surfaceElevated,
required this.border,
required this.borderMuted,
required this.accent,
required this.textPrimary,
required this.textSecondary,
required this.textOnSurfaceDark,
required this.textMuted,
required this.textDisabled,
required this.textHint,
required this.fill,
required this.hover,
required this.shadowSoft,
required this.shadowDim,
required this.destructiveAccent,
required this.textOnAccent,
required this.overlay,
required this.transparent,
required this.dragTargetBorder,
required this.syncPreparing,
required this.syncEncrypting,
required this.syncUploading,
required this.syncWaiting,
required this.syncDecrypting,
required this.success,
});
final Gradient backdropGradient;
final Color drawerBackground;
final Color cardBackground;
final List<Color> categoryColors;
final Color surfaceElevated;
final Color border;
final Color borderMuted;
final Color accent;
final Color textPrimary;
final Color textSecondary;
final Color textOnSurfaceDark;
final Color textMuted;
final Color textDisabled;
final Color textHint;
final Color fill;
final Color hover;
final Color shadowSoft;
final Color shadowDim;
final Color destructiveAccent;
final Color textOnAccent;
final Color overlay;
final Color transparent;
final Color dragTargetBorder;
final Color syncPreparing;
final Color syncEncrypting;
final Color syncUploading;
final Color syncWaiting;
final Color syncDecrypting;
final Color success;
@override
AppPalette copyWith({
Gradient? backdropGradient,
Color? drawerBackground,
Color? cardBackground,
List<Color>? categoryColors,
Color? surfaceElevated,
Color? border,
Color? borderMuted,
Color? accent,
Color? textPrimary,
Color? textSecondary,
Color? textOnSurfaceDark,
Color? textMuted,
Color? textDisabled,
Color? textHint,
Color? fill,
Color? hover,
Color? shadowSoft,
Color? shadowDim,
Color? destructiveAccent,
Color? textOnAccent,
Color? overlay,
Color? transparent,
Color? dragTargetBorder,
Color? syncPreparing,
Color? syncEncrypting,
Color? syncUploading,
Color? syncWaiting,
Color? syncDecrypting,
Color? success,
}) {
return AppPalette(
backdropGradient: backdropGradient ?? this.backdropGradient,
drawerBackground: drawerBackground ?? this.drawerBackground,
cardBackground: cardBackground ?? this.cardBackground,
categoryColors: categoryColors ?? this.categoryColors,
surfaceElevated: surfaceElevated ?? this.surfaceElevated,
border: border ?? this.border,
borderMuted: borderMuted ?? this.borderMuted,
accent: accent ?? this.accent,
textPrimary: textPrimary ?? this.textPrimary,
textSecondary: textSecondary ?? this.textSecondary,
textOnSurfaceDark: textOnSurfaceDark ?? this.textOnSurfaceDark,
textMuted: textMuted ?? this.textMuted,
textDisabled: textDisabled ?? this.textDisabled,
textHint: textHint ?? this.textHint,
fill: fill ?? this.fill,
hover: hover ?? this.hover,
shadowSoft: shadowSoft ?? this.shadowSoft,
shadowDim: shadowDim ?? this.shadowDim,
destructiveAccent: destructiveAccent ?? this.destructiveAccent,
textOnAccent: textOnAccent ?? this.textOnAccent,
overlay: overlay ?? this.overlay,
transparent: transparent ?? this.transparent,
dragTargetBorder: dragTargetBorder ?? this.dragTargetBorder,
syncPreparing: syncPreparing ?? this.syncPreparing,
syncEncrypting: syncEncrypting ?? this.syncEncrypting,
syncUploading: syncUploading ?? this.syncUploading,
syncWaiting: syncWaiting ?? this.syncWaiting,
syncDecrypting: syncDecrypting ?? this.syncDecrypting,
success: success ?? this.success,
);
}
@override
AppPalette lerp(ThemeExtension<AppPalette>? other, double t) {
if (other is! AppPalette) return this;
return AppPalette(
backdropGradient:
LinearGradient.lerp(
backdropGradient as LinearGradient,
other.backdropGradient as LinearGradient,
t,
) ??
backdropGradient,
drawerBackground:
Color.lerp(drawerBackground, other.drawerBackground, t) ??
drawerBackground,
cardBackground:
Color.lerp(cardBackground, other.cardBackground, t) ?? cardBackground,
categoryColors: t < 0.5 ? categoryColors : other.categoryColors,
surfaceElevated:
Color.lerp(surfaceElevated, other.surfaceElevated, t) ??
surfaceElevated,
border: Color.lerp(border, other.border, t) ?? border,
borderMuted: Color.lerp(borderMuted, other.borderMuted, t) ?? borderMuted,
accent: Color.lerp(accent, other.accent, t) ?? accent,
textPrimary: Color.lerp(textPrimary, other.textPrimary, t) ?? textPrimary,
textSecondary:
Color.lerp(textSecondary, other.textSecondary, t) ?? textSecondary,
textOnSurfaceDark:
Color.lerp(textOnSurfaceDark, other.textOnSurfaceDark, t) ??
textOnSurfaceDark,
textMuted: Color.lerp(textMuted, other.textMuted, t) ?? textMuted,
textDisabled:
Color.lerp(textDisabled, other.textDisabled, t) ?? textDisabled,
textHint: Color.lerp(textHint, other.textHint, t) ?? textHint,
fill: Color.lerp(fill, other.fill, t) ?? fill,
hover: Color.lerp(hover, other.hover, t) ?? hover,
shadowSoft: Color.lerp(shadowSoft, other.shadowSoft, t) ?? shadowSoft,
shadowDim: Color.lerp(shadowDim, other.shadowDim, t) ?? shadowDim,
destructiveAccent:
Color.lerp(destructiveAccent, other.destructiveAccent, t) ??
destructiveAccent,
textOnAccent:
Color.lerp(textOnAccent, other.textOnAccent, t) ?? textOnAccent,
overlay: Color.lerp(overlay, other.overlay, t) ?? overlay,
transparent: Color.lerp(transparent, other.transparent, t) ?? transparent,
dragTargetBorder:
Color.lerp(dragTargetBorder, other.dragTargetBorder, t) ??
dragTargetBorder,
syncPreparing:
Color.lerp(syncPreparing, other.syncPreparing, t) ?? syncPreparing,
syncEncrypting:
Color.lerp(syncEncrypting, other.syncEncrypting, t) ?? syncEncrypting,
syncUploading:
Color.lerp(syncUploading, other.syncUploading, t) ?? syncUploading,
syncWaiting: Color.lerp(syncWaiting, other.syncWaiting, t) ?? syncWaiting,
syncDecrypting:
Color.lerp(syncDecrypting, other.syncDecrypting, t) ?? syncDecrypting,
success: Color.lerp(success, other.success, t) ?? success,
);
}
static AppPalette fromBrightness(Brightness brightness, {Color? seedColor}) {
final bool isLight = brightness == Brightness.light;
return AppPalette(
backdropGradient: isLight
? const LinearGradient(
colors: <Color>[
Color(0xFFFFFFFF),
Color(0xFFF2F4F6),
Color(0xFFECEFF1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: const LinearGradient(
colors: <Color>[
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
drawerBackground: isLight
? const Color(0xFFF7F9FA)
: const Color.fromARGB(255, 30, 31, 35),
cardBackground: isLight
? const Color(0xFFFFFFFF)
: const Color.fromRGBO(24, 25, 26, 1),
categoryColors: defaultCategoryColors,
surfaceElevated: isLight
? const Color(0xFFF0F2F4)
: const Color(0xFF303134),
border: isLight
? const Color.fromRGBO(15, 23, 32, 0.06)
: const Color.fromRGBO(255, 255, 255, 0.12),
borderMuted: isLight
? const Color.fromRGBO(15, 23, 32, 0.03)
: const Color.fromRGBO(255, 255, 255, 0.08),
accent:
seedColor ??
(isLight
? AppPalette.lightDefaultThemeSeedColor
: AppPalette.darkDefaultThemeSeedColor),
textPrimary: isLight ? const Color(0xFF0F1720) : Colors.white,
textSecondary: isLight ? const Color(0xFF374151) : Colors.white70,
textOnSurfaceDark: isLight ? Colors.black87 : Colors.black87,
textMuted: isLight ? const Color(0xFF6B7280) : Colors.white54,
textDisabled: isLight ? const Color(0xFFBDBDBD) : Colors.white24,
textHint: isLight
? const Color.fromRGBO(15, 23, 32, 0.30)
: const Color.fromRGBO(255, 255, 255, 0.30),
fill: isLight
? const Color.fromRGBO(15, 23, 32, 0.02)
: const Color.fromRGBO(255, 255, 255, 0.05),
hover: isLight
? const Color.fromRGBO(15, 23, 32, 0.04)
: const Color.fromRGBO(255, 255, 255, 0.10),
shadowSoft: isLight
? const Color.fromRGBO(0, 0, 0, 0.08)
: const Color.fromRGBO(0, 0, 0, 0.25),
shadowDim: isLight
? const Color.fromARGB(40, 0, 0, 0)
: const Color.fromARGB(54, 0, 0, 0),
destructiveAccent: Colors.redAccent,
textOnAccent: isLight ? Colors.white : Colors.black,
overlay: isLight
? const Color.fromARGB(120, 0, 0, 0)
: const Color.fromARGB(140, 0, 0, 0),
transparent: Colors.transparent,
dragTargetBorder: isLight
? const Color(0xFF1565C0)
: const Color(0xFF42A5F5),
syncPreparing: isLight
? const Color.fromARGB(255, 120, 120, 120)
: const Color.fromARGB(255, 165, 165, 165),
syncEncrypting: isLight
? const Color.fromARGB(255, 2, 136, 209)
: const Color.fromARGB(255, 109, 191, 255),
syncUploading: isLight
? const Color.fromARGB(255, 2, 119, 189)
: const Color.fromARGB(255, 98, 190, 255),
syncWaiting: const Color.fromARGB(255, 150, 150, 150),
syncDecrypting: isLight
? const Color.fromARGB(255, 76, 175, 80)
: const Color.fromARGB(255, 154, 194, 112),
success: Colors.green,
);
}
}
+23 -9
View File
@@ -1,23 +1,37 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class AppTheme { class AppTheme {
static ThemeData theme({Color seedColor = Colors.amber}) { static ThemeData theme({
Color seedColor = Colors.amber,
Brightness brightness = Brightness.dark,
}) {
final Brightness foregroundBrightness = final Brightness foregroundBrightness =
ThemeData.estimateBrightnessForColor(seedColor); ThemeData.estimateBrightnessForColor(seedColor);
final Color foregroundColor = final Color foregroundColor = foregroundBrightness == Brightness.dark
foregroundBrightness == Brightness.dark ? Colors.white : Colors.black; ? Colors.white
: Colors.black87;
final ColorScheme scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
);
final AppPalette palette = AppPalette.fromBrightness(
brightness,
seedColor: seedColor,
);
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
scaffoldBackgroundColor: const Color.fromRGBO(31, 32, 33, 1), scaffoldBackgroundColor: scheme.surface,
colorScheme: ColorScheme.fromSeed( colorScheme: scheme,
seedColor: seedColor, extensions: <ThemeExtension<dynamic>>[palette],
brightness: Brightness.dark, brightness: brightness,
),
floatingActionButtonTheme: FloatingActionButtonThemeData( floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: seedColor, backgroundColor: seedColor,
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
), ),
); );
} }
} }
+12 -11
View File
@@ -1,18 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class CategoryStyle { class CategoryStyle {
CategoryStyle._(); CategoryStyle._();
static const List<Color> colors = <Color>[ static const List<Color> colors = AppPalette.defaultCategoryColors;
Colors.amber,
Colors.blue, static List<Color> colorsOf(BuildContext context) {
Colors.green, final AppPalette? palette = Theme.of(context).extension<AppPalette>();
Colors.purple, if (palette != null) {
Colors.red, return palette.categoryColors;
Colors.teal, }
Colors.orange,
Colors.grey, return AppPalette.defaultCategoryColors;
]; }
static const List<IconData> icons = <IconData>[ static const List<IconData> icons = <IconData>[
Icons.label_outline_rounded, Icons.label_outline_rounded,
@@ -38,4 +39,4 @@ class CategoryStyle {
return Icons.folder_outlined; return Icons.folder_outlined;
} }
} }
+32 -24
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/models/category.dart'; import 'package:notas/models/category.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart'; import 'package:notas/widgets/category_style.dart';
class MenuDrawer extends StatelessWidget { class MenuDrawer extends StatelessWidget {
@@ -20,12 +21,12 @@ class MenuDrawer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Container( return Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color.fromARGB(255, 30, 31, 35), color: palette.drawerBackground,
border: Border( border: Border(right: BorderSide(color: palette.border, width: 0.5)),
right: BorderSide(color: Colors.white12, width: 0.5),
),
), ),
child: Column( child: Column(
children: [ children: [
@@ -50,7 +51,7 @@ class MenuDrawer extends StatelessWidget {
CategoryStyle.iconForCodePoint( CategoryStyle.iconForCodePoint(
category.iconCodePoint, category.iconCodePoint,
); );
return _MenuItemTile( return _MenuItemTile(
icon: categoryIcon, icon: categoryIcon,
label: category.name, label: category.name,
@@ -59,17 +60,21 @@ class MenuDrawer extends StatelessWidget {
onLongPress: onEditCategory == null onLongPress: onEditCategory == null
? null ? null
: () => onEditCategory?.call(category), : () => onEditCategory?.call(category),
iconColor: Color(category.colorValue ?? 0xFFFFC107), iconColor: Color(
textColor: Color(category.colorValue ?? 0xFFFFC107), category.colorValue ?? palette.accent.toARGB32(),
),
textColor: Color(
category.colorValue ?? palette.accent.toARGB32(),
),
trailing: IconButton( trailing: IconButton(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 0, minWidth: 0,
minHeight: 0, minHeight: 0,
), ),
icon: const Icon( icon: Icon(
Icons.more_vert, Icons.more_vert,
color: Colors.white70, color: palette.textSecondary,
size: 20, size: 20,
), ),
onPressed: () => onEditCategory?.call(category), onPressed: () => onEditCategory?.call(category),
@@ -92,10 +97,10 @@ class MenuDrawer extends StatelessWidget {
label: 'Mis notas borradas', label: 'Mis notas borradas',
selected: selectedItem == 'deleted_notes', selected: selectedItem == 'deleted_notes',
onTap: () => onMenuItemTapped?.call('deleted_notes'), onTap: () => onMenuItemTapped?.call('deleted_notes'),
iconColor: Colors.redAccent, iconColor: palette.destructiveAccent,
textColor: Colors.redAccent, textColor: palette.destructiveAccent,
), ),
const Divider(color: Colors.white12, height: 16), Divider(color: palette.border, height: 16),
_MenuItemTile( _MenuItemTile(
icon: Icons.settings, icon: Icons.settings,
label: 'Configuración', label: 'Configuración',
@@ -138,11 +143,12 @@ class _MenuItemTileState extends State<_MenuItemTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
final bool active = widget.selected || _hovering; final bool active = widget.selected || _hovering;
final Color backgroundColor = active final Color backgroundColor = active ? palette.hover : palette.transparent;
? Colors.white.withValues(alpha: 0.10) final Color foregroundColor = active
: Colors.transparent; ? palette.textPrimary
final Color foregroundColor = active ? Colors.white : Colors.white70; : palette.textSecondary;
final Widget? trailing = _hovering ? widget.trailing : null; final Widget? trailing = _hovering ? widget.trailing : null;
return MouseRegion( return MouseRegion(
@@ -155,11 +161,7 @@ class _MenuItemTileState extends State<_MenuItemTile> {
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
margin: const EdgeInsets.only( margin: const EdgeInsets.only(right: 8, top: 2, bottom: 2),
right: 8,
top: 2,
bottom: 2,
),
child: Material( child: Material(
color: backgroundColor, color: backgroundColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -169,11 +171,17 @@ class _MenuItemTileState extends State<_MenuItemTile> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 8), contentPadding: const EdgeInsets.only(left: 16, right: 8),
leading: Icon(widget.icon, color: widget.iconColor ?? foregroundColor), leading: Icon(
widget.icon,
color: widget.iconColor ?? foregroundColor,
),
trailing: trailing, trailing: trailing,
title: Text( title: Text(
widget.label, widget.label,
style: TextStyle(color: widget.textColor ?? foregroundColor, fontSize: 14), style: TextStyle(
color: widget.textColor ?? foregroundColor,
fontSize: 14,
),
), ),
onTap: widget.onTap, onTap: widget.onTap,
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
+138 -115
View File
@@ -1,141 +1,164 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/data/note_body.dart';
import 'package:notas/models/category.dart';
import 'package:notas/models/note.dart'; import 'package:notas/models/note.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart';
// Small presentational widget for a note inside the grid. class NoteCard extends StatelessWidget {
// Keep this widget lightweight and layout-agnostic: it should not force
// width/height constraints (so it works inside different parent layouts
// like MasonryGridView or Draggable feedback). Visual styling only.
class NoteCard extends StatefulWidget {
const NoteCard({ const NoteCard({
super.key, super.key,
required this.note, required this.note,
this.onTap, this.category,
this.isDragging = false, this.isSelected = false,
this.borderColor, this.borderColor,
this.onTap,
this.onDelete,
this.onChangeCategory,
this.showSelectionBorder = true,
}); });
final Note note; final Note note;
final VoidCallback? onTap; final Category? category;
final bool isDragging; final bool isSelected;
final Color? borderColor; final Color? borderColor;
final VoidCallback? onTap;
@override final VoidCallback? onDelete;
State<NoteCard> createState() => _NoteCardState(); final ValueChanged<BuildContext>? onChangeCategory;
} final bool showSelectionBorder;
class _NoteCardState extends State<NoteCard> {
bool _isPressed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool showGrabbing = widget.isDragging || _isPressed; final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
final String bodyText = noteBodyToPlainText(note.body).trim();
final Color? categoryColor = category?.colorValue == null
? null
: Color(category!.colorValue!);
final IconData? categoryIcon = category == null
? null
: CategoryStyle.iconForCodePoint(category!.iconCodePoint);
return MouseRegion( return Material(
cursor: showGrabbing ? SystemMouseCursors.grabbing : SystemMouseCursors.grab, color: Colors.transparent, // 1. Fondo completamente transparente
child: GestureDetector( shape: BorderDirectional(
onTapDown: widget.onTap == null start: BorderSide(
? null color: (isSelected && showSelectionBorder)
: (_) { ? palette.accent
setState(() { : Colors.transparent,
_isPressed = true; width: isSelected ? 1.6 : 1.0,
}); ),
}, ),
onTapUp: widget.onTap == null child: InkWell(
? null borderRadius: BorderRadius.circular(14),
: (_) { onTap: onTap,
setState(() { hoverColor:
_isPressed = false; Colors.transparent, // 2. Desactiva el efecto hover (pasar el ratón)
}); splashColor:
}, Colors.transparent, // 3. Desactiva el efecto de onda al hacer clic
onTapCancel: widget.onTap == null highlightColor:
? null Colors.transparent, // Desactiva el brillo al mantener pulsado
: () { child: Padding(
setState(() { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
_isPressed = false; child: Row(
}); crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
onTap: widget.onTap, Expanded(
child: Container( child: Column(
padding: const EdgeInsets.all(16), mainAxisSize: MainAxisSize.min,
decoration: BoxDecoration( crossAxisAlignment: CrossAxisAlignment.start,
color: const Color.fromRGBO(24, 25, 26, 1), children: [
borderRadius: BorderRadius.circular(12), Row(
border: Border.all( crossAxisAlignment: CrossAxisAlignment.center,
color: widget.borderColor ?? Colors.white24, children: [
width: 1, if (categoryIcon != null) ...[
), SizedBox(
), width: 18,
child: LayoutBuilder( height: 18,
builder: (BuildContext context, BoxConstraints constraints) { child: Icon(
// Estimate whether the body will exceed 20 lines without always categoryIcon,
// running the expensive TextPainter layout. This heuristic counts size: 18,
// newline characters and estimates wrapped lines based on an color: categoryColor ?? palette.textSecondary,
// average characters-per-line to handle many short lines well. ),
final List<String> rawLines = widget.note.body.split('\n'); ),
const int avgCharsPerLine = 40; const SizedBox(width: 4),
int estimatedLines = 0; ],
for (final String line in rawLines) { Expanded(
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1; child: Text(
} note.title.isEmpty ? 'Sin título' : note.title,
maxLines: 1,
final bool needsPreciseMeasurement = estimatedLines > 20; overflow: TextOverflow.ellipsis,
final bool isBodyTruncated; style: TextStyle(
color: palette.textPrimary,
if (needsPreciseMeasurement) { fontSize: 15,
final TextPainter textPainter = TextPainter( fontWeight: FontWeight.w700,
text: TextSpan( ),
text: widget.note.body, ),
style: const TextStyle(color: Colors.white70, fontSize: 14), ),
), ],
maxLines: 20,
textDirection: TextDirection.ltr,
)..layout(maxWidth: constraints.maxWidth);
isBodyTruncated = textPainter.didExceedMaxLines;
} else {
isBodyTruncated = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.note.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
), ),
maxLines: 2, const SizedBox(height: 6),
overflow: TextOverflow.ellipsis, Text(
), bodyText.isEmpty ? ' ' : bodyText,
const SizedBox(height: 8), maxLines: 1,
Text( overflow: TextOverflow.ellipsis,
widget.note.body,
style: const TextStyle(color: Colors.white70, fontSize: 14),
maxLines: 20,
overflow: TextOverflow.clip,
),
if (isBodyTruncated) ...[
const SizedBox(height: 4),
const Text(
'...',
style: TextStyle( style: TextStyle(
color: Colors.white54, color: palette.textSecondary,
fontSize: 18, fontSize: 14,
height: 1, height: 1.2,
), ),
), ),
const SizedBox(height: 4),
], ],
], ),
); ),
}, const SizedBox(width: 8),
Builder(
builder: (BuildContext buttonContext) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert, color: palette.textSecondary),
color: palette.surfaceElevated,
elevation: 10,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
onSelected: (String value) {
switch (value) {
case 'category':
onChangeCategory?.call(buttonContext);
return;
case 'delete':
onDelete?.call();
return;
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'category',
child: Text('Modificar categoría'),
),
PopupMenuItem<String>(
value: 'delete',
child: Row(
children: const [
Icon(Icons.delete_outline, color: Colors.red),
SizedBox(width: 10),
Text(
'Eliminar',
style: TextStyle(color: Colors.red),
),
],
),
),
],
);
},
),
],
), ),
), ),
), ),
); );
} }
} }
+28 -23
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class SearchAppBar extends StatefulWidget { class SearchAppBar extends StatefulWidget {
const SearchAppBar({ const SearchAppBar({
@@ -51,22 +52,23 @@ class _SearchAppBarState extends State<SearchAppBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.transparent, color: palette.transparent,
border: Border( border: Border(bottom: BorderSide(color: palette.border, width: 0.5)),
bottom: BorderSide(
color: Colors.white.withValues(alpha: 0.12),
width: 0.5,
),
),
), ),
padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7), padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7),
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
onPressed: widget.onLeadingPressed ?? widget.onMenuPressed, onPressed: widget.onLeadingPressed ?? widget.onMenuPressed,
icon: Icon(widget.leadingIcon, color: Colors.white70, size: 20), icon: Icon(
widget.leadingIcon,
color: palette.textSecondary,
size: 20,
),
tooltip: widget.leadingTooltip, tooltip: widget.leadingTooltip,
splashRadius: 18, splashRadius: 18,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40), constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
@@ -84,18 +86,21 @@ class _SearchAppBarState extends State<SearchAppBar> {
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: widget.onSearchChanged, onChanged: widget.onSearchChanged,
style: const TextStyle(color: Colors.white, fontSize: 13), style: TextStyle(
cursorColor: Colors.white70, color: palette.textPrimary,
fontSize: 13,
),
cursorColor: palette.textSecondary,
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.searchHint, hintText: widget.searchHint,
hintStyle: TextStyle( hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5), color: palette.textSecondary.withValues(alpha: 0.6),
), ),
suffixIcon: _searchController.text.isNotEmpty suffixIcon: _searchController.text.isNotEmpty
? IconButton( ? IconButton(
icon: const Icon( icon: Icon(
Icons.clear, Icons.clear,
color: Colors.white70, color: palette.textSecondary,
size: 18, size: 18,
), ),
onPressed: () { onPressed: () {
@@ -107,37 +112,37 @@ class _SearchAppBarState extends State<SearchAppBar> {
minHeight: 36, minHeight: 36,
), ),
) )
: const Padding( : Padding(
padding: EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: Icon( child: Icon(
Icons.search, Icons.search,
color: Colors.white70, color: palette.textSecondary,
size: 18, size: 18,
), ),
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.2), color: palette.border,
width: 0.5, width: 0.5,
), ),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.2), color: palette.border,
width: 0.5, width: 0.5,
), ),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.4), color: palette.accent,
width: 0.5, width: 0.6,
), ),
), ),
filled: true, filled: true,
fillColor: Colors.white.withValues(alpha: 0.05), fillColor: palette.fill,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@@ -151,8 +156,8 @@ class _SearchAppBarState extends State<SearchAppBar> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
widget.titleText ?? '', widget.titleText ?? '',
style: const TextStyle( style: TextStyle(
color: Colors.white, color: palette.textPrimary,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
+12 -9
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/sync_status.dart'; import 'package:notas/widgets/sync_status.dart';
class SyncStatusIndicator extends StatelessWidget { class SyncStatusIndicator extends StatelessWidget {
@@ -86,12 +87,14 @@ class SyncStatusIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
switch (status) { switch (status) {
case SyncStatus.idle: case SyncStatus.idle:
return Tooltip( return Tooltip(
message: _messageForStatus(), message: _messageForStatus(),
child: _buildIndicator( child: _buildIndicator(
const Icon(Icons.cloud_outlined, size: 16, color: Colors.white38), Icon(Icons.cloud_outlined, size: 16, color: palette.textSecondary),
), ),
); );
@@ -101,7 +104,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.sync, icon: Icons.sync,
color: const Color.fromARGB(255, 165, 165, 165), color: palette.syncPreparing,
determinate: false, determinate: false,
), ),
), ),
@@ -113,7 +116,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.cloud_upload_outlined, icon: Icons.cloud_upload_outlined,
color: const Color.fromARGB(255, 109, 191, 255), color: palette.syncEncrypting,
determinate: true, determinate: true,
), ),
), ),
@@ -125,7 +128,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.cloud_upload, icon: Icons.cloud_upload,
color: const Color.fromARGB(255, 98, 190, 255), color: palette.syncUploading,
determinate: false, determinate: false,
), ),
), ),
@@ -137,7 +140,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.cloud_sync_outlined, icon: Icons.cloud_sync_outlined,
color: const Color.fromARGB(255, 150, 150, 150), color: palette.syncWaiting,
determinate: false, determinate: false,
), ),
), ),
@@ -149,7 +152,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.cloud_download_outlined, icon: Icons.cloud_download_outlined,
color: const Color.fromARGB(255, 154, 194, 112), color: palette.syncDecrypting,
determinate: true, determinate: true,
), ),
), ),
@@ -161,7 +164,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator( child: _buildIndicator(
_buildStatusBadge( _buildStatusBadge(
icon: Icons.sync, icon: Icons.sync,
color: const Color.fromARGB(255, 150, 150, 150), color: palette.syncWaiting,
determinate: false, determinate: false,
), ),
), ),
@@ -171,7 +174,7 @@ class SyncStatusIndicator extends StatelessWidget {
return Tooltip( return Tooltip(
message: _messageForStatus(), message: _messageForStatus(),
child: _buildIndicator( child: _buildIndicator(
const Icon(Icons.check_circle, size: 16, color: Colors.green), Icon(Icons.check_circle, size: 16, color: palette.success),
), ),
); );
@@ -179,7 +182,7 @@ class SyncStatusIndicator extends StatelessWidget {
return Tooltip( return Tooltip(
message: _messageForStatus(), message: _messageForStatus(),
child: _buildIndicator( child: _buildIndicator(
const Icon(Icons.error, size: 16, color: Colors.red), Icon(Icons.error, size: 16, color: palette.destructiveAccent),
), ),
); );
} }
@@ -8,6 +8,7 @@
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar = g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar); window_manager_plugin_register_with_registrar(window_manager_registrar);
+1
View File
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux flutter_secure_storage_linux
screen_retriever_linux screen_retriever_linux
url_launcher_linux
window_manager window_manager
) )
@@ -7,14 +7,18 @@ import Foundation
import flutter_secure_storage_darwin import flutter_secure_storage_darwin
import local_auth_darwin import local_auth_darwin
import quill_native_bridge_macos
import screen_retriever_macos import screen_retriever_macos
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+323 -70
View File
@@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "99.0.0" version: "100.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.1.0" version: "13.0.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -141,10 +141,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_assets name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.2.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -161,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: d687bec93342bf6a764a116d15c8694ebeff10e633dc28a39dd3144f7195024e
url: "https://pub.dev"
source: hosted
version: "0.3.5+3"
crypto: crypto:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -177,6 +185,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.0" version: "2.9.0"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -185,30 +201,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.9" version: "1.0.9"
dart_quill_delta:
dependency: transitive
description:
name: dart_quill_delta
sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4
url: "https://pub.dev"
source: hosted
version: "10.8.3"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.8" version: "3.1.9"
diff_match_patch:
dependency: transitive
description:
name: diff_match_patch
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
url: "https://pub.dev"
source: hosted
version: "0.4.1"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e" sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.33.0" version: "2.34.0"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa sha256: "042a5fb507ab5697f67eb55b75cfff2f665701f6606926136d6d4e85f81ff837"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.33.0" version: "2.34.1+1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -225,14 +257,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: file:
dependency: transitive dependency: transitive
description: description:
@@ -241,6 +265,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -254,6 +294,54 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_colorpicker:
dependency: transitive
description:
name: flutter_colorpicker
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_keyboard_visibility_linux:
dependency: transitive
description:
name: flutter_keyboard_visibility_linux
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_macos:
dependency: transitive
description:
name: flutter_keyboard_visibility_macos
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_temp_fork:
dependency: transitive
description:
name: flutter_keyboard_visibility_temp_fork
sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048
url: "https://pub.dev"
source: hosted
version: "0.1.5"
flutter_keyboard_visibility_windows:
dependency: transitive
description:
name: flutter_keyboard_visibility_windows
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -270,38 +358,59 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.34" version: "2.0.35"
flutter_quill:
dependency: "direct main"
description:
name: flutter_quill
sha256: "3ee7125b2dd3f3bce3ebdaac722a72f0c8aff3db9aa19053a9d777db12d71c98"
url: "https://pub.dev"
source: hosted
version: "11.5.1"
flutter_quill_delta_from_html:
dependency: transitive
description:
name: flutter_quill_delta_from_html
sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2"
url: "https://pub.dev"
source: hosted
version: "1.5.3"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_secure_storage name: flutter_secure_storage
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62" sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.0" version: "10.3.1"
flutter_secure_storage_darwin: flutter_secure_storage_darwin:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_darwin name: flutter_secure_storage_darwin
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb" sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.3.2"
flutter_secure_storage_linux: flutter_secure_storage_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_linux name: flutter_secure_storage_linux
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
flutter_secure_storage_platform_interface: flutter_secure_storage_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -322,10 +431,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_windows name: flutter_secure_storage_windows
sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8" sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.1" version: "4.1.0"
flutter_staggered_grid_view: flutter_staggered_grid_view:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -364,18 +473,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: hooks name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "2.0.2"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.6" version: "1.6.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@@ -396,10 +513,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.9.1"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -484,10 +601,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: b201c006fa769c23386f89aa6837ec0eb8179fcfb212eadcf87b422b3f9a6a78 sha256: fdb936d59ab945c7af297defd67bd1ed87b11b6db1bc16d01e94677a8f1c38ec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "2.0.9"
local_auth_darwin: local_auth_darwin:
dependency: transitive dependency: transitive
description: description:
@@ -520,6 +637,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
source: hosted
version: "7.3.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -556,18 +681,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: native_toolchain_c name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.6" version: "0.19.2"
objective_c: objective_c:
dependency: transitive dependency: transitive
description: description:
name: objective_c name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.3.0" version: "9.4.1"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -588,10 +713,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@@ -612,18 +737,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.2"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
@@ -688,6 +813,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
quill_native_bridge:
dependency: transitive
description:
name: quill_native_bridge
sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18"
url: "https://pub.dev"
source: hosted
version: "11.1.0"
quill_native_bridge_android:
dependency: transitive
description:
name: quill_native_bridge_android
sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569
url: "https://pub.dev"
source: hosted
version: "0.0.1+2"
quill_native_bridge_ios:
dependency: transitive
description:
name: quill_native_bridge_ios
sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f
url: "https://pub.dev"
source: hosted
version: "0.0.1"
quill_native_bridge_macos:
dependency: transitive
description:
name: quill_native_bridge_macos
sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77"
url: "https://pub.dev"
source: hosted
version: "0.0.1"
quill_native_bridge_platform_interface:
dependency: transitive
description:
name: quill_native_bridge_platform_interface
sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526"
url: "https://pub.dev"
source: hosted
version: "0.0.2+1"
quill_native_bridge_web:
dependency: transitive
description:
name: quill_native_bridge_web
sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
quill_native_bridge_windows:
dependency: transitive
description:
name: quill_native_bridge_windows
sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
quiver:
dependency: transitive
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.2"
recase: recase:
dependency: transitive dependency: transitive
description: description:
@@ -708,42 +897,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
screen_retriever_linux: screen_retriever_linux:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever_linux name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
screen_retriever_macos: screen_retriever_macos:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever_macos name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
screen_retriever_platform_interface: screen_retriever_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever_platform_interface name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
screen_retriever_windows: screen_retriever_windows:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever_windows name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -756,10 +945,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.23" version: "2.4.26"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -841,18 +1030,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3 name: sqlite3
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.3"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
name: sqlparser name: sqlparser
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978 sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.44.4" version: "0.44.5"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -909,6 +1098,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev"
source: hosted
version: "6.3.32"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid: uuid:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -969,10 +1222,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.0" version: "5.15.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -993,10 +1246,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.6.1" version: "7.0.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -1006,5 +1259,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.11.5 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.44.0"
+10 -8
View File
@@ -2,7 +2,7 @@ name: notas
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
@@ -44,10 +44,14 @@ dependencies:
flutter_secure_storage: ^10.2.0 flutter_secure_storage: ^10.2.0
local_auth: ^3.0.1 local_auth: ^3.0.1
sqlite3: ^3.3.1 sqlite3: ^3.3.1
http: ^0.13.6 http: ^1.6.0
crypto: ^3.0.6 crypto: ^3.0.6
cryptography: ^2.7.0 cryptography: ^2.7.0
uuid: ^4.0.0 uuid: ^4.0.0
flutter_quill: ^11.5.1
flutter_localizations:
sdk: flutter
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -68,7 +72,6 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.
@@ -113,27 +116,26 @@ hooks:
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
flutter_launcher_icons: flutter_launcher_icons:
# Configuración general y móvil # Configuración general y móvil
image_path: "assets/icon.png" image_path: "assets/icon.png"
android: true android: true
ios: false ios: false
# Configuración para Windows # Configuración para Windows
windows: windows:
generate: true generate: true
image_path: "assets/icon.png" image_path: "assets/icon.png"
icon_size: 256 # Tamaño máximo requerido por Windows icon_size: 256 # Tamaño máximo requerido por Windows
# Configuración para Web (Opcional, pero recomendado) # Configuración para Web (Opcional, pero recomendado)
web: web:
generate: false generate: false
image_path: "assets/icon.png" image_path: "assets/icon.png"
background_color: "#FFFFFF" # Cambia esto al color de fondo de tu app background_color: "#FFFFFF" # Cambia esto al color de fondo de tu app
theme_color: "#FFFFFF" theme_color: "#FFFFFF"
# Configuración para macOS (Por si en el futuro compilas para Mac) # Configuración para macOS (Por si en el futuro compilas para Mac)
macos: macos:
generate: true generate: true
image_path: "assets/icon.png" image_path: "assets/icon.png"
+51 -34
View File
@@ -1,31 +1,39 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:notas/models/category.dart';
import 'package:notas/models/note.dart'; import 'package:notas/models/note.dart';
import 'package:notas/screens/note_editor_screen.dart'; import 'package:notas/screens/note_editor_screen.dart';
void main() { void main() {
testWidgets('saves a note when only the category changes', ( testWidgets('autosaves a note when only the category changes', (
WidgetTester tester, WidgetTester tester,
) async { ) async {
Note? savedNote; Note? savedNote;
final Note initialNote = Note(
title: 'Sin título',
body: '',
createdAt: DateTime(2026, 5, 21),
updatedAt: DateTime(2026, 5, 21),
position: 0,
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
],
home: Scaffold( home: Scaffold(
body: NoteEditorScreen( body: NoteEditorScreen(
note: null, repository: null,
categoryId: null, note: initialNote,
categories: <Category>[ saveNote: (Note note) async => note,
Category( onSaved: (Note result) {
id: 'work', savedNote = result;
name: 'Trabajo',
updatedAt: DateTime(2026, 5, 21),
),
],
onComplete: (dynamic result) {
savedNote = result as Note?;
}, },
), ),
), ),
@@ -38,44 +46,53 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('Trabajo').last); await tester.tap(find.text('Trabajo').last);
await tester.pumpAndSettle(); await tester.pump();
await tester.pump(const Duration(seconds: 2));
await tester.tap(find.text('Guardar'));
await tester.pumpAndSettle();
expect(savedNote, isNotNull); expect(savedNote, isNotNull);
expect(savedNote!.categoryId, 'work'); expect(savedNote!.categoryId, 'work');
expect(savedNote!.title, 'Sin título'); expect(savedNote!.title, 'Sin título');
}); });
testWidgets('only completes once when save is tapped twice', ( testWidgets('debounces multiple edits into a single save', (
WidgetTester tester, WidgetTester tester,
) async { ) async {
int completionCount = 0; int saveCount = 0;
final Note initialNote = Note(
title: 'Sin título',
body: '',
createdAt: DateTime(2026, 5, 21),
updatedAt: DateTime(2026, 5, 21),
position: 0,
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
],
home: Scaffold( home: Scaffold(
body: NoteEditorScreen( body: NoteEditorScreen(
note: null, repository: null,
categoryId: null, note: initialNote,
categories: <Category>[], saveNote: (Note note) async {
onComplete: (dynamic result) { saveCount += 1;
if (result is Note) { return note;
completionCount += 1;
}
}, },
), ),
), ),
), ),
); );
await tester.enterText(find.byType(TextField).first, 'Nota de prueba'); await tester.enterText(find.byType(TextField).first, 'Primera versión');
await tester.pump(const Duration(milliseconds: 300));
await tester.enterText(find.byType(TextField).first, 'Segunda versión');
await tester.pump(const Duration(seconds: 2));
await tester.tap(find.text('Guardar')); expect(saveCount, 1);
await tester.tap(find.text('Guardar'));
await tester.pumpAndSettle();
expect(completionCount, 1);
}); });
} }
+1 -1
View File
@@ -8,6 +8,6 @@ void main() {
await tester.pumpWidget(const NotesApp()); await tester.pumpWidget(const NotesApp());
expect(find.byType(MaterialApp), findsOneWidget); expect(find.byType(MaterialApp), findsOneWidget);
expect(find.text('Mis Notas'), findsWidgets); expect(find.text('Preparando el vault local...'), findsWidgets);
}); });
} }
@@ -6,18 +6,24 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.h> #include <local_auth_windows/local_auth_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
LocalAuthPluginRegisterWithRegistrar( LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin")); registry->GetRegistrarForPlugin("LocalAuthPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar( WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin")); registry->GetRegistrarForPlugin("WindowManagerPlugin"));
} }
+2
View File
@@ -3,9 +3,11 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows flutter_secure_storage_windows
local_auth_windows local_auth_windows
screen_retriever_windows screen_retriever_windows
url_launcher_windows
window_manager window_manager
) )
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB