feat: aggiungi guida utente e icona launcher
Guida utente: - Pulsante info nell'AppBar principale - InfoBottomSheet: icona app, versione, autore, licenza, link alla guida - GuidePage: tre tab (Pasti / Converti / Spesa) con card e step numerati, portata dalla DocsPanel Vue originale AppBar globale in app.dart con titolo dinamico per tab corrente. Icona launcher: - flutter_launcher_icons eseguito in build.sh prima di flutter build apk (sia debug che release) per applicare assets/icon-only.png all'APK. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ if [[ "$RELEASE" == "true" ]]; then
|
||||
-v "${PROJECT_ROOT}:/workspace" \
|
||||
-v "${HOME}/.gradle:/root/.gradle" \
|
||||
biteplan-build \
|
||||
bash -c "cd /workspace && flutter pub get && flutter build apk --release"
|
||||
bash -c "cd /workspace && flutter pub get && flutter pub run flutter_launcher_icons && flutter build apk --release"
|
||||
|
||||
echo "→ Zipalign..."
|
||||
docker run --rm \
|
||||
@@ -67,7 +67,7 @@ else
|
||||
-v "${PROJECT_ROOT}:/workspace" \
|
||||
-v "${HOME}/.gradle:/root/.gradle" \
|
||||
biteplan-build \
|
||||
bash -c "cd /workspace && flutter pub get && flutter build apk --debug"
|
||||
bash -c "cd /workspace && flutter pub get && flutter pub run flutter_launcher_icons && flutter build apk --debug"
|
||||
|
||||
cp "${PROJECT_ROOT}/build/app/outputs/flutter-apk/app-debug.apk" \
|
||||
"${PROJECT_ROOT}/dist/biteplan-debug.apk"
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'core/theme/app_theme.dart';
|
||||
import 'features/meal_planner/presentation/pages/meal_planner_page.dart';
|
||||
import 'features/converter/presentation/pages/converter_page.dart';
|
||||
import 'features/shopping_list/presentation/pages/shopping_list_page.dart';
|
||||
import 'features/guide/presentation/widgets/info_bottom_sheet.dart';
|
||||
|
||||
class BitePlanApp extends StatelessWidget {
|
||||
const BitePlanApp({super.key});
|
||||
@@ -29,6 +30,8 @@ class _MainScaffoldState extends State<_MainScaffold> {
|
||||
int _currentIndex = 0;
|
||||
late final List<Widget> _pages;
|
||||
|
||||
static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -44,6 +47,16 @@ class _MainScaffoldState extends State<_MainScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_titles[_currentIndex]),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
tooltip: 'Informazioni',
|
||||
onPressed: () => showInfoBottomSheet(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _pages,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
const String kAppVersion = '2.0.0';
|
||||
const String kAppAuthor = 'Davide Grilli';
|
||||
const String kAppLicense = 'EUPL v1.2';
|
||||
|
||||
const List<String> kDayIds = [
|
||||
'lunedi',
|
||||
'martedi',
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
|
||||
class GuidePage extends StatelessWidget {
|
||||
const GuidePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Guida'),
|
||||
bottom: TabBar(
|
||||
tabs: const [
|
||||
Tab(text: 'Pasti'),
|
||||
Tab(text: 'Converti'),
|
||||
Tab(text: 'Spesa'),
|
||||
],
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
indicatorColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
_PastiTab(),
|
||||
_ConvertiTab(),
|
||||
_SpesaTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pasti ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class _PastiTab extends StatelessWidget {
|
||||
const _PastiTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: const [
|
||||
_DocCard(
|
||||
title: 'Aggiungere un alimento',
|
||||
child: _Steps(steps: [
|
||||
'Tocca il giorno per espandere la card',
|
||||
'Scegli il pasto: Colazione, Pranzo o Cena',
|
||||
'Scrivi il nome nel campo di testo',
|
||||
'Premi + o Invio per aggiungerlo',
|
||||
]),
|
||||
),
|
||||
_TipCard(
|
||||
text:
|
||||
'Premi "Genera lista della spesa" in fondo alla pagina per importare automaticamente tutti gli alimenti della settimana, senza duplicati.',
|
||||
),
|
||||
_DocCard(
|
||||
title: 'Rimuovere un alimento',
|
||||
child: _Body(
|
||||
text:
|
||||
'Tocca il pulsante × a destra dell\'elemento per eliminarlo.'),
|
||||
),
|
||||
_DocCard(
|
||||
title: 'Salvataggio automatico',
|
||||
child: _Body(
|
||||
text:
|
||||
'I dati vengono salvati automaticamente sul dispositivo. Non serve premere nessun tasto.'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converti ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _ConvertiTab extends StatelessWidget {
|
||||
const _ConvertiTab();
|
||||
|
||||
static const _categories = [
|
||||
('Cereali e pasta', 'Riso (4 varietà), pasta, farro, orzo, quinoa, cous cous'),
|
||||
('Legumi secchi', 'Ceci, fagioli, lenticchie'),
|
||||
('Verdure', 'Carote, zucchine, patate, spinaci, broccoli, asparagi e altri'),
|
||||
('Carni', 'Pollo petto, tacchino fesa, hamburger, vitello'),
|
||||
('Pesce', 'Tonno, merluzzo, spigola, sogliola'),
|
||||
('Uova', 'Uovo al tegamino, frittata'),
|
||||
];
|
||||
|
||||
static const _methods = ['Bollitura', 'Padella', 'Forno', 'Friggitrice ad aria'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const _DocCard(
|
||||
title: 'Come usarlo',
|
||||
child: _Steps(steps: [
|
||||
'Cerca l\'alimento nel campo (es. pollo, riso)',
|
||||
'Seleziona il metodo di cottura dall\'elenco',
|
||||
'Inserisci il peso in grammi',
|
||||
'Il risultato appare in tempo reale',
|
||||
'Premi il pulsante "Cambia" per invertire crudo / cotto',
|
||||
]),
|
||||
),
|
||||
_DocCard(
|
||||
title: 'Alimenti disponibili',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: scheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: _categories.indexed.map((entry) {
|
||||
final (i, cat) = entry;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: i < _categories.length - 1
|
||||
? Border(
|
||||
bottom: BorderSide(color: scheme.outlineVariant))
|
||||
: null,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 9),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(cat.$1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(cat.$2,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: scheme.onSurfaceVariant)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: _methods
|
||||
.map((m) => Chip(
|
||||
label: Text(m,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spesa ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class _SpesaTab extends StatelessWidget {
|
||||
const _SpesaTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: const [
|
||||
_DocCard(
|
||||
title: 'Aggiungere un elemento',
|
||||
child: _Body(
|
||||
text: 'Scrivi il nome nel campo in alto e premi + o Invio.'),
|
||||
),
|
||||
_TipCard(
|
||||
text:
|
||||
'Vai alla tab Pasti e premi "Genera lista della spesa" per importare automaticamente gli alimenti pianificati, senza duplicati.',
|
||||
),
|
||||
_DocCard(
|
||||
title: 'Spuntare un elemento',
|
||||
child: _Body(
|
||||
text:
|
||||
'Tocca la casella a sinistra. Gli elementi completati vengono spostati in una sezione separata con testo barrato.'),
|
||||
),
|
||||
_DocCard(
|
||||
title: 'Rimuovere / svuotare',
|
||||
child: _Body(
|
||||
text:
|
||||
'Tocca × per rimuovere un singolo elemento, oppure "Svuota lista" in fondo per eliminare tutto (richiede conferma).'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Componenti condivisi ──────────────────────────────────────────────────────
|
||||
|
||||
class _DocCard extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
const _DocCard({required this.title, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.7,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
const SizedBox(height: 10),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TipCard extends StatelessWidget {
|
||||
final String text;
|
||||
const _TipCard({required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primaryContainer.withValues(alpha: 0.4),
|
||||
border: Border(left: BorderSide(color: scheme.primary, width: 3)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 14, color: scheme.primary),
|
||||
const SizedBox(width: 5),
|
||||
Text('SUGGERIMENTO',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.7,
|
||||
color: scheme.primary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(text, style: const TextStyle(fontSize: 14, height: 1.5)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Steps extends StatelessWidget {
|
||||
final List<String> steps;
|
||||
const _Steps({required this.steps});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: steps.indexed.map((entry) {
|
||||
final (i, step) = entry;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text('${i + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(step,
|
||||
style:
|
||||
const TextStyle(fontSize: 14, height: 1.5)),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Body extends StatelessWidget {
|
||||
final String text;
|
||||
const _Body({required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(text, style: const TextStyle(fontSize: 14, height: 1.5));
|
||||
}
|
||||
}
|
||||
|
||||
// ignore footer version — accessed via kAppVersion constant
|
||||
String get _footerText => 'BitePlan · v$kAppVersion · $kAppAuthor';
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../pages/guide_page.dart';
|
||||
|
||||
void showInfoBottomSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) => const _InfoSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
class _InfoSheet extends StatelessWidget {
|
||||
const _InfoSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// handle
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// icona + nome + versione
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
'assets/icon-only.png',
|
||||
width: 52,
|
||||
height: 52,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('BitePlan',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text('Versione $kAppVersion',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// righe info
|
||||
_InfoCard(children: [
|
||||
_InfoRow(label: 'Autore', value: kAppAuthor),
|
||||
_InfoRow(label: 'Licenza', value: kAppLicense),
|
||||
_InfoRowButton(
|
||||
label: 'Guida',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const GuidePage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
const _InfoCard({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: scheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(children: children),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(value,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: scheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRowButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
const _InfoRowButton({required this.label, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||
Row(
|
||||
children: [
|
||||
Text('Apri',
|
||||
style: TextStyle(
|
||||
color: scheme.primary, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.chevron_right, size: 18, color: scheme.primary),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user