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,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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user