Compare commits

..

16 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
Marcos 814f8f7c04 style: Format code for consistency and readability across database and note positioning files 2026-05-22 17:31:49 +02:00
Marcos 729e575a60 feat: Implement note positioning logic and tests for position conversion 2026-05-22 17:31:40 +02:00
43 changed files with 3177 additions and 2400 deletions
+2
View File
@@ -43,3 +43,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/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/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/api_client.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/settings_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/widgets/sync_status.dart';
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 _windowSizeSaveDelay = Duration(milliseconds: 350);
static const String _themeSeedColorKey = 'theme_seed_color_v1';
static const String _themeModeKey = 'theme_mode_v1';
final LocalVaultService _vaultService = LocalVaultService.instance;
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
@@ -69,6 +73,138 @@ class _NotesAppState extends State<NotesApp>
int _syncOperationId = 0;
int _homeRefreshToken = 0;
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
void initState() {
@@ -79,6 +215,7 @@ class _NotesAppState extends State<NotesApp>
windowManager.setPreventClose(true);
}
_loadThemeSeedColor();
_loadThemeMode();
_bootstrapVault();
}
@@ -105,13 +242,13 @@ class _NotesAppState extends State<NotesApp>
setState(() {
_themeSeedColor = Color(storedColorValue);
_themeData = AppTheme.theme(seedColor: _themeSeedColor);
_updateThemeData();
});
}
Future<void> _setThemeSeedColor(Color color) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_themeSeedColorKey, color.value);
await prefs.setInt(_themeSeedColorKey, color.toARGB32());
if (!mounted) {
return;
@@ -119,15 +256,54 @@ class _NotesAppState extends State<NotesApp>
setState(() {
_themeSeedColor = color;
_themeData = AppTheme.theme(seedColor: _themeSeedColor);
_updateThemeData();
});
}
// Cached ThemeData to avoid recomputing on every build.
ThemeData? _themeData;
Future<void> _loadThemeMode() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int? stored = prefs.getInt(_themeModeKey);
if (!mounted) return;
ThemeData get _theme =>
_themeData ??= AppTheme.theme(seedColor: _themeSeedColor);
setState(() {
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
void didChangeAppLifecycleState(AppLifecycleState state) {
@@ -418,32 +594,38 @@ class _NotesAppState extends State<NotesApp>
if (available) {
bool activated = await _vaultService.enableBiometricProtection();
while (!activated) {
// Ask the user to retry or skip
final BuildContext? dialogCtx = _navigatorKey.currentContext;
if (dialogCtx == null) {
break;
}
// Ask the user to retry or skip - pass currentContext directly in builder
final NavigatorState? navigator = _navigatorKey.currentState;
final NavigatorState navigator = Navigator.of(dialogCtx);
if (navigator == null) break;
if (!mounted) return;
final bool? retry = await showDialog<bool>(
context: dialogCtx,
builder: (BuildContext context) => AlertDialog(
title: const Text('No se pudo activar la biometría'),
content: const Text(
'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'),
context: context,
builder: (BuildContext dialogContext) {
final AppPalette palette = _activePalette();
return AlertDialog(
backgroundColor: palette.cardBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: palette.border),
),
FilledButton(
onPressed: () => navigator.pop(true),
child: const Text('Reintentar'),
title: const Text('No se pudo activar la biometría'),
content: const Text(
'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) {
@@ -766,8 +948,8 @@ class _NotesAppState extends State<NotesApp>
_homeRefreshToken += 1;
});
// Reset to idle after 3 seconds
Future<void>.delayed(const Duration(seconds: 3), () {
// Keep the completion state visible briefly so it can be read.
Future<void>.delayed(const Duration(seconds: 1), () {
if (mounted && syncOperationId == _syncOperationId) {
setState(() {
_syncStatus = SyncStatus.idle;
@@ -819,6 +1001,7 @@ class _NotesAppState extends State<NotesApp>
}
Widget _buildMainShell(NoteRepository repository) {
final AppPalette palette = _activePalette();
final Widget activeScreen = _currentSection == _AppSection.home
? HomeScreen(
key: const ValueKey<String>('home-screen'),
@@ -839,6 +1022,8 @@ class _NotesAppState extends State<NotesApp>
onForceSync: () => _performSync(forceFull: true),
currentSeedColor: _themeSeedColor,
onThemeColorSelected: _setThemeSeedColor,
currentThemeMode: _themeMode,
onThemeModeSelected: _setThemeMode,
);
return Shortcuts(
@@ -858,17 +1043,7 @@ class _NotesAppState extends State<NotesApp>
autofocus: true,
child: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
@@ -896,6 +1071,28 @@ class _NotesAppState extends State<NotesApp>
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',
debugShowCheckedModeBanner: false,
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,
);
}
+122 -48
View File
@@ -5,6 +5,8 @@ import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:notas/data/note_positioning.dart';
part 'app_database.g.dart';
@DataClassName('DbCategory')
@@ -16,7 +18,8 @@ class Categories extends Table {
BoolColumn get isDeleted =>
boolean().named('is_deleted').withDefault(const Constant(false))();
IntColumn get colorValue => integer().nullable().named('color_value')();
IntColumn get iconCodePoint => integer().nullable().named('icon_code_point')();
IntColumn get iconCodePoint =>
integer().nullable().named('icon_code_point')();
BoolColumn get isDirty =>
boolean().named('is_dirty').withDefault(const Constant(true))();
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
@@ -38,17 +41,17 @@ class Notes extends Table {
BoolColumn get isDeleted =>
boolean().named('is_deleted').withDefault(const Constant(false))();
TextColumn get categoryId => text().nullable().named('category_id')();
BoolColumn get isDirty =>
BoolColumn get isDirty =>
boolean().named('is_dirty').withDefault(const Constant(true))();
@override
Set<Column> get primaryKey => {id};
@override
Set<Column> get primaryKey => {id};
}
@DriftDatabase(tables: [Notes, Categories])
class AppDatabase extends _$AppDatabase {
@override
int get schemaVersion => 3;
int get schemaVersion => 4;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -69,6 +72,30 @@ class AppDatabase extends _$AppDatabase {
await customStatement('UPDATE categories SET color_value = NULL');
await customStatement('UPDATE categories SET icon_code_point = NULL');
}
if (from < 4) {
final List<DbNote> activeNotes =
await (select(notes)
..where((n) => n.isDeleted.equals(false))
..orderBy([
(note) => OrderingTerm(expression: note.sortIndex),
]))
.get();
final List<int> rebalancedPositions = rebalanceNotePositions(
activeNotes.length,
);
for (var index = 0; index < activeNotes.length; index += 1) {
final DbNote row = activeNotes[index];
await (update(notes)..where((n) => n.id.equals(row.id))).write(
NotesCompanion(
sortIndex: Value(rebalancedPositions[index]),
updatedAt: Value(DateTime.now()),
isDirty: const Value(true),
),
);
}
}
},
);
@@ -97,17 +124,38 @@ class AppDatabase extends _$AppDatabase {
// ========== Notes ==========
Future<List<DbNote>> getAllNotes() {
return (select(notes)
..orderBy([(note) => OrderingTerm(expression: note.sortIndex)])
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
])
..where((n) => n.isDeleted.equals(false)))
.get();
}
Future<int> insertNoteAtTop(NotesCompanion note) {
return transaction(() async {
await customStatement(
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE is_deleted = 0',
);
return into(notes).insert(note.copyWith(sortIndex: const Value<int>(0)));
final DbNote? topNote =
await (select(notes)
..where((n) => n.isDeleted.equals(false))
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
])
..limit(1))
.getSingleOrNull();
final int nextSortIndex = topNote == null
? 0
: topNote.sortIndex + notePositionStep;
await into(
notes,
).insert(note.copyWith(sortIndex: Value<int>(nextSortIndex)));
return nextSortIndex;
});
}
@@ -133,11 +181,6 @@ class AppDatabase extends _$AppDatabase {
isDirty: const Value(true),
),
);
await customStatement(
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND is_deleted = 0',
[removedIndex],
);
}
Future<void> deleteNoteAndShift({
@@ -165,44 +208,67 @@ class AppDatabase extends _$AppDatabase {
required int oldIndex,
required int newIndex,
}) {
if (oldIndex == newIndex) {
return Future<void>.value();
}
return transaction(() async {
final List<DbNote> all = await (select(
notes,
)..where((n) => n.isDeleted.equals(false))).get();
final List<DbNote> orderedNotes =
await (select(notes)
..where((n) => n.isDeleted.equals(false))
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
]))
.get();
final int count = all.length;
if (count == 0) {
final int currentIndex = orderedNotes.indexWhere(
(DbNote row) => row.id == id,
);
if (currentIndex == -1) {
return;
}
final int maxIndex = count - 1;
final int safeOld = oldIndex.clamp(0, maxIndex);
final int safeNew = newIndex.clamp(0, maxIndex);
if (safeOld == safeNew) {
final int safeNewIndex = newIndex.clamp(0, orderedNotes.length - 1);
if (currentIndex == safeNewIndex) {
return;
}
if (safeOld < safeNew) {
await customStatement(
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0',
[safeOld, safeNew],
);
final DbNote movedNote = orderedNotes.removeAt(currentIndex);
orderedNotes.insert(safeNewIndex, movedNote);
final int? newStoredPosition;
if (safeNewIndex == 0) {
newStoredPosition = orderedNotes[1].sortIndex + notePositionStep;
} else if (safeNewIndex == orderedNotes.length - 1) {
newStoredPosition =
orderedNotes[orderedNotes.length - 2].sortIndex - notePositionStep;
} else {
await customStatement(
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0',
[safeNew, safeOld],
newStoredPosition = midpointNotePosition(
higherPosition: orderedNotes[safeNewIndex - 1].sortIndex,
lowerPosition: orderedNotes[safeNewIndex + 1].sortIndex,
);
}
if (newStoredPosition == null) {
final List<int> rebalancedPositions = rebalanceNotePositions(
orderedNotes.length,
);
for (var index = 0; index < orderedNotes.length; index += 1) {
final DbNote row = orderedNotes[index];
await (update(notes)..where((n) => n.id.equals(row.id))).write(
NotesCompanion(
sortIndex: Value<int>(rebalancedPositions[index]),
updatedAt: Value<DateTime>(DateTime.now()),
isDirty: const Value(true),
),
);
}
return;
}
await (update(notes)..where((n) => n.id.equals(id))).write(
NotesCompanion(
sortIndex: Value<int>(safeNew),
sortIndex: Value<int>(newStoredPosition),
updatedAt: Value<DateTime>(DateTime.now()),
isDirty: const Value(true),
),
@@ -217,15 +283,23 @@ class AppDatabase extends _$AppDatabase {
}
Future<List<DbNote>> getDeletedNotes() {
// A note is considered deleted (in the trash) when `is_deleted` is true
// and at least one of `title` or `body` is not empty. Previously the
// query required both title AND body to be non-empty which excluded
// notes that had an empty body (common) from appearing in the trash.
return (select(notes)..where(
(n) => n.isDeleted.equals(true) &
(n.title.isNotValue('') | n.body.isNotValue('')),
))
.get();
// A note is considered deleted (in the trash) when `is_deleted` is true
// and at least one of `title` or `body` is not empty. Previously the
// query required both title AND body to be non-empty which excluded
// notes that had an empty body (common) from appearing in the trash.
return (select(notes)
..where(
(n) =>
n.isDeleted.equals(true) &
(n.title.isNotValue('') | n.body.isNotValue('')),
)
..orderBy([
(note) => OrderingTerm(
expression: note.sortIndex,
mode: OrderingMode.desc,
),
]))
.get();
}
Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
+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());
}
+46
View File
@@ -0,0 +1,46 @@
const int notePositionScale = 10;
const int notePositionStep = 1000 * notePositionScale;
const int notePositionRebalanceThreshold = 1;
int toStoredNotePosition(double position) {
return (position * notePositionScale).round();
}
double fromStoredNotePosition(int storedPosition) {
return storedPosition / notePositionScale;
}
int nextTopNotePosition(Iterable<int> storedPositions) {
int? highestPosition;
for (final int position in storedPositions) {
if (highestPosition == null || position > highestPosition) {
highestPosition = position;
}
}
if (highestPosition == null) {
return 0;
}
return highestPosition + notePositionStep;
}
int? midpointNotePosition({
required int higherPosition,
required int lowerPosition,
}) {
final int gap = higherPosition - lowerPosition;
if (gap <= notePositionRebalanceThreshold) {
return null;
}
return lowerPosition + (gap ~/ 2);
}
List<int> rebalanceNotePositions(int itemCount) {
return List<int>.generate(
itemCount,
(int index) => (itemCount - 1 - index) * notePositionStep,
);
}
+20 -12
View File
@@ -6,6 +6,7 @@ import 'dart:io' show Platform;
import 'package:drift/drift.dart';
import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart';
import 'package:notas/data/note_positioning.dart';
import 'package:notas/data/sync_models.dart';
import 'package:notas/models/note.dart';
import 'package:notas/models/category.dart';
@@ -55,12 +56,16 @@ class NoteRepository {
return categories;
}
Future<DateTime?> getLastSyncAt() async {
return _authApi.getLastSyncAt();
}
Future<void> createCategory(Category category) async {
debugPrint('createCategory called with: ${category.name}');
final DbCategory? existingCategory = await (
_database.select(_database.categories)..where((c) => c.id.equals(category.id))
).getSingleOrNull();
final DbCategory? existingCategory = await (_database.select(
_database.categories,
)..where((c) => c.id.equals(category.id))).getSingleOrNull();
final int effectiveServerVersion = math.max(
category.serverVersion,
existingCategory?.serverVersion ?? category.serverVersion,
@@ -91,7 +96,7 @@ class NoteRepository {
}
Future<Note> createNote(Note note) async {
await _database.insertNoteAtTop(
final int storedPosition = await _database.insertNoteAtTop(
NotesCompanion.insert(
id: note.id,
title: note.title,
@@ -106,7 +111,10 @@ class NoteRepository {
),
);
return note.copyWith(position: 0, isDirty: true);
return note.copyWith(
position: fromStoredNotePosition(storedPosition),
isDirty: true,
);
}
Future<Note> updateNote(Note note) async {
@@ -137,7 +145,7 @@ class NoteRepository {
isDeleted: false,
isPermanentlyDeleted: false,
isDirty: true,
position: row.sortIndex.toDouble(),
position: fromStoredNotePosition(row.sortIndex),
);
}
@@ -364,7 +372,7 @@ class NoteRepository {
body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position.round(),
sortIndex: toStoredNotePosition(noteResponse.position),
serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted,
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
@@ -382,7 +390,7 @@ class NoteRepository {
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position.round()),
sortIndex: Value(toStoredNotePosition(noteResponse.position)),
serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted),
categoryId: Value(
@@ -412,7 +420,7 @@ class NoteRepository {
body: row.body,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
position: row.sortIndex.toDouble(),
position: fromStoredNotePosition(row.sortIndex),
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row),
@@ -585,7 +593,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
'updatedAt': row.updatedAt.toIso8601String(),
'categoryId': row.categoryId,
'serverVersion': row.serverVersion,
'position': row.sortIndex,
'position': fromStoredNotePosition(row.sortIndex),
'isDeleted': row.isDeleted,
'isPermanentlyDeleted': isPermanentlyDeleted,
};
@@ -635,7 +643,7 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
'encryptedTitle': encryptedTitle,
'encryptedBody': encryptedBody,
'serverVersion': note['serverVersion']! as int,
'position': note['position']! as int,
'position': (note['position'] as num).toDouble(),
'isDeleted': note['isDeleted']! as bool,
'isPermanentlyDeleted': isPermanentlyDeleted,
'updatedAt': note['updatedAt']! as String,
@@ -668,7 +676,7 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
masterKey,
);
} catch (e) {
print('Failed to decrypt note ${note['id']}: $e');
debugPrint('Failed to decrypt note ${note['id']}: $e');
}
} else {
decryptedTitle = '';
+19 -20
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class BiometricChoiceScreen extends StatelessWidget {
const BiometricChoiceScreen({
@@ -14,19 +15,11 @@ class BiometricChoiceScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
@@ -39,12 +32,12 @@ class BiometricChoiceScreen extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF1D1E20),
color: palette.cardBackground,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
border: Border.all(color: palette.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.35),
color: palette.shadowSoft,
blurRadius: 30,
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?',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
color: palette.textSecondary,
height: 1.4,
),
),
@@ -82,13 +75,17 @@ class BiometricChoiceScreen extends StatelessWidget {
FilledButton(
onPressed: isBusy ? null : onEnableBiometrics,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
padding: const EdgeInsets.symmetric(
vertical: 14,
),
),
child: isBusy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('Sí, activar huella'),
),
@@ -96,9 +93,11 @@ class BiometricChoiceScreen extends StatelessWidget {
OutlinedButton(
onPressed: isBusy ? null : onSkipBiometrics,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Colors.white24),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 14,
),
side: BorderSide(color: palette.border),
foregroundColor: palette.textPrimary,
),
child: const Text('No, entrar sin huella'),
),
+17 -18
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class BiometricGateScreen extends StatefulWidget {
const BiometricGateScreen({
@@ -38,19 +39,11 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
@@ -63,12 +56,12 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF1D1E20),
color: palette.cardBackground,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
border: Border.all(color: palette.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.35),
color: palette.shadowSoft,
blurRadius: 30,
offset: const Offset(0, 18),
),
@@ -98,21 +91,27 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
'Pon tu huella o cara para entrar a tus notas.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
color: palette.textSecondary,
height: 1.4,
),
),
const SizedBox(height: 22),
FilledButton(
onPressed: widget.isBusy ? null : widget.onUnlockRequested,
onPressed: widget.isBusy
? null
: widget.onUnlockRequested,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
padding: const EdgeInsets.symmetric(
vertical: 14,
),
),
child: widget.isBusy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('Desbloquear'),
),
+1084 -1045
View File
File diff suppressed because it is too large Load Diff
+177 -642
View File
@@ -1,669 +1,217 @@
import 'dart:async';
import 'dart:math' as math;
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/platform/app_platform.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.
import 'package:notas/theme/app_palette.dart';
class NoteEditorScreen extends StatefulWidget {
const NoteEditorScreen({
super.key,
this.repository,
this.saveNote,
required this.note,
this.categoryId,
this.categories = const <Category>[],
this.onComplete,
this.embedded = false,
this.onSaved,
});
final Note? note;
final String? categoryId;
final List<Category> categories;
final ValueChanged<dynamic>? onComplete;
final NoteRepository? repository;
final Future<Note> Function(Note note)? saveNote;
final Note note;
final bool embedded;
final ValueChanged<Note>? onSaved;
@override
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> {
late TextEditingController _titleController;
late TextEditingController _bodyController;
late Note _currentNote;
late bool _isNewNote;
String? _selectedCategoryId;
final GlobalKey _categorySelectorKey = GlobalKey();
OverlayEntry? _categoryMenuEntry;
bool _didComplete = false;
static const Duration _debounceDuration = Duration(seconds: 1);
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
void initState() {
super.initState();
_isNewNote = widget.note == null;
if (_isNewNote) {
final DateTime now = DateTime.now();
_currentNote = Note(
title: '',
body: '',
createdAt: now,
updatedAt: now,
position: 0,
categoryId: widget.categoryId,
);
} else {
_currentNote = widget.note!;
}
_selectedCategoryId = _currentNote.categoryId ?? widget.categoryId;
_titleController = TextEditingController(text: _currentNote.title);
_bodyController = TextEditingController(text: _currentNote.body);
_baselineNote = widget.note;
_titleController = TextEditingController(text: widget.note.title)
..addListener(_scheduleSave);
_bodyController = QuillController(
document: noteBodyToDocument(widget.note.body),
selection: const TextSelection.collapsed(offset: 0),
)..addListener(_scheduleSave);
_bodyFocusNode = FocusNode();
_bodyScrollController = ScrollController();
}
@override
void dispose() {
_closeCategoryMenu();
_debounceTimer?.cancel();
_titleController.dispose();
_bodyController.dispose();
_bodyFocusNode.dispose();
_bodyScrollController.dispose();
super.dispose();
}
void _complete(dynamic result) {
if (_didComplete) {
String _bodyAsJson() {
return noteDocumentToStorageJson(_bodyController.document);
}
void _scheduleSave() {
_debounceTimer?.cancel();
_debounceTimer = Timer(_debounceDuration, () {
unawaited(_saveNow());
});
}
Future<void> _saveNow() async {
if (!mounted) {
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 body = _bodyController.text.trim();
final bool categoryChanged = _selectedCategoryId != _currentNote.categoryId;
if (title.isEmpty && body.isEmpty && !categoryChanged) {
_complete(null);
return;
}
final Note updatedNote = _currentNote.copyWith(
final String body = _bodyAsJson();
final Note draft = _baselineNote.copyWith(
title: title.isEmpty ? 'Sin título' : title,
body: body,
categoryId: _selectedCategoryId,
categoryId: _baselineNote.categoryId,
updatedAt: DateTime.now(),
isDirty: true,
);
_complete(updatedNote);
}
final bool hasChanges =
draft.title != _baselineNote.title ||
draft.body != _baselineNote.body ||
draft.categoryId != _baselineNote.categoryId;
Widget _buildDeleteConfirmationDialog({
required ValueChanged<bool> onConfirmed,
}) {
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;
if (!hasChanges) {
return;
}
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
if (overlayState == null) {
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;
if (_isSaving) {
_saveQueued = true;
return;
}
final Completer<bool> completer = Completer<bool>();
late final OverlayEntry entry;
bool didRemove = false;
entry = OverlayEntry(
builder: (BuildContext overlayContext) {
final ValueChanged<bool> close = (bool confirmed) {
if (!completer.isCompleted) {
completer.complete(confirmed);
}
if (!didRemove && entry.mounted) {
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),
),
),
],
),
_isSaving = true;
try {
final Note saved = widget.saveNote != null
? await widget.saveNote!(draft)
: await widget.repository!.updateNote(draft);
_baselineNote = saved;
widget.onSaved?.call(saved);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('No se pudo guardar la nota: $error')),
);
},
);
overlayState.insert(entry);
return completer.future;
}
Category? _categoryById(String? id) {
for (final Category category in widget.categories) {
if (category.id == id) {
return category;
}
} finally {
_isSaving = false;
if (_saveQueued) {
_saveQueued = false;
unawaited(_saveNow());
}
}
return null;
}
Color _categoryBackgroundColor(Category? category) {
if (category?.colorValue == null) {
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;
Widget _buildEditorBody() {
final AppPalette palette = _paletteOf(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.white12, width: 1)),
),
child: Row(
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(
'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(
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: TextField(
controller: _titleController,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
style: TextStyle(
color: palette.textPrimary,
fontSize: 26,
fontWeight: FontWeight.w700,
),
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Título',
hintStyle: TextStyle(color: Colors.white30),
hintStyle: TextStyle(color: palette.textHint),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
SizedBox(height: titleSpacing),
Expanded(
child: TextField(
controller: _bodyController,
keyboardType: TextInputType.multiline,
maxLines: null,
expands: true,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
height: 1.6,
),
decoration: const InputDecoration(
hintText: 'Escribe tu nota...',
hintStyle: TextStyle(color: Colors.white30),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
],
),
),
],
),
const SizedBox(height: 10),
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
child: QuillEditor.basic(
controller: _bodyController,
focusNode: _bodyFocusNode,
scrollController: _bodyScrollController,
config: QuillEditorConfig(
scrollable: true,
padding: EdgeInsets.zero,
autoFocus: false,
expands: true,
placeholder: 'Escribe tu nota...',
keyboardAppearance: Theme.of(context).brightness,
),
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.white12, width: 1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!_isNewNote)
IconButton(
onPressed: _deleteNote,
icon: const Icon(Icons.delete_outline, color: Colors.red),
tooltip: 'Eliminar nota',
)
else
const SizedBox(width: 48),
FilledButton(onPressed: _saveNote, child: const Text('Guardar')),
],
const SizedBox(height: 8),
QuillSimpleToolbar(
controller: _bodyController,
config: const QuillSimpleToolbarConfig(
color: Colors.transparent,
showBoldButton: true,
showItalicButton: true,
showUnderLineButton: true,
showStrikeThrough: false,
showInlineCode: false,
showColorButton: false,
showBackgroundColorButton: false,
showClearFormat: false,
showAlignmentButtons: false,
showHeaderStyle: false,
showListNumbers: true,
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,
),
),
],
@@ -672,51 +220,38 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
@override
Widget build(BuildContext context) {
if (_isMobileLayout) {
return Material(
color: Colors.transparent,
child: SafeArea(
child: Container(
color: const Color.fromARGB(255, 24, 25, 26),
child: _buildEditorContent(isMobile: true),
),
),
);
final AppPalette palette = _paletteOf(context);
final Widget editor = Padding(
padding: const EdgeInsets.all(8),
child: _buildEditorBody(),
);
if (widget.embedded) {
return editor;
}
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double maxWidth = math.min(constraints.maxWidth - 32, 600);
final double maxHeight = math.min(constraints.maxHeight - 32, 720);
return Stack(
children: [
Positioned.fill(
child: ModalBarrier(
dismissible: false,
color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5),
),
),
Positioned.fill(
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),
),
return Container(
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: const Text('Editar nota'),
backgroundColor: Colors.transparent,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: palette.border, width: 0.5),
),
),
),
],
);
},
),
),
body: SafeArea(child: editor),
),
);
}
}
+359 -228
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.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/data/api_client.dart';
@@ -11,6 +12,8 @@ class SettingsScreen extends StatefulWidget {
required this.onForceSync,
required this.currentSeedColor,
required this.onThemeColorSelected,
required this.currentThemeMode,
required this.onThemeModeSelected,
});
final Future<void> Function() onDeleteAllData;
@@ -18,6 +21,8 @@ class SettingsScreen extends StatefulWidget {
final Future<void> Function() onForceSync;
final Color currentSeedColor;
final Future<void> Function(Color color) onThemeColorSelected;
final ThemeMode currentThemeMode;
final Future<void> Function(ThemeMode mode) onThemeModeSelected;
@override
State<SettingsScreen> createState() => _SettingsScreenState();
@@ -29,32 +34,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _isServerDeleting = false;
bool _isThemeSaving = false;
final TextEditingController _endpointController = TextEditingController();
final TextEditingController _encryptionKeyController = TextEditingController();
final TextEditingController _encryptionKeyController =
TextEditingController();
bool _endpointLoading = true;
bool _encryptionKeyLoading = false;
bool _encryptionKeyVisible = false;
late Color _selectedSeedColor;
late ThemeMode _selectedThemeMode;
static const List<Color> _themeColorOptions = <Color>[
Colors.amber,
Colors.blue,
Colors.teal,
Colors.green,
Colors.pink,
Colors.purple,
];
static const List<Color> _themeColorOptions = AppPalette.themeSeedColors;
Future<void> _confirmAndDeleteAll() async {
final bool? confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Borrar todos los datos'),
content: const Text('¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. 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: const Text('Borrar', style: TextStyle(color: Colors.red))),
],
),
builder: (context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return AlertDialog(
backgroundColor: palette.cardBackground,
shape: RoundedRectangleBorder(
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;
@@ -69,7 +88,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return;
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) {
if (!mounted) return;
@@ -77,35 +98,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al borrar los datos: $error')),
);
} finally {
if (!mounted) return;
setState(() {
_isBusy = false;
});
if (mounted) {
setState(() {
_isBusy = false;
});
}
}
}
Future<void> _confirmAndDeleteServerData() async {
final bool? confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Borrar toda la info del servidor'),
content: const Text(
'¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
builder: (context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return AlertDialog(
backgroundColor: palette.cardBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: palette.border),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text(
'Borrar',
style: TextStyle(color: Colors.red),
title: const Text('Borrar toda la info del servidor'),
content: const Text(
'¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.',
),
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;
@@ -115,11 +145,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
try {
final Map<String, dynamic> response =
await AuthApi.instance.deleteAllServerData();
final Map<String, dynamic> response = await AuthApi.instance
.deleteAllServerData();
if (response['error'] == true) {
throw Exception(response['body'] ?? response['message'] ?? 'Error desconocido');
throw Exception(
response['body'] ?? response['message'] ?? 'Error desconocido',
);
}
await AuthApi.instance.clearTokens();
@@ -127,7 +159,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return;
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) {
if (!mounted) return;
@@ -135,10 +169,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al borrar la info del servidor: $error')),
);
} finally {
if (!mounted) return;
setState(() {
_isServerDeleting = false;
});
if (mounted) {
setState(() {
_isServerDeleting = false;
});
}
}
}
@@ -166,11 +201,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(content: Text('Error al forzar la sincronización: $error')),
);
} finally {
if (!mounted) return;
setState(() {
_isSyncing = false;
});
if (mounted) {
setState(() {
_isSyncing = false;
});
}
}
}
@@ -212,6 +247,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void initState() {
super.initState();
_selectedSeedColor = widget.currentSeedColor;
_selectedThemeMode = widget.currentThemeMode;
_loadEndpoint();
}
@@ -222,6 +258,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.currentSeedColor != _selectedSeedColor) {
_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 {
@@ -239,7 +295,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
try {
final String? encryptionKey = await LocalVaultService.instance.readEncryptionKey();
final String? encryptionKey = await LocalVaultService.instance
.readEncryptionKey();
if (!mounted) return;
@@ -284,10 +341,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
required bool isLoading,
required IconData icon,
}) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
backgroundColor: palette.destructiveAccent,
foregroundColor: palette.textPrimary,
textStyle: const TextStyle(fontWeight: FontWeight.w600),
),
onPressed: onPressed,
@@ -303,16 +362,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
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 =
ThemeData.estimateBrightnessForColor(color) == Brightness.dark
? Colors.white
: Colors.black;
? palette.textPrimary
: palette.textOnAccent;
return Semantics(
button: true,
selected: isSelected,
label: 'Color ${color.value.toRadixString(16)}',
label: 'Color ${color.toARGB32().toRadixString(16)}',
child: Tooltip(
message: isSelected ? 'Color actual' : 'Usar este color',
child: InkWell(
@@ -326,12 +386,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
color: color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? Colors.white : Colors.white24,
color: isSelected ? palette.textPrimary : palette.textSecondary,
width: isSelected ? 2.5 : 1.2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.25),
color: palette.shadowSoft,
blurRadius: 8,
offset: const Offset(0, 3),
),
@@ -341,11 +401,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
alignment: Alignment.center,
children: [
if (isSelected)
Icon(
Icons.check,
size: 22,
color: foregroundColor,
),
Icon(Icons.check, size: 22, color: foregroundColor),
],
),
),
@@ -366,10 +422,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
try {
await ApiConfig.setEndpoint(value);
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) {
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();
if (!mounted) return;
_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({
@@ -430,20 +492,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
@@ -455,162 +510,238 @@ class _SettingsScreenState extends State<SettingsScreen> {
titleText: 'Configuración',
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Expanded(
child: Text('Borrar datos locales:'),
),
_buildDestructiveButton(
label: 'Borrar',
onPressed: (_isBusy || _isServerDeleting)
? null
: _confirmAndDeleteAll,
isLoading: _isBusy,
icon: Icons.delete_forever,
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(
child: Text('Borrar info del servidor:'),
),
_buildDestructiveButton(
label: 'Borrar',
onPressed: (_isBusy || _isSyncing || _isServerDeleting)
? null
: _confirmAndDeleteServerData,
isLoading: _isServerDeleting,
icon: Icons.cloud_off,
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(
child: Text('Forzar sincronizacion total:'),
),
ElevatedButton.icon(
onPressed: (_isBusy || _isSyncing || _isServerDeleting)
? null
: _forceSync,
icon: _isSyncing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync),
label: const Text('Sincronizar'),
),
],
),
const SizedBox(height: 24),
const Text('Color del esquema'),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
for (final Color color in _themeColorOptions)
_buildThemeColorButton(color),
],
),
const SizedBox(height: 24),
const Text('API endpoint (ej: https://notas-api.lpncnd.es/api)'),
const SizedBox(height: 8),
_buildResponsiveInputActionsRow(
input: _endpointLoading
? const SizedBox(height: 48, child: Center(child: CircularProgressIndicator()))
: TextField(
controller: _endpointController,
style: const TextStyle(color: Colors.white),
keyboardType: TextInputType.url,
decoration: InputDecoration(
labelText: 'API endpoint',
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)),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Expanded(
child: Text('Borrar datos locales:'),
),
_buildDestructiveButton(
label: 'Borrar',
onPressed: (_isBusy || _isServerDeleting)
? null
: _confirmAndDeleteAll,
isLoading: _isBusy,
icon: Icons.delete_forever,
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(
child: Text('Borrar info del servidor:'),
),
_buildDestructiveButton(
label: 'Borrar',
onPressed:
(_isBusy || _isSyncing || _isServerDeleting)
? null
: _confirmAndDeleteServerData,
isLoading: _isServerDeleting,
icon: Icons.cloud_off,
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(
child: Text('Forzar sincronizacion total:'),
),
ElevatedButton.icon(
onPressed:
(_isBusy || _isSyncing || _isServerDeleting)
? null
: _forceSync,
icon: _isSyncing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.sync),
label: const Text('Sincronizar'),
),
],
),
const SizedBox(height: 24),
const Text('Apariencia'),
const SizedBox(height: 8),
Column(
children: [
RadioGroup<ThemeMode>(
groupValue: _selectedThemeMode,
onChanged: (ThemeMode? v) {
if (v != null) {
_selectThemeMode(v);
}
},
child: Column(
children: [
RadioListTile<ThemeMode>(
title: const Text(
'Seguir modo del sistema',
),
value: ThemeMode.system,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
RadioListTile<ThemeMode>(
title: const Text('Modo claro'),
value: ThemeMode.light,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
RadioListTile<ThemeMode>(
title: const Text('Modo oscuro'),
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: [
ElevatedButton(
onPressed: _encryptionKeyLoading ? null : _loadEncryptionKey,
child: _encryptionKeyLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Mostrar'),
const SizedBox(height: 24),
const Text(
'API endpoint (ej: https://notas-api.lpncnd.es/api)',
),
const SizedBox(height: 8),
_buildResponsiveInputActionsRow(
input: _endpointLoading
? const SizedBox(
height: 48,
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(
onPressed: _encryptionKeyVisible ? _hideEncryptionKey : null,
child: const Text('Ocultar'),
),
],
),
],
actions: [
ElevatedButton(
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'),
),
],
),
],
),
),
),
),
+65 -40
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:notas/data/api_client.dart';
import 'package:notas/theme/app_palette.dart';
class VaultAccessScreen extends StatefulWidget {
const VaultAccessScreen({
@@ -11,7 +12,8 @@ class VaultAccessScreen extends StatefulWidget {
});
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() onContinueWithoutAccount;
@@ -73,19 +75,11 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF191A1D),
Color(0xFF222326),
Color(0xFF101114),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
@@ -98,12 +92,12 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF1D1E20),
color: palette.cardBackground,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
border: Border.all(color: palette.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.35),
color: palette.shadowSoft,
blurRadius: 30,
offset: const Offset(0, 18),
),
@@ -115,7 +109,7 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
children: [
const Icon(
Icons.lock_outline,
color: Colors.amber,
color: Colors.white,
size: 44,
),
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.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
color: palette.textSecondary,
height: 1.4,
),
),
@@ -141,7 +135,9 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
_endpointLoading
? const SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
child: Center(
child: CircularProgressIndicator(),
),
)
: TextField(
controller: _endpointController,
@@ -150,20 +146,29 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'API endpoint',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
fillColor: palette.fill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(
color: palette.border,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(
color: palette.border,
),
),
focusedBorder: OutlineInputBorder(
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),
decoration: InputDecoration(
labelText: 'Usuario',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
fillColor: palette.fill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(color: palette.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(color: palette.border),
),
focusedBorder: OutlineInputBorder(
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),
decoration: InputDecoration(
labelText: 'Contraseña',
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
labelStyle: TextStyle(
color: palette.textSecondary,
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
fillColor: palette.fill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(color: palette.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
borderSide: BorderSide(color: palette.border),
),
focusedBorder: OutlineInputBorder(
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),
FilledButton(
onPressed: widget.isBusy ? null : _handleCreateAccount,
onPressed: widget.isBusy
? null
: _handleCreateAccount,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
padding: const EdgeInsets.symmetric(
vertical: 14,
),
),
child: widget.isBusy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('Crear cuenta'),
),
@@ -235,15 +256,19 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
OutlinedButton(
onPressed: widget.isBusy ? null : _handleSignIn,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Colors.white24),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 14,
),
side: BorderSide(color: palette.border),
foregroundColor: palette.textPrimary,
),
child: const Text('Iniciar sesión'),
),
const SizedBox(height: 18),
TextButton(
onPressed: widget.isBusy ? null : widget.onContinueWithoutAccount,
onPressed: widget.isBusy
? null
: widget.onContinueWithoutAccount,
child: const Text('Entrar sin cuenta'),
),
],
+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,
);
}
}
+22 -8
View File
@@ -1,19 +1,33 @@
import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class AppTheme {
static ThemeData theme({Color seedColor = Colors.amber}) {
static ThemeData theme({
Color seedColor = Colors.amber,
Brightness brightness = Brightness.dark,
}) {
final Brightness foregroundBrightness =
ThemeData.estimateBrightnessForColor(seedColor);
final Color foregroundColor =
foregroundBrightness == Brightness.dark ? Colors.white : Colors.black;
final Color foregroundColor = foregroundBrightness == Brightness.dark
? Colors.white
: Colors.black87;
final ColorScheme scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
);
final AppPalette palette = AppPalette.fromBrightness(
brightness,
seedColor: seedColor,
);
return ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: const Color.fromRGBO(31, 32, 33, 1),
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
scaffoldBackgroundColor: scheme.surface,
colorScheme: scheme,
extensions: <ThemeExtension<dynamic>>[palette],
brightness: brightness,
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: seedColor,
foregroundColor: foregroundColor,
+11 -10
View File
@@ -1,18 +1,19 @@
import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
class CategoryStyle {
CategoryStyle._();
static const List<Color> colors = <Color>[
Colors.amber,
Colors.blue,
Colors.green,
Colors.purple,
Colors.red,
Colors.teal,
Colors.orange,
Colors.grey,
];
static const List<Color> colors = AppPalette.defaultCategoryColors;
static List<Color> colorsOf(BuildContext context) {
final AppPalette? palette = Theme.of(context).extension<AppPalette>();
if (palette != null) {
return palette.categoryColors;
}
return AppPalette.defaultCategoryColors;
}
static const List<IconData> icons = <IconData>[
Icons.label_outline_rounded,
+31 -23
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:notas/models/category.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart';
class MenuDrawer extends StatelessWidget {
@@ -20,12 +21,12 @@ class MenuDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(255, 30, 31, 35),
border: Border(
right: BorderSide(color: Colors.white12, width: 0.5),
),
decoration: BoxDecoration(
color: palette.drawerBackground,
border: Border(right: BorderSide(color: palette.border, width: 0.5)),
),
child: Column(
children: [
@@ -59,17 +60,21 @@ class MenuDrawer extends StatelessWidget {
onLongPress: onEditCategory == null
? null
: () => onEditCategory?.call(category),
iconColor: Color(category.colorValue ?? 0xFFFFC107),
textColor: Color(category.colorValue ?? 0xFFFFC107),
iconColor: Color(
category.colorValue ?? palette.accent.toARGB32(),
),
textColor: Color(
category.colorValue ?? palette.accent.toARGB32(),
),
trailing: IconButton(
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 0,
minHeight: 0,
),
icon: const Icon(
icon: Icon(
Icons.more_vert,
color: Colors.white70,
color: palette.textSecondary,
size: 20,
),
onPressed: () => onEditCategory?.call(category),
@@ -92,10 +97,10 @@ class MenuDrawer extends StatelessWidget {
label: 'Mis notas borradas',
selected: selectedItem == 'deleted_notes',
onTap: () => onMenuItemTapped?.call('deleted_notes'),
iconColor: Colors.redAccent,
textColor: Colors.redAccent,
iconColor: palette.destructiveAccent,
textColor: palette.destructiveAccent,
),
const Divider(color: Colors.white12, height: 16),
Divider(color: palette.border, height: 16),
_MenuItemTile(
icon: Icons.settings,
label: 'Configuración',
@@ -138,11 +143,12 @@ class _MenuItemTileState extends State<_MenuItemTile> {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
final bool active = widget.selected || _hovering;
final Color backgroundColor = active
? Colors.white.withValues(alpha: 0.10)
: Colors.transparent;
final Color foregroundColor = active ? Colors.white : Colors.white70;
final Color backgroundColor = active ? palette.hover : palette.transparent;
final Color foregroundColor = active
? palette.textPrimary
: palette.textSecondary;
final Widget? trailing = _hovering ? widget.trailing : null;
return MouseRegion(
@@ -155,11 +161,7 @@ class _MenuItemTileState extends State<_MenuItemTile> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
margin: const EdgeInsets.only(
right: 8,
top: 2,
bottom: 2,
),
margin: const EdgeInsets.only(right: 8, top: 2, bottom: 2),
child: Material(
color: backgroundColor,
borderRadius: const BorderRadius.only(
@@ -169,11 +171,17 @@ class _MenuItemTileState extends State<_MenuItemTile> {
clipBehavior: Clip.antiAlias,
child: ListTile(
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,
title: Text(
widget.label,
style: TextStyle(color: widget.textColor ?? foregroundColor, fontSize: 14),
style: TextStyle(
color: widget.textColor ?? foregroundColor,
fontSize: 14,
),
),
onTap: widget.onTap,
onLongPress: widget.onLongPress,
+137 -114
View File
@@ -1,138 +1,161 @@
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/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart';
// Small presentational widget for a note inside the grid.
// 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 {
class NoteCard extends StatelessWidget {
const NoteCard({
super.key,
required this.note,
this.onTap,
this.isDragging = false,
this.category,
this.isSelected = false,
this.borderColor,
this.onTap,
this.onDelete,
this.onChangeCategory,
this.showSelectionBorder = true,
});
final Note note;
final VoidCallback? onTap;
final bool isDragging;
final Category? category;
final bool isSelected;
final Color? borderColor;
@override
State<NoteCard> createState() => _NoteCardState();
}
class _NoteCardState extends State<NoteCard> {
bool _isPressed = false;
final VoidCallback? onTap;
final VoidCallback? onDelete;
final ValueChanged<BuildContext>? onChangeCategory;
final bool showSelectionBorder;
@override
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(
cursor: showGrabbing ? SystemMouseCursors.grabbing : SystemMouseCursors.grab,
child: GestureDetector(
onTapDown: widget.onTap == null
? null
: (_) {
setState(() {
_isPressed = true;
});
},
onTapUp: widget.onTap == null
? null
: (_) {
setState(() {
_isPressed = false;
});
},
onTapCancel: widget.onTap == null
? null
: () {
setState(() {
_isPressed = false;
});
},
onTap: widget.onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(24, 25, 26, 1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.borderColor ?? Colors.white24,
width: 1,
),
),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Estimate whether the body will exceed 20 lines without always
// running the expensive TextPainter layout. This heuristic counts
// newline characters and estimates wrapped lines based on an
// average characters-per-line to handle many short lines well.
final List<String> rawLines = widget.note.body.split('\n');
const int avgCharsPerLine = 40;
int estimatedLines = 0;
for (final String line in rawLines) {
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
}
final bool needsPreciseMeasurement = estimatedLines > 20;
final bool isBodyTruncated;
if (needsPreciseMeasurement) {
final TextPainter textPainter = TextPainter(
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,
return Material(
color: Colors.transparent, // 1. Fondo completamente transparente
shape: BorderDirectional(
start: BorderSide(
color: (isSelected && showSelectionBorder)
? palette.accent
: Colors.transparent,
width: isSelected ? 1.6 : 1.0,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onTap,
hoverColor:
Colors.transparent, // 2. Desactiva el efecto hover (pasar el ratón)
splashColor:
Colors.transparent, // 3. Desactiva el efecto de onda al hacer clic
highlightColor:
Colors.transparent, // Desactiva el brillo al mantener pulsado
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (categoryIcon != null) ...[
SizedBox(
width: 18,
height: 18,
child: Icon(
categoryIcon,
size: 18,
color: categoryColor ?? palette.textSecondary,
),
),
const SizedBox(width: 4),
],
Expanded(
child: Text(
note.title.isEmpty ? 'Sin título' : note.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
widget.note.body,
style: const TextStyle(color: Colors.white70, fontSize: 14),
maxLines: 20,
overflow: TextOverflow.clip,
),
if (isBodyTruncated) ...[
const SizedBox(height: 4),
const Text(
'...',
const SizedBox(height: 6),
Text(
bodyText.isEmpty ? ' ' : bodyText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white54,
fontSize: 18,
height: 1,
color: palette.textSecondary,
fontSize: 14,
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:notas/theme/app_palette.dart';
class SearchAppBar extends StatefulWidget {
const SearchAppBar({
@@ -51,22 +52,23 @@ class _SearchAppBarState extends State<SearchAppBar> {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
return Container(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: Colors.white.withValues(alpha: 0.12),
width: 0.5,
),
),
color: palette.transparent,
border: Border(bottom: BorderSide(color: palette.border, width: 0.5)),
),
padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7),
child: Row(
children: [
IconButton(
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,
splashRadius: 18,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
@@ -84,18 +86,21 @@ class _SearchAppBarState extends State<SearchAppBar> {
child: TextField(
controller: _searchController,
onChanged: widget.onSearchChanged,
style: const TextStyle(color: Colors.white, fontSize: 13),
cursorColor: Colors.white70,
style: TextStyle(
color: palette.textPrimary,
fontSize: 13,
),
cursorColor: palette.textSecondary,
decoration: InputDecoration(
hintText: widget.searchHint,
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
color: palette.textSecondary.withValues(alpha: 0.6),
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(
icon: Icon(
Icons.clear,
color: Colors.white70,
color: palette.textSecondary,
size: 18,
),
onPressed: () {
@@ -107,37 +112,37 @@ class _SearchAppBarState extends State<SearchAppBar> {
minHeight: 36,
),
)
: const Padding(
padding: EdgeInsets.only(right: 8),
: Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(
Icons.search,
color: Colors.white70,
color: palette.textSecondary,
size: 18,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.2),
color: palette.border,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.2),
color: palette.border,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.white.withValues(alpha: 0.4),
width: 0.5,
color: palette.accent,
width: 0.6,
),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
fillColor: palette.fill,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
@@ -151,8 +156,8 @@ class _SearchAppBarState extends State<SearchAppBar> {
alignment: Alignment.centerLeft,
child: Text(
widget.titleText ?? '',
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: palette.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
+12 -9
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/sync_status.dart';
class SyncStatusIndicator extends StatelessWidget {
@@ -86,12 +87,14 @@ class SyncStatusIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
switch (status) {
case SyncStatus.idle:
return Tooltip(
message: _messageForStatus(),
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(
_buildStatusBadge(
icon: Icons.sync,
color: const Color.fromARGB(255, 165, 165, 165),
color: palette.syncPreparing,
determinate: false,
),
),
@@ -113,7 +116,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator(
_buildStatusBadge(
icon: Icons.cloud_upload_outlined,
color: const Color.fromARGB(255, 109, 191, 255),
color: palette.syncEncrypting,
determinate: true,
),
),
@@ -125,7 +128,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator(
_buildStatusBadge(
icon: Icons.cloud_upload,
color: const Color.fromARGB(255, 98, 190, 255),
color: palette.syncUploading,
determinate: false,
),
),
@@ -137,7 +140,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator(
_buildStatusBadge(
icon: Icons.cloud_sync_outlined,
color: const Color.fromARGB(255, 150, 150, 150),
color: palette.syncWaiting,
determinate: false,
),
),
@@ -149,7 +152,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator(
_buildStatusBadge(
icon: Icons.cloud_download_outlined,
color: const Color.fromARGB(255, 154, 194, 112),
color: palette.syncDecrypting,
determinate: true,
),
),
@@ -161,7 +164,7 @@ class SyncStatusIndicator extends StatelessWidget {
child: _buildIndicator(
_buildStatusBadge(
icon: Icons.sync,
color: const Color.fromARGB(255, 150, 150, 150),
color: palette.syncWaiting,
determinate: false,
),
),
@@ -171,7 +174,7 @@ class SyncStatusIndicator extends StatelessWidget {
return Tooltip(
message: _messageForStatus(),
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(
message: _messageForStatus(),
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 <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) 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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
+1
View File
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
screen_retriever_linux
url_launcher_linux
window_manager
)
@@ -7,14 +7,18 @@ import Foundation
import flutter_secure_storage_darwin
import local_auth_darwin
import quill_native_bridge_macos
import screen_retriever_macos
import shared_preferences_foundation
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
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
description:
name: _fe_analyzer_shared
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev"
source: hosted
version: "99.0.0"
version: "100.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "13.0.0"
archive:
dependency: transitive
description:
@@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.2.1"
collection:
dependency: transitive
description:
@@ -161,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -177,6 +185,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -185,30 +201,46 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
name: dart_style
sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev"
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:
dependency: "direct main"
description:
name: drift
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
url: "https://pub.dev"
source: hosted
version: "2.33.0"
version: "2.34.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
sha256: "042a5fb507ab5697f67eb55b75cfff2f665701f6606926136d6d4e85f81ff837"
url: "https://pub.dev"
source: hosted
version: "2.33.0"
version: "2.34.1+1"
fake_async:
dependency: transitive
description:
@@ -225,14 +257,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file:
dependency: transitive
description:
@@ -241,6 +265,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -254,6 +294,54 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
@@ -270,38 +358,59 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
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:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
version: "10.3.1"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
version: "0.3.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
@@ -322,10 +431,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8"
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.1.0"
flutter_staggered_grid_view:
dependency: "direct main"
description:
@@ -364,18 +473,26 @@ packages:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
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:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
@@ -396,10 +513,10 @@ packages:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.9.1"
intl:
dependency: "direct main"
description:
@@ -484,10 +601,10 @@ packages:
dependency: transitive
description:
name: local_auth_android
sha256: b201c006fa769c23386f89aa6837ec0eb8179fcfb212eadcf87b422b3f9a6a78
sha256: fdb936d59ab945c7af297defd67bd1ed87b11b6db1bc16d01e94677a8f1c38ec
url: "https://pub.dev"
source: hosted
version: "2.0.8"
version: "2.0.9"
local_auth_darwin:
dependency: transitive
description:
@@ -520,6 +637,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
source: hosted
version: "7.3.1"
matcher:
dependency: transitive
description:
@@ -556,18 +681,18 @@ packages:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev"
source: hosted
version: "0.17.6"
version: "0.19.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "9.4.1"
package_config:
dependency: transitive
description:
@@ -588,10 +713,10 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
@@ -612,18 +737,18 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
@@ -688,6 +813,70 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -708,42 +897,42 @@ packages:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
shared_preferences:
dependency: "direct main"
description:
@@ -756,10 +945,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.dev"
source: hosted
version: "2.4.23"
version: "2.4.26"
shared_preferences_foundation:
dependency: transitive
description:
@@ -841,18 +1030,18 @@ packages:
dependency: "direct main"
description:
name: sqlite3
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.3.3"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d"
url: "https://pub.dev"
source: hosted
version: "0.44.4"
version: "0.44.5"
stack_trace:
dependency: transitive
description:
@@ -909,6 +1098,70 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -969,10 +1222,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "5.15.0"
window_manager:
dependency: "direct main"
description:
@@ -993,10 +1246,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
yaml:
dependency: transitive
description:
@@ -1006,5 +1259,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.5 <4.0.0"
flutter: ">=3.38.4"
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
+6 -4
View File
@@ -2,7 +2,7 @@ name: notas
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# 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.
# A version number is three numbers separated by dots, like 1.2.43
@@ -44,10 +44,14 @@ dependencies:
flutter_secure_storage: ^10.2.0
local_auth: ^3.0.1
sqlite3: ^3.3.1
http: ^0.13.6
http: ^1.6.0
crypto: ^3.0.6
cryptography: ^2.7.0
uuid: ^4.0.0
flutter_quill: ^11.5.1
flutter_localizations:
sdk: flutter
dev_dependencies:
flutter_test:
@@ -68,7 +72,6 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
@@ -113,7 +116,6 @@ hooks:
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
# Configuración general y móvil
image_path: "assets/icon.png"
+50 -33
View File
@@ -1,31 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:notas/models/category.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:notas/models/note.dart';
import 'package:notas/screens/note_editor_screen.dart';
void main() {
testWidgets('saves a note when only the category changes', (
testWidgets('autosaves a note when only the category changes', (
WidgetTester tester,
) async {
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(
MaterialApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
],
home: Scaffold(
body: NoteEditorScreen(
note: null,
categoryId: null,
categories: <Category>[
Category(
id: 'work',
name: 'Trabajo',
updatedAt: DateTime(2026, 5, 21),
),
],
onComplete: (dynamic result) {
savedNote = result as Note?;
repository: null,
note: initialNote,
saveNote: (Note note) async => note,
onSaved: (Note result) {
savedNote = result;
},
),
),
@@ -38,44 +46,53 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('Trabajo').last);
await tester.pumpAndSettle();
await tester.tap(find.text('Guardar'));
await tester.pumpAndSettle();
await tester.pump();
await tester.pump(const Duration(seconds: 2));
expect(savedNote, isNotNull);
expect(savedNote!.categoryId, 'work');
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,
) 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(
MaterialApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
],
home: Scaffold(
body: NoteEditorScreen(
note: null,
categoryId: null,
categories: <Category>[],
onComplete: (dynamic result) {
if (result is Note) {
completionCount += 1;
}
repository: null,
note: initialNote,
saveNote: (Note note) async {
saveCount += 1;
return note;
},
),
),
),
);
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'));
await tester.tap(find.text('Guardar'));
await tester.pumpAndSettle();
expect(completionCount, 1);
expect(saveCount, 1);
});
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:notas/data/note_positioning.dart';
void main() {
test('converts between stored and displayed positions', () {
expect(toStoredNotePosition(1500.5), 15005);
expect(fromStoredNotePosition(15005), 1500.5);
});
test('supports descending gaps and rebalance', () {
expect(nextTopNotePosition(<int>[0, 10000, 20000]), 30000);
expect(
midpointNotePosition(higherPosition: 20000, lowerPosition: 10000),
15000,
);
expect(midpointNotePosition(higherPosition: 2, lowerPosition: 1), isNull);
expect(rebalanceNotePositions(3), <int>[20000, 10000, 0]);
});
}
+1 -1
View File
@@ -8,6 +8,6 @@ void main() {
await tester.pumpWidget(const NotesApp());
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 <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.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>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
+2
View File
@@ -3,9 +3,11 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
local_auth_windows
screen_retriever_windows
url_launcher_windows
window_manager
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB