Add Windows runner files for high DPI support and console output
- Created runner.exe.manifest to enable DPI awareness and dark mode support. - Implemented utility functions in utils.cpp and utils.h for console creation and command line argument handling. - Developed Win32Window class in win32_window.cpp and win32_window.h to manage high DPI-aware windows, including theme updates and message handling.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:notas/platform/app_platform.dart';
|
||||
import 'package:notas/screens/home_screen.dart';
|
||||
import 'package:notas/theme/app_theme.dart';
|
||||
import 'package:notas/platform/window_state.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class NotesApp extends StatefulWidget {
|
||||
const NotesApp({super.key});
|
||||
|
||||
@override
|
||||
State<NotesApp> createState() => _NotesAppState();
|
||||
}
|
||||
|
||||
class _NotesAppState extends State<NotesApp> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isDesktop) {
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isDesktop) {
|
||||
windowManager.removeListener(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveWindowSize() async {
|
||||
if (await windowManager.isFullScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await windowManager.isMaximized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Size currentSize = await windowManager.getSize();
|
||||
await WindowStateStore.instance.saveWindowSize(currentSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
_saveWindowSize();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResized() {
|
||||
_saveWindowSize();
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Mis Notas',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.theme,
|
||||
home: const HomeScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@DataClassName('DbNote')
|
||||
class Notes extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text()();
|
||||
TextColumn get body => text()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
IntColumn get sortIndex => integer().named('sort_index')();
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [Notes])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
Future<List<DbNote>> getAllNotes() {
|
||||
return (select(notes)..orderBy([
|
||||
(note) => OrderingTerm(expression: note.sortIndex),
|
||||
])).get();
|
||||
}
|
||||
|
||||
Future<int> insertNoteAtTop(NotesCompanion note) {
|
||||
return transaction(() async {
|
||||
await customStatement('UPDATE notes SET sort_index = sort_index + 1');
|
||||
return into(notes).insert(note.copyWith(sortIndex: const Value<int>(0)));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> replaceAllNotes(List<NotesCompanion> noteList) {
|
||||
return transaction(() async {
|
||||
await delete(notes).go();
|
||||
|
||||
for (final NotesCompanion note in noteList) {
|
||||
await into(notes).insert(note);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateNoteRow(DbNote note) {
|
||||
return update(notes).replace(note);
|
||||
}
|
||||
|
||||
Future<void> deleteNoteAndShift({
|
||||
required int id,
|
||||
required int removedIndex,
|
||||
}) {
|
||||
return transaction(() async {
|
||||
await (delete(notes)..where((note) => note.id.equals(id))).go();
|
||||
await customStatement(
|
||||
'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ?',
|
||||
[removedIndex],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> moveNote({
|
||||
required int id,
|
||||
required int oldIndex,
|
||||
required int newIndex,
|
||||
}) {
|
||||
if (oldIndex == newIndex) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
return transaction(() async {
|
||||
if (oldIndex < newIndex) {
|
||||
await customStatement(
|
||||
'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ? AND sort_index <= ?',
|
||||
[oldIndex, newIndex],
|
||||
);
|
||||
} else {
|
||||
await customStatement(
|
||||
'UPDATE notes SET sort_index = sort_index + 1 WHERE sort_index >= ? AND sort_index < ?',
|
||||
[newIndex, oldIndex],
|
||||
);
|
||||
}
|
||||
|
||||
await customStatement(
|
||||
'UPDATE notes SET sort_index = ? WHERE id = ?',
|
||||
[newIndex, id],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final Directory supportDir = await getApplicationSupportDirectory();
|
||||
final File file = File(p.join(supportDir.path, 'notes.sqlite'));
|
||||
|
||||
return NativeDatabase(file);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_database.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
class $NotesTable extends Notes with TableInfo<$NotesTable, DbNote> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$NotesTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'PRIMARY KEY AUTOINCREMENT',
|
||||
),
|
||||
);
|
||||
static const VerificationMeta _titleMeta = const VerificationMeta('title');
|
||||
@override
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _bodyMeta = const VerificationMeta('body');
|
||||
@override
|
||||
late final GeneratedColumn<String> body = GeneratedColumn<String>(
|
||||
'body',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _sortIndexMeta = const VerificationMeta(
|
||||
'sortIndex',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<int> sortIndex = GeneratedColumn<int>(
|
||||
'sort_index',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
sortIndex,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'notes';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<DbNote> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('title')) {
|
||||
context.handle(
|
||||
_titleMeta,
|
||||
title.isAcceptableOrUnknown(data['title']!, _titleMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_titleMeta);
|
||||
}
|
||||
if (data.containsKey('body')) {
|
||||
context.handle(
|
||||
_bodyMeta,
|
||||
body.isAcceptableOrUnknown(data['body']!, _bodyMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_bodyMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_updatedAtMeta);
|
||||
}
|
||||
if (data.containsKey('sort_index')) {
|
||||
context.handle(
|
||||
_sortIndexMeta,
|
||||
sortIndex.isAcceptableOrUnknown(data['sort_index']!, _sortIndexMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_sortIndexMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
DbNote map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return DbNote(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
title: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}title'],
|
||||
)!,
|
||||
body: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}body'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
sortIndex: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}sort_index'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$NotesTable createAlias(String alias) {
|
||||
return $NotesTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class DbNote extends DataClass implements Insertable<DbNote> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int sortIndex;
|
||||
const DbNote({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.sortIndex,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['title'] = Variable<String>(title);
|
||||
map['body'] = Variable<String>(body);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['updated_at'] = Variable<DateTime>(updatedAt);
|
||||
map['sort_index'] = Variable<int>(sortIndex);
|
||||
return map;
|
||||
}
|
||||
|
||||
NotesCompanion toCompanion(bool nullToAbsent) {
|
||||
return NotesCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
body: Value(body),
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(updatedAt),
|
||||
sortIndex: Value(sortIndex),
|
||||
);
|
||||
}
|
||||
|
||||
factory DbNote.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return DbNote(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
body: serializer.fromJson<String>(json['body']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
sortIndex: serializer.fromJson<int>(json['sortIndex']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'body': serializer.toJson<String>(body),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'sortIndex': serializer.toJson<int>(sortIndex),
|
||||
};
|
||||
}
|
||||
|
||||
DbNote copyWith({
|
||||
int? id,
|
||||
String? title,
|
||||
String? body,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
int? sortIndex,
|
||||
}) => DbNote(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
body: body ?? this.body,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
sortIndex: sortIndex ?? this.sortIndex,
|
||||
);
|
||||
DbNote copyWithCompanion(NotesCompanion data) {
|
||||
return DbNote(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
title: data.title.present ? data.title.value : this.title,
|
||||
body: data.body.present ? data.body.value : this.body,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
sortIndex: data.sortIndex.present ? data.sortIndex.value : this.sortIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('DbNote(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('body: $body, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('sortIndex: $sortIndex')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, title, body, createdAt, updatedAt, sortIndex);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is DbNote &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.body == this.body &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.sortIndex == this.sortIndex);
|
||||
}
|
||||
|
||||
class NotesCompanion extends UpdateCompanion<DbNote> {
|
||||
final Value<int> id;
|
||||
final Value<String> title;
|
||||
final Value<String> body;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime> updatedAt;
|
||||
final Value<int> sortIndex;
|
||||
const NotesCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.body = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.updatedAt = const Value.absent(),
|
||||
this.sortIndex = const Value.absent(),
|
||||
});
|
||||
NotesCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required int sortIndex,
|
||||
}) : title = Value(title),
|
||||
body = Value(body),
|
||||
createdAt = Value(createdAt),
|
||||
updatedAt = Value(updatedAt),
|
||||
sortIndex = Value(sortIndex);
|
||||
static Insertable<DbNote> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? title,
|
||||
Expression<String>? body,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? updatedAt,
|
||||
Expression<int>? sortIndex,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (body != null) 'body': body,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (sortIndex != null) 'sort_index': sortIndex,
|
||||
});
|
||||
}
|
||||
|
||||
NotesCompanion copyWith({
|
||||
Value<int>? id,
|
||||
Value<String>? title,
|
||||
Value<String>? body,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime>? updatedAt,
|
||||
Value<int>? sortIndex,
|
||||
}) {
|
||||
return NotesCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
body: body ?? this.body,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
sortIndex: sortIndex ?? this.sortIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
}
|
||||
if (body.present) {
|
||||
map['body'] = Variable<String>(body.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
if (sortIndex.present) {
|
||||
map['sort_index'] = Variable<int>(sortIndex.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NotesCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('body: $body, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('sortIndex: $sortIndex')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
late final $NotesTable notes = $NotesTable(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [notes];
|
||||
}
|
||||
|
||||
typedef $$NotesTableCreateCompanionBuilder =
|
||||
NotesCompanion Function({
|
||||
Value<int> id,
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required int sortIndex,
|
||||
});
|
||||
typedef $$NotesTableUpdateCompanionBuilder =
|
||||
NotesCompanion Function({
|
||||
Value<int> id,
|
||||
Value<String> title,
|
||||
Value<String> body,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime> updatedAt,
|
||||
Value<int> sortIndex,
|
||||
});
|
||||
|
||||
class $$NotesTableFilterComposer extends Composer<_$AppDatabase, $NotesTable> {
|
||||
$$NotesTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get title => $composableBuilder(
|
||||
column: $table.title,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get body => $composableBuilder(
|
||||
column: $table.body,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get sortIndex => $composableBuilder(
|
||||
column: $table.sortIndex,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$NotesTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $NotesTable> {
|
||||
$$NotesTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get title => $composableBuilder(
|
||||
column: $table.title,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get body => $composableBuilder(
|
||||
column: $table.body,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get sortIndex => $composableBuilder(
|
||||
column: $table.sortIndex,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$NotesTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $NotesTable> {
|
||||
$$NotesTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get title =>
|
||||
$composableBuilder(column: $table.title, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get body =>
|
||||
$composableBuilder(column: $table.body, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get sortIndex =>
|
||||
$composableBuilder(column: $table.sortIndex, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$NotesTableTableManager
|
||||
extends
|
||||
RootTableManager<
|
||||
_$AppDatabase,
|
||||
$NotesTable,
|
||||
DbNote,
|
||||
$$NotesTableFilterComposer,
|
||||
$$NotesTableOrderingComposer,
|
||||
$$NotesTableAnnotationComposer,
|
||||
$$NotesTableCreateCompanionBuilder,
|
||||
$$NotesTableUpdateCompanionBuilder,
|
||||
(DbNote, BaseReferences<_$AppDatabase, $NotesTable, DbNote>),
|
||||
DbNote,
|
||||
PrefetchHooks Function()
|
||||
> {
|
||||
$$NotesTableTableManager(_$AppDatabase db, $NotesTable table)
|
||||
: super(
|
||||
TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$NotesTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$NotesTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$NotesTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> title = const Value.absent(),
|
||||
Value<String> body = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<DateTime> updatedAt = const Value.absent(),
|
||||
Value<int> sortIndex = const Value.absent(),
|
||||
}) => NotesCompanion(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
sortIndex: sortIndex,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
Value<int> id = const Value.absent(),
|
||||
required String title,
|
||||
required String body,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required int sortIndex,
|
||||
}) => NotesCompanion.insert(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
sortIndex: sortIndex,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$NotesTableProcessedTableManager =
|
||||
ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$NotesTable,
|
||||
DbNote,
|
||||
$$NotesTableFilterComposer,
|
||||
$$NotesTableOrderingComposer,
|
||||
$$NotesTableAnnotationComposer,
|
||||
$$NotesTableCreateCompanionBuilder,
|
||||
$$NotesTableUpdateCompanionBuilder,
|
||||
(DbNote, BaseReferences<_$AppDatabase, $NotesTable, DbNote>),
|
||||
DbNote,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
$AppDatabaseManager(this._db);
|
||||
$$NotesTableTableManager get notes =>
|
||||
$$NotesTableTableManager(_db, _db.notes);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:notas/data/app_database.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
|
||||
class NoteRepository {
|
||||
NoteRepository({AppDatabase? database}) : _database = database ?? _sharedDatabase;
|
||||
|
||||
static final AppDatabase _sharedDatabase = AppDatabase();
|
||||
|
||||
final AppDatabase _database;
|
||||
|
||||
Future<List<Note>> loadNotes() async {
|
||||
return _loadNotesFromDatabase();
|
||||
}
|
||||
|
||||
Future<Note> createNote(Note note) async {
|
||||
final int id = await _database.insertNoteAtTop(
|
||||
NotesCompanion.insert(
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
createdAt: note.createdAt,
|
||||
updatedAt: note.updatedAt,
|
||||
sortIndex: 0,
|
||||
),
|
||||
);
|
||||
|
||||
return note.copyWith(id: id, index: 0);
|
||||
}
|
||||
|
||||
Future<Note> updateNote(Note note) async {
|
||||
final int noteId = note.id ?? (throw ArgumentError('Note id is required to update a note.'));
|
||||
|
||||
await _database.updateNoteRow(
|
||||
DbNote(
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
createdAt: note.createdAt,
|
||||
updatedAt: note.updatedAt,
|
||||
sortIndex: note.index,
|
||||
),
|
||||
);
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
Future<void> deleteNote(Note note) async {
|
||||
final int noteId = note.id ?? (throw ArgumentError('Note id is required to delete a note.'));
|
||||
|
||||
await _database.deleteNoteAndShift(
|
||||
id: noteId,
|
||||
removedIndex: note.index,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> moveNote(Note note, int newIndex) async {
|
||||
final int noteId = note.id ?? (throw ArgumentError('Note id is required to reorder a note.'));
|
||||
|
||||
await _database.moveNote(
|
||||
id: noteId,
|
||||
oldIndex: note.index,
|
||||
newIndex: newIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Note>> _loadNotesFromDatabase() async {
|
||||
final List<DbNote> rows = await _database.getAllNotes();
|
||||
return rows.map(_fromRow).toList();
|
||||
}
|
||||
|
||||
Note _fromRow(DbNote row) {
|
||||
return Note(
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
body: row.body,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
index: row.sortIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:notas/app.dart';
|
||||
import 'package:notas/platform/window_bootstrap.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await bootstrapWindow();
|
||||
runApp(const NotesApp());
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Model: Note
|
||||
// - Representa una nota guardada en la app.
|
||||
// - `id` viene de SQLite y sirve como identificador estable.
|
||||
// - `index` representa el orden visual dentro de la lista.
|
||||
class Note {
|
||||
const Note({
|
||||
this.id,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final String title;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int index;
|
||||
|
||||
Note copyWith({
|
||||
int? id,
|
||||
String? title,
|
||||
String? body,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
int? index,
|
||||
}) {
|
||||
return Note(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
body: body ?? this.body,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
index: index ?? this.index,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is Note && id != null && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id?.hashCode ?? Object.hash(title, body, createdAt, updatedAt, index);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'app_platform_stub.dart' if (dart.library.io) 'app_platform_io.dart';
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
bool get isIOS => Platform.isIOS;
|
||||
|
||||
bool get isLinux => Platform.isLinux;
|
||||
|
||||
bool get isMacOS => Platform.isMacOS;
|
||||
|
||||
bool get isWindows => Platform.isWindows;
|
||||
|
||||
bool get isDesktop => isLinux || isMacOS || isWindows;
|
||||
@@ -0,0 +1,11 @@
|
||||
bool get isAndroid => false;
|
||||
|
||||
bool get isIOS => false;
|
||||
|
||||
bool get isLinux => false;
|
||||
|
||||
bool get isMacOS => false;
|
||||
|
||||
bool get isWindows => false;
|
||||
|
||||
bool get isDesktop => false;
|
||||
@@ -0,0 +1 @@
|
||||
export 'window_bootstrap_stub.dart' if (dart.library.io) 'window_bootstrap_io.dart';
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:notas/platform/app_platform.dart';
|
||||
import 'package:notas/platform/window_state.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
Future<void> bootstrapWindow() async {
|
||||
if (!isDesktop) {
|
||||
return;
|
||||
}
|
||||
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
final Size initialSize =
|
||||
await WindowStateStore.instance.loadWindowSize() ?? const Size(900, 700);
|
||||
|
||||
final WindowOptions windowOptions = WindowOptions(
|
||||
size: initialSize,
|
||||
minimumSize: Size(400, 600),
|
||||
center: true,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
);
|
||||
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.setMinimumSize(const Size(400, 600));
|
||||
await windowManager.setSize(initialSize);
|
||||
await windowManager.focus();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Future<void> bootstrapWindow() async {}
|
||||
@@ -0,0 +1 @@
|
||||
export 'window_state_stub.dart' if (dart.library.io) 'window_state_io.dart';
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WindowStateStore {
|
||||
WindowStateStore._();
|
||||
|
||||
static final WindowStateStore instance = WindowStateStore._();
|
||||
|
||||
static const String _widthKey = 'window_width';
|
||||
static const String _heightKey = 'window_height';
|
||||
|
||||
Future<Size?> loadWindowSize() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final double? width = prefs.getDouble(_widthKey);
|
||||
final double? height = prefs.getDouble(_heightKey);
|
||||
|
||||
if (width == null || height == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Size(width, height);
|
||||
}
|
||||
|
||||
Future<void> saveWindowSize(Size size) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_widthKey, size.width);
|
||||
await prefs.setDouble(_heightKey, size.height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class WindowStateStore {
|
||||
WindowStateStore._();
|
||||
|
||||
static final WindowStateStore instance = WindowStateStore._();
|
||||
|
||||
Future<Size?> loadWindowSize() async {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveWindowSize(Size size) async {}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
import 'package:notas/data/note_repository.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/screens/note_editor_screen.dart';
|
||||
import 'package:notas/widgets/app_title_bar.dart';
|
||||
import 'package:notas/widgets/menu_drawer.dart';
|
||||
import 'package:notas/widgets/note_card.dart';
|
||||
import 'package:notas/widgets/search_app_bar.dart';
|
||||
|
||||
// HomeScreen: main entry showing notes in a responsive masonry grid.
|
||||
// Key behaviors implemented here:
|
||||
// - Load/save notes via `NoteRepository` (SQLite through Drift)
|
||||
// - Open `NoteEditorScreen` for create/edit
|
||||
// - Drag & drop reordering (updates `index` in the database)
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
final NoteRepository _repository = NoteRepository();
|
||||
List<Note> _notes = <Note>[];
|
||||
String _searchQuery = '';
|
||||
bool _isLoading = true;
|
||||
bool _isDragging = false;
|
||||
bool _isMenuOpen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadNotes();
|
||||
}
|
||||
|
||||
Future<void> _loadNotes() async {
|
||||
final List<Note> storedNotes = await _repository.loadNotes();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = storedNotes;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openNoteComposer() async {
|
||||
final dynamic result = await NoteEditorScreen.showDialog(context);
|
||||
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result is Note) {
|
||||
final Note createdNote = await _repository.createNote(result);
|
||||
final List<Note> updatedNotes = _normalizeNotes(<Note>[createdNote, ..._notes]);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = updatedNotes;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteNote(Note note) async {
|
||||
await _repository.deleteNote(note);
|
||||
|
||||
final List<Note> updatedNotes = _normalizeNotes(
|
||||
_notes.where((Note item) => item.id != note.id).toList(),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = updatedNotes;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _reorderNote(int oldIndex, int newIndex) async {
|
||||
if (oldIndex == newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<Note> updatedNotes = [..._notes];
|
||||
final Note movedNote = updatedNotes.removeAt(oldIndex);
|
||||
updatedNotes.insert(newIndex, movedNote);
|
||||
|
||||
await _repository.moveNote(movedNote, newIndex);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = _normalizeNotes(updatedNotes);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openNoteEditor(Note note) async {
|
||||
final dynamic result = await NoteEditorScreen.showDialog(context, note: note);
|
||||
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == 'delete') {
|
||||
await _deleteNote(note);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result is Note) {
|
||||
final int noteIndex = _notes.indexWhere((Note item) => item == note);
|
||||
if (noteIndex != -1) {
|
||||
final Note savedNote = await _repository.updateNote(result);
|
||||
final List<Note> updatedNotes = [..._notes];
|
||||
updatedNotes[noteIndex] = savedNote;
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = _normalizeNotes(updatedNotes);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Note> _normalizeNotes(List<Note> notes) {
|
||||
return notes.asMap().entries.map((MapEntry<int, Note> entry) {
|
||||
return entry.value.copyWith(index: entry.key);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Note> _getFilteredNotes() {
|
||||
if (_searchQuery.isEmpty) {
|
||||
return _notes;
|
||||
}
|
||||
|
||||
final String query = _searchQuery.toLowerCase();
|
||||
return _notes
|
||||
.where((Note note) =>
|
||||
note.title.toLowerCase().contains(query) ||
|
||||
note.body.toLowerCase().contains(query))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double width = MediaQuery.of(context).size.width;
|
||||
final int crossAxisCount = math.max((width / 250).floor().round(), 2);
|
||||
|
||||
final Widget body = _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _notes.isEmpty
|
||||
? const _EmptyState()
|
||||
: MouseRegion(
|
||||
cursor: _isDragging ? SystemMouseCursors.grabbing : SystemMouseCursors.basic,
|
||||
child: MasonryGridView.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
itemCount: _getFilteredNotes().length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final List<Note> filteredNotes = _getFilteredNotes();
|
||||
return DragTarget<int>(
|
||||
onAcceptWithDetails: (DragTargetDetails<int> details) {
|
||||
final Note targetNote = filteredNotes[index];
|
||||
final int originalTargetIndex = _notes.indexOf(targetNote);
|
||||
_reorderNote(details.data, originalTargetIndex);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double cellWidth = constraints.maxWidth;
|
||||
|
||||
return Draggable<int>(
|
||||
data: _notes.indexOf(filteredNotes[index]),
|
||||
onDragStarted: () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
});
|
||||
},
|
||||
onDragEnd: (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDragging = false;
|
||||
});
|
||||
},
|
||||
onDraggableCanceled: (_, _) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDragging = false;
|
||||
});
|
||||
},
|
||||
feedback: MouseRegion(
|
||||
cursor: SystemMouseCursors.grabbing,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
elevation: 8,
|
||||
child: SizedBox(
|
||||
width: cellWidth,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.97, end: 1.0),
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: Alignment.topLeft,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: NoteCard(note: filteredNotes[index], onTap: () {}, isDragging: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
childWhenDragging: MouseRegion(
|
||||
cursor: SystemMouseCursors.grabbing,
|
||||
child: Opacity(
|
||||
opacity: 0.3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromRGBO(24, 25, 26, 1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
filteredNotes[index].title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
filteredNotes[index].body,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
maxLines: 20,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: candidateData.isNotEmpty
|
||||
? Border.all(color: Colors.blue.shade400, width: 2)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: NoteCard(
|
||||
key: ValueKey<int>(filteredNotes[index].id ?? filteredNotes[index].index),
|
||||
note: filteredNotes[index],
|
||||
onTap: () => _openNoteEditor(filteredNotes[index]),
|
||||
isDragging: _isDragging,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const AppTitleBar(),
|
||||
SearchAppBar(
|
||||
onMenuPressed: () {
|
||||
setState(() {
|
||||
_isMenuOpen = !_isMenuOpen;
|
||||
});
|
||||
},
|
||||
onSearchChanged: (String query) {
|
||||
setState(() {
|
||||
_searchQuery = query;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
child: body,
|
||||
),
|
||||
// Dark overlay when menu is open; clicking it closes the menu.
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isMenuOpen,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: _isMenuOpen ? 0.5 : 0.0,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isMenuOpen = false;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
left: _isMenuOpen ? 0 : -280,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 280,
|
||||
child: Material(
|
||||
color: const Color.fromRGBO(24, 25, 26, 1),
|
||||
elevation: 8,
|
||||
child: MenuDrawer(
|
||||
onMenuItemTapped: (String item) {
|
||||
setState(() {
|
||||
_isMenuOpen = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _openNoteComposer,
|
||||
child: const MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.note_add_outlined, color: Colors.white54, size: 48),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Aún no hay notas',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Pulsa el botón + para crear la primera.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:notas/models/note.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 {
|
||||
const NoteEditorScreen({super.key, required this.note});
|
||||
|
||||
final Note? note;
|
||||
|
||||
@override
|
||||
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
||||
|
||||
static Future<dynamic> showDialog(BuildContext context, {Note? note}) {
|
||||
return showGeneralDialog<dynamic>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||
transitionDuration: const Duration(milliseconds: 200),
|
||||
pageBuilder: (context, animation, secondaryAnimation) {
|
||||
return NoteEditorScreen(note: note);
|
||||
},
|
||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _bodyController;
|
||||
late Note _currentNote;
|
||||
late bool _isNewNote;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isNewNote = widget.note?.id == null;
|
||||
|
||||
if (_isNewNote) {
|
||||
final DateTime now = DateTime.now();
|
||||
_currentNote = Note(
|
||||
title: '',
|
||||
body: '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
index: 0,
|
||||
);
|
||||
} else {
|
||||
_currentNote = widget.note!;
|
||||
}
|
||||
|
||||
_titleController = TextEditingController(text: _currentNote.title);
|
||||
_bodyController = TextEditingController(text: _currentNote.body);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_bodyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _closeWithoutSaving() {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _saveNote() {
|
||||
final String title = _titleController.text.trim();
|
||||
final String body = _bodyController.text.trim();
|
||||
|
||||
if (title.isEmpty && body.isEmpty) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
final Note updatedNote = _currentNote.copyWith(
|
||||
title: title.isEmpty ? 'Sin título' : title,
|
||||
body: body,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
Navigator.of(context).pop(updatedNote);
|
||||
}
|
||||
|
||||
void _deleteNote() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: const Color(0xFF303134),
|
||||
title: const Text('Eliminar nota', style: TextStyle(color: Colors.white)),
|
||||
content: const Text(
|
||||
'¿Estás seguro de que deseas eliminar esta nota?',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancelar', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop('delete');
|
||||
},
|
||||
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF202124),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header con botones y fechas
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.white12, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _closeWithoutSaving,
|
||||
icon: const Icon(Icons.close, color: Colors.white70),
|
||||
tooltip: 'Cerrar sin guardar',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Creado: ${_formatDate(_currentNote.createdAt)}',
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 12),
|
||||
),
|
||||
if (_currentNote.updatedAt != _currentNote.createdAt)
|
||||
Text(
|
||||
'Modificado: ${_formatDate(_currentNote.updatedAt)}',
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Contenido editable
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Título
|
||||
TextField(
|
||||
controller: _titleController,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Título',
|
||||
hintStyle: TextStyle(color: Colors.white30),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Cuerpo de la nota
|
||||
TextField(
|
||||
controller: _bodyController,
|
||||
maxLines: null,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
height: 1.6,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Escribe tu nota...',
|
||||
hintStyle: TextStyle(color: Colors.white30),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Footer con botones de acción
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.white12, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Botón de borrar (izquierda) - solo para notas existentes
|
||||
if (!_isNewNote)
|
||||
IconButton(
|
||||
onPressed: _deleteNote,
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
tooltip: 'Eliminar nota',
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 48), // Espacio para mantener alineación
|
||||
// Botón de guardar (derecha)
|
||||
FilledButton(
|
||||
onPressed: _saveNote,
|
||||
child: const Text('Guardar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get theme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: const Color.fromRGBO(31, 32, 33, 1),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.amber,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.amber,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'app_title_bar_stub.dart' if (dart.library.io) 'app_title_bar_io.dart';
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:notas/platform/app_platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AppTitleBar extends StatelessWidget {
|
||||
const AppTitleBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isAndroid || isIOS) {
|
||||
return const SizedBox(height: 10);
|
||||
}
|
||||
|
||||
if (isMacOS) {
|
||||
return const SizedBox(
|
||||
height: 28,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Mis Notas',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isLinux) {
|
||||
return const _KdeTitleBar();
|
||||
}
|
||||
|
||||
return const SizedBox(
|
||||
height: 40,
|
||||
child: WindowCaption(
|
||||
brightness: Brightness.dark,
|
||||
title: Text('Mis Notas', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KdeTitleBar extends StatefulWidget {
|
||||
const _KdeTitleBar();
|
||||
|
||||
@override
|
||||
State<_KdeTitleBar> createState() => _KdeTitleBarState();
|
||||
}
|
||||
|
||||
class _KdeTitleBarState extends State<_KdeTitleBar> with WindowListener {
|
||||
bool _isFullScreen = false;
|
||||
bool _isMaximized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isDesktop) {
|
||||
windowManager.addListener(this);
|
||||
_refreshWindowState();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isDesktop) {
|
||||
windowManager.removeListener(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refreshWindowState() async {
|
||||
final bool isFullScreen = await windowManager.isFullScreen();
|
||||
final bool isMaximized = await windowManager.isMaximized();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isFullScreen = isFullScreen;
|
||||
_isMaximized = isMaximized;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
setState(() {
|
||||
_isFullScreen = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
setState(() {
|
||||
_isFullScreen = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
setState(() {
|
||||
_isMaximized = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
setState(() {
|
||||
_isMaximized = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconData maximizeIcon =
|
||||
(_isFullScreen || _isMaximized) ? Icons.fullscreen_exit : Icons.fullscreen;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: Stack(
|
||||
children: [
|
||||
const DragToMoveArea(
|
||||
child: SizedBox.expand(),
|
||||
),
|
||||
const IgnorePointer(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Mis Notas',
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(255, 163, 163, 163),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_KdeButton(
|
||||
icon: Icons.minimize,
|
||||
onPressed: () => windowManager.minimize(),
|
||||
),
|
||||
_KdeButton(
|
||||
icon: maximizeIcon,
|
||||
onPressed: () async {
|
||||
if (await windowManager.isFullScreen()) {
|
||||
await windowManager.setFullScreen(false);
|
||||
} else if (await windowManager.isMaximized()) {
|
||||
await windowManager.unmaximize();
|
||||
} else {
|
||||
await windowManager.maximize();
|
||||
}
|
||||
},
|
||||
),
|
||||
_KdeButton(
|
||||
icon: Icons.close,
|
||||
isClose: true,
|
||||
onPressed: () => windowManager.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KdeButton extends StatelessWidget {
|
||||
const _KdeButton({
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.isClose = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final bool isClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onPressed,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: double.infinity,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkResponse(
|
||||
highlightShape: BoxShape.circle,
|
||||
containedInkWell: false,
|
||||
radius: 10,
|
||||
hoverColor: isClose ? Colors.red : Colors.white.withValues(alpha: 0.08),
|
||||
onTap: onPressed,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white70,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTitleBar extends StatelessWidget {
|
||||
const AppTitleBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MenuDrawer extends StatelessWidget {
|
||||
const MenuDrawer({
|
||||
super.key,
|
||||
this.onMenuItemTapped,
|
||||
});
|
||||
|
||||
final ValueChanged<String>? onMenuItemTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(31, 32, 33, 1),
|
||||
border: Border(
|
||||
right: BorderSide(color: Colors.white12, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_MenuItemTile(
|
||||
icon: Icons.note,
|
||||
label: 'Todas mis notas',
|
||||
onTap: () => onMenuItemTapped?.call('all_notes'),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: const [
|
||||
SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.white12, height: 16),
|
||||
_MenuItemTile(
|
||||
icon: Icons.settings,
|
||||
label: 'Configuración',
|
||||
onTap: () => onMenuItemTapped?.call('settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItemTile extends StatelessWidget {
|
||||
const _MenuItemTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.white70),
|
||||
title: Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
onTap: onTap,
|
||||
hoverColor: Colors.white.withValues(alpha: 0.1),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:notas/models/note.dart';
|
||||
|
||||
// Small presentational widget for a note inside the grid.
|
||||
// Keep this widget lightweight and layout-agnostic: it should not force
|
||||
// width/height constraints (so it works inside different parent layouts
|
||||
// like MasonryGridView or Draggable feedback). Visual styling only.
|
||||
|
||||
class NoteCard extends StatefulWidget {
|
||||
const NoteCard({super.key, required this.note, this.onTap, this.isDragging = false});
|
||||
|
||||
final Note note;
|
||||
final VoidCallback? onTap;
|
||||
final bool isDragging;
|
||||
|
||||
@override
|
||||
State<NoteCard> createState() => _NoteCardState();
|
||||
}
|
||||
|
||||
class _NoteCardState extends State<NoteCard> {
|
||||
bool _isPressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool showGrabbing = widget.isDragging || _isPressed;
|
||||
|
||||
return MouseRegion(
|
||||
cursor: showGrabbing ? SystemMouseCursors.grabbing : SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
onTapDown: widget.onTap == null
|
||||
? null
|
||||
: (_) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
},
|
||||
onTapUp: widget.onTap == null
|
||||
? null
|
||||
: (_) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: widget.onTap == null
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromRGBO(24, 25, 26, 1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24, width: 1),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// Estimate whether the body will exceed 20 lines without always
|
||||
// running the expensive TextPainter layout. This heuristic counts
|
||||
// newline characters and estimates wrapped lines based on an
|
||||
// average characters-per-line to handle many short lines well.
|
||||
final List<String> rawLines = widget.note.body.split('\n');
|
||||
const int avgCharsPerLine = 40;
|
||||
int estimatedLines = 0;
|
||||
for (final String line in rawLines) {
|
||||
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
|
||||
}
|
||||
|
||||
final bool needsPreciseMeasurement = estimatedLines > 20;
|
||||
final bool isBodyTruncated;
|
||||
|
||||
if (needsPreciseMeasurement) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.note.body,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
maxLines: 20,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: constraints.maxWidth);
|
||||
|
||||
isBodyTruncated = textPainter.didExceedMaxLines;
|
||||
} else {
|
||||
isBodyTruncated = false;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.note.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.note.body,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
maxLines: 20,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
if (isBodyTruncated) ...[
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'...',
|
||||
style: TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchAppBar extends StatefulWidget {
|
||||
const SearchAppBar({
|
||||
super.key,
|
||||
this.onMenuPressed,
|
||||
this.onSearchChanged,
|
||||
this.searchHint = 'Buscar notas...',
|
||||
});
|
||||
|
||||
final VoidCallback? onMenuPressed;
|
||||
final ValueChanged<String>? onSearchChanged;
|
||||
final String searchHint;
|
||||
|
||||
@override
|
||||
State<SearchAppBar> createState() => _SearchAppBarState();
|
||||
}
|
||||
|
||||
class _SearchAppBarState extends State<SearchAppBar> {
|
||||
late TextEditingController _searchController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromRGBO(31, 32, 33, 1),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 7, bottom: 7),
|
||||
child: Row(
|
||||
children: [
|
||||
// Menu button (fixed on left)
|
||||
IconButton(
|
||||
onPressed: widget.onMenuPressed,
|
||||
icon: const Icon(Icons.menu, color: Colors.white70, size: 20),
|
||||
tooltip: 'Menú',
|
||||
splashRadius: 18,
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Search input (centered with max width)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: widget.onSearchChanged,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
cursorColor: Colors.white70,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.searchHint,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Colors.white70,
|
||||
size: 18,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: Colors.white70,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
widget.onSearchChanged?.call('');
|
||||
},
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user