Compare commits
21 Commits
f595f33f4a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b201da0552 | |||
| 9b6d92c372 | |||
| f662e59547 | |||
| 78dddd571a | |||
| c2db704155 | |||
| b00da9ae88 | |||
| 972006c29f | |||
| 82515960f6 | |||
| 710be805ee | |||
| d849c25ed6 | |||
| 7c1d4e5fd8 | |||
| 1dede9eb78 | |||
| 29881183ed | |||
| f4bb5104e2 | |||
| 814f8f7c04 | |||
| 729e575a60 | |||
| cdfd4f9342 | |||
| a31cc12b7e | |||
| 2069de55ae | |||
| e0f226d3bc | |||
| 27e1199178 |
@@ -43,3 +43,5 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
problems-report.html
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 951 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 19 KiB |
@@ -3,6 +3,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
import 'package:notas/data/app_database.dart';
|
import 'package:notas/data/app_database.dart';
|
||||||
import 'package:notas/data/api_client.dart';
|
import 'package:notas/data/api_client.dart';
|
||||||
import 'package:notas/data/local_vault_service.dart';
|
import 'package:notas/data/local_vault_service.dart';
|
||||||
@@ -14,6 +16,7 @@ import 'package:notas/screens/biometric_gate_screen.dart';
|
|||||||
import 'package:notas/screens/home_screen.dart';
|
import 'package:notas/screens/home_screen.dart';
|
||||||
import 'package:notas/screens/settings_screen.dart';
|
import 'package:notas/screens/settings_screen.dart';
|
||||||
import 'package:notas/screens/vault_access_screen.dart';
|
import 'package:notas/screens/vault_access_screen.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
import 'package:notas/theme/app_theme.dart';
|
import 'package:notas/theme/app_theme.dart';
|
||||||
import 'package:notas/widgets/sync_status.dart';
|
import 'package:notas/widgets/sync_status.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -43,6 +46,7 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
static const Duration _syncInterval = Duration(minutes: 5);
|
static const Duration _syncInterval = Duration(minutes: 5);
|
||||||
static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350);
|
static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350);
|
||||||
static const String _themeSeedColorKey = 'theme_seed_color_v1';
|
static const String _themeSeedColorKey = 'theme_seed_color_v1';
|
||||||
|
static const String _themeModeKey = 'theme_mode_v1';
|
||||||
|
|
||||||
final LocalVaultService _vaultService = LocalVaultService.instance;
|
final LocalVaultService _vaultService = LocalVaultService.instance;
|
||||||
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
|
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
|
||||||
@@ -69,6 +73,138 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
int _syncOperationId = 0;
|
int _syncOperationId = 0;
|
||||||
int _homeRefreshToken = 0;
|
int _homeRefreshToken = 0;
|
||||||
Color _themeSeedColor = Colors.amber;
|
Color _themeSeedColor = Colors.amber;
|
||||||
|
ThemeMode _themeMode = ThemeMode.system;
|
||||||
|
|
||||||
|
// Cached ThemeData for light and dark variants.
|
||||||
|
ThemeData? _lightTheme;
|
||||||
|
ThemeData? _darkTheme;
|
||||||
|
|
||||||
|
bool _isSyncBannerVisible() {
|
||||||
|
switch (_syncStatus) {
|
||||||
|
case SyncStatus.preparing:
|
||||||
|
case SyncStatus.encrypting:
|
||||||
|
case SyncStatus.uploading:
|
||||||
|
case SyncStatus.waitingResponse:
|
||||||
|
case SyncStatus.decrypting:
|
||||||
|
case SyncStatus.syncing:
|
||||||
|
case SyncStatus.synced:
|
||||||
|
case SyncStatus.error:
|
||||||
|
return true;
|
||||||
|
case SyncStatus.idle:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSyncBanner(BuildContext context) {
|
||||||
|
if (!_isSyncBannerVisible()) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final AppPalette palette = _activePalette();
|
||||||
|
final String message =
|
||||||
|
_syncErrorMessage ?? _syncDetailMessage ?? 'Sincronizando...';
|
||||||
|
final double? progress = _syncProgress;
|
||||||
|
final IconData icon;
|
||||||
|
final Color accentColor;
|
||||||
|
|
||||||
|
switch (_syncStatus) {
|
||||||
|
case SyncStatus.preparing:
|
||||||
|
case SyncStatus.encrypting:
|
||||||
|
case SyncStatus.uploading:
|
||||||
|
case SyncStatus.waitingResponse:
|
||||||
|
case SyncStatus.decrypting:
|
||||||
|
case SyncStatus.syncing:
|
||||||
|
icon = Icons.cloud_sync_outlined;
|
||||||
|
accentColor = palette.textSecondary;
|
||||||
|
break;
|
||||||
|
case SyncStatus.synced:
|
||||||
|
icon = Icons.check_circle;
|
||||||
|
accentColor = palette.success;
|
||||||
|
break;
|
||||||
|
case SyncStatus.error:
|
||||||
|
icon = Icons.error;
|
||||||
|
accentColor = palette.destructiveAccent;
|
||||||
|
break;
|
||||||
|
case SyncStatus.idle:
|
||||||
|
icon = Icons.cloud_sync_outlined;
|
||||||
|
accentColor = palette.textSecondary;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: palette.surfaceElevated,
|
||||||
|
elevation: 12,
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: accentColor.withValues(alpha: 0.45)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: accentColor, size: 18),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: palette.textPrimary,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_syncStatus == SyncStatus.preparing ||
|
||||||
|
_syncStatus == SyncStatus.encrypting ||
|
||||||
|
_syncStatus == SyncStatus.uploading ||
|
||||||
|
_syncStatus == SyncStatus.waitingResponse ||
|
||||||
|
_syncStatus == SyncStatus.decrypting ||
|
||||||
|
_syncStatus == SyncStatus.syncing) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
minHeight: 4,
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: palette.borderMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Brightness _effectiveBrightness() {
|
||||||
|
switch (_themeMode) {
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return Brightness.dark;
|
||||||
|
case ThemeMode.light:
|
||||||
|
return Brightness.light;
|
||||||
|
case ThemeMode.system:
|
||||||
|
return WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPalette _activePalette() {
|
||||||
|
return AppPalette.fromBrightness(
|
||||||
|
_effectiveBrightness(),
|
||||||
|
seedColor: _themeSeedColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -79,6 +215,7 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
}
|
}
|
||||||
_loadThemeSeedColor();
|
_loadThemeSeedColor();
|
||||||
|
_loadThemeMode();
|
||||||
_bootstrapVault();
|
_bootstrapVault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +242,13 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_themeSeedColor = Color(storedColorValue);
|
_themeSeedColor = Color(storedColorValue);
|
||||||
_themeData = AppTheme.theme(seedColor: _themeSeedColor);
|
_updateThemeData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setThemeSeedColor(Color color) async {
|
Future<void> _setThemeSeedColor(Color color) async {
|
||||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setInt(_themeSeedColorKey, color.value);
|
await prefs.setInt(_themeSeedColorKey, color.toARGB32());
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -119,15 +256,54 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_themeSeedColor = color;
|
_themeSeedColor = color;
|
||||||
_themeData = AppTheme.theme(seedColor: _themeSeedColor);
|
_updateThemeData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cached ThemeData to avoid recomputing on every build.
|
Future<void> _loadThemeMode() async {
|
||||||
ThemeData? _themeData;
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final int? stored = prefs.getInt(_themeModeKey);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
ThemeData get _theme =>
|
setState(() {
|
||||||
_themeData ??= AppTheme.theme(seedColor: _themeSeedColor);
|
if (stored == null) {
|
||||||
|
_themeMode = ThemeMode.system;
|
||||||
|
} else if (stored == 1) {
|
||||||
|
_themeMode = ThemeMode.light;
|
||||||
|
} else if (stored == 2) {
|
||||||
|
_themeMode = ThemeMode.dark;
|
||||||
|
} else {
|
||||||
|
_themeMode = ThemeMode.system;
|
||||||
|
}
|
||||||
|
_updateThemeData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setThemeMode(ThemeMode mode) async {
|
||||||
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final int stored = mode == ThemeMode.system
|
||||||
|
? 0
|
||||||
|
: (mode == ThemeMode.light ? 1 : 2);
|
||||||
|
await prefs.setInt(_themeModeKey, stored);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_themeMode = mode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateThemeData() {
|
||||||
|
_lightTheme = AppTheme.theme(
|
||||||
|
seedColor: _themeSeedColor,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
|
_darkTheme = AppTheme.theme(
|
||||||
|
seedColor: _themeSeedColor,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
// Updated light/dark themes regenerated
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
@@ -418,32 +594,38 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
if (available) {
|
if (available) {
|
||||||
bool activated = await _vaultService.enableBiometricProtection();
|
bool activated = await _vaultService.enableBiometricProtection();
|
||||||
while (!activated) {
|
while (!activated) {
|
||||||
// Ask the user to retry or skip
|
// Ask the user to retry or skip - pass currentContext directly in builder
|
||||||
final BuildContext? dialogCtx = _navigatorKey.currentContext;
|
final NavigatorState? navigator = _navigatorKey.currentState;
|
||||||
if (dialogCtx == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
final NavigatorState navigator = Navigator.of(dialogCtx);
|
if (navigator == null) break;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
final bool? retry = await showDialog<bool>(
|
final bool? retry = await showDialog<bool>(
|
||||||
context: dialogCtx,
|
context: context,
|
||||||
builder: (BuildContext context) => AlertDialog(
|
builder: (BuildContext dialogContext) {
|
||||||
title: const Text('No se pudo activar la biometría'),
|
final AppPalette palette = _activePalette();
|
||||||
content: const Text(
|
return AlertDialog(
|
||||||
'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?',
|
backgroundColor: palette.cardBackground,
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
actions: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
TextButton(
|
side: BorderSide(color: palette.border),
|
||||||
onPressed: () => navigator.pop(false),
|
|
||||||
child: const Text('Entrar sin huella'),
|
|
||||||
),
|
),
|
||||||
FilledButton(
|
title: const Text('No se pudo activar la biometría'),
|
||||||
onPressed: () => navigator.pop(true),
|
content: const Text(
|
||||||
child: const Text('Reintentar'),
|
'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?',
|
||||||
),
|
),
|
||||||
],
|
actions: [
|
||||||
),
|
TextButton(
|
||||||
|
onPressed: () => navigator.pop(false),
|
||||||
|
child: const Text('Entrar sin huella'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => navigator.pop(true),
|
||||||
|
child: const Text('Reintentar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (retry != true) {
|
if (retry != true) {
|
||||||
@@ -766,8 +948,8 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
_homeRefreshToken += 1;
|
_homeRefreshToken += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset to idle after 3 seconds
|
// Keep the completion state visible briefly so it can be read.
|
||||||
Future<void>.delayed(const Duration(seconds: 3), () {
|
Future<void>.delayed(const Duration(seconds: 1), () {
|
||||||
if (mounted && syncOperationId == _syncOperationId) {
|
if (mounted && syncOperationId == _syncOperationId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_syncStatus = SyncStatus.idle;
|
_syncStatus = SyncStatus.idle;
|
||||||
@@ -819,6 +1001,7 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainShell(NoteRepository repository) {
|
Widget _buildMainShell(NoteRepository repository) {
|
||||||
|
final AppPalette palette = _activePalette();
|
||||||
final Widget activeScreen = _currentSection == _AppSection.home
|
final Widget activeScreen = _currentSection == _AppSection.home
|
||||||
? HomeScreen(
|
? HomeScreen(
|
||||||
key: const ValueKey<String>('home-screen'),
|
key: const ValueKey<String>('home-screen'),
|
||||||
@@ -839,6 +1022,8 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
onForceSync: () => _performSync(forceFull: true),
|
onForceSync: () => _performSync(forceFull: true),
|
||||||
currentSeedColor: _themeSeedColor,
|
currentSeedColor: _themeSeedColor,
|
||||||
onThemeColorSelected: _setThemeSeedColor,
|
onThemeColorSelected: _setThemeSeedColor,
|
||||||
|
currentThemeMode: _themeMode,
|
||||||
|
onThemeModeSelected: _setThemeMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Shortcuts(
|
return Shortcuts(
|
||||||
@@ -858,17 +1043,7 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF191A1D),
|
|
||||||
Color(0xFF222326),
|
|
||||||
Color(0xFF101114),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -896,6 +1071,28 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
child: activeScreen,
|
child: activeScreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
switchInCurve: Curves.easeOutCubic,
|
||||||
|
switchOutCurve: Curves.easeInCubic,
|
||||||
|
transitionBuilder:
|
||||||
|
(Widget child, Animation<double> animation) {
|
||||||
|
final Animation<Offset> slideAnimation =
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.35),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation);
|
||||||
|
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: slideAnimation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _buildSyncBanner(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1028,7 +1225,25 @@ class _NotesAppState extends State<NotesApp>
|
|||||||
title: 'Mis Notas',
|
title: 'Mis Notas',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
scaffoldMessengerKey: _scaffoldMessengerKey,
|
scaffoldMessengerKey: _scaffoldMessengerKey,
|
||||||
theme: _theme,
|
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
FlutterQuillLocalizations.delegate,
|
||||||
|
],
|
||||||
|
theme:
|
||||||
|
_lightTheme ??
|
||||||
|
AppTheme.theme(
|
||||||
|
seedColor: _themeSeedColor,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
darkTheme:
|
||||||
|
_darkTheme ??
|
||||||
|
AppTheme.theme(
|
||||||
|
seedColor: _themeSeedColor,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
themeMode: _themeMode,
|
||||||
home: homeWidget,
|
home: homeWidget,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ import 'package:drift/native.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import 'package:notas/data/note_positioning.dart';
|
||||||
|
|
||||||
part 'app_database.g.dart';
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
@DataClassName('DbCategory')
|
@DataClassName('DbCategory')
|
||||||
class Categories extends Table {
|
class Categories extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get encryptedName => text().named('encrypted_name')();
|
TextColumn get name => text().named('name')();
|
||||||
IntColumn get serverVersion =>
|
IntColumn get serverVersion =>
|
||||||
integer().named('server_version').withDefault(const Constant(0))();
|
integer().named('server_version').withDefault(const Constant(0))();
|
||||||
BoolColumn get isDeleted =>
|
BoolColumn get isDeleted =>
|
||||||
boolean().named('is_deleted').withDefault(const Constant(false))();
|
boolean().named('is_deleted').withDefault(const Constant(false))();
|
||||||
IntColumn get colorValue => integer().nullable().named('color_value')();
|
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 =>
|
BoolColumn get isDirty =>
|
||||||
boolean().named('is_dirty').withDefault(const Constant(true))();
|
boolean().named('is_dirty').withDefault(const Constant(true))();
|
||||||
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
|
DateTimeColumn get updatedAt => dateTime().named('updated_at')();
|
||||||
@@ -38,17 +41,17 @@ class Notes extends Table {
|
|||||||
BoolColumn get isDeleted =>
|
BoolColumn get isDeleted =>
|
||||||
boolean().named('is_deleted').withDefault(const Constant(false))();
|
boolean().named('is_deleted').withDefault(const Constant(false))();
|
||||||
TextColumn get categoryId => text().nullable().named('category_id')();
|
TextColumn get categoryId => text().nullable().named('category_id')();
|
||||||
BoolColumn get isDirty =>
|
BoolColumn get isDirty =>
|
||||||
boolean().named('is_dirty').withDefault(const Constant(true))();
|
boolean().named('is_dirty').withDefault(const Constant(true))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|
||||||
@DriftDatabase(tables: [Notes, Categories])
|
@DriftDatabase(tables: [Notes, Categories])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
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 color_value = NULL');
|
||||||
await customStatement('UPDATE categories SET icon_code_point = 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 ==========
|
// ========== Notes ==========
|
||||||
Future<List<DbNote>> getAllNotes() {
|
Future<List<DbNote>> getAllNotes() {
|
||||||
return (select(notes)
|
return (select(notes)
|
||||||
..orderBy([(note) => OrderingTerm(expression: note.sortIndex)])
|
..orderBy([
|
||||||
|
(note) => OrderingTerm(
|
||||||
|
expression: note.sortIndex,
|
||||||
|
mode: OrderingMode.desc,
|
||||||
|
),
|
||||||
|
])
|
||||||
..where((n) => n.isDeleted.equals(false)))
|
..where((n) => n.isDeleted.equals(false)))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> insertNoteAtTop(NotesCompanion note) {
|
Future<int> insertNoteAtTop(NotesCompanion note) {
|
||||||
return transaction(() async {
|
return transaction(() async {
|
||||||
await customStatement(
|
final DbNote? topNote =
|
||||||
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE is_deleted = 0',
|
await (select(notes)
|
||||||
);
|
..where((n) => n.isDeleted.equals(false))
|
||||||
return into(notes).insert(note.copyWith(sortIndex: const Value<int>(0)));
|
..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),
|
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({
|
Future<void> deleteNoteAndShift({
|
||||||
@@ -165,44 +208,67 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
required int oldIndex,
|
required int oldIndex,
|
||||||
required int newIndex,
|
required int newIndex,
|
||||||
}) {
|
}) {
|
||||||
if (oldIndex == newIndex) {
|
|
||||||
return Future<void>.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
return transaction(() async {
|
return transaction(() async {
|
||||||
final List<DbNote> all = await (select(
|
final List<DbNote> orderedNotes =
|
||||||
notes,
|
await (select(notes)
|
||||||
)..where((n) => n.isDeleted.equals(false))).get();
|
..where((n) => n.isDeleted.equals(false))
|
||||||
|
..orderBy([
|
||||||
|
(note) => OrderingTerm(
|
||||||
|
expression: note.sortIndex,
|
||||||
|
mode: OrderingMode.desc,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
|
|
||||||
final int count = all.length;
|
final int currentIndex = orderedNotes.indexWhere(
|
||||||
if (count == 0) {
|
(DbNote row) => row.id == id,
|
||||||
|
);
|
||||||
|
if (currentIndex == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int maxIndex = count - 1;
|
final int safeNewIndex = newIndex.clamp(0, orderedNotes.length - 1);
|
||||||
|
if (currentIndex == safeNewIndex) {
|
||||||
final int safeOld = oldIndex.clamp(0, maxIndex);
|
|
||||||
final int safeNew = newIndex.clamp(0, maxIndex);
|
|
||||||
|
|
||||||
if (safeOld == safeNew) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safeOld < safeNew) {
|
final DbNote movedNote = orderedNotes.removeAt(currentIndex);
|
||||||
await customStatement(
|
orderedNotes.insert(safeNewIndex, movedNote);
|
||||||
'UPDATE notes SET sort_index = sort_index - 1, is_dirty = 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0',
|
|
||||||
[safeOld, safeNew],
|
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 {
|
} else {
|
||||||
await customStatement(
|
newStoredPosition = midpointNotePosition(
|
||||||
'UPDATE notes SET sort_index = sort_index + 1, is_dirty = 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0',
|
higherPosition: orderedNotes[safeNewIndex - 1].sortIndex,
|
||||||
[safeNew, safeOld],
|
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(
|
await (update(notes)..where((n) => n.id.equals(id))).write(
|
||||||
NotesCompanion(
|
NotesCompanion(
|
||||||
sortIndex: Value<int>(safeNew),
|
sortIndex: Value<int>(newStoredPosition),
|
||||||
updatedAt: Value<DateTime>(DateTime.now()),
|
updatedAt: Value<DateTime>(DateTime.now()),
|
||||||
isDirty: const Value(true),
|
isDirty: const Value(true),
|
||||||
),
|
),
|
||||||
@@ -217,15 +283,23 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DbNote>> getDeletedNotes() {
|
Future<List<DbNote>> getDeletedNotes() {
|
||||||
// A note is considered deleted (in the trash) when `is_deleted` is true
|
// 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
|
// 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
|
// query required both title AND body to be non-empty which excluded
|
||||||
// notes that had an empty body (common) from appearing in the trash.
|
// notes that had an empty body (common) from appearing in the trash.
|
||||||
return (select(notes)..where(
|
return (select(notes)
|
||||||
(n) => n.isDeleted.equals(true) &
|
..where(
|
||||||
(n.title.isNotValue('') | n.body.isNotValue('')),
|
(n) =>
|
||||||
))
|
n.isDeleted.equals(true) &
|
||||||
.get();
|
(n.title.isNotValue('') | n.body.isNotValue('')),
|
||||||
|
)
|
||||||
|
..orderBy([
|
||||||
|
(note) => OrderingTerm(
|
||||||
|
expression: note.sortIndex,
|
||||||
|
mode: OrderingMode.desc,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
|
Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
|
||||||
|
|||||||
@@ -629,12 +629,10 @@ class $CategoriesTable extends Categories
|
|||||||
type: DriftSqlType.string,
|
type: DriftSqlType.string,
|
||||||
requiredDuringInsert: true,
|
requiredDuringInsert: true,
|
||||||
);
|
);
|
||||||
static const VerificationMeta _encryptedNameMeta = const VerificationMeta(
|
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||||
'encryptedName',
|
|
||||||
);
|
|
||||||
@override
|
@override
|
||||||
late final GeneratedColumn<String> encryptedName = GeneratedColumn<String>(
|
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||||
'encrypted_name',
|
'name',
|
||||||
aliasedName,
|
aliasedName,
|
||||||
false,
|
false,
|
||||||
type: DriftSqlType.string,
|
type: DriftSqlType.string,
|
||||||
@@ -667,21 +665,6 @@ class $CategoriesTable extends Categories
|
|||||||
),
|
),
|
||||||
defaultValue: const Constant(false),
|
defaultValue: const Constant(false),
|
||||||
);
|
);
|
||||||
static const VerificationMeta _isDirtyMeta = const VerificationMeta(
|
|
||||||
'isDirty',
|
|
||||||
);
|
|
||||||
@override
|
|
||||||
late final GeneratedColumn<bool> isDirty = GeneratedColumn<bool>(
|
|
||||||
'is_dirty',
|
|
||||||
aliasedName,
|
|
||||||
false,
|
|
||||||
type: DriftSqlType.bool,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
|
||||||
'CHECK ("is_dirty" IN (0, 1))',
|
|
||||||
),
|
|
||||||
defaultValue: const Constant(true),
|
|
||||||
);
|
|
||||||
static const VerificationMeta _colorValueMeta = const VerificationMeta(
|
static const VerificationMeta _colorValueMeta = const VerificationMeta(
|
||||||
'colorValue',
|
'colorValue',
|
||||||
);
|
);
|
||||||
@@ -704,6 +687,21 @@ class $CategoriesTable extends Categories
|
|||||||
type: DriftSqlType.int,
|
type: DriftSqlType.int,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _isDirtyMeta = const VerificationMeta(
|
||||||
|
'isDirty',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> isDirty = GeneratedColumn<bool>(
|
||||||
|
'is_dirty',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_dirty" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(true),
|
||||||
|
);
|
||||||
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
);
|
);
|
||||||
@@ -718,12 +716,12 @@ class $CategoriesTable extends Categories
|
|||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
id,
|
id,
|
||||||
encryptedName,
|
name,
|
||||||
serverVersion,
|
serverVersion,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
isDirty,
|
|
||||||
colorValue,
|
colorValue,
|
||||||
iconCodePoint,
|
iconCodePoint,
|
||||||
|
isDirty,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
@@ -743,16 +741,13 @@ class $CategoriesTable extends Categories
|
|||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_idMeta);
|
context.missing(_idMeta);
|
||||||
}
|
}
|
||||||
if (data.containsKey('encrypted_name')) {
|
if (data.containsKey('name')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_encryptedNameMeta,
|
_nameMeta,
|
||||||
encryptedName.isAcceptableOrUnknown(
|
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
|
||||||
data['encrypted_name']!,
|
|
||||||
_encryptedNameMeta,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_encryptedNameMeta);
|
context.missing(_nameMeta);
|
||||||
}
|
}
|
||||||
if (data.containsKey('server_version')) {
|
if (data.containsKey('server_version')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
@@ -769,12 +764,6 @@ class $CategoriesTable extends Categories
|
|||||||
isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta),
|
isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (data.containsKey('is_dirty')) {
|
|
||||||
context.handle(
|
|
||||||
_isDirtyMeta,
|
|
||||||
isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (data.containsKey('color_value')) {
|
if (data.containsKey('color_value')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_colorValueMeta,
|
_colorValueMeta,
|
||||||
@@ -790,6 +779,12 @@ class $CategoriesTable extends Categories
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('is_dirty')) {
|
||||||
|
context.handle(
|
||||||
|
_isDirtyMeta,
|
||||||
|
isDirty.isAcceptableOrUnknown(data['is_dirty']!, _isDirtyMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('updated_at')) {
|
if (data.containsKey('updated_at')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_updatedAtMeta,
|
_updatedAtMeta,
|
||||||
@@ -811,9 +806,9 @@ class $CategoriesTable extends Categories
|
|||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
data['${effectivePrefix}id'],
|
data['${effectivePrefix}id'],
|
||||||
)!,
|
)!,
|
||||||
encryptedName: attachedDatabase.typeMapping.read(
|
name: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
data['${effectivePrefix}encrypted_name'],
|
data['${effectivePrefix}name'],
|
||||||
)!,
|
)!,
|
||||||
serverVersion: attachedDatabase.typeMapping.read(
|
serverVersion: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
@@ -823,10 +818,6 @@ class $CategoriesTable extends Categories
|
|||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_deleted'],
|
data['${effectivePrefix}is_deleted'],
|
||||||
)!,
|
)!,
|
||||||
isDirty: attachedDatabase.typeMapping.read(
|
|
||||||
DriftSqlType.bool,
|
|
||||||
data['${effectivePrefix}is_dirty'],
|
|
||||||
)!,
|
|
||||||
colorValue: attachedDatabase.typeMapping.read(
|
colorValue: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}color_value'],
|
data['${effectivePrefix}color_value'],
|
||||||
@@ -835,6 +826,10 @@ class $CategoriesTable extends Categories
|
|||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}icon_code_point'],
|
data['${effectivePrefix}icon_code_point'],
|
||||||
),
|
),
|
||||||
|
isDirty: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_dirty'],
|
||||||
|
)!,
|
||||||
updatedAt: attachedDatabase.typeMapping.read(
|
updatedAt: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}updated_at'],
|
data['${effectivePrefix}updated_at'],
|
||||||
@@ -850,37 +845,37 @@ class $CategoriesTable extends Categories
|
|||||||
|
|
||||||
class DbCategory extends DataClass implements Insertable<DbCategory> {
|
class DbCategory extends DataClass implements Insertable<DbCategory> {
|
||||||
final String id;
|
final String id;
|
||||||
final String encryptedName;
|
final String name;
|
||||||
final int serverVersion;
|
final int serverVersion;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
final bool isDirty;
|
|
||||||
final int? colorValue;
|
final int? colorValue;
|
||||||
final int? iconCodePoint;
|
final int? iconCodePoint;
|
||||||
|
final bool isDirty;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
const DbCategory({
|
const DbCategory({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.encryptedName,
|
required this.name,
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
required this.isDeleted,
|
required this.isDeleted,
|
||||||
required this.isDirty,
|
|
||||||
this.colorValue,
|
this.colorValue,
|
||||||
this.iconCodePoint,
|
this.iconCodePoint,
|
||||||
|
required this.isDirty,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, Expression>{};
|
final map = <String, Expression>{};
|
||||||
map['id'] = Variable<String>(id);
|
map['id'] = Variable<String>(id);
|
||||||
map['encrypted_name'] = Variable<String>(encryptedName);
|
map['name'] = Variable<String>(name);
|
||||||
map['server_version'] = Variable<int>(serverVersion);
|
map['server_version'] = Variable<int>(serverVersion);
|
||||||
map['is_deleted'] = Variable<bool>(isDeleted);
|
map['is_deleted'] = Variable<bool>(isDeleted);
|
||||||
map['is_dirty'] = Variable<bool>(isDirty);
|
|
||||||
if (!nullToAbsent || colorValue != null) {
|
if (!nullToAbsent || colorValue != null) {
|
||||||
map['color_value'] = Variable<int>(colorValue);
|
map['color_value'] = Variable<int>(colorValue);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || iconCodePoint != null) {
|
if (!nullToAbsent || iconCodePoint != null) {
|
||||||
map['icon_code_point'] = Variable<int>(iconCodePoint);
|
map['icon_code_point'] = Variable<int>(iconCodePoint);
|
||||||
}
|
}
|
||||||
|
map['is_dirty'] = Variable<bool>(isDirty);
|
||||||
map['updated_at'] = Variable<DateTime>(updatedAt);
|
map['updated_at'] = Variable<DateTime>(updatedAt);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -888,16 +883,16 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
CategoriesCompanion toCompanion(bool nullToAbsent) {
|
CategoriesCompanion toCompanion(bool nullToAbsent) {
|
||||||
return CategoriesCompanion(
|
return CategoriesCompanion(
|
||||||
id: Value(id),
|
id: Value(id),
|
||||||
encryptedName: Value(encryptedName),
|
name: Value(name),
|
||||||
serverVersion: Value(serverVersion),
|
serverVersion: Value(serverVersion),
|
||||||
isDeleted: Value(isDeleted),
|
isDeleted: Value(isDeleted),
|
||||||
isDirty: Value(isDirty),
|
|
||||||
colorValue: colorValue == null && nullToAbsent
|
colorValue: colorValue == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value<int?>(colorValue),
|
: Value(colorValue),
|
||||||
iconCodePoint: iconCodePoint == null && nullToAbsent
|
iconCodePoint: iconCodePoint == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value<int?>(iconCodePoint),
|
: Value(iconCodePoint),
|
||||||
|
isDirty: Value(isDirty),
|
||||||
updatedAt: Value(updatedAt),
|
updatedAt: Value(updatedAt),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -909,12 +904,12 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return DbCategory(
|
return DbCategory(
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
encryptedName: serializer.fromJson<String>(json['encryptedName']),
|
name: serializer.fromJson<String>(json['name']),
|
||||||
serverVersion: serializer.fromJson<int>(json['serverVersion']),
|
serverVersion: serializer.fromJson<int>(json['serverVersion']),
|
||||||
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
isDeleted: serializer.fromJson<bool>(json['isDeleted']),
|
||||||
isDirty: serializer.fromJson<bool>(json['isDirty']),
|
|
||||||
colorValue: serializer.fromJson<int?>(json['colorValue']),
|
colorValue: serializer.fromJson<int?>(json['colorValue']),
|
||||||
iconCodePoint: serializer.fromJson<int?>(json['iconCodePoint']),
|
iconCodePoint: serializer.fromJson<int?>(json['iconCodePoint']),
|
||||||
|
isDirty: serializer.fromJson<bool>(json['isDirty']),
|
||||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -923,52 +918,52 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'encryptedName': serializer.toJson<String>(encryptedName),
|
'name': serializer.toJson<String>(name),
|
||||||
'serverVersion': serializer.toJson<int>(serverVersion),
|
'serverVersion': serializer.toJson<int>(serverVersion),
|
||||||
'isDeleted': serializer.toJson<bool>(isDeleted),
|
'isDeleted': serializer.toJson<bool>(isDeleted),
|
||||||
'isDirty': serializer.toJson<bool>(isDirty),
|
|
||||||
'colorValue': serializer.toJson<int?>(colorValue),
|
'colorValue': serializer.toJson<int?>(colorValue),
|
||||||
'iconCodePoint': serializer.toJson<int?>(iconCodePoint),
|
'iconCodePoint': serializer.toJson<int?>(iconCodePoint),
|
||||||
|
'isDirty': serializer.toJson<bool>(isDirty),
|
||||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DbCategory copyWith({
|
DbCategory copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? encryptedName,
|
String? name,
|
||||||
int? serverVersion,
|
int? serverVersion,
|
||||||
bool? isDeleted,
|
bool? isDeleted,
|
||||||
|
Value<int?> colorValue = const Value.absent(),
|
||||||
|
Value<int?> iconCodePoint = const Value.absent(),
|
||||||
bool? isDirty,
|
bool? isDirty,
|
||||||
int? colorValue,
|
|
||||||
int? iconCodePoint,
|
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
}) => DbCategory(
|
}) => DbCategory(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
encryptedName: encryptedName ?? this.encryptedName,
|
name: name ?? this.name,
|
||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
|
colorValue: colorValue.present ? colorValue.value : this.colorValue,
|
||||||
|
iconCodePoint: iconCodePoint.present
|
||||||
|
? iconCodePoint.value
|
||||||
|
: this.iconCodePoint,
|
||||||
isDirty: isDirty ?? this.isDirty,
|
isDirty: isDirty ?? this.isDirty,
|
||||||
colorValue: colorValue ?? this.colorValue,
|
|
||||||
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
);
|
);
|
||||||
DbCategory copyWithCompanion(CategoriesCompanion data) {
|
DbCategory copyWithCompanion(CategoriesCompanion data) {
|
||||||
return DbCategory(
|
return DbCategory(
|
||||||
id: data.id.present ? data.id.value : this.id,
|
id: data.id.present ? data.id.value : this.id,
|
||||||
encryptedName: data.encryptedName.present
|
name: data.name.present ? data.name.value : this.name,
|
||||||
? data.encryptedName.value
|
|
||||||
: this.encryptedName,
|
|
||||||
serverVersion: data.serverVersion.present
|
serverVersion: data.serverVersion.present
|
||||||
? data.serverVersion.value
|
? data.serverVersion.value
|
||||||
: this.serverVersion,
|
: this.serverVersion,
|
||||||
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
|
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
|
||||||
isDirty: data.isDirty.present ? data.isDirty.value : this.isDirty,
|
|
||||||
colorValue: data.colorValue.present
|
colorValue: data.colorValue.present
|
||||||
? data.colorValue.value
|
? data.colorValue.value
|
||||||
: this.colorValue,
|
: this.colorValue,
|
||||||
iconCodePoint: data.iconCodePoint.present
|
iconCodePoint: data.iconCodePoint.present
|
||||||
? data.iconCodePoint.value
|
? data.iconCodePoint.value
|
||||||
: this.iconCodePoint,
|
: this.iconCodePoint,
|
||||||
|
isDirty: data.isDirty.present ? data.isDirty.value : this.isDirty,
|
||||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -977,9 +972,11 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('DbCategory(')
|
return (StringBuffer('DbCategory(')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('encryptedName: $encryptedName, ')
|
..write('name: $name, ')
|
||||||
..write('serverVersion: $serverVersion, ')
|
..write('serverVersion: $serverVersion, ')
|
||||||
..write('isDeleted: $isDeleted, ')
|
..write('isDeleted: $isDeleted, ')
|
||||||
|
..write('colorValue: $colorValue, ')
|
||||||
|
..write('iconCodePoint: $iconCodePoint, ')
|
||||||
..write('isDirty: $isDirty, ')
|
..write('isDirty: $isDirty, ')
|
||||||
..write('updatedAt: $updatedAt')
|
..write('updatedAt: $updatedAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
@@ -989,9 +986,11 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
id,
|
id,
|
||||||
encryptedName,
|
name,
|
||||||
serverVersion,
|
serverVersion,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
|
colorValue,
|
||||||
|
iconCodePoint,
|
||||||
isDirty,
|
isDirty,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
);
|
);
|
||||||
@@ -1000,66 +999,68 @@ class DbCategory extends DataClass implements Insertable<DbCategory> {
|
|||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is DbCategory &&
|
(other is DbCategory &&
|
||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.encryptedName == this.encryptedName &&
|
other.name == this.name &&
|
||||||
other.serverVersion == this.serverVersion &&
|
other.serverVersion == this.serverVersion &&
|
||||||
other.isDeleted == this.isDeleted &&
|
other.isDeleted == this.isDeleted &&
|
||||||
|
other.colorValue == this.colorValue &&
|
||||||
|
other.iconCodePoint == this.iconCodePoint &&
|
||||||
other.isDirty == this.isDirty &&
|
other.isDirty == this.isDirty &&
|
||||||
other.updatedAt == this.updatedAt);
|
other.updatedAt == this.updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
||||||
final Value<String> id;
|
final Value<String> id;
|
||||||
final Value<String> encryptedName;
|
final Value<String> name;
|
||||||
final Value<int> serverVersion;
|
final Value<int> serverVersion;
|
||||||
final Value<bool> isDeleted;
|
final Value<bool> isDeleted;
|
||||||
|
final Value<int?> colorValue;
|
||||||
|
final Value<int?> iconCodePoint;
|
||||||
final Value<bool> isDirty;
|
final Value<bool> isDirty;
|
||||||
final Value<DateTime> updatedAt;
|
final Value<DateTime> updatedAt;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
final Value<int?> colorValue;
|
|
||||||
final Value<int?> iconCodePoint;
|
|
||||||
const CategoriesCompanion({
|
const CategoriesCompanion({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.encryptedName = const Value.absent(),
|
this.name = const Value.absent(),
|
||||||
this.serverVersion = const Value.absent(),
|
this.serverVersion = const Value.absent(),
|
||||||
this.isDeleted = const Value.absent(),
|
this.isDeleted = const Value.absent(),
|
||||||
this.isDirty = const Value.absent(),
|
|
||||||
this.colorValue = const Value.absent(),
|
this.colorValue = const Value.absent(),
|
||||||
this.iconCodePoint = const Value.absent(),
|
this.iconCodePoint = const Value.absent(),
|
||||||
|
this.isDirty = const Value.absent(),
|
||||||
this.updatedAt = const Value.absent(),
|
this.updatedAt = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
});
|
});
|
||||||
CategoriesCompanion.insert({
|
CategoriesCompanion.insert({
|
||||||
required String id,
|
required String id,
|
||||||
required String encryptedName,
|
required String name,
|
||||||
this.serverVersion = const Value.absent(),
|
this.serverVersion = const Value.absent(),
|
||||||
this.isDeleted = const Value.absent(),
|
this.isDeleted = const Value.absent(),
|
||||||
this.isDirty = const Value.absent(),
|
|
||||||
this.colorValue = const Value.absent(),
|
this.colorValue = const Value.absent(),
|
||||||
this.iconCodePoint = const Value.absent(),
|
this.iconCodePoint = const Value.absent(),
|
||||||
|
this.isDirty = const Value.absent(),
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
}) : id = Value(id),
|
}) : id = Value(id),
|
||||||
encryptedName = Value(encryptedName),
|
name = Value(name),
|
||||||
updatedAt = Value(updatedAt);
|
updatedAt = Value(updatedAt);
|
||||||
static Insertable<DbCategory> custom({
|
static Insertable<DbCategory> custom({
|
||||||
Expression<String>? id,
|
Expression<String>? id,
|
||||||
Expression<String>? encryptedName,
|
Expression<String>? name,
|
||||||
Expression<int>? serverVersion,
|
Expression<int>? serverVersion,
|
||||||
Expression<bool>? isDeleted,
|
Expression<bool>? isDeleted,
|
||||||
Expression<bool>? isDirty,
|
|
||||||
Expression<int>? colorValue,
|
Expression<int>? colorValue,
|
||||||
Expression<int>? iconCodePoint,
|
Expression<int>? iconCodePoint,
|
||||||
|
Expression<bool>? isDirty,
|
||||||
Expression<DateTime>? updatedAt,
|
Expression<DateTime>? updatedAt,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
if (encryptedName != null) 'encrypted_name': encryptedName,
|
if (name != null) 'name': name,
|
||||||
if (serverVersion != null) 'server_version': serverVersion,
|
if (serverVersion != null) 'server_version': serverVersion,
|
||||||
if (isDeleted != null) 'is_deleted': isDeleted,
|
if (isDeleted != null) 'is_deleted': isDeleted,
|
||||||
if (isDirty != null) 'is_dirty': isDirty,
|
|
||||||
if (colorValue != null) 'color_value': colorValue,
|
if (colorValue != null) 'color_value': colorValue,
|
||||||
if (iconCodePoint != null) 'icon_code_point': iconCodePoint,
|
if (iconCodePoint != null) 'icon_code_point': iconCodePoint,
|
||||||
|
if (isDirty != null) 'is_dirty': isDirty,
|
||||||
if (updatedAt != null) 'updated_at': updatedAt,
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
});
|
});
|
||||||
@@ -1067,23 +1068,23 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
|
|
||||||
CategoriesCompanion copyWith({
|
CategoriesCompanion copyWith({
|
||||||
Value<String>? id,
|
Value<String>? id,
|
||||||
Value<String>? encryptedName,
|
Value<String>? name,
|
||||||
Value<int>? serverVersion,
|
Value<int>? serverVersion,
|
||||||
Value<bool>? isDeleted,
|
Value<bool>? isDeleted,
|
||||||
|
Value<int?>? colorValue,
|
||||||
|
Value<int?>? iconCodePoint,
|
||||||
Value<bool>? isDirty,
|
Value<bool>? isDirty,
|
||||||
Value<int>? colorValue,
|
|
||||||
Value<int>? iconCodePoint,
|
|
||||||
Value<DateTime>? updatedAt,
|
Value<DateTime>? updatedAt,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return CategoriesCompanion(
|
return CategoriesCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
encryptedName: encryptedName ?? this.encryptedName,
|
name: name ?? this.name,
|
||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
isDeleted: isDeleted ?? this.isDeleted,
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
isDirty: isDirty ?? this.isDirty,
|
|
||||||
colorValue: colorValue ?? this.colorValue,
|
colorValue: colorValue ?? this.colorValue,
|
||||||
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
iconCodePoint: iconCodePoint ?? this.iconCodePoint,
|
||||||
|
isDirty: isDirty ?? this.isDirty,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
);
|
);
|
||||||
@@ -1095,8 +1096,8 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
if (id.present) {
|
if (id.present) {
|
||||||
map['id'] = Variable<String>(id.value);
|
map['id'] = Variable<String>(id.value);
|
||||||
}
|
}
|
||||||
if (encryptedName.present) {
|
if (name.present) {
|
||||||
map['encrypted_name'] = Variable<String>(encryptedName.value);
|
map['name'] = Variable<String>(name.value);
|
||||||
}
|
}
|
||||||
if (serverVersion.present) {
|
if (serverVersion.present) {
|
||||||
map['server_version'] = Variable<int>(serverVersion.value);
|
map['server_version'] = Variable<int>(serverVersion.value);
|
||||||
@@ -1104,15 +1105,15 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
if (isDeleted.present) {
|
if (isDeleted.present) {
|
||||||
map['is_deleted'] = Variable<bool>(isDeleted.value);
|
map['is_deleted'] = Variable<bool>(isDeleted.value);
|
||||||
}
|
}
|
||||||
if (isDirty.present) {
|
|
||||||
map['is_dirty'] = Variable<bool>(isDirty.value);
|
|
||||||
}
|
|
||||||
if (colorValue.present) {
|
if (colorValue.present) {
|
||||||
map['color_value'] = Variable<int>(colorValue.value);
|
map['color_value'] = Variable<int>(colorValue.value);
|
||||||
}
|
}
|
||||||
if (iconCodePoint.present) {
|
if (iconCodePoint.present) {
|
||||||
map['icon_code_point'] = Variable<int>(iconCodePoint.value);
|
map['icon_code_point'] = Variable<int>(iconCodePoint.value);
|
||||||
}
|
}
|
||||||
|
if (isDirty.present) {
|
||||||
|
map['is_dirty'] = Variable<bool>(isDirty.value);
|
||||||
|
}
|
||||||
if (updatedAt.present) {
|
if (updatedAt.present) {
|
||||||
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
||||||
}
|
}
|
||||||
@@ -1126,9 +1127,11 @@ class CategoriesCompanion extends UpdateCompanion<DbCategory> {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('CategoriesCompanion(')
|
return (StringBuffer('CategoriesCompanion(')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('encryptedName: $encryptedName, ')
|
..write('name: $name, ')
|
||||||
..write('serverVersion: $serverVersion, ')
|
..write('serverVersion: $serverVersion, ')
|
||||||
..write('isDeleted: $isDeleted, ')
|
..write('isDeleted: $isDeleted, ')
|
||||||
|
..write('colorValue: $colorValue, ')
|
||||||
|
..write('iconCodePoint: $iconCodePoint, ')
|
||||||
..write('isDirty: $isDirty, ')
|
..write('isDirty: $isDirty, ')
|
||||||
..write('updatedAt: $updatedAt, ')
|
..write('updatedAt: $updatedAt, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
@@ -1444,9 +1447,11 @@ typedef $$NotesTableProcessedTableManager =
|
|||||||
typedef $$CategoriesTableCreateCompanionBuilder =
|
typedef $$CategoriesTableCreateCompanionBuilder =
|
||||||
CategoriesCompanion Function({
|
CategoriesCompanion Function({
|
||||||
required String id,
|
required String id,
|
||||||
required String encryptedName,
|
required String name,
|
||||||
Value<int> serverVersion,
|
Value<int> serverVersion,
|
||||||
Value<bool> isDeleted,
|
Value<bool> isDeleted,
|
||||||
|
Value<int?> colorValue,
|
||||||
|
Value<int?> iconCodePoint,
|
||||||
Value<bool> isDirty,
|
Value<bool> isDirty,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
@@ -1454,9 +1459,11 @@ typedef $$CategoriesTableCreateCompanionBuilder =
|
|||||||
typedef $$CategoriesTableUpdateCompanionBuilder =
|
typedef $$CategoriesTableUpdateCompanionBuilder =
|
||||||
CategoriesCompanion Function({
|
CategoriesCompanion Function({
|
||||||
Value<String> id,
|
Value<String> id,
|
||||||
Value<String> encryptedName,
|
Value<String> name,
|
||||||
Value<int> serverVersion,
|
Value<int> serverVersion,
|
||||||
Value<bool> isDeleted,
|
Value<bool> isDeleted,
|
||||||
|
Value<int?> colorValue,
|
||||||
|
Value<int?> iconCodePoint,
|
||||||
Value<bool> isDirty,
|
Value<bool> isDirty,
|
||||||
Value<DateTime> updatedAt,
|
Value<DateTime> updatedAt,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
@@ -1476,8 +1483,8 @@ class $$CategoriesTableFilterComposer
|
|||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
ColumnFilters<String> get encryptedName => $composableBuilder(
|
ColumnFilters<String> get name => $composableBuilder(
|
||||||
column: $table.encryptedName,
|
column: $table.name,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1491,6 +1498,16 @@ class $$CategoriesTableFilterComposer
|
|||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get colorValue => $composableBuilder(
|
||||||
|
column: $table.colorValue,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get iconCodePoint => $composableBuilder(
|
||||||
|
column: $table.iconCodePoint,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnFilters<bool> get isDirty => $composableBuilder(
|
ColumnFilters<bool> get isDirty => $composableBuilder(
|
||||||
column: $table.isDirty,
|
column: $table.isDirty,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
@@ -1516,8 +1533,8 @@ class $$CategoriesTableOrderingComposer
|
|||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
ColumnOrderings<String> get encryptedName => $composableBuilder(
|
ColumnOrderings<String> get name => $composableBuilder(
|
||||||
column: $table.encryptedName,
|
column: $table.name,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1531,6 +1548,16 @@ class $$CategoriesTableOrderingComposer
|
|||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get colorValue => $composableBuilder(
|
||||||
|
column: $table.colorValue,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get iconCodePoint => $composableBuilder(
|
||||||
|
column: $table.iconCodePoint,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnOrderings<bool> get isDirty => $composableBuilder(
|
ColumnOrderings<bool> get isDirty => $composableBuilder(
|
||||||
column: $table.isDirty,
|
column: $table.isDirty,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
@@ -1554,10 +1581,8 @@ class $$CategoriesTableAnnotationComposer
|
|||||||
GeneratedColumn<String> get id =>
|
GeneratedColumn<String> get id =>
|
||||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
GeneratedColumn<String> get encryptedName => $composableBuilder(
|
GeneratedColumn<String> get name =>
|
||||||
column: $table.encryptedName,
|
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||||
builder: (column) => column,
|
|
||||||
);
|
|
||||||
|
|
||||||
GeneratedColumn<int> get serverVersion => $composableBuilder(
|
GeneratedColumn<int> get serverVersion => $composableBuilder(
|
||||||
column: $table.serverVersion,
|
column: $table.serverVersion,
|
||||||
@@ -1567,6 +1592,16 @@ class $$CategoriesTableAnnotationComposer
|
|||||||
GeneratedColumn<bool> get isDeleted =>
|
GeneratedColumn<bool> get isDeleted =>
|
||||||
$composableBuilder(column: $table.isDeleted, builder: (column) => column);
|
$composableBuilder(column: $table.isDeleted, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get colorValue => $composableBuilder(
|
||||||
|
column: $table.colorValue,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get iconCodePoint => $composableBuilder(
|
||||||
|
column: $table.iconCodePoint,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedColumn<bool> get isDirty =>
|
GeneratedColumn<bool> get isDirty =>
|
||||||
$composableBuilder(column: $table.isDirty, builder: (column) => column);
|
$composableBuilder(column: $table.isDirty, builder: (column) => column);
|
||||||
|
|
||||||
@@ -1606,17 +1641,21 @@ class $$CategoriesTableTableManager
|
|||||||
updateCompanionCallback:
|
updateCompanionCallback:
|
||||||
({
|
({
|
||||||
Value<String> id = const Value.absent(),
|
Value<String> id = const Value.absent(),
|
||||||
Value<String> encryptedName = const Value.absent(),
|
Value<String> name = const Value.absent(),
|
||||||
Value<int> serverVersion = const Value.absent(),
|
Value<int> serverVersion = const Value.absent(),
|
||||||
Value<bool> isDeleted = const Value.absent(),
|
Value<bool> isDeleted = const Value.absent(),
|
||||||
|
Value<int?> colorValue = const Value.absent(),
|
||||||
|
Value<int?> iconCodePoint = const Value.absent(),
|
||||||
Value<bool> isDirty = const Value.absent(),
|
Value<bool> isDirty = const Value.absent(),
|
||||||
Value<DateTime> updatedAt = const Value.absent(),
|
Value<DateTime> updatedAt = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => CategoriesCompanion(
|
}) => CategoriesCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
encryptedName: encryptedName,
|
name: name,
|
||||||
serverVersion: serverVersion,
|
serverVersion: serverVersion,
|
||||||
isDeleted: isDeleted,
|
isDeleted: isDeleted,
|
||||||
|
colorValue: colorValue,
|
||||||
|
iconCodePoint: iconCodePoint,
|
||||||
isDirty: isDirty,
|
isDirty: isDirty,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
@@ -1624,17 +1663,21 @@ class $$CategoriesTableTableManager
|
|||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
required String id,
|
required String id,
|
||||||
required String encryptedName,
|
required String name,
|
||||||
Value<int> serverVersion = const Value.absent(),
|
Value<int> serverVersion = const Value.absent(),
|
||||||
Value<bool> isDeleted = const Value.absent(),
|
Value<bool> isDeleted = const Value.absent(),
|
||||||
|
Value<int?> colorValue = const Value.absent(),
|
||||||
|
Value<int?> iconCodePoint = const Value.absent(),
|
||||||
Value<bool> isDirty = const Value.absent(),
|
Value<bool> isDirty = const Value.absent(),
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => CategoriesCompanion.insert(
|
}) => CategoriesCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
encryptedName: encryptedName,
|
name: name,
|
||||||
serverVersion: serverVersion,
|
serverVersion: serverVersion,
|
||||||
isDeleted: isDeleted,
|
isDeleted: isDeleted,
|
||||||
|
colorValue: colorValue,
|
||||||
|
iconCodePoint: iconCodePoint,
|
||||||
isDirty: isDirty,
|
isDirty: isDirty,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ class NoteEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Desencripta el contenido de una nota usando el master key
|
/// Desencripta el contenido de una nota usando el master key
|
||||||
static Future<String> decryptNote(
|
static Future<String> decrypt(
|
||||||
String encodedBox,
|
String encodedBox,
|
||||||
String masterKey,
|
String masterKey,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'dart:io' show Platform;
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:notas/data/app_database.dart';
|
import 'package:notas/data/app_database.dart';
|
||||||
import 'package:notas/data/api_client.dart';
|
import 'package:notas/data/api_client.dart';
|
||||||
|
import 'package:notas/data/note_positioning.dart';
|
||||||
import 'package:notas/data/sync_models.dart';
|
import 'package:notas/data/sync_models.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/models/category.dart';
|
import 'package:notas/models/category.dart';
|
||||||
@@ -39,43 +40,43 @@ class NoteRepository {
|
|||||||
final List<DbCategory> dbCategories = await _database.getAllCategories();
|
final List<DbCategory> dbCategories = await _database.getAllCategories();
|
||||||
final List<Category> categories = [];
|
final List<Category> categories = [];
|
||||||
for (final DbCategory row in dbCategories) {
|
for (final DbCategory row in dbCategories) {
|
||||||
try {
|
categories.add(
|
||||||
final String decryptedName = await NoteEncryption.decryptNote(
|
Category(
|
||||||
row.encryptedName,
|
id: row.id,
|
||||||
_masterKey,
|
name: row.name,
|
||||||
);
|
serverVersion: row.serverVersion,
|
||||||
categories.add(
|
isDeleted: row.isDeleted,
|
||||||
Category(
|
updatedAt: row.updatedAt,
|
||||||
id: row.id,
|
isDirty: row.isDirty,
|
||||||
name: decryptedName,
|
colorValue: row.colorValue,
|
||||||
serverVersion: row.serverVersion,
|
iconCodePoint: row.iconCodePoint,
|
||||||
isDeleted: row.isDeleted,
|
),
|
||||||
updatedAt: row.updatedAt,
|
);
|
||||||
isDirty: row.isDirty,
|
|
||||||
colorValue: row.colorValue,
|
|
||||||
iconCodePoint: row.iconCodePoint,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error al desencriptar categoría: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DateTime?> getLastSyncAt() async {
|
||||||
|
return _authApi.getLastSyncAt();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> createCategory(Category category) async {
|
Future<void> createCategory(Category category) async {
|
||||||
debugPrint('createCategory called with: ${category.name}');
|
debugPrint('createCategory called with: ${category.name}');
|
||||||
final String encryptedName = await NoteEncryption.encryptNote(
|
|
||||||
category.name,
|
final DbCategory? existingCategory = await (_database.select(
|
||||||
_masterKey,
|
_database.categories,
|
||||||
|
)..where((c) => c.id.equals(category.id))).getSingleOrNull();
|
||||||
|
final int effectiveServerVersion = math.max(
|
||||||
|
category.serverVersion,
|
||||||
|
existingCategory?.serverVersion ?? category.serverVersion,
|
||||||
);
|
);
|
||||||
debugPrint('Category name encrypted');
|
|
||||||
await _database.upsertCategory(
|
await _database.upsertCategory(
|
||||||
CategoriesCompanion.insert(
|
CategoriesCompanion.insert(
|
||||||
id: category.id,
|
id: category.id,
|
||||||
encryptedName: encryptedName,
|
name: category.name,
|
||||||
updatedAt: category.updatedAt,
|
updatedAt: category.updatedAt,
|
||||||
serverVersion: const Value(0),
|
serverVersion: Value(effectiveServerVersion),
|
||||||
isDeleted: const Value(false),
|
isDeleted: const Value(false),
|
||||||
isDirty: const Value(true),
|
isDirty: const Value(true),
|
||||||
colorValue: Value<int?>(category.colorValue),
|
colorValue: Value<int?>(category.colorValue),
|
||||||
@@ -95,7 +96,7 @@ class NoteRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Note> createNote(Note note) async {
|
Future<Note> createNote(Note note) async {
|
||||||
await _database.insertNoteAtTop(
|
final int storedPosition = await _database.insertNoteAtTop(
|
||||||
NotesCompanion.insert(
|
NotesCompanion.insert(
|
||||||
id: note.id,
|
id: note.id,
|
||||||
title: note.title,
|
title: note.title,
|
||||||
@@ -110,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 {
|
Future<Note> updateNote(Note note) async {
|
||||||
@@ -141,7 +145,7 @@ class NoteRepository {
|
|||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
isPermanentlyDeleted: false,
|
isPermanentlyDeleted: false,
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
position: row.sortIndex.toDouble(),
|
position: fromStoredNotePosition(row.sortIndex),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,19 +321,19 @@ class NoteRepository {
|
|||||||
// Apply categories from server
|
// Apply categories from server
|
||||||
for (final SyncCategoryResponse catResponse
|
for (final SyncCategoryResponse catResponse
|
||||||
in response.changes.categories) {
|
in response.changes.categories) {
|
||||||
// Store the encrypted blob received from the server directly in the DB.
|
final Category category = await catResponse.toCategory(
|
||||||
// Decryption is only performed when loading categories for display.
|
masterKey: _masterKey,
|
||||||
final String encryptedBlob = catResponse.encryptedName;
|
);
|
||||||
|
|
||||||
await _database.upsertCategory(
|
await _database.upsertCategory(
|
||||||
CategoriesCompanion(
|
CategoriesCompanion(
|
||||||
id: Value(catResponse.id),
|
id: Value(category.id),
|
||||||
encryptedName: Value(encryptedBlob),
|
name: Value(category.name),
|
||||||
serverVersion: Value(catResponse.serverVersion),
|
serverVersion: Value(category.serverVersion),
|
||||||
isDeleted: Value(catResponse.isDeleted),
|
isDeleted: Value(category.isDeleted),
|
||||||
colorValue: Value<int?>(catResponse.colorValue),
|
colorValue: Value<int?>(category.colorValue),
|
||||||
iconCodePoint: Value<int?>(catResponse.iconCodePoint),
|
iconCodePoint: Value<int?>(category.iconCodePoint),
|
||||||
updatedAt: Value(catResponse.updatedAt),
|
updatedAt: Value(category.updatedAt),
|
||||||
isDirty: const Value(false),
|
isDirty: const Value(false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -368,7 +372,7 @@ class NoteRepository {
|
|||||||
body: isPermanentlyDeleted ? '' : decryptedBody,
|
body: isPermanentlyDeleted ? '' : decryptedBody,
|
||||||
createdAt: existingNote.createdAt,
|
createdAt: existingNote.createdAt,
|
||||||
updatedAt: noteResponse.updatedAt,
|
updatedAt: noteResponse.updatedAt,
|
||||||
sortIndex: noteResponse.position.round(),
|
sortIndex: toStoredNotePosition(noteResponse.position),
|
||||||
serverVersion: noteResponse.serverVersion,
|
serverVersion: noteResponse.serverVersion,
|
||||||
isDeleted: noteResponse.isDeleted,
|
isDeleted: noteResponse.isDeleted,
|
||||||
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
|
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
|
||||||
@@ -386,7 +390,7 @@ class NoteRepository {
|
|||||||
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
|
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
|
||||||
createdAt: Value(noteResponse.updatedAt),
|
createdAt: Value(noteResponse.updatedAt),
|
||||||
updatedAt: Value(noteResponse.updatedAt),
|
updatedAt: Value(noteResponse.updatedAt),
|
||||||
sortIndex: Value(noteResponse.position.round()),
|
sortIndex: Value(toStoredNotePosition(noteResponse.position)),
|
||||||
serverVersion: Value(noteResponse.serverVersion),
|
serverVersion: Value(noteResponse.serverVersion),
|
||||||
isDeleted: Value(noteResponse.isDeleted),
|
isDeleted: Value(noteResponse.isDeleted),
|
||||||
categoryId: Value(
|
categoryId: Value(
|
||||||
@@ -416,7 +420,7 @@ class NoteRepository {
|
|||||||
body: row.body,
|
body: row.body,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
position: row.sortIndex.toDouble(),
|
position: fromStoredNotePosition(row.sortIndex),
|
||||||
serverVersion: row.serverVersion,
|
serverVersion: row.serverVersion,
|
||||||
isDeleted: row.isDeleted,
|
isDeleted: row.isDeleted,
|
||||||
isPermanentlyDeleted: _isPermanentlyDeleted(row),
|
isPermanentlyDeleted: _isPermanentlyDeleted(row),
|
||||||
@@ -490,16 +494,11 @@ Future<List<SyncCategoryPayload>> _encryptCategories(
|
|||||||
final List<SyncCategoryPayload> payloads = [];
|
final List<SyncCategoryPayload> payloads = [];
|
||||||
|
|
||||||
for (final DbCategory row in categories) {
|
for (final DbCategory row in categories) {
|
||||||
// The DB already stores the encrypted name blob in `encryptedName`.
|
|
||||||
// Use it directly when building the sync payload and preserve
|
|
||||||
// color/icon values from the DB row so they are sent to the server.
|
|
||||||
final String encryptedName = row.encryptedName;
|
|
||||||
|
|
||||||
payloads.add(
|
payloads.add(
|
||||||
SyncCategoryPayload.fromCategory(
|
await SyncCategoryPayload.fromCategory(
|
||||||
Category(
|
Category(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.encryptedName,
|
name: row.name,
|
||||||
serverVersion: row.serverVersion,
|
serverVersion: row.serverVersion,
|
||||||
isDeleted: row.isDeleted,
|
isDeleted: row.isDeleted,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
@@ -507,7 +506,7 @@ Future<List<SyncCategoryPayload>> _encryptCategories(
|
|||||||
colorValue: row.colorValue,
|
colorValue: row.colorValue,
|
||||||
iconCodePoint: row.iconCodePoint,
|
iconCodePoint: row.iconCodePoint,
|
||||||
),
|
),
|
||||||
encryptedName: encryptedName,
|
masterKey: masterKey,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -594,7 +593,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
|
|||||||
'updatedAt': row.updatedAt.toIso8601String(),
|
'updatedAt': row.updatedAt.toIso8601String(),
|
||||||
'categoryId': row.categoryId,
|
'categoryId': row.categoryId,
|
||||||
'serverVersion': row.serverVersion,
|
'serverVersion': row.serverVersion,
|
||||||
'position': row.sortIndex,
|
'position': fromStoredNotePosition(row.sortIndex),
|
||||||
'isDeleted': row.isDeleted,
|
'isDeleted': row.isDeleted,
|
||||||
'isPermanentlyDeleted': isPermanentlyDeleted,
|
'isPermanentlyDeleted': isPermanentlyDeleted,
|
||||||
};
|
};
|
||||||
@@ -644,7 +643,7 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
|
|||||||
'encryptedTitle': encryptedTitle,
|
'encryptedTitle': encryptedTitle,
|
||||||
'encryptedBody': encryptedBody,
|
'encryptedBody': encryptedBody,
|
||||||
'serverVersion': note['serverVersion']! as int,
|
'serverVersion': note['serverVersion']! as int,
|
||||||
'position': note['position']! as int,
|
'position': (note['position'] as num).toDouble(),
|
||||||
'isDeleted': note['isDeleted']! as bool,
|
'isDeleted': note['isDeleted']! as bool,
|
||||||
'isPermanentlyDeleted': isPermanentlyDeleted,
|
'isPermanentlyDeleted': isPermanentlyDeleted,
|
||||||
'updatedAt': note['updatedAt']! as String,
|
'updatedAt': note['updatedAt']! as String,
|
||||||
@@ -668,16 +667,16 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
|
|||||||
String decryptedBody = 'Encrypted';
|
String decryptedBody = 'Encrypted';
|
||||||
if (!isPermanentlyDeleted) {
|
if (!isPermanentlyDeleted) {
|
||||||
try {
|
try {
|
||||||
decryptedTitle = await NoteEncryption.decryptNote(
|
decryptedTitle = await NoteEncryption.decrypt(
|
||||||
note['encryptedTitle']! as String,
|
note['encryptedTitle']! as String,
|
||||||
masterKey,
|
masterKey,
|
||||||
);
|
);
|
||||||
decryptedBody = await NoteEncryption.decryptNote(
|
decryptedBody = await NoteEncryption.decrypt(
|
||||||
note['encryptedBody']! as String,
|
note['encryptedBody']! as String,
|
||||||
masterKey,
|
masterKey,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Failed to decrypt note ${note['id']}: $e');
|
debugPrint('Failed to decrypt note ${note['id']}: $e');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
decryptedTitle = '';
|
decryptedTitle = '';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:notas/data/note_encryption.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/models/category.dart';
|
import 'package:notas/models/category.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
@@ -108,13 +109,13 @@ class SyncCategoryPayload {
|
|||||||
final int? iconCodePoint;
|
final int? iconCodePoint;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
|
||||||
factory SyncCategoryPayload.fromCategory(
|
static Future<SyncCategoryPayload> fromCategory(
|
||||||
Category category, {
|
Category category, {
|
||||||
required String encryptedName,
|
required String masterKey,
|
||||||
}) {
|
}) async {
|
||||||
return SyncCategoryPayload(
|
return SyncCategoryPayload(
|
||||||
id: category.id,
|
id: category.id,
|
||||||
encryptedName: encryptedName,
|
encryptedName: await NoteEncryption.encryptNote(category.name, masterKey),
|
||||||
serverVersion: category.serverVersion,
|
serverVersion: category.serverVersion,
|
||||||
isDeleted: category.isDeleted,
|
isDeleted: category.isDeleted,
|
||||||
colorValue: category.colorValue,
|
colorValue: category.colorValue,
|
||||||
@@ -249,10 +250,10 @@ class SyncCategoryResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Category toCategory({required String name}) {
|
Future<Category> toCategory({required String masterKey}) async {
|
||||||
return Category(
|
return Category(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: await NoteEncryption.decrypt(encryptedName, masterKey),
|
||||||
serverVersion: serverVersion,
|
serverVersion: serverVersion,
|
||||||
isDeleted: isDeleted,
|
isDeleted: isDeleted,
|
||||||
colorValue: colorValue,
|
colorValue: colorValue,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class BiometricChoiceScreen extends StatelessWidget {
|
class BiometricChoiceScreen extends StatelessWidget {
|
||||||
const BiometricChoiceScreen({
|
const BiometricChoiceScreen({
|
||||||
@@ -14,19 +15,11 @@ class BiometricChoiceScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF191A1D),
|
|
||||||
Color(0xFF222326),
|
|
||||||
Color(0xFF101114),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -39,12 +32,12 @@ class BiometricChoiceScreen extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1D1E20),
|
color: palette.cardBackground,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
border: Border.all(color: palette.border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.35),
|
color: palette.shadowSoft,
|
||||||
blurRadius: 30,
|
blurRadius: 30,
|
||||||
offset: const Offset(0, 18),
|
offset: const Offset(0, 18),
|
||||||
),
|
),
|
||||||
@@ -74,7 +67,7 @@ class BiometricChoiceScreen extends StatelessWidget {
|
|||||||
'¿Quieres que la app te pida huella o cara antes de entrar a tus notas?',
|
'¿Quieres que la app te pida huella o cara antes de entrar a tus notas?',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.72),
|
color: palette.textSecondary,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -82,13 +75,17 @@ class BiometricChoiceScreen extends StatelessWidget {
|
|||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: isBusy ? null : onEnableBiometrics,
|
onPressed: isBusy ? null : onEnableBiometrics,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: isBusy
|
child: isBusy
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text('Sí, activar huella'),
|
: const Text('Sí, activar huella'),
|
||||||
),
|
),
|
||||||
@@ -96,9 +93,11 @@ class BiometricChoiceScreen extends StatelessWidget {
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: isBusy ? null : onSkipBiometrics,
|
onPressed: isBusy ? null : onSkipBiometrics,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(
|
||||||
side: const BorderSide(color: Colors.white24),
|
vertical: 14,
|
||||||
foregroundColor: Colors.white,
|
),
|
||||||
|
side: BorderSide(color: palette.border),
|
||||||
|
foregroundColor: palette.textPrimary,
|
||||||
),
|
),
|
||||||
child: const Text('No, entrar sin huella'),
|
child: const Text('No, entrar sin huella'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class BiometricGateScreen extends StatefulWidget {
|
class BiometricGateScreen extends StatefulWidget {
|
||||||
const BiometricGateScreen({
|
const BiometricGateScreen({
|
||||||
@@ -38,19 +39,11 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF191A1D),
|
|
||||||
Color(0xFF222326),
|
|
||||||
Color(0xFF101114),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -63,12 +56,12 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1D1E20),
|
color: palette.cardBackground,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
border: Border.all(color: palette.border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.35),
|
color: palette.shadowSoft,
|
||||||
blurRadius: 30,
|
blurRadius: 30,
|
||||||
offset: const Offset(0, 18),
|
offset: const Offset(0, 18),
|
||||||
),
|
),
|
||||||
@@ -98,21 +91,27 @@ class _BiometricGateScreenState extends State<BiometricGateScreen> {
|
|||||||
'Pon tu huella o cara para entrar a tus notas.',
|
'Pon tu huella o cara para entrar a tus notas.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.72),
|
color: palette.textSecondary,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 22),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: widget.isBusy ? null : widget.onUnlockRequested,
|
onPressed: widget.isBusy
|
||||||
|
? null
|
||||||
|
: widget.onUnlockRequested,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: widget.isBusy
|
child: widget.isBusy
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text('Desbloquear'),
|
: const Text('Desbloquear'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,669 +1,217 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
import 'package:notas/models/category.dart';
|
import 'package:notas/data/note_body.dart';
|
||||||
|
import 'package:notas/data/note_repository.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/platform/app_platform.dart';
|
import 'package:notas/theme/app_palette.dart';
|
||||||
import 'package:notas/widgets/category_style.dart';
|
|
||||||
|
|
||||||
// NoteEditorScreen: unified UI for creating and editing notes.
|
|
||||||
// - Use `NoteEditorScreen.showDialog(context, note: existing)` to edit.
|
|
||||||
// - Use `NoteEditorScreen.showDialog(context)` to create a new note.
|
|
||||||
// The screen returns either a `Note` (saved) or the string `'delete'` when
|
|
||||||
// the user confirmed deletion. `null` indicates the user closed without saving.
|
|
||||||
|
|
||||||
class NoteEditorScreen extends StatefulWidget {
|
class NoteEditorScreen extends StatefulWidget {
|
||||||
const NoteEditorScreen({
|
const NoteEditorScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.repository,
|
||||||
|
this.saveNote,
|
||||||
required this.note,
|
required this.note,
|
||||||
this.categoryId,
|
this.embedded = false,
|
||||||
this.categories = const <Category>[],
|
this.onSaved,
|
||||||
this.onComplete,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Note? note;
|
final NoteRepository? repository;
|
||||||
final String? categoryId;
|
final Future<Note> Function(Note note)? saveNote;
|
||||||
final List<Category> categories;
|
final Note note;
|
||||||
final ValueChanged<dynamic>? onComplete;
|
final bool embedded;
|
||||||
|
final ValueChanged<Note>? onSaved;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
||||||
|
|
||||||
static Future<dynamic> _showGeneralEditorDialog(
|
|
||||||
BuildContext context, {
|
|
||||||
Note? note,
|
|
||||||
String? categoryId,
|
|
||||||
List<Category> categories = const <Category>[],
|
|
||||||
}) {
|
|
||||||
return showGeneralDialog<dynamic>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
barrierColor: Colors.transparent,
|
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
|
||||||
return NoteEditorScreen(
|
|
||||||
note: note,
|
|
||||||
categoryId: categoryId,
|
|
||||||
categories: categories,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
return ScaleTransition(scale: animation, child: child);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<dynamic> showDialog(
|
|
||||||
BuildContext context, {
|
|
||||||
Note? note,
|
|
||||||
String? categoryId,
|
|
||||||
List<Category> categories = const <Category>[],
|
|
||||||
}) {
|
|
||||||
if (isAndroid || isIOS) {
|
|
||||||
return _showGeneralEditorDialog(
|
|
||||||
context,
|
|
||||||
note: note,
|
|
||||||
categoryId: categoryId,
|
|
||||||
categories: categories,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
|
|
||||||
if (overlayState == null) {
|
|
||||||
return _showGeneralEditorDialog(
|
|
||||||
context,
|
|
||||||
note: note,
|
|
||||||
categoryId: categoryId,
|
|
||||||
categories: categories,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Completer<dynamic> completer = Completer<dynamic>();
|
|
||||||
late final OverlayEntry entry;
|
|
||||||
|
|
||||||
entry = OverlayEntry(
|
|
||||||
builder: (BuildContext overlayContext) {
|
|
||||||
return NoteEditorScreen(
|
|
||||||
note: note,
|
|
||||||
categoryId: categoryId,
|
|
||||||
categories: categories,
|
|
||||||
onComplete: (dynamic result) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(result);
|
|
||||||
}
|
|
||||||
if (entry.mounted) {
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
overlayState.insert(entry);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||||
late TextEditingController _titleController;
|
static const Duration _debounceDuration = Duration(seconds: 1);
|
||||||
late TextEditingController _bodyController;
|
|
||||||
late Note _currentNote;
|
|
||||||
late bool _isNewNote;
|
|
||||||
String? _selectedCategoryId;
|
|
||||||
final GlobalKey _categorySelectorKey = GlobalKey();
|
|
||||||
OverlayEntry? _categoryMenuEntry;
|
|
||||||
bool _didComplete = false;
|
|
||||||
|
|
||||||
bool get _isMobileLayout => isAndroid || isIOS;
|
late final TextEditingController _titleController;
|
||||||
|
late final QuillController _bodyController;
|
||||||
|
late final FocusNode _bodyFocusNode;
|
||||||
|
late final ScrollController _bodyScrollController;
|
||||||
|
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
bool _isSaving = false;
|
||||||
|
bool _saveQueued = false;
|
||||||
|
late Note _baselineNote;
|
||||||
|
|
||||||
|
AppPalette _paletteOf(BuildContext context) {
|
||||||
|
return Theme.of(context).extension<AppPalette>() ??
|
||||||
|
AppPalette.fromBrightness(Theme.of(context).brightness);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_isNewNote = widget.note == null;
|
_baselineNote = widget.note;
|
||||||
|
_titleController = TextEditingController(text: widget.note.title)
|
||||||
if (_isNewNote) {
|
..addListener(_scheduleSave);
|
||||||
final DateTime now = DateTime.now();
|
_bodyController = QuillController(
|
||||||
_currentNote = Note(
|
document: noteBodyToDocument(widget.note.body),
|
||||||
title: '',
|
selection: const TextSelection.collapsed(offset: 0),
|
||||||
body: '',
|
)..addListener(_scheduleSave);
|
||||||
createdAt: now,
|
_bodyFocusNode = FocusNode();
|
||||||
updatedAt: now,
|
_bodyScrollController = ScrollController();
|
||||||
position: 0,
|
|
||||||
categoryId: widget.categoryId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_currentNote = widget.note!;
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedCategoryId = _currentNote.categoryId ?? widget.categoryId;
|
|
||||||
|
|
||||||
_titleController = TextEditingController(text: _currentNote.title);
|
|
||||||
_bodyController = TextEditingController(text: _currentNote.body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_closeCategoryMenu();
|
_debounceTimer?.cancel();
|
||||||
_titleController.dispose();
|
_titleController.dispose();
|
||||||
_bodyController.dispose();
|
_bodyController.dispose();
|
||||||
|
_bodyFocusNode.dispose();
|
||||||
|
_bodyScrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _complete(dynamic result) {
|
String _bodyAsJson() {
|
||||||
if (_didComplete) {
|
return noteDocumentToStorageJson(_bodyController.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleSave() {
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
_debounceTimer = Timer(_debounceDuration, () {
|
||||||
|
unawaited(_saveNow());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveNow() async {
|
||||||
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_didComplete = true;
|
|
||||||
_closeCategoryMenu();
|
|
||||||
|
|
||||||
final ValueChanged<dynamic>? callback = widget.onComplete;
|
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
callback(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).pop(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeWithoutSaving() {
|
|
||||||
_complete(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveNote() {
|
|
||||||
final String title = _titleController.text.trim();
|
final String title = _titleController.text.trim();
|
||||||
final String body = _bodyController.text.trim();
|
final String body = _bodyAsJson();
|
||||||
final bool categoryChanged = _selectedCategoryId != _currentNote.categoryId;
|
final Note draft = _baselineNote.copyWith(
|
||||||
|
|
||||||
if (title.isEmpty && body.isEmpty && !categoryChanged) {
|
|
||||||
_complete(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Note updatedNote = _currentNote.copyWith(
|
|
||||||
title: title.isEmpty ? 'Sin título' : title,
|
title: title.isEmpty ? 'Sin título' : title,
|
||||||
body: body,
|
body: body,
|
||||||
categoryId: _selectedCategoryId,
|
categoryId: _baselineNote.categoryId,
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
_complete(updatedNote);
|
final bool hasChanges =
|
||||||
}
|
draft.title != _baselineNote.title ||
|
||||||
|
draft.body != _baselineNote.body ||
|
||||||
|
draft.categoryId != _baselineNote.categoryId;
|
||||||
|
|
||||||
Widget _buildDeleteConfirmationDialog({
|
if (!hasChanges) {
|
||||||
required ValueChanged<bool> onConfirmed,
|
return;
|
||||||
}) {
|
|
||||||
final bool isDeletedNote = _currentNote.isDeleted;
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
backgroundColor: const Color(0xFF303134),
|
|
||||||
title: Text(
|
|
||||||
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar nota',
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
content: Text(
|
|
||||||
isDeletedNote
|
|
||||||
? 'Esta nota ya está borrada. Si la eliminas ahora, se borrará permanentemente.'
|
|
||||||
: '¿Estás seguro de que deseas eliminar esta nota?',
|
|
||||||
style: const TextStyle(color: Colors.white70),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => onConfirmed(false),
|
|
||||||
child: const Text(
|
|
||||||
'Cancelar',
|
|
||||||
style: TextStyle(color: Colors.white70),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => onConfirmed(true),
|
|
||||||
child: Text(
|
|
||||||
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _showDeleteConfirmation() async {
|
|
||||||
if (_isMobileLayout) {
|
|
||||||
final bool? confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
barrierColor: Colors.transparent,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return _buildDeleteConfirmationDialog(
|
|
||||||
onConfirmed: (bool confirmed) =>
|
|
||||||
Navigator.of(dialogContext).pop(confirmed),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return confirmed ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
|
if (_isSaving) {
|
||||||
if (overlayState == null) {
|
_saveQueued = true;
|
||||||
final bool? confirmed = await showDialog<bool>(
|
return;
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
barrierColor: Colors.transparent,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return _buildDeleteConfirmationDialog(
|
|
||||||
onConfirmed: (bool confirmed) =>
|
|
||||||
Navigator.of(dialogContext).pop(confirmed),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return confirmed ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Completer<bool> completer = Completer<bool>();
|
_isSaving = true;
|
||||||
late final OverlayEntry entry;
|
try {
|
||||||
bool didRemove = false;
|
final Note saved = widget.saveNote != null
|
||||||
|
? await widget.saveNote!(draft)
|
||||||
entry = OverlayEntry(
|
: await widget.repository!.updateNote(draft);
|
||||||
builder: (BuildContext overlayContext) {
|
_baselineNote = saved;
|
||||||
final ValueChanged<bool> close = (bool confirmed) {
|
widget.onSaved?.call(saved);
|
||||||
if (!completer.isCompleted) {
|
} catch (error) {
|
||||||
completer.complete(confirmed);
|
if (mounted) {
|
||||||
}
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
if (!didRemove && entry.mounted) {
|
SnackBar(content: Text('No se pudo guardar la nota: $error')),
|
||||||
didRemove = true;
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
const Positioned.fill(
|
|
||||||
child: ModalBarrier(
|
|
||||||
dismissible: false,
|
|
||||||
color: Color.fromARGB(140, 0, 0, 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 420),
|
|
||||||
child: _buildDeleteConfirmationDialog(onConfirmed: close),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
} finally {
|
||||||
|
_isSaving = false;
|
||||||
overlayState.insert(entry);
|
if (_saveQueued) {
|
||||||
return completer.future;
|
_saveQueued = false;
|
||||||
}
|
unawaited(_saveNow());
|
||||||
|
|
||||||
Category? _categoryById(String? id) {
|
|
||||||
for (final Category category in widget.categories) {
|
|
||||||
if (category.id == id) {
|
|
||||||
return category;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _categoryBackgroundColor(Category? category) {
|
Widget _buildEditorBody() {
|
||||||
if (category?.colorValue == null) {
|
final AppPalette palette = _paletteOf(context);
|
||||||
return Colors.white.withValues(alpha: 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Color(category!.colorValue!);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _categoryForegroundColor(Category? category) {
|
|
||||||
if (category == null || category.colorValue == null) {
|
|
||||||
return Colors.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Color background = Color(category.colorValue!);
|
|
||||||
return background.computeLuminance() > 0.55 ? Colors.black87 : Colors.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCategorySelectorBox({Category? category}) {
|
|
||||||
final String label = category?.name ?? 'Sin categoría';
|
|
||||||
final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint);
|
|
||||||
final Color backgroundColor = _categoryBackgroundColor(category);
|
|
||||||
final Color foregroundColor = _categoryForegroundColor(category);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
key: _categorySelectorKey,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: category?.colorValue != null
|
|
||||||
? backgroundColor.withValues(alpha: 0.85)
|
|
||||||
: Colors.white24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: foregroundColor, size: 15),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: foregroundColor,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_drop_down,
|
|
||||||
color: foregroundColor.withValues(alpha: 0.9),
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeCategoryMenu() {
|
|
||||||
final OverlayEntry? entry = _categoryMenuEntry;
|
|
||||||
if (entry != null && entry.mounted) {
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
_categoryMenuEntry = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleCategoryMenu() {
|
|
||||||
if (_categoryMenuEntry != null) {
|
|
||||||
_closeCategoryMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
|
|
||||||
if (overlayState == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_categoryMenuEntry = OverlayEntry(
|
|
||||||
builder: (BuildContext overlayContext) {
|
|
||||||
final Size screenSize = MediaQuery.of(overlayContext).size;
|
|
||||||
final double menuWidth = math.min(screenSize.width - 32, 320);
|
|
||||||
final double menuHeight = math.min(screenSize.height - 32, 360);
|
|
||||||
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onTap: _closeCategoryMenu,
|
|
||||||
child: const SizedBox.expand(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: menuWidth,
|
|
||||||
maxHeight: menuHeight,
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
elevation: 10,
|
|
||||||
color: const Color(0xFF303134),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
_buildCategoryMenuItem(
|
|
||||||
category: null,
|
|
||||||
label: 'Sin categoría',
|
|
||||||
isSelected: _selectedCategoryId == null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedCategoryId = null;
|
|
||||||
});
|
|
||||||
_closeCategoryMenu();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
for (final Category category in widget.categories)
|
|
||||||
_buildCategoryMenuItem(
|
|
||||||
category: category,
|
|
||||||
label: category.name,
|
|
||||||
isSelected: _selectedCategoryId == category.id,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedCategoryId = category.id;
|
|
||||||
});
|
|
||||||
_closeCategoryMenu();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
overlayState.insert(_categoryMenuEntry!);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCategoryMenuItem({
|
|
||||||
required Category? category,
|
|
||||||
required String label,
|
|
||||||
required bool isSelected,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
final Color backgroundColor = _categoryBackgroundColor(category);
|
|
||||||
final Color foregroundColor = _categoryForegroundColor(category);
|
|
||||||
final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint);
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
||||||
color: isSelected ? Colors.white.withValues(alpha: 0.08) : null,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: category?.colorValue != null
|
|
||||||
? backgroundColor.withValues(alpha: 0.85)
|
|
||||||
: Colors.white24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Icon(icon, size: 16, color: foregroundColor),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: foregroundColor,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isSelected)
|
|
||||||
Icon(Icons.check, color: foregroundColor, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _deleteNote() async {
|
|
||||||
final bool confirmed = await _showDeleteConfirmation();
|
|
||||||
if (!mounted || !confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_complete('delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
|
||||||
final DateTime now = DateTime.now();
|
|
||||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
|
||||||
final DateTime yesterday = today.subtract(const Duration(days: 1));
|
|
||||||
final DateTime noteDate = DateTime(date.year, date.month, date.day);
|
|
||||||
|
|
||||||
if (noteDate == today) {
|
|
||||||
return 'Hoy ${DateFormat('HH:mm').format(date)}';
|
|
||||||
} else if (noteDate == yesterday) {
|
|
||||||
return 'Ayer ${DateFormat('HH:mm').format(date)}';
|
|
||||||
} else {
|
|
||||||
return DateFormat('dd/MM/yyyy HH:mm').format(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEditorContent({required bool isMobile}) {
|
|
||||||
final double titleSpacing = isMobile ? 16.0 : 8.0;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Expanded(
|
||||||
border: Border(bottom: BorderSide(color: Colors.white12, width: 1)),
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.only(left: 8),
|
||||||
child: Row(
|
child: TextField(
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _closeWithoutSaving,
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white70),
|
|
||||||
tooltip: 'Cerrar sin guardar',
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Creado: ${_formatDate(_currentNote.createdAt)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white54,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_currentNote.updatedAt != _currentNote.createdAt)
|
|
||||||
Text(
|
|
||||||
'Modificado: ${_formatDate(_currentNote.updatedAt)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white54,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 150),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 150,
|
|
||||||
child: KeyedSubtree(
|
|
||||||
key: const ValueKey<String>('category_selector'),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: _toggleCategoryMenu,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: _buildCategorySelectorBox(
|
|
||||||
category: _categoryById(_selectedCategoryId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: _titleController,
|
controller: _titleController,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: palette.textPrimary,
|
||||||
fontSize: 28,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Título',
|
hintText: 'Título',
|
||||||
hintStyle: TextStyle(color: Colors.white30),
|
hintStyle: TextStyle(color: palette.textHint),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: titleSpacing),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TextField(
|
],
|
||||||
controller: _bodyController,
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
const SizedBox(height: 10),
|
||||||
maxLines: null,
|
Expanded(
|
||||||
expands: true,
|
child: Container(
|
||||||
style: const TextStyle(
|
padding: const EdgeInsets.all(8),
|
||||||
color: Colors.white,
|
child: QuillEditor.basic(
|
||||||
fontSize: 16,
|
controller: _bodyController,
|
||||||
height: 1.6,
|
focusNode: _bodyFocusNode,
|
||||||
),
|
scrollController: _bodyScrollController,
|
||||||
decoration: const InputDecoration(
|
config: QuillEditorConfig(
|
||||||
hintText: 'Escribe tu nota...',
|
scrollable: true,
|
||||||
hintStyle: TextStyle(color: Colors.white30),
|
padding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
autoFocus: false,
|
||||||
contentPadding: EdgeInsets.zero,
|
expands: true,
|
||||||
),
|
placeholder: 'Escribe tu nota...',
|
||||||
),
|
keyboardAppearance: Theme.of(context).brightness,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(height: 8),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
QuillSimpleToolbar(
|
||||||
decoration: BoxDecoration(
|
controller: _bodyController,
|
||||||
border: Border(top: BorderSide(color: Colors.white12, width: 1)),
|
config: const QuillSimpleToolbarConfig(
|
||||||
),
|
color: Colors.transparent,
|
||||||
child: Row(
|
showBoldButton: true,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
showItalicButton: true,
|
||||||
children: [
|
showUnderLineButton: true,
|
||||||
if (!_isNewNote)
|
showStrikeThrough: false,
|
||||||
IconButton(
|
showInlineCode: false,
|
||||||
onPressed: _deleteNote,
|
showColorButton: false,
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
showBackgroundColorButton: false,
|
||||||
tooltip: 'Eliminar nota',
|
showClearFormat: false,
|
||||||
)
|
showAlignmentButtons: false,
|
||||||
else
|
showHeaderStyle: false,
|
||||||
const SizedBox(width: 48),
|
showListNumbers: true,
|
||||||
FilledButton(onPressed: _saveNote, child: const Text('Guardar')),
|
showListBullets: true,
|
||||||
],
|
showListCheck: true,
|
||||||
|
showCodeBlock: false,
|
||||||
|
showQuote: false,
|
||||||
|
showIndent: false,
|
||||||
|
showLink: false,
|
||||||
|
showUndo: false,
|
||||||
|
showRedo: false,
|
||||||
|
showDividers: false,
|
||||||
|
showFontFamily: false,
|
||||||
|
showFontSize: false,
|
||||||
|
showDirection: false,
|
||||||
|
showSearchButton: false,
|
||||||
|
showSubscript: false,
|
||||||
|
showSuperscript: false,
|
||||||
|
multiRowsDisplay: false,
|
||||||
|
axis: Axis.horizontal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -672,51 +220,38 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isMobileLayout) {
|
final AppPalette palette = _paletteOf(context);
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
final Widget editor = Padding(
|
||||||
child: SafeArea(
|
padding: const EdgeInsets.all(8),
|
||||||
child: Container(
|
child: _buildEditorBody(),
|
||||||
color: const Color.fromARGB(255, 24, 25, 26),
|
);
|
||||||
child: _buildEditorContent(isMobile: true),
|
|
||||||
),
|
if (widget.embedded) {
|
||||||
),
|
return editor;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return Container(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
final double maxWidth = math.min(constraints.maxWidth - 32, 600);
|
child: Scaffold(
|
||||||
final double maxHeight = math.min(constraints.maxHeight - 32, 720);
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
return Stack(
|
title: const Text('Editar nota'),
|
||||||
children: [
|
backgroundColor: Colors.transparent,
|
||||||
Positioned.fill(
|
elevation: 0,
|
||||||
child: ModalBarrier(
|
bottom: PreferredSize(
|
||||||
dismissible: false,
|
preferredSize: const Size.fromHeight(1),
|
||||||
color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5),
|
child: Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
),
|
border: Border(
|
||||||
Positioned.fill(
|
bottom: BorderSide(color: palette.border, width: 0.5),
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: maxWidth,
|
|
||||||
height: maxHeight,
|
|
||||||
child: Material(
|
|
||||||
color: const Color.fromRGBO(24, 25, 26, 1),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: Colors.white24, width: 1),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: _buildEditorContent(isMobile: false),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
),
|
||||||
},
|
body: SafeArea(child: editor),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:notas/data/local_vault_service.dart';
|
import 'package:notas/data/local_vault_service.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
import 'package:notas/widgets/search_app_bar.dart';
|
import 'package:notas/widgets/search_app_bar.dart';
|
||||||
import 'package:notas/data/api_client.dart';
|
import 'package:notas/data/api_client.dart';
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ class SettingsScreen extends StatefulWidget {
|
|||||||
required this.onForceSync,
|
required this.onForceSync,
|
||||||
required this.currentSeedColor,
|
required this.currentSeedColor,
|
||||||
required this.onThemeColorSelected,
|
required this.onThemeColorSelected,
|
||||||
|
required this.currentThemeMode,
|
||||||
|
required this.onThemeModeSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Future<void> Function() onDeleteAllData;
|
final Future<void> Function() onDeleteAllData;
|
||||||
@@ -18,6 +21,8 @@ class SettingsScreen extends StatefulWidget {
|
|||||||
final Future<void> Function() onForceSync;
|
final Future<void> Function() onForceSync;
|
||||||
final Color currentSeedColor;
|
final Color currentSeedColor;
|
||||||
final Future<void> Function(Color color) onThemeColorSelected;
|
final Future<void> Function(Color color) onThemeColorSelected;
|
||||||
|
final ThemeMode currentThemeMode;
|
||||||
|
final Future<void> Function(ThemeMode mode) onThemeModeSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
@@ -29,32 +34,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _isServerDeleting = false;
|
bool _isServerDeleting = false;
|
||||||
bool _isThemeSaving = false;
|
bool _isThemeSaving = false;
|
||||||
final TextEditingController _endpointController = TextEditingController();
|
final TextEditingController _endpointController = TextEditingController();
|
||||||
final TextEditingController _encryptionKeyController = TextEditingController();
|
final TextEditingController _encryptionKeyController =
|
||||||
|
TextEditingController();
|
||||||
bool _endpointLoading = true;
|
bool _endpointLoading = true;
|
||||||
bool _encryptionKeyLoading = false;
|
bool _encryptionKeyLoading = false;
|
||||||
bool _encryptionKeyVisible = false;
|
bool _encryptionKeyVisible = false;
|
||||||
late Color _selectedSeedColor;
|
late Color _selectedSeedColor;
|
||||||
|
late ThemeMode _selectedThemeMode;
|
||||||
|
|
||||||
static const List<Color> _themeColorOptions = <Color>[
|
static const List<Color> _themeColorOptions = AppPalette.themeSeedColors;
|
||||||
Colors.amber,
|
|
||||||
Colors.blue,
|
|
||||||
Colors.teal,
|
|
||||||
Colors.green,
|
|
||||||
Colors.pink,
|
|
||||||
Colors.purple,
|
|
||||||
];
|
|
||||||
|
|
||||||
Future<void> _confirmAndDeleteAll() async {
|
Future<void> _confirmAndDeleteAll() async {
|
||||||
final bool? confirmed = await showDialog<bool>(
|
final bool? confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
title: const Text('Borrar todos los datos'),
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
content: const Text('¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.'),
|
return AlertDialog(
|
||||||
actions: [
|
backgroundColor: palette.cardBackground,
|
||||||
TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancelar')),
|
shape: RoundedRectangleBorder(
|
||||||
TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Borrar', style: TextStyle(color: Colors.red))),
|
borderRadius: BorderRadius.circular(12),
|
||||||
],
|
side: BorderSide(color: palette.border),
|
||||||
),
|
),
|
||||||
|
title: const Text('Borrar todos los datos'),
|
||||||
|
content: const Text(
|
||||||
|
'¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text(
|
||||||
|
'Borrar',
|
||||||
|
style: TextStyle(color: palette.destructiveAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
@@ -69,7 +88,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Todos los datos locales han sido eliminados.')),
|
const SnackBar(
|
||||||
|
content: Text('Todos los datos locales han sido eliminados.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -77,35 +98,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
SnackBar(content: Text('Error al borrar los datos: $error')),
|
SnackBar(content: Text('Error al borrar los datos: $error')),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (!mounted) return;
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBusy = false;
|
_isBusy = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _confirmAndDeleteServerData() async {
|
Future<void> _confirmAndDeleteServerData() async {
|
||||||
final bool? confirmed = await showDialog<bool>(
|
final bool? confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
title: const Text('Borrar toda la info del servidor'),
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
content: const Text(
|
return AlertDialog(
|
||||||
'¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.',
|
backgroundColor: palette.cardBackground,
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
actions: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
TextButton(
|
side: BorderSide(color: palette.border),
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Cancelar'),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
title: const Text('Borrar toda la info del servidor'),
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
content: const Text(
|
||||||
child: const Text(
|
'¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.',
|
||||||
'Borrar',
|
),
|
||||||
style: TextStyle(color: Colors.red),
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
),
|
),
|
||||||
),
|
TextButton(
|
||||||
],
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
),
|
child: Text(
|
||||||
|
'Borrar',
|
||||||
|
style: TextStyle(color: palette.destructiveAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) return;
|
if (confirmed != true) return;
|
||||||
@@ -115,11 +145,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Map<String, dynamic> response =
|
final Map<String, dynamic> response = await AuthApi.instance
|
||||||
await AuthApi.instance.deleteAllServerData();
|
.deleteAllServerData();
|
||||||
|
|
||||||
if (response['error'] == true) {
|
if (response['error'] == true) {
|
||||||
throw Exception(response['body'] ?? response['message'] ?? 'Error desconocido');
|
throw Exception(
|
||||||
|
response['body'] ?? response['message'] ?? 'Error desconocido',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await AuthApi.instance.clearTokens();
|
await AuthApi.instance.clearTokens();
|
||||||
@@ -127,7 +159,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Toda la información del servidor ha sido eliminada.')),
|
const SnackBar(
|
||||||
|
content: Text('Toda la información del servidor ha sido eliminada.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -135,10 +169,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
SnackBar(content: Text('Error al borrar la info del servidor: $error')),
|
SnackBar(content: Text('Error al borrar la info del servidor: $error')),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (!mounted) return;
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isServerDeleting = false;
|
_isServerDeleting = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +201,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
SnackBar(content: Text('Error al forzar la sincronización: $error')),
|
SnackBar(content: Text('Error al forzar la sincronización: $error')),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (!mounted) return;
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
setState(() {
|
_isSyncing = false;
|
||||||
_isSyncing = false;
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +247,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedSeedColor = widget.currentSeedColor;
|
_selectedSeedColor = widget.currentSeedColor;
|
||||||
|
_selectedThemeMode = widget.currentThemeMode;
|
||||||
_loadEndpoint();
|
_loadEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +258,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
widget.currentSeedColor != _selectedSeedColor) {
|
widget.currentSeedColor != _selectedSeedColor) {
|
||||||
_selectedSeedColor = widget.currentSeedColor;
|
_selectedSeedColor = widget.currentSeedColor;
|
||||||
}
|
}
|
||||||
|
if (oldWidget.currentThemeMode != widget.currentThemeMode) {
|
||||||
|
_selectedThemeMode = widget.currentThemeMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectThemeMode(ThemeMode mode) async {
|
||||||
|
if (_selectedThemeMode == mode) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedThemeMode = mode;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await widget.onThemeModeSelected(mode);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('No se pudo guardar la preferencia de tema: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEndpoint() async {
|
Future<void> _loadEndpoint() async {
|
||||||
@@ -239,7 +295,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final String? encryptionKey = await LocalVaultService.instance.readEncryptionKey();
|
final String? encryptionKey = await LocalVaultService.instance
|
||||||
|
.readEncryptionKey();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -284,10 +341,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
required bool isLoading,
|
required bool isLoading,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
}) {
|
}) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return ElevatedButton.icon(
|
return ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.redAccent,
|
backgroundColor: palette.destructiveAccent,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: palette.textPrimary,
|
||||||
textStyle: const TextStyle(fontWeight: FontWeight.w600),
|
textStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
@@ -303,16 +362,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThemeColorButton(Color color) {
|
Widget _buildThemeColorButton(Color color) {
|
||||||
final bool isSelected = _selectedSeedColor.value == color.value;
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
final bool isSelected = _selectedSeedColor.toARGB32() == color.toARGB32();
|
||||||
final Color foregroundColor =
|
final Color foregroundColor =
|
||||||
ThemeData.estimateBrightnessForColor(color) == Brightness.dark
|
ThemeData.estimateBrightnessForColor(color) == Brightness.dark
|
||||||
? Colors.white
|
? palette.textPrimary
|
||||||
: Colors.black;
|
: palette.textOnAccent;
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
label: 'Color ${color.value.toRadixString(16)}',
|
label: 'Color ${color.toARGB32().toRadixString(16)}',
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: isSelected ? 'Color actual' : 'Usar este color',
|
message: isSelected ? 'Color actual' : 'Usar este color',
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -326,12 +386,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
color: color,
|
color: color,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected ? Colors.white : Colors.white24,
|
color: isSelected ? palette.textPrimary : palette.textSecondary,
|
||||||
width: isSelected ? 2.5 : 1.2,
|
width: isSelected ? 2.5 : 1.2,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.25),
|
color: palette.shadowSoft,
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 3),
|
offset: const Offset(0, 3),
|
||||||
),
|
),
|
||||||
@@ -341,11 +401,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
Icon(
|
Icon(Icons.check, size: 22, color: foregroundColor),
|
||||||
Icons.check,
|
|
||||||
size: 22,
|
|
||||||
color: foregroundColor,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -366,10 +422,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
try {
|
try {
|
||||||
await ApiConfig.setEndpoint(value);
|
await ApiConfig.setEndpoint(value);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Endpoint guardado')));
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Endpoint guardado')));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error guardando endpoint: $e')));
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error guardando endpoint: $e')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +438,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final String endpoint = await ApiConfig.getEndpoint();
|
final String endpoint = await ApiConfig.getEndpoint();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_endpointController.text = endpoint;
|
_endpointController.text = endpoint;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Endpoint restaurado al valor por defecto')));
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Endpoint restaurado al valor por defecto')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildResponsiveInputActionsRow({
|
Widget _buildResponsiveInputActionsRow({
|
||||||
@@ -430,20 +492,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF191A1D),
|
|
||||||
Color(0xFF222326),
|
|
||||||
Color(0xFF101114),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -455,162 +510,238 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
titleText: 'Configuración',
|
titleText: 'Configuración',
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Column(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
Row(
|
||||||
child: Text('Borrar datos locales:'),
|
children: [
|
||||||
),
|
const Expanded(
|
||||||
_buildDestructiveButton(
|
child: Text('Borrar datos locales:'),
|
||||||
label: 'Borrar',
|
),
|
||||||
onPressed: (_isBusy || _isServerDeleting)
|
_buildDestructiveButton(
|
||||||
? null
|
label: 'Borrar',
|
||||||
: _confirmAndDeleteAll,
|
onPressed: (_isBusy || _isServerDeleting)
|
||||||
isLoading: _isBusy,
|
? null
|
||||||
icon: Icons.delete_forever,
|
: _confirmAndDeleteAll,
|
||||||
),
|
isLoading: _isBusy,
|
||||||
],
|
icon: Icons.delete_forever,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
Row(
|
),
|
||||||
children: [
|
const SizedBox(height: 16),
|
||||||
const Expanded(
|
Row(
|
||||||
child: Text('Borrar info del servidor:'),
|
children: [
|
||||||
),
|
const Expanded(
|
||||||
_buildDestructiveButton(
|
child: Text('Borrar info del servidor:'),
|
||||||
label: 'Borrar',
|
),
|
||||||
onPressed: (_isBusy || _isSyncing || _isServerDeleting)
|
_buildDestructiveButton(
|
||||||
? null
|
label: 'Borrar',
|
||||||
: _confirmAndDeleteServerData,
|
onPressed:
|
||||||
isLoading: _isServerDeleting,
|
(_isBusy || _isSyncing || _isServerDeleting)
|
||||||
icon: Icons.cloud_off,
|
? null
|
||||||
),
|
: _confirmAndDeleteServerData,
|
||||||
],
|
isLoading: _isServerDeleting,
|
||||||
),
|
icon: Icons.cloud_off,
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Row(
|
],
|
||||||
children: [
|
),
|
||||||
const Expanded(
|
const SizedBox(height: 16),
|
||||||
child: Text('Forzar sincronizacion total:'),
|
Row(
|
||||||
),
|
children: [
|
||||||
ElevatedButton.icon(
|
const Expanded(
|
||||||
onPressed: (_isBusy || _isSyncing || _isServerDeleting)
|
child: Text('Forzar sincronizacion total:'),
|
||||||
? null
|
),
|
||||||
: _forceSync,
|
ElevatedButton.icon(
|
||||||
icon: _isSyncing
|
onPressed:
|
||||||
? const SizedBox(
|
(_isBusy || _isSyncing || _isServerDeleting)
|
||||||
width: 16,
|
? null
|
||||||
height: 16,
|
: _forceSync,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
icon: _isSyncing
|
||||||
)
|
? const SizedBox(
|
||||||
: const Icon(Icons.sync),
|
width: 16,
|
||||||
label: const Text('Sincronizar'),
|
height: 16,
|
||||||
),
|
child: CircularProgressIndicator(
|
||||||
],
|
strokeWidth: 2,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
)
|
||||||
const Text('Color del esquema'),
|
: const Icon(Icons.sync),
|
||||||
const SizedBox(height: 8),
|
label: const Text('Sincronizar'),
|
||||||
Wrap(
|
),
|
||||||
spacing: 12,
|
],
|
||||||
runSpacing: 12,
|
),
|
||||||
children: [
|
const SizedBox(height: 24),
|
||||||
for (final Color color in _themeColorOptions)
|
const Text('Apariencia'),
|
||||||
_buildThemeColorButton(color),
|
const SizedBox(height: 8),
|
||||||
],
|
Column(
|
||||||
),
|
children: [
|
||||||
const SizedBox(height: 24),
|
RadioGroup<ThemeMode>(
|
||||||
const Text('API endpoint (ej: https://notas-api.lpncnd.es/api)'),
|
groupValue: _selectedThemeMode,
|
||||||
const SizedBox(height: 8),
|
onChanged: (ThemeMode? v) {
|
||||||
_buildResponsiveInputActionsRow(
|
if (v != null) {
|
||||||
input: _endpointLoading
|
_selectThemeMode(v);
|
||||||
? const SizedBox(height: 48, child: Center(child: CircularProgressIndicator()))
|
}
|
||||||
: TextField(
|
},
|
||||||
controller: _endpointController,
|
child: Column(
|
||||||
style: const TextStyle(color: Colors.white),
|
children: [
|
||||||
keyboardType: TextInputType.url,
|
RadioListTile<ThemeMode>(
|
||||||
decoration: InputDecoration(
|
title: const Text(
|
||||||
labelText: 'API endpoint',
|
'Seguir modo del sistema',
|
||||||
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
),
|
||||||
filled: true,
|
value: ThemeMode.system,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
RadioListTile<ThemeMode>(
|
||||||
borderRadius: BorderRadius.circular(14),
|
title: const Text('Modo claro'),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
value: ThemeMode.light,
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
RadioListTile<ThemeMode>(
|
||||||
borderRadius: BorderRadius.circular(14),
|
title: const Text('Modo oscuro'),
|
||||||
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
|
value: ThemeMode.dark,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _endpointLoading ? null : _saveEndpoint,
|
|
||||||
child: const Text('Guardar'),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: _endpointLoading ? null : _resetEndpoint,
|
|
||||||
child: const Text('Restaurar'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
const Text('Clave de cifrado local'),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildResponsiveInputActionsRow(
|
|
||||||
input: TextField(
|
|
||||||
controller: _encryptionKeyController,
|
|
||||||
readOnly: true,
|
|
||||||
obscureText: !_encryptionKeyVisible,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: _encryptionKeyVisible ? 'Clave de cifrado' : 'Oculta hasta pulsar mostrar',
|
|
||||||
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('Color del esquema'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
for (final Color color in _themeColorOptions)
|
||||||
|
_buildThemeColorButton(color),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
const Text(
|
||||||
onPressed: _encryptionKeyLoading ? null : _loadEncryptionKey,
|
'API endpoint (ej: https://notas-api.lpncnd.es/api)',
|
||||||
child: _encryptionKeyLoading
|
),
|
||||||
? const SizedBox(
|
const SizedBox(height: 8),
|
||||||
width: 16,
|
_buildResponsiveInputActionsRow(
|
||||||
height: 16,
|
input: _endpointLoading
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
? const SizedBox(
|
||||||
)
|
height: 48,
|
||||||
: const Text('Mostrar'),
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TextField(
|
||||||
|
controller: _endpointController,
|
||||||
|
style: TextStyle(color: palette.textPrimary),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'API endpoint',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: palette.fill,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: palette.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: palette.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: palette.accent,
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _endpointLoading
|
||||||
|
? null
|
||||||
|
: _saveEndpoint,
|
||||||
|
child: const Text('Guardar'),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _endpointLoading
|
||||||
|
? null
|
||||||
|
: _resetEndpoint,
|
||||||
|
child: const Text('Restaurar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text('Clave de cifrado local'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildResponsiveInputActionsRow(
|
||||||
|
input: TextField(
|
||||||
|
controller: _encryptionKeyController,
|
||||||
|
readOnly: true,
|
||||||
|
obscureText: !_encryptionKeyVisible,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
style: TextStyle(color: palette.textPrimary),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: _encryptionKeyVisible
|
||||||
|
? 'Clave de cifrado'
|
||||||
|
: 'Oculta hasta pulsar mostrar',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: palette.fill,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(color: palette.border),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(color: palette.border),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: palette.accent,
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
actions: [
|
||||||
onPressed: _encryptionKeyVisible ? _hideEncryptionKey : null,
|
ElevatedButton(
|
||||||
child: const Text('Ocultar'),
|
onPressed: _encryptionKeyLoading
|
||||||
),
|
? null
|
||||||
],
|
: _loadEncryptionKey,
|
||||||
),
|
child: _encryptionKeyLoading
|
||||||
],
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Mostrar'),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _encryptionKeyVisible
|
||||||
|
? _hideEncryptionKey
|
||||||
|
: null,
|
||||||
|
child: const Text('Ocultar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:notas/data/api_client.dart';
|
import 'package:notas/data/api_client.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class VaultAccessScreen extends StatefulWidget {
|
class VaultAccessScreen extends StatefulWidget {
|
||||||
const VaultAccessScreen({
|
const VaultAccessScreen({
|
||||||
@@ -11,7 +12,8 @@ class VaultAccessScreen extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final bool isBusy;
|
final bool isBusy;
|
||||||
final Future<void> Function(String email, String password) onCreateAccountPressed;
|
final Future<void> Function(String email, String password)
|
||||||
|
onCreateAccountPressed;
|
||||||
final Future<void> Function(String email, String password) onSignInPressed;
|
final Future<void> Function(String email, String password) onSignInPressed;
|
||||||
final Future<void> Function() onContinueWithoutAccount;
|
final Future<void> Function() onContinueWithoutAccount;
|
||||||
|
|
||||||
@@ -73,19 +75,11 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF191A1D),
|
|
||||||
Color(0xFF222326),
|
|
||||||
Color(0xFF101114),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -98,12 +92,12 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1D1E20),
|
color: palette.cardBackground,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
border: Border.all(color: palette.border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.35),
|
color: palette.shadowSoft,
|
||||||
blurRadius: 30,
|
blurRadius: 30,
|
||||||
offset: const Offset(0, 18),
|
offset: const Offset(0, 18),
|
||||||
),
|
),
|
||||||
@@ -115,7 +109,7 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.lock_outline,
|
Icons.lock_outline,
|
||||||
color: Colors.amber,
|
color: Colors.white,
|
||||||
size: 44,
|
size: 44,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -133,7 +127,7 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.',
|
'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.72),
|
color: palette.textSecondary,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -141,7 +135,9 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
_endpointLoading
|
_endpointLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: TextField(
|
: TextField(
|
||||||
controller: _endpointController,
|
controller: _endpointController,
|
||||||
@@ -150,20 +146,29 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'API endpoint',
|
labelText: 'API endpoint',
|
||||||
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
labelStyle: TextStyle(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
fillColor: palette.fill,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(
|
||||||
|
color: palette.border,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(
|
||||||
|
color: palette.border,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
|
borderSide: BorderSide(
|
||||||
|
color: palette.accent,
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -175,20 +180,25 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Usuario',
|
labelText: 'Usuario',
|
||||||
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
labelStyle: TextStyle(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
fillColor: palette.fill,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(color: palette.border),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(color: palette.border),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
|
borderSide: BorderSide(
|
||||||
|
color: palette.accent,
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -200,34 +210,45 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Contraseña',
|
labelText: 'Contraseña',
|
||||||
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
labelStyle: TextStyle(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
fillColor: palette.fill,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(color: palette.border),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
|
borderSide: BorderSide(color: palette.border),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderSide: const BorderSide(color: Colors.amber, width: 1.2),
|
borderSide: BorderSide(
|
||||||
|
color: palette.accent,
|
||||||
|
width: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 22),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: widget.isBusy ? null : _handleCreateAccount,
|
onPressed: widget.isBusy
|
||||||
|
? null
|
||||||
|
: _handleCreateAccount,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: widget.isBusy
|
child: widget.isBusy
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text('Crear cuenta'),
|
: const Text('Crear cuenta'),
|
||||||
),
|
),
|
||||||
@@ -235,15 +256,19 @@ class _VaultAccessScreenState extends State<VaultAccessScreen> {
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: widget.isBusy ? null : _handleSignIn,
|
onPressed: widget.isBusy ? null : _handleSignIn,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(
|
||||||
side: const BorderSide(color: Colors.white24),
|
vertical: 14,
|
||||||
foregroundColor: Colors.white,
|
),
|
||||||
|
side: BorderSide(color: palette.border),
|
||||||
|
foregroundColor: palette.textPrimary,
|
||||||
),
|
),
|
||||||
child: const Text('Iniciar sesión'),
|
child: const Text('Iniciar sesión'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: widget.isBusy ? null : widget.onContinueWithoutAccount,
|
onPressed: widget.isBusy
|
||||||
|
? null
|
||||||
|
: widget.onContinueWithoutAccount,
|
||||||
child: const Text('Entrar sin cuenta'),
|
child: const Text('Entrar sin cuenta'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
static ThemeData theme({Color seedColor = Colors.amber}) {
|
static ThemeData theme({
|
||||||
|
Color seedColor = Colors.amber,
|
||||||
|
Brightness brightness = Brightness.dark,
|
||||||
|
}) {
|
||||||
final Brightness foregroundBrightness =
|
final Brightness foregroundBrightness =
|
||||||
ThemeData.estimateBrightnessForColor(seedColor);
|
ThemeData.estimateBrightnessForColor(seedColor);
|
||||||
final Color foregroundColor =
|
final Color foregroundColor = foregroundBrightness == Brightness.dark
|
||||||
foregroundBrightness == Brightness.dark ? Colors.white : Colors.black;
|
? Colors.white
|
||||||
|
: Colors.black87;
|
||||||
|
|
||||||
|
final ColorScheme scheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: seedColor,
|
||||||
|
brightness: brightness,
|
||||||
|
);
|
||||||
|
|
||||||
|
final AppPalette palette = AppPalette.fromBrightness(
|
||||||
|
brightness,
|
||||||
|
seedColor: seedColor,
|
||||||
|
);
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
scaffoldBackgroundColor: const Color.fromRGBO(31, 32, 33, 1),
|
scaffoldBackgroundColor: scheme.surface,
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: scheme,
|
||||||
seedColor: seedColor,
|
extensions: <ThemeExtension<dynamic>>[palette],
|
||||||
brightness: Brightness.dark,
|
brightness: brightness,
|
||||||
),
|
|
||||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
backgroundColor: seedColor,
|
backgroundColor: seedColor,
|
||||||
foregroundColor: foregroundColor,
|
foregroundColor: foregroundColor,
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class CategoryStyle {
|
class CategoryStyle {
|
||||||
CategoryStyle._();
|
CategoryStyle._();
|
||||||
|
|
||||||
static const List<Color> colors = <Color>[
|
static const List<Color> colors = AppPalette.defaultCategoryColors;
|
||||||
Colors.amber,
|
|
||||||
Colors.blue,
|
static List<Color> colorsOf(BuildContext context) {
|
||||||
Colors.green,
|
final AppPalette? palette = Theme.of(context).extension<AppPalette>();
|
||||||
Colors.purple,
|
if (palette != null) {
|
||||||
Colors.red,
|
return palette.categoryColors;
|
||||||
Colors.teal,
|
}
|
||||||
Colors.orange,
|
|
||||||
Colors.grey,
|
return AppPalette.defaultCategoryColors;
|
||||||
];
|
}
|
||||||
|
|
||||||
static const List<IconData> icons = <IconData>[
|
static const List<IconData> icons = <IconData>[
|
||||||
Icons.folder,
|
Icons.label_outline_rounded,
|
||||||
Icons.work,
|
Icons.work,
|
||||||
Icons.star,
|
Icons.star,
|
||||||
Icons.home,
|
Icons.home,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:notas/models/category.dart';
|
import 'package:notas/models/category.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
import 'package:notas/widgets/category_style.dart';
|
import 'package:notas/widgets/category_style.dart';
|
||||||
|
|
||||||
class MenuDrawer extends StatelessWidget {
|
class MenuDrawer extends StatelessWidget {
|
||||||
@@ -20,12 +21,12 @@ class MenuDrawer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color.fromARGB(255, 30, 31, 35),
|
color: palette.drawerBackground,
|
||||||
border: Border(
|
border: Border(right: BorderSide(color: palette.border, width: 0.5)),
|
||||||
right: BorderSide(color: Colors.white12, width: 0.5),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -56,23 +57,28 @@ class MenuDrawer extends StatelessWidget {
|
|||||||
label: category.name,
|
label: category.name,
|
||||||
selected: selectedItem == categoryId,
|
selected: selectedItem == categoryId,
|
||||||
onTap: () => onMenuItemTapped?.call(categoryId),
|
onTap: () => onMenuItemTapped?.call(categoryId),
|
||||||
iconColor: Color(category.colorValue ?? 0xFFFFC107),
|
onLongPress: onEditCategory == null
|
||||||
textColor: Color(category.colorValue ?? 0xFFFFC107),
|
? null
|
||||||
trailing: selectedItem == categoryId
|
: () => onEditCategory?.call(category),
|
||||||
? IconButton(
|
iconColor: Color(
|
||||||
padding: const EdgeInsets.all(8),
|
category.colorValue ?? palette.accent.toARGB32(),
|
||||||
constraints: const BoxConstraints(
|
),
|
||||||
minWidth: 0,
|
textColor: Color(
|
||||||
minHeight: 0,
|
category.colorValue ?? palette.accent.toARGB32(),
|
||||||
),
|
),
|
||||||
icon: const Icon(
|
trailing: IconButton(
|
||||||
Icons.more_vert,
|
padding: const EdgeInsets.all(8),
|
||||||
color: Colors.white70,
|
constraints: const BoxConstraints(
|
||||||
size: 20,
|
minWidth: 0,
|
||||||
),
|
minHeight: 0,
|
||||||
onPressed: () => onEditCategory?.call(category),
|
),
|
||||||
)
|
icon: Icon(
|
||||||
: null,
|
Icons.more_vert,
|
||||||
|
color: palette.textSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () => onEditCategory?.call(category),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
@@ -91,10 +97,10 @@ class MenuDrawer extends StatelessWidget {
|
|||||||
label: 'Mis notas borradas',
|
label: 'Mis notas borradas',
|
||||||
selected: selectedItem == 'deleted_notes',
|
selected: selectedItem == 'deleted_notes',
|
||||||
onTap: () => onMenuItemTapped?.call('deleted_notes'),
|
onTap: () => onMenuItemTapped?.call('deleted_notes'),
|
||||||
iconColor: Colors.redAccent,
|
iconColor: palette.destructiveAccent,
|
||||||
textColor: Colors.redAccent,
|
textColor: palette.destructiveAccent,
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white12, height: 16),
|
Divider(color: palette.border, height: 16),
|
||||||
_MenuItemTile(
|
_MenuItemTile(
|
||||||
icon: Icons.settings,
|
icon: Icons.settings,
|
||||||
label: 'Configuración',
|
label: 'Configuración',
|
||||||
@@ -113,6 +119,7 @@ class _MenuItemTile extends StatefulWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
this.textColor,
|
this.textColor,
|
||||||
this.trailing,
|
this.trailing,
|
||||||
@@ -122,6 +129,7 @@ class _MenuItemTile extends StatefulWidget {
|
|||||||
final String label;
|
final String label;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
final Color? textColor;
|
final Color? textColor;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
@@ -135,11 +143,13 @@ class _MenuItemTileState extends State<_MenuItemTile> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
final bool active = widget.selected || _hovering;
|
final bool active = widget.selected || _hovering;
|
||||||
final Color backgroundColor = active
|
final Color backgroundColor = active ? palette.hover : palette.transparent;
|
||||||
? Colors.white.withValues(alpha: 0.10)
|
final Color foregroundColor = active
|
||||||
: Colors.transparent;
|
? palette.textPrimary
|
||||||
final Color foregroundColor = active ? Colors.white : Colors.white70;
|
: palette.textSecondary;
|
||||||
|
final Widget? trailing = _hovering ? widget.trailing : null;
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) {
|
onEnter: (_) {
|
||||||
@@ -151,11 +161,7 @@ class _MenuItemTileState extends State<_MenuItemTile> {
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
margin: const EdgeInsets.only(
|
margin: const EdgeInsets.only(right: 8, top: 2, bottom: 2),
|
||||||
right: 8,
|
|
||||||
top: 2,
|
|
||||||
bottom: 2,
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@@ -165,13 +171,20 @@ class _MenuItemTileState extends State<_MenuItemTile> {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
leading: Icon(widget.icon, color: widget.iconColor ?? foregroundColor),
|
leading: Icon(
|
||||||
trailing: widget.trailing,
|
widget.icon,
|
||||||
|
color: widget.iconColor ?? foregroundColor,
|
||||||
|
),
|
||||||
|
trailing: trailing,
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.label,
|
widget.label,
|
||||||
style: TextStyle(color: widget.textColor ?? foregroundColor, fontSize: 14),
|
style: TextStyle(
|
||||||
|
color: widget.textColor ?? foregroundColor,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,138 +1,161 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:notas/data/note_body.dart';
|
||||||
|
import 'package:notas/models/category.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
import 'package:notas/widgets/category_style.dart';
|
||||||
|
|
||||||
// Small presentational widget for a note inside the grid.
|
class NoteCard extends StatelessWidget {
|
||||||
// Keep this widget lightweight and layout-agnostic: it should not force
|
|
||||||
// width/height constraints (so it works inside different parent layouts
|
|
||||||
// like MasonryGridView or Draggable feedback). Visual styling only.
|
|
||||||
|
|
||||||
class NoteCard extends StatefulWidget {
|
|
||||||
const NoteCard({
|
const NoteCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.note,
|
required this.note,
|
||||||
this.onTap,
|
this.category,
|
||||||
this.isDragging = false,
|
this.isSelected = false,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
|
this.onTap,
|
||||||
|
this.onDelete,
|
||||||
|
this.onChangeCategory,
|
||||||
|
this.showSelectionBorder = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Note note;
|
final Note note;
|
||||||
final VoidCallback? onTap;
|
final Category? category;
|
||||||
final bool isDragging;
|
final bool isSelected;
|
||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
|
final VoidCallback? onTap;
|
||||||
@override
|
final VoidCallback? onDelete;
|
||||||
State<NoteCard> createState() => _NoteCardState();
|
final ValueChanged<BuildContext>? onChangeCategory;
|
||||||
}
|
final bool showSelectionBorder;
|
||||||
|
|
||||||
class _NoteCardState extends State<NoteCard> {
|
|
||||||
bool _isPressed = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bool showGrabbing = widget.isDragging || _isPressed;
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
final String bodyText = noteBodyToPlainText(note.body).trim();
|
||||||
|
final Color? categoryColor = category?.colorValue == null
|
||||||
|
? null
|
||||||
|
: Color(category!.colorValue!);
|
||||||
|
final IconData? categoryIcon = category == null
|
||||||
|
? null
|
||||||
|
: CategoryStyle.iconForCodePoint(category!.iconCodePoint);
|
||||||
|
|
||||||
return MouseRegion(
|
return Material(
|
||||||
cursor: showGrabbing ? SystemMouseCursors.grabbing : SystemMouseCursors.grab,
|
color: Colors.transparent, // 1. Fondo completamente transparente
|
||||||
child: GestureDetector(
|
shape: BorderDirectional(
|
||||||
onTapDown: widget.onTap == null
|
start: BorderSide(
|
||||||
? null
|
color: (isSelected && showSelectionBorder)
|
||||||
: (_) {
|
? palette.accent
|
||||||
setState(() {
|
: Colors.transparent,
|
||||||
_isPressed = true;
|
width: isSelected ? 1.6 : 1.0,
|
||||||
});
|
),
|
||||||
},
|
),
|
||||||
onTapUp: widget.onTap == null
|
child: InkWell(
|
||||||
? null
|
borderRadius: BorderRadius.circular(14),
|
||||||
: (_) {
|
onTap: onTap,
|
||||||
setState(() {
|
hoverColor:
|
||||||
_isPressed = false;
|
Colors.transparent, // 2. Desactiva el efecto hover (pasar el ratón)
|
||||||
});
|
splashColor:
|
||||||
},
|
Colors.transparent, // 3. Desactiva el efecto de onda al hacer clic
|
||||||
onTapCancel: widget.onTap == null
|
highlightColor:
|
||||||
? null
|
Colors.transparent, // Desactiva el brillo al mantener pulsado
|
||||||
: () {
|
child: Padding(
|
||||||
setState(() {
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
_isPressed = false;
|
child: Row(
|
||||||
});
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
},
|
children: [
|
||||||
onTap: widget.onTap,
|
Expanded(
|
||||||
child: Container(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16),
|
mainAxisSize: MainAxisSize.min,
|
||||||
decoration: BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
color: const Color.fromRGBO(24, 25, 26, 1),
|
children: [
|
||||||
borderRadius: BorderRadius.circular(12),
|
Row(
|
||||||
border: Border.all(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
color: widget.borderColor ?? Colors.white24,
|
children: [
|
||||||
width: 1,
|
if (categoryIcon != null) ...[
|
||||||
),
|
SizedBox(
|
||||||
),
|
width: 18,
|
||||||
child: LayoutBuilder(
|
height: 18,
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
child: Icon(
|
||||||
// Estimate whether the body will exceed 20 lines without always
|
categoryIcon,
|
||||||
// running the expensive TextPainter layout. This heuristic counts
|
size: 18,
|
||||||
// newline characters and estimates wrapped lines based on an
|
color: categoryColor ?? palette.textSecondary,
|
||||||
// average characters-per-line to handle many short lines well.
|
),
|
||||||
final List<String> rawLines = widget.note.body.split('\n');
|
),
|
||||||
const int avgCharsPerLine = 40;
|
const SizedBox(width: 4),
|
||||||
int estimatedLines = 0;
|
],
|
||||||
for (final String line in rawLines) {
|
Expanded(
|
||||||
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
|
child: Text(
|
||||||
}
|
note.title.isEmpty ? 'Sin título' : note.title,
|
||||||
|
maxLines: 1,
|
||||||
final bool needsPreciseMeasurement = estimatedLines > 20;
|
overflow: TextOverflow.ellipsis,
|
||||||
final bool isBodyTruncated;
|
style: TextStyle(
|
||||||
|
color: palette.textPrimary,
|
||||||
if (needsPreciseMeasurement) {
|
fontSize: 15,
|
||||||
final TextPainter textPainter = TextPainter(
|
fontWeight: FontWeight.w700,
|
||||||
text: TextSpan(
|
),
|
||||||
text: widget.note.body,
|
),
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
),
|
||||||
),
|
],
|
||||||
maxLines: 20,
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
)..layout(maxWidth: constraints.maxWidth);
|
|
||||||
|
|
||||||
isBodyTruncated = textPainter.didExceedMaxLines;
|
|
||||||
} else {
|
|
||||||
isBodyTruncated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.note.title,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
const SizedBox(height: 6),
|
||||||
overflow: TextOverflow.ellipsis,
|
Text(
|
||||||
),
|
bodyText.isEmpty ? ' ' : bodyText,
|
||||||
const SizedBox(height: 8),
|
maxLines: 1,
|
||||||
Text(
|
overflow: TextOverflow.ellipsis,
|
||||||
widget.note.body,
|
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
|
||||||
maxLines: 20,
|
|
||||||
overflow: TextOverflow.clip,
|
|
||||||
),
|
|
||||||
if (isBodyTruncated) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
const Text(
|
|
||||||
'...',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white54,
|
color: palette.textSecondary,
|
||||||
fontSize: 18,
|
fontSize: 14,
|
||||||
height: 1,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
),
|
||||||
},
|
const SizedBox(width: 8),
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext buttonContext) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
icon: Icon(Icons.more_vert, color: palette.textSecondary),
|
||||||
|
color: palette.surfaceElevated,
|
||||||
|
elevation: 10,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
onSelected: (String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'category':
|
||||||
|
onChangeCategory?.call(buttonContext);
|
||||||
|
return;
|
||||||
|
case 'delete':
|
||||||
|
onDelete?.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<String>>[
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'category',
|
||||||
|
child: Text('Modificar categoría'),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.delete_outline, color: Colors.red),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'Eliminar',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
|
||||||
class SearchAppBar extends StatefulWidget {
|
class SearchAppBar extends StatefulWidget {
|
||||||
const SearchAppBar({
|
const SearchAppBar({
|
||||||
@@ -51,22 +52,23 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: palette.transparent,
|
||||||
border: Border(
|
border: Border(bottom: BorderSide(color: palette.border, width: 0.5)),
|
||||||
bottom: BorderSide(
|
|
||||||
color: Colors.white.withValues(alpha: 0.12),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7),
|
padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: widget.onLeadingPressed ?? widget.onMenuPressed,
|
onPressed: widget.onLeadingPressed ?? widget.onMenuPressed,
|
||||||
icon: Icon(widget.leadingIcon, color: Colors.white70, size: 20),
|
icon: Icon(
|
||||||
|
widget.leadingIcon,
|
||||||
|
color: palette.textSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
tooltip: widget.leadingTooltip,
|
tooltip: widget.leadingTooltip,
|
||||||
splashRadius: 18,
|
splashRadius: 18,
|
||||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||||
@@ -84,18 +86,21 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
onChanged: widget.onSearchChanged,
|
onChanged: widget.onSearchChanged,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: TextStyle(
|
||||||
cursorColor: Colors.white70,
|
color: palette.textPrimary,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
cursorColor: palette.textSecondary,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: widget.searchHint,
|
hintText: widget.searchHint,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
color: palette.textSecondary.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
suffixIcon: _searchController.text.isNotEmpty
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.clear,
|
Icons.clear,
|
||||||
color: Colors.white70,
|
color: palette.textSecondary,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -107,37 +112,37 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Padding(
|
: Padding(
|
||||||
padding: EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.search,
|
Icons.search,
|
||||||
color: Colors.white70,
|
color: palette.textSecondary,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: palette.border,
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: palette.border,
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Colors.white.withValues(alpha: 0.4),
|
color: palette.accent,
|
||||||
width: 0.5,
|
width: 0.6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
fillColor: palette.fill,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
@@ -151,8 +156,8 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.titleText ?? '',
|
widget.titleText ?? '',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: palette.textPrimary,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:notas/theme/app_palette.dart';
|
||||||
import 'package:notas/widgets/sync_status.dart';
|
import 'package:notas/widgets/sync_status.dart';
|
||||||
|
|
||||||
class SyncStatusIndicator extends StatelessWidget {
|
class SyncStatusIndicator extends StatelessWidget {
|
||||||
@@ -86,12 +87,14 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case SyncStatus.idle:
|
case SyncStatus.idle:
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: _messageForStatus(),
|
message: _messageForStatus(),
|
||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
const Icon(Icons.cloud_outlined, size: 16, color: Colors.white38),
|
Icon(Icons.cloud_outlined, size: 16, color: palette.textSecondary),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,7 +104,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
color: const Color.fromARGB(255, 165, 165, 165),
|
color: palette.syncPreparing,
|
||||||
determinate: false,
|
determinate: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -113,7 +116,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.cloud_upload_outlined,
|
icon: Icons.cloud_upload_outlined,
|
||||||
color: const Color.fromARGB(255, 109, 191, 255),
|
color: palette.syncEncrypting,
|
||||||
determinate: true,
|
determinate: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -125,7 +128,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.cloud_upload,
|
icon: Icons.cloud_upload,
|
||||||
color: const Color.fromARGB(255, 98, 190, 255),
|
color: palette.syncUploading,
|
||||||
determinate: false,
|
determinate: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -137,7 +140,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.cloud_sync_outlined,
|
icon: Icons.cloud_sync_outlined,
|
||||||
color: const Color.fromARGB(255, 150, 150, 150),
|
color: palette.syncWaiting,
|
||||||
determinate: false,
|
determinate: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -149,7 +152,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.cloud_download_outlined,
|
icon: Icons.cloud_download_outlined,
|
||||||
color: const Color.fromARGB(255, 154, 194, 112),
|
color: palette.syncDecrypting,
|
||||||
determinate: true,
|
determinate: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -161,7 +164,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
_buildStatusBadge(
|
_buildStatusBadge(
|
||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
color: const Color.fromARGB(255, 150, 150, 150),
|
color: palette.syncWaiting,
|
||||||
determinate: false,
|
determinate: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -171,7 +174,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: _messageForStatus(),
|
message: _messageForStatus(),
|
||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
const Icon(Icons.check_circle, size: 16, color: Colors.green),
|
Icon(Icons.check_circle, size: 16, color: palette.success),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -179,7 +182,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
|||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: _messageForStatus(),
|
message: _messageForStatus(),
|
||||||
child: _buildIndicator(
|
child: _buildIndicator(
|
||||||
const Icon(Icons.error, size: 16, color: Colors.red),
|
Icon(Icons.error, size: 16, color: palette.destructiveAccent),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
screen_retriever_linux
|
screen_retriever_linux
|
||||||
|
url_launcher_linux
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,18 @@ import Foundation
|
|||||||
|
|
||||||
import flutter_secure_storage_darwin
|
import flutter_secure_storage_darwin
|
||||||
import local_auth_darwin
|
import local_auth_darwin
|
||||||
|
import quill_native_bridge_macos
|
||||||
import screen_retriever_macos
|
import screen_retriever_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||||
|
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
||||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 813 B |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -5,18 +5,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
|
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "99.0.0"
|
version: "100.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
|
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.1.0"
|
version: "13.0.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -141,10 +141,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_assets
|
name: code_assets
|
||||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.2.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -161,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: d687bec93342bf6a764a116d15c8694ebeff10e633dc28a39dd3144f7195024e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+3"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -177,6 +185,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.0"
|
version: "2.9.0"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -185,30 +201,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "1.0.9"
|
||||||
|
dart_quill_delta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_quill_delta
|
||||||
|
sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.8.3"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98
|
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.8"
|
version: "3.1.9"
|
||||||
|
diff_match_patch:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: diff_match_patch
|
||||||
|
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
drift:
|
drift:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
|
sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.33.0"
|
version: "2.34.0"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
|
sha256: "042a5fb507ab5697f67eb55b75cfff2f665701f6606926136d6d4e85f81ff837"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.33.0"
|
version: "2.34.1+1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -225,14 +257,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
ffi_leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ffi_leak_tracker
|
|
||||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.2"
|
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -241,6 +265,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -254,6 +294,54 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_colorpicker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_colorpicker
|
||||||
|
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
flutter_keyboard_visibility_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_linux
|
||||||
|
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_macos
|
||||||
|
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_platform_interface
|
||||||
|
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
flutter_keyboard_visibility_temp_fork:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_temp_fork
|
||||||
|
sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.5"
|
||||||
|
flutter_keyboard_visibility_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_windows
|
||||||
|
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -270,38 +358,59 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.34"
|
version: "2.0.35"
|
||||||
|
flutter_quill:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_quill
|
||||||
|
sha256: "3ee7125b2dd3f3bce3ebdaac722a72f0c8aff3db9aa19053a9d777db12d71c98"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.5.1"
|
||||||
|
flutter_quill_delta_from_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_quill_delta_from_html
|
||||||
|
sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.3"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
|
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.0"
|
version: "10.3.1"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_darwin
|
name: flutter_secure_storage_darwin
|
||||||
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
|
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.2"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_linux
|
name: flutter_secure_storage_linux
|
||||||
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
|
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.1"
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -322,10 +431,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_windows
|
name: flutter_secure_storage_windows
|
||||||
sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8"
|
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.1"
|
version: "4.1.0"
|
||||||
flutter_staggered_grid_view:
|
flutter_staggered_grid_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -364,18 +473,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "2.0.2"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.6"
|
version: "1.6.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -396,10 +513,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "4.9.1"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -484,10 +601,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: b201c006fa769c23386f89aa6837ec0eb8179fcfb212eadcf87b422b3f9a6a78
|
sha256: fdb936d59ab945c7af297defd67bd1ed87b11b6db1bc16d01e94677a8f1c38ec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.9"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -520,6 +637,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.3.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -556,18 +681,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: native_toolchain_c
|
name: native_toolchain_c
|
||||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.6"
|
version: "0.19.2"
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: objective_c
|
name: objective_c
|
||||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
version: "9.4.1"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -588,10 +713,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -612,18 +737,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -688,6 +813,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
quill_native_bridge:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge
|
||||||
|
sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.1.0"
|
||||||
|
quill_native_bridge_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_android
|
||||||
|
sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1+2"
|
||||||
|
quill_native_bridge_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_ios
|
||||||
|
sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quill_native_bridge_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_macos
|
||||||
|
sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quill_native_bridge_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_platform_interface
|
||||||
|
sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
quill_native_bridge_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_web
|
||||||
|
sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
quill_native_bridge_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_windows
|
||||||
|
sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
recase:
|
recase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -708,42 +897,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever
|
name: screen_retriever
|
||||||
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
|
sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
screen_retriever_linux:
|
screen_retriever_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever_linux
|
name: screen_retriever_linux
|
||||||
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
|
sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
screen_retriever_macos:
|
screen_retriever_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever_macos
|
name: screen_retriever_macos
|
||||||
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
|
sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
screen_retriever_platform_interface:
|
screen_retriever_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever_platform_interface
|
name: screen_retriever_platform_interface
|
||||||
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
|
sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
screen_retriever_windows:
|
screen_retriever_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever_windows
|
name: screen_retriever_windows
|
||||||
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
|
sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -756,10 +945,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.23"
|
version: "2.4.26"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -841,18 +1030,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3
|
name: sqlite3
|
||||||
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
|
sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.3.3"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
|
sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.44.4"
|
version: "0.44.5"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -909,6 +1098,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.32"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -969,10 +1222,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.0"
|
version: "5.15.0"
|
||||||
window_manager:
|
window_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -993,10 +1246,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "7.0.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1006,5 +1259,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.5 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.38.4"
|
flutter: ">=3.44.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: notas
|
|||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
@@ -44,10 +44,14 @@ dependencies:
|
|||||||
flutter_secure_storage: ^10.2.0
|
flutter_secure_storage: ^10.2.0
|
||||||
local_auth: ^3.0.1
|
local_auth: ^3.0.1
|
||||||
sqlite3: ^3.3.1
|
sqlite3: ^3.3.1
|
||||||
http: ^0.13.6
|
http: ^1.6.0
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
cryptography: ^2.7.0
|
cryptography: ^2.7.0
|
||||||
uuid: ^4.0.0
|
uuid: ^4.0.0
|
||||||
|
flutter_quill: ^11.5.1
|
||||||
|
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -68,7 +72,6 @@ dev_dependencies:
|
|||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
@@ -113,7 +116,6 @@ hooks:
|
|||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/to/font-from-package
|
# see https://flutter.dev/to/font-from-package
|
||||||
|
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
# Configuración general y móvil
|
# Configuración general y móvil
|
||||||
image_path: "assets/icon.png"
|
image_path: "assets/icon.png"
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
import 'package:notas/models/category.dart';
|
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/screens/note_editor_screen.dart';
|
import 'package:notas/screens/note_editor_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('saves a note when only the category changes', (
|
testWidgets('autosaves a note when only the category changes', (
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
) async {
|
) async {
|
||||||
Note? savedNote;
|
Note? savedNote;
|
||||||
|
|
||||||
|
final Note initialNote = Note(
|
||||||
|
title: 'Sin título',
|
||||||
|
body: '',
|
||||||
|
createdAt: DateTime(2026, 5, 21),
|
||||||
|
updatedAt: DateTime(2026, 5, 21),
|
||||||
|
position: 0,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
FlutterQuillLocalizations.delegate,
|
||||||
|
],
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: NoteEditorScreen(
|
body: NoteEditorScreen(
|
||||||
note: null,
|
repository: null,
|
||||||
categoryId: null,
|
note: initialNote,
|
||||||
categories: <Category>[
|
saveNote: (Note note) async => note,
|
||||||
Category(
|
onSaved: (Note result) {
|
||||||
id: 'work',
|
savedNote = result;
|
||||||
name: 'Trabajo',
|
|
||||||
updatedAt: DateTime(2026, 5, 21),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onComplete: (dynamic result) {
|
|
||||||
savedNote = result as Note?;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -38,44 +46,53 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.text('Trabajo').last);
|
await tester.tap(find.text('Trabajo').last);
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
await tester.tap(find.text('Guardar'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(savedNote, isNotNull);
|
expect(savedNote, isNotNull);
|
||||||
expect(savedNote!.categoryId, 'work');
|
expect(savedNote!.categoryId, 'work');
|
||||||
expect(savedNote!.title, 'Sin título');
|
expect(savedNote!.title, 'Sin título');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('only completes once when save is tapped twice', (
|
testWidgets('debounces multiple edits into a single save', (
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
) async {
|
) async {
|
||||||
int completionCount = 0;
|
int saveCount = 0;
|
||||||
|
|
||||||
|
final Note initialNote = Note(
|
||||||
|
title: 'Sin título',
|
||||||
|
body: '',
|
||||||
|
createdAt: DateTime(2026, 5, 21),
|
||||||
|
updatedAt: DateTime(2026, 5, 21),
|
||||||
|
position: 0,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
FlutterQuillLocalizations.delegate,
|
||||||
|
],
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: NoteEditorScreen(
|
body: NoteEditorScreen(
|
||||||
note: null,
|
repository: null,
|
||||||
categoryId: null,
|
note: initialNote,
|
||||||
categories: <Category>[],
|
saveNote: (Note note) async {
|
||||||
onComplete: (dynamic result) {
|
saveCount += 1;
|
||||||
if (result is Note) {
|
return note;
|
||||||
completionCount += 1;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.enterText(find.byType(TextField).first, 'Nota de prueba');
|
await tester.enterText(find.byType(TextField).first, 'Primera versión');
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
await tester.enterText(find.byType(TextField).first, 'Segunda versión');
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
|
||||||
await tester.tap(find.text('Guardar'));
|
expect(saveCount, 1);
|
||||||
await tester.tap(find.text('Guardar'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(completionCount, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,6 @@ void main() {
|
|||||||
await tester.pumpWidget(const NotesApp());
|
await tester.pumpWidget(const NotesApp());
|
||||||
|
|
||||||
expect(find.byType(MaterialApp), findsOneWidget);
|
expect(find.byType(MaterialApp), findsOneWidget);
|
||||||
expect(find.text('Mis Notas'), findsWidgets);
|
expect(find.text('Preparando el vault local...'), findsWidgets);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,24 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <local_auth_windows/local_auth_plugin.h>
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
LocalAuthPluginRegisterWithRegistrar(
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
WindowManagerPluginRegisterWithRegistrar(
|
WindowManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
local_auth_windows
|
local_auth_windows
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.0 KiB |