288 lines
9.4 KiB
Dart
288 lines
9.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
import 'package:flux/core/routes/routes.dart';
|
|
import 'package:flux/features/notes/blocs/notes_cubit.dart';
|
|
import 'package:flux/features/notes/data/notes_repository.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
|
import 'package:flux/features/notes/models/note_model.dart';
|
|
|
|
class NotesListScreen extends StatefulWidget {
|
|
const NotesListScreen({super.key});
|
|
|
|
@override
|
|
State<NotesListScreen> createState() => _NotesListScreenState();
|
|
}
|
|
|
|
class _NotesListScreenState extends State<NotesListScreen> {
|
|
late final AppLifecycleListener _lifecycleListener;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Inizializziamo il sensore del ciclo di vita
|
|
_lifecycleListener = AppLifecycleListener(
|
|
onPause: () {
|
|
// L'utente ha messo l'app in background (es. per rispondere a un messaggio su WhatsApp)
|
|
// Chiudiamo i rubinetti per non sprecare risorse e prevenire crash
|
|
context.read<NotesCubit>().stopListening();
|
|
debugPrint('App in background: Stream sospesi.');
|
|
},
|
|
onResume: () {
|
|
// L'utente è tornato sull'app!
|
|
// Riappriamo i rubinetti, Supabase ricreerà una connessione fresca
|
|
context.read<NotesCubit>().startListening();
|
|
debugPrint('App in foreground: Stream riattivati.');
|
|
},
|
|
);
|
|
|
|
// Facciamo partire gli stream la primissima volta che la schermata si carica
|
|
context.read<NotesCubit>().startListening();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Pulizia fondamentale
|
|
_lifecycleListener.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// Logica Ninja: Crea la nota vuota, prende l'ID, e apre il form
|
|
Future<void> _createNewNoteAndNavigate(BuildContext context) async {
|
|
final sessionState = context.read<SessionCubit>().state;
|
|
final companyId = sessionState.company!.id!;
|
|
final currentStaffId = sessionState.currentStaffMember!.id!;
|
|
|
|
// 1. Creiamo la nota vuota
|
|
final emptyNote = NoteModel.empty(
|
|
createdBy: currentStaffId,
|
|
companyId: companyId,
|
|
).copyWith(color: '#FFF59D');
|
|
|
|
// Mostriamo un loading veloce se serve (opzionale)
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Creazione nota in corso...'),
|
|
duration: Duration(milliseconds: 500),
|
|
),
|
|
);
|
|
|
|
try {
|
|
// 2. Scriviamo su DB per avere l'ID
|
|
final noteId = await GetIt.I.get<NotesRepository>().saveNote(emptyNote);
|
|
if (context.mounted) {
|
|
// 3. Spingiamo l'utente nel form con la nota già provvista di ID!
|
|
context.pushNamed(
|
|
Routes.noteForm,
|
|
pathParameters: ({'id': noteId}),
|
|
extra: emptyNote.copyWith(id: noteId),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Errore: Impossibile creare la nota. $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor:
|
|
Colors.grey.shade900, // Sfondo neutro per far risaltare i post-it
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Bacheca Note',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
centerTitle: false,
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => _createNewNoteAndNavigate(context),
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Nuova Nota'),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
body: BlocBuilder<NotesCubit, NotesState>(
|
|
builder: (context, state) {
|
|
if (state.status == NotesStatus.loading && state.notes.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (state.status == NotesStatus.failure) {
|
|
return Center(
|
|
child: Text(
|
|
'Errore nel caricamento: ${state.errorMessage}',
|
|
style: const TextStyle(color: Colors.red),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state.notes.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.sticky_note_2_outlined,
|
|
size: 64,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Nessuna nota presente.',
|
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 18),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text('Clicca su "Nuova Nota" per iniziare.'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Ordiniamo le note: prima le pinnate, poi le altre (se non le ordina già il DB)
|
|
final sortedNotes = List<NoteModel>.from(state.notes)
|
|
..sort((a, b) {
|
|
if (a.isPinned && !b.isPinned) return -1;
|
|
if (!a.isPinned && b.isPinned) return 1;
|
|
return 0; // Se vuoi puoi aggiungere l'ordinamento per data qui
|
|
});
|
|
|
|
return _buildMasonryGrid(context, sortedNotes);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMasonryGrid(BuildContext context, List<NoteModel> notes) {
|
|
// Calcoliamo quante colonne mostrare in base alla larghezza dello schermo
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
int crossAxisCount = 2; // Mobile
|
|
if (screenWidth > 600) crossAxisCount = 3; // Tablet
|
|
if (screenWidth > 900) crossAxisCount = 4; // Desktop piccolo
|
|
if (screenWidth > 1200) crossAxisCount = 5; // Desktop grande
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: MasonryGridView.count(
|
|
crossAxisCount: crossAxisCount,
|
|
mainAxisSpacing: 12,
|
|
crossAxisSpacing: 12,
|
|
itemCount: notes.length,
|
|
padding: const EdgeInsets.only(
|
|
bottom: 80,
|
|
top: 8,
|
|
), // Spazio per non coprire col FAB
|
|
itemBuilder: (context, index) {
|
|
final note = notes[index];
|
|
return _buildNoteCard(context, note);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNoteCard(BuildContext context, NoteModel note) {
|
|
final noteColor = Color(
|
|
int.parse('FF${note.color.replaceAll('#', '')}', radix: 16),
|
|
);
|
|
|
|
return GestureDetector(
|
|
onTap: () => context.pushNamed(
|
|
Routes.noteForm,
|
|
pathParameters: {'id': note.id!},
|
|
extra: note,
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: noteColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(2, 2),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.black.withValues(alpha: 0.05)),
|
|
),
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min, // Fondamentale per il Masonry!
|
|
children: [
|
|
if (note.title != null && note.title!.isNotEmpty) ...[
|
|
Text(
|
|
note.title!,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
if (note.content != null && note.content!.isNotEmpty) ...[
|
|
Text(
|
|
note.content!,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.black87,
|
|
height: 1.3,
|
|
),
|
|
// Limitiamo le righe in bacheca per non avere post-it lunghi 3 metri
|
|
maxLines: 8,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
|
|
// Footer con icone di stato (Pin, Condivisione, Allegati)
|
|
_buildCardFooter(note),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCardFooter(NoteModel note) {
|
|
final hasStatusIcons =
|
|
note.isPinned || note.isSharedAll || note.collaboratorIds.isNotEmpty;
|
|
|
|
if (!hasStatusIcons) return const SizedBox.shrink();
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 12.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
if (note.isSharedAll || note.collaboratorIds.isNotEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.only(right: 8.0),
|
|
child: Icon(
|
|
Icons.people_alt_outlined,
|
|
size: 16,
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
|
|
const Padding(
|
|
padding: EdgeInsets.only(right: 8.0),
|
|
child: Icon(Icons.attachment, size: 16, color: Colors.black54),
|
|
),
|
|
if (note.isPinned)
|
|
const Icon(Icons.push_pin, size: 16, color: Colors.black54),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|