Reestructuracion de la app

This commit is contained in:
2026-06-29 20:32:47 +02:00
parent 710be805ee
commit 82515960f6
6 changed files with 1040 additions and 1868 deletions
+77
View File
@@ -79,6 +79,79 @@ class _NotesAppState extends State<NotesApp>
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:
return true;
case SyncStatus.idle:
case SyncStatus.synced:
case SyncStatus.error:
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;
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: palette.border)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(Icons.cloud_sync_outlined, color: palette.textSecondary, 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,
),
),
),
],
),
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:
@@ -964,6 +1037,10 @@ class _NotesAppState extends State<NotesApp>
child: activeScreen,
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: _buildSyncBanner(context),
),
],
),
),
+4
View File
@@ -56,6 +56,10 @@ class NoteRepository {
return categories;
}
Future<DateTime?> getLastSyncAt() async {
return _authApi.getLastSyncAt();
}
Future<void> createCategory(Category category) async {
debugPrint('createCategory called with: ${category.name}');
+597 -1067
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+87 -127
View File
@@ -4,150 +4,110 @@ import 'package:notas/data/note_body.dart';
import 'package:notas/models/note.dart';
import 'package:notas/theme/app_palette.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.isSelected = false,
this.borderColor,
this.onTap,
this.onDelete,
this.onChangeCategory,
});
final Note note;
final VoidCallback? onTap;
final bool isDragging;
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;
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
final bool showGrabbing = widget.isDragging || _isPressed;
final String bodyText = noteBodyToPlainText(note.body).trim();
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: palette.cardBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.borderColor ?? palette.textDisabled,
width: 1,
return Material(
color: Colors.transparent, // 1. Fondo completamente transparente
shape: BorderDirectional(
start: BorderSide(
color: isSelected ? 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: 14, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
note.title.isEmpty ? 'Sin título' : note.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textPrimary,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
bodyText.isEmpty ? ' ' : bodyText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: palette.textSecondary,
fontSize: 13,
height: 1.2,
),
),
],
),
),
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 String bodyText = noteBodyToPlainText(widget.note.body);
final List<String> rawLines = bodyText.split('\n');
const int avgCharsPerLine = 40;
int estimatedLines = 0;
for (final String line in rawLines) {
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
const SizedBox(width: 8),
PopupMenuButton<String>(
tooltip: 'Más opciones',
icon: Icon(
Icons.more_vert,
color: palette.textSecondary,
),
onOpened: () {},
onSelected: (String value) {
switch (value) {
case 'delete':
onDelete?.call();
return;
case 'category':
onChangeCategory?.call(context);
return;
}
final bool needsPreciseMeasurement = estimatedLines > 15;
final bool isBodyTruncated;
if (needsPreciseMeasurement) {
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: bodyText,
style: TextStyle(
color: palette.textSecondary,
fontSize: 14,
),
),
maxLines: 15,
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: TextStyle(
color: palette.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
bodyText,
style: TextStyle(
color: palette.textSecondary,
fontSize: 14,
),
maxLines: 15,
overflow: TextOverflow.clip,
),
if (isBodyTruncated) ...[
const SizedBox(height: 4),
Text(
'...',
style: TextStyle(
color: palette.textMuted,
fontSize: 18,
height: 1,
),
),
],
],
);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'delete',
child: Text('Eliminar nota'),
),
const PopupMenuItem<String>(
value: 'category',
child: Text('Cambiar categoría'),
),
],
),
),
],
),
);
}
),
),
);
}
}