Compare commits

...

4 Commits

Author SHA1 Message Date
solocla a7e4c5f083 fixed things and icon 2026-08-22 16:09:05 +02:00
solocla e87292dda7 checkpoint prima del rename package 2026-08-22 14:59:44 +02:00
solocla 770c89ca3f fixed pages 2026-08-22 08:26:27 +02:00
solocla 64eade33f3 fixed bugs 2026-08-21 13:11:55 +02:00
70 changed files with 1514 additions and 620 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ plugins {
}
android {
namespace = "com.example.yogibook_app"
namespace = "it.yogasoul.yogibook"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +21,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.yogibook_app"
applicationId = "it.yogasoul.yogibook"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
+2 -1
View File
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:label="yogibook_app"
android:label="YogiBook"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -1,5 +0,0 @@
package com.example.yogibook_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,5 @@
package it.yogasoul.yogibook
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+8 -8
View File
@@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 856 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+2
View File
@@ -28,6 +28,8 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSFaceIDUsageDescription</key>
<string>Usiamo Face ID per accedere rapidamente e in sicurezza al tuo account YogiBook.</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
+12
View File
@@ -0,0 +1,12 @@
import '../models/school.dart';
/// Scuola unica dell'app (YogaSoul).
/// Il DB non ha il concetto di "scuola": questo oggetto serve solo alle
/// pagine che mostrano nome/indirizzo. Definito qui una volta sola così
/// login e splash (e chiunque altro) usano lo stesso dato.
const School kYogaSoulSchool = School(
id: 1,
name: 'YogaSoul',
logo: null,
addressFull: 'via Valassina 62/B Seregno - Sala Contesto Yoga',
);
+2 -2
View File
@@ -3,7 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'screens/login_page.dart';
import 'screens/splash_page.dart';
import 'state/app_state.dart';
Future<void> main() async {
@@ -32,7 +32,7 @@ class MyApp extends StatelessWidget {
supportedLocales: const [Locale('it', 'IT'), Locale('en', 'US')],
locale: const Locale('it', 'IT'),
home: const LoginPage(),
home: const SplashPage(),
);
}
}
+6 -1
View File
@@ -4,7 +4,12 @@ class School {
final String? logo;
final String? addressFull;
School({required this.id, required this.name, this.logo, this.addressFull});
const School({
required this.id,
required this.name,
this.logo,
this.addressFull,
});
factory School.fromJson(Map<String, dynamic> j) {
return School(
+7
View File
@@ -5,6 +5,7 @@ import 'package:image_picker/image_picker.dart';
import '../models/school.dart';
import '../services/vanguard_api.dart';
import '../widgets/yogibook_background.dart';
import '../widgets/app_bottom_nav.dart';
class AccountPage extends StatefulWidget {
final String token;
@@ -238,6 +239,12 @@ class _AccountPageState extends State<AccountPage> {
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
),
bottomNavigationBar: AppBottomNav(
currentIndex: 2,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
body: YogibookBackground(
child: SafeArea(
top: false,
+19 -46
View File
@@ -35,8 +35,6 @@ class _HomePageState extends State<HomePage> {
static const Color kBg = Color(0xFFF6F6FB);
static const Color kGreen = Color(0xFF10B981);
int bottomIndex = 0; // Home
late final String zenQuote;
@override
@@ -224,44 +222,6 @@ class _HomePageState extends State<HomePage> {
);
}
void _handleBottomNav(int i) {
setState(() => bottomIndex = i);
if (i == 0) return;
if (i == 1) {
_goLessons();
return;
}
if (i == 2) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AccountPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
).then((_) {
if (mounted) setState(() => bottomIndex = 0);
});
return;
}
if (i == 3) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -299,8 +259,10 @@ class _HomePageState extends State<HomePage> {
),
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
currentIndex: 0,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
body: YogibookBackground(
@@ -378,16 +340,27 @@ class _HomePageState extends State<HomePage> {
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OrdersDetailPage(token: widget.token),
builder: (_) => OrdersDetailPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
),
),
_HomeTile(
icon: Icons.person_rounded,
title: 'Account',
subtitle: 'Profilo e impostazioni (TODO)',
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: Account')),
subtitle: 'Profilo e impostazioni',
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AccountPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
),
),
+124 -116
View File
@@ -8,6 +8,7 @@ import 'home_page.dart';
import 'meditation_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import 'package:url_launcher/url_launcher.dart';
import '../widgets/yogibook_background.dart';
import 'reschedule_page.dart';
import 'orders_page.dart';
@@ -37,8 +38,6 @@ class _LessonsPageState extends State<LessonsPage> {
LessonsSummary? summary;
List<Lesson> lessons = [];
int bottomIndex = 1;
@override
void initState() {
super.initState();
@@ -104,48 +103,6 @@ class _LessonsPageState extends State<LessonsPage> {
String get _name => (widget.userFirstName ?? '').trim();
String get _avatarLetter => _name.isNotEmpty ? _name[0].toUpperCase() : 'U';
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
return;
}
if (i == 1) return; // sei già su Lezioni
if (i == 2) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: vai ad Account')));
return;
}
if (i == 3) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -195,8 +152,10 @@ class _LessonsPageState extends State<LessonsPage> {
],
),
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
currentIndex: 1,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
body: YogibookBackground(
@@ -221,40 +180,83 @@ class _LessonsPageState extends State<LessonsPage> {
),
),
// Bottone "Programma lezioni" (solo se ci sono ticket residui)
if (summary != null && summary!.toSchedule > 0)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.add, color: Colors.white),
label: Text(
'Programma lezioni (${summary!.toSchedule} disponibili)',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OrdersPage(token: widget.token),
// Bottoni azione: Programma (condizionale) + Acquista (sempre)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Row(
children: [
if (summary != null && summary!.toSchedule > 0) ...[
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4F46E5),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
);
_load();
},
icon: const Icon(Icons.add, color: Colors.white),
label: Text(
'Programma (${summary!.toSchedule})',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OrdersPage(token: widget.token),
),
);
_load();
},
),
),
const SizedBox(width: 8),
],
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF59E0B),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(
Icons.shopping_bag_outlined,
color: Colors.white,
),
label: const Text(
'Acquista pacchetti',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
onPressed: () async {
final uri = Uri.parse(
'https://yogasoul.it/abbonamenti-prezzi-yoga-seregno/',
);
if (!await launchUrl(
uri,
mode: LaunchMode.externalApplication,
)) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossibile aprire il link'),
),
);
}
},
),
),
),
],
),
),
Expanded(
child: loading
@@ -388,47 +390,53 @@ class _SummaryStrip extends StatelessWidget {
_SummaryItem('Perse', summary.lost, const Color(0xFFFBC7C4)),
];
return SizedBox(
height: 76,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (_, i) {
final it = items[i];
return Container(
width: 96,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: it.color,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${it.value}',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
// 3 colonne, spaziatura tra i box
const spacing = 8.0;
const columns = 3;
return LayoutBuilder(
builder: (context, constraints) {
final boxWidth =
(constraints.maxWidth - spacing * (columns - 1)) / columns;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: items.map((it) {
return Container(
width: boxWidth,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 6),
decoration: BoxDecoration(
color: it.color,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${it.value}',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 2),
Text(
it.label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
const SizedBox(height: 2),
Text(
it.label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
],
),
);
}).toList(),
);
},
);
}
}
+43 -32
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import '../services/vanguard_api.dart';
import '../services/auth_storage.dart';
import '../models/school.dart';
import '../config/app_school.dart';
import 'home_page.dart';
class LoginPage extends StatefulWidget {
@@ -18,15 +20,7 @@ class _LoginPageState extends State<LoginPage> {
// ---- UI state
bool loading = false;
String error = '';
// Scuola unica (YogaSoul). Il DB non ha il concetto di scuola:
// questo oggetto serve solo a soddisfare le pagine che mostrano nome/indirizzo.
static final School _yogaSoulSchool = School(
id: 1,
name: 'YogaSoul',
logo: null,
addressFull: 'via Valassina 62/B Seregno - Sala Contesto Yoga',
);
bool _obscurePassword = true;
@override
void dispose() {
@@ -47,6 +41,9 @@ class _LoginPageState extends State<LoginPage> {
password: passwordController.text.trim(),
);
// Se "ricordami" è attivo, salva il nuovo token in cassaforte.
await AuthStorage.instance.saveTokenIfRemember(token);
if (!mounted) return;
// Bypass SelectSchoolPage: scuola unica, si va dritti alla Home.
@@ -55,13 +52,27 @@ class _LoginPageState extends State<LoginPage> {
MaterialPageRoute(
builder: (_) => HomePage(
token: token,
school: _yogaSoulSchool,
school: kYogaSoulSchool,
userFirstName: null,
),
),
);
} catch (e) {
setState(() => error = 'Errore login: $e');
final msg = e.toString();
String friendly;
if (msg.contains('401') || msg.contains('403')) {
friendly = 'Email o password non corretti.';
} else if (msg.contains('SocketException') ||
msg.contains('Failed host lookup') ||
msg.contains('Connection')) {
friendly = 'Nessuna connessione. Controlla la rete e riprova.';
} else if (msg.contains('timed out') ||
msg.contains('TimeoutException')) {
friendly = 'Il server non risponde. Riprova tra poco.';
} else {
friendly = 'Accesso non riuscito. Riprova.';
}
setState(() => error = friendly);
} finally {
if (mounted) setState(() => loading = false);
}
@@ -175,44 +186,44 @@ class _LoginPageState extends State<LoginPage> {
controller: emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
decoration: const InputDecoration(
labelText: 'Email / Username',
border: const OutlineInputBorder(),
enabled: !loading,
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: passwordController,
obscureText: true,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onSubmitted: (_) => loading ? null : doLogin(),
decoration: InputDecoration(
labelText: 'Password',
border: const OutlineInputBorder(),
enabled: !loading,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: loading
? null
: () => setState(
() => _obscurePassword = !_obscurePassword,
),
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: loading ? null : forgotPassword,
child: const Text('Recupero password'),
),
TextButton(
onPressed: () =>
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Registrazione: dopo.'),
),
),
child: const Text('Registrati'),
),
],
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: loading ? null : forgotPassword,
child: const Text('Recupero password'),
),
),
if (error.isNotEmpty) ...[
+8 -84
View File
@@ -50,9 +50,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
DateTime? expiryDate;
bool uploading = false;
// ✅ questa pagina è "sezione account"
int bottomIndex = 2;
@override
void initState() {
super.initState();
@@ -224,19 +221,10 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
}
Future<void> _openFileUrl(String url) async {
// url tipico: "/userarea/certificate/xxx"
// fix plural errato se presente
var fixed = url.replaceFirst(
'/userarea/certificates/',
'/userarea/certificate/',
);
// se non ha /public davanti, lo aggiungiamo
if (!fixed.startsWith('/public/')) {
fixed = '/public${fixed.startsWith('/') ? '' : '/'}$fixed';
}
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$fixed';
// L'API restituisce già "/user/document/<file>".
// Costruiamo l'URL assoluto senza manipolazioni.
final path = url.startsWith('/') ? url : '/$url';
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$path';
final uri = Uri.parse(abs);
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
@@ -286,54 +274,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf');
Future<void> _goTo(Widget page) async {
if (!mounted) return;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page));
}
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
_goTo(
HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 1) {
_goTo(
LessonsPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 2) {
// siamo già in "area account", non facciamo nulla (o TODO)
return;
}
if (i == 3) {
_goTo(
MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -359,10 +299,11 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
],
),
// ✅ Bottom nav standard (come MeditationPage)
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
currentIndex: 2,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
body: YogibookBackground(
@@ -438,7 +379,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
final filename = (c['filename'] ?? '').toString();
final uploadedAt = (c['uploaded_at'] ?? '').toString();
final expiry = (c['expiry_date'] ?? '').toString();
final notes = (c['notes'] ?? '').toString();
final fileUrl = (c['file_url'] ?? '').toString();
return Card(
@@ -539,11 +479,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
],
),
if (notes.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Text(notes, style: const TextStyle(fontSize: 12)),
],
const SizedBox(height: 10),
Row(
children: [
@@ -677,17 +612,6 @@ class _UploadCard extends StatelessWidget {
label: Text(expiryLabel),
),
const SizedBox(height: 10),
TextField(
controller: notesController,
minLines: 2,
maxLines: 4,
decoration: const InputDecoration(
labelText: 'Note (opzionale)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
SizedBox(
+9 -63
View File
@@ -64,8 +64,6 @@ class _MeditationPageState extends State<MeditationPage>
late final AnimationController _pulseCtrl;
late final AnimationController _squareCtrl;
int bottomIndex = 3; // Meditazione
@override
void initState() {
super.initState();
@@ -256,42 +254,6 @@ class _MeditationPageState extends State<MeditationPage>
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page));
}
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
_goTo(
HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 1) {
_goTo(
LessonsPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 2) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: vai ad Account')));
return;
}
// i == 3 => già qui
}
@override
Widget build(BuildContext context) {
final bubbleScale = Tween<double>(
@@ -340,31 +302,15 @@ class _MeditationPageState extends State<MeditationPage>
),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
selectedItemColor: kGreen,
unselectedItemColor: Colors.black54,
currentIndex: bottomIndex,
onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
bottomNavigationBar: AppBottomNav(
currentIndex: 3,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
onBeforeLeave: () async {
_pause();
await _stopMusic();
},
),
body: YogibookBackground(
child: SafeArea(
+147 -17
View File
@@ -1,15 +1,24 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/lesson.dart';
import '../models/school.dart';
import '../services/lessons_api.dart';
import '../widgets/yogibook_background.dart';
import '../widgets/app_bottom_nav.dart';
/// Schermata "I miei ordini": lista ordini con conteggi.
/// Toccando un ordine si apre il dettaglio (box + lezioni).
class OrdersDetailPage extends StatefulWidget {
final String token;
final School school;
final String? userFirstName;
const OrdersDetailPage({super.key, required this.token});
const OrdersDetailPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override
State<OrdersDetailPage> createState() => _OrdersDetailPageState();
@@ -63,6 +72,12 @@ class _OrdersDetailPageState extends State<OrdersDetailPage> {
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
),
bottomNavigationBar: AppBottomNav(
currentIndex: 1,
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
body: YogibookBackground(
child: SafeArea(
top: false,
@@ -77,23 +92,90 @@ class _OrdersDetailPageState extends State<OrdersDetailPage> {
itemCount: orders.length,
itemBuilder: (_, i) {
final o = orders[i];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
o.serviceName.isEmpty
? 'Ordine #${o.orderId}'
: o.serviceName,
style: const TextStyle(fontWeight: FontWeight.w800),
),
subtitle: Text(
'Ordine #${o.orderId}${o.tickets} lezioni\n'
'Da programmare: ${o.toSchedule} • Scadenza: ${_fmtDate(o.expireOn)}'
'${o.isExpired ? " (scaduto)" : ""}',
),
isThreeLine: true,
trailing: const Icon(Icons.chevron_right),
return Container(
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
],
),
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => _openDetail(o),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
o.serviceName.isEmpty
? 'Ordine #${o.orderId}'
: o.serviceName,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
),
),
const SizedBox(height: 6),
Text(
'Ordine #${o.orderId}${o.tickets} lezioni',
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'Da programmare: ${o.toSchedule}',
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
// Riga riprogrammazioni + scadenza (chip)
Wrap(
spacing: 8,
runSpacing: 6,
children: [
_MiniChip(
icon: Icons.autorenew,
label:
'Riprog. ${o.reprogrammed}/${o.maxReschedule}',
color: const Color(0xFF10B981),
),
_MiniChip(
icon: Icons.event_busy,
label:
'Scad. ${_fmtDate(o.expireOn)}',
color: o.isExpired
? const Color(0xFFDC3545)
: const Color(0xFFF59E0B),
),
],
),
],
),
),
const SizedBox(width: 6),
const Icon(
Icons.chevron_right,
color: Colors.black38,
),
],
),
),
),
);
},
@@ -227,6 +309,15 @@ class _OrderDetailSheet extends StatelessWidget {
'${order.isExpired ? " (scaduto)" : ""}',
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
const SizedBox(height: 4),
Text(
'Riprogrammazioni usate: ${order.reprogrammed} / ${order.maxReschedule}',
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const Divider(height: 24),
const Text(
@@ -320,3 +411,42 @@ class _OrderDetailSheet extends StatelessWidget {
);
}
}
class _MiniChip extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
const _MiniChip({
required this.icon,
required this.label,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color.withValues(alpha: 0.5), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: color,
),
),
],
),
);
}
}
+138 -31
View File
@@ -173,37 +173,10 @@ class _ReschedulePageState extends State<ReschedulePage> {
itemCount: slots.length,
itemBuilder: (_, i) {
final s = slots[i];
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
title: Text(
s.className,
style: const TextStyle(
fontWeight: FontWeight.w800,
),
),
subtitle: Text(
'${_weekdayLabel(s.date)} ${s.date}${s.time}\n'
'Posti liberi: ${s.freePlaces}/${s.maxCapacity}'
'${s.alreadyBooked ? " • Sei già prenotato" : ""}',
),
isThreeLine: true,
trailing: s.bookable
? ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: green,
),
onPressed: () => _confirm(s),
child: const Text('Scegli'),
)
: const Text(
'Non disp.',
style: TextStyle(
color: Colors.black38,
fontWeight: FontWeight.w700,
),
),
),
return _SlotCard(
slot: s,
weekday: _weekdayLabel(s.date),
onChoose: () => _confirm(s),
);
},
),
@@ -262,3 +235,137 @@ class _ReschedulePageState extends State<ReschedulePage> {
}
}
}
class _SlotCard extends StatelessWidget {
final AvailableSlot slot;
final String weekday;
final VoidCallback onChoose;
const _SlotCard({
required this.slot,
required this.weekday,
required this.onChoose,
});
@override
Widget build(BuildContext context) {
const green = Color(0xFF10B981);
const orange = Color(0xFFF59E0B);
// Determina lo stato dello slot
final bool isBooked = slot.alreadyBooked;
final bool isFull = !slot.bookable && !isBooked;
final bool isSelectable = slot.bookable && !isBooked;
// Colori in base allo stato
Color bgColor;
Color borderColor;
if (isBooked) {
bgColor = const Color(0xFFFFF4E5); // arancio chiaro
borderColor = orange;
} else if (isFull) {
bgColor = const Color(0xFFF3F3F5); // grigio chiaro
borderColor = const Color(0xFFD9D9E0);
} else {
bgColor = Colors.white;
borderColor = const Color(0x14000000);
}
// Etichetta / badge a destra
Widget trailing;
if (isSelectable) {
trailing = ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: onChoose,
child: const Text('Scegli'),
);
} else if (isBooked) {
trailing = _StatusBadge(text: 'Già prenotato', color: orange);
} else {
trailing = _StatusBadge(
text: 'Al completo',
color: const Color(0xFF9AA0A6),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.fromLTRB(14, 12, 12, 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: borderColor, width: 1.5),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
slot.className,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
'$weekday ${slot.date}${slot.time}',
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
'Posti liberi: ${slot.freePlaces}/${slot.maxCapacity}',
style: TextStyle(
fontSize: 12,
color: isFull ? orange : Colors.black45,
fontWeight: FontWeight.w700,
),
),
],
),
),
const SizedBox(width: 10),
trailing,
],
),
);
}
}
class _StatusBadge extends StatelessWidget {
final String text;
final Color color;
const _StatusBadge({required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color, width: 1.5),
),
child: Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w800,
fontSize: 12,
),
),
);
}
}
+238
View File
@@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import '../models/school.dart';
import '../services/auth_storage.dart';
import '../widgets/yogibook_background.dart';
class SettingsPage extends StatefulWidget {
final String token;
final School school;
final String? userFirstName;
const SettingsPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
static const Color kGreen = Color(0xFF10B981);
final _auth = AuthStorage.instance;
bool _loading = true;
bool _rememberMe = false;
bool _biometric = false;
bool _biometricAvailableOnDevice = false;
// evita doppi tap durante il salvataggio
bool _busy = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final remember = await _auth.isRememberMeEnabled();
final bio = await _auth.isBiometricEnabled();
final canBio = await _auth.canUseBiometrics();
if (!mounted) return;
setState(() {
_rememberMe = remember;
_biometric = bio;
_biometricAvailableOnDevice = canBio;
_loading = false;
});
}
void _snack(String msg) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
// "Ricordami": se acceso salva SUBITO il token corrente in cassaforte,
// così funziona già da questa sessione (non solo dal prossimo login).
Future<void> _onRememberChanged(bool value) async {
if (_busy) return;
setState(() => _busy = true);
try {
await _auth.setRememberMe(value);
if (value) {
// salva il token che questa pagina ha in mano
await _auth.saveTokenIfRemember(widget.token);
}
// spegnere ricordami spegne anche la biometria (dipendenza)
final bioNow = await _auth.isBiometricEnabled();
if (!mounted) return;
setState(() {
_rememberMe = value;
_biometric = bioNow;
});
_snack(
value ? 'Accesso rapido attivato.' : 'Accesso rapido disattivato.',
);
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _onBiometricChanged(bool value) async {
if (_busy) return;
// non attivabile senza impronte registrate / hardware
if (value && !_biometricAvailableOnDevice) {
_snack(
'Nessuna impronta o volto registrati sul dispositivo. '
'Configurali nelle impostazioni del telefono, poi riprova.',
);
return;
}
setState(() => _busy = true);
try {
await _auth.setBiometric(value);
if (!mounted) return;
setState(() => _biometric = value);
_snack(
value
? 'Sblocco biometrico attivato.'
: 'Sblocco biometrico disattivato.',
);
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
'Impostazioni',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
),
),
body: YogibookBackground(
child: SafeArea(
top: false,
child: _loading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
children: [
_sectionTitle('Accesso'),
const SizedBox(height: 10),
_accessCard(),
const SizedBox(height: 16),
_infoNote(),
],
),
),
),
);
}
Widget _sectionTitle(String t) => Text(
t,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w900),
);
Widget _accessCard() {
// la biometria è attivabile solo se "ricordami" è ON
final biometricEnabled = _rememberMe;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
],
),
child: Column(
children: [
SwitchListTile(
activeColor: kGreen,
value: _rememberMe,
onChanged: _busy ? null : _onRememberChanged,
secondary: const Icon(Icons.vpn_key_rounded),
title: const Text(
'Ricordami',
style: TextStyle(fontWeight: FontWeight.w800),
),
subtitle: const Text(
'Resta connesso su questo dispositivo senza reinserire la password.',
),
),
const Divider(height: 1),
SwitchListTile(
activeColor: kGreen,
value: _biometric && biometricEnabled,
onChanged: (_busy || !biometricEnabled)
? null
: _onBiometricChanged,
secondary: Icon(
Icons.fingerprint_rounded,
color: biometricEnabled ? null : Colors.black26,
),
title: Text(
'Sblocco con impronta / volto',
style: TextStyle(
fontWeight: FontWeight.w800,
color: biometricEnabled ? null : Colors.black38,
),
),
subtitle: Text(
biometricEnabled
? 'Chiedi la biometria all\'avvio per accedere.'
: 'Attiva prima "Ricordami" per usare lo sblocco biometrico.',
style: TextStyle(color: biometricEnabled ? null : Colors.black38),
),
),
],
),
);
}
Widget _infoNote() {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF6F6FB),
borderRadius: BorderRadius.circular(14),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline, size: 20, color: Colors.black45),
const SizedBox(width: 10),
Expanded(
child: Text(
_biometricAvailableOnDevice
? 'Il token di accesso è salvato in forma cifrata sul dispositivo. '
'Con lo sblocco biometrico attivo, viene usato solo dopo il riconoscimento.'
: 'Questo dispositivo non ha impronta o volto configurati: '
'lo sblocco biometrico non è disponibile, ma "Ricordami" funziona lo stesso.',
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
),
],
),
);
}
}
+224
View File
@@ -0,0 +1,224 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../config/app_school.dart';
import '../services/auth_storage.dart';
import 'login_page.dart';
import 'home_page.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
// rosso bordeaux ripreso dal cuore del logo YogaSoul
static const Color kRed = Color(0xFFB01E28);
Timer? _timer;
int _seconds = 5; // countdown iniziale
// Evita doppie navigazioni: countdown, tap e sblocco possono
// altrimenti innescare più push.
bool _navigated = false;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
if (!mounted) return;
setState(() => _seconds--);
if (_seconds <= 0) {
t.cancel();
_finishSplash();
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
// Fine dello splash: prova lo sblocco, poi instrada.
Future<void> _finishSplash() async {
if (_navigated) return;
final outcome = await AuthStorage.instance.tryUnlockAtStartup();
if (!mounted || _navigated) return;
switch (outcome.result) {
case UnlockResult.success:
_navigated = true;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: outcome.token!,
school: kYogaSoulSchool,
userFirstName: null,
),
),
);
break;
case UnlockResult.noSavedLogin:
case UnlockResult.biometricFailed:
case UnlockResult.biometricUnavailable:
_goToLogin();
break;
}
}
// Tap sullo schermo: salta il countdown ma passa comunque dallo sblocco.
void _onTapSkip() {
if (_navigated) return;
_timer?.cancel();
_finishSplash();
}
void _goToLogin() {
if (_navigated || !mounted) return;
_navigated = true;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const LoginPage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: _onTapSkip,
child: Stack(
fit: StackFit.expand,
children: [
// ---- Foto di sfondo full-screen
Image.asset('assets/images/splash_bg.png', fit: BoxFit.cover),
// ---- Contenuto (logo + testo) in alto
SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
// Logo YogaSoul
Image.asset(
'assets/images/yogasoul_logo.png',
height: 72,
fit: BoxFit.contain,
),
const SizedBox(height: 26),
// ---- Blocco testo su velo bianco morbido per leggibilità
Container(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 16,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.42),
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
const Text(
'Il tuo spazio.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
height: 1.1,
fontWeight: FontWeight.w900,
color: Color(0xFF1F1F1F),
),
),
const Text(
'La tua energia.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
height: 1.1,
fontWeight: FontWeight.w900,
color: kRed,
),
),
const SizedBox(height: 14),
const Text(
'Ti diamo il benvenuto in YogiBook,\n'
'lapp della scuola YogaSoul — Seregno.\n'
'Ti accompagna nelle tue lezioni e nei tuoi pacchetti.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w600,
color: Color(0xFF2A2A2A),
),
),
],
),
),
],
),
),
),
// ---- Logo YogiBook + countdown in basso
Positioned(
left: 0,
right: 0,
bottom: 30,
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.75),
borderRadius: BorderRadius.circular(16),
),
child: Image.asset(
'assets/images/yogibook_logo.png',
height: 40,
fit: BoxFit.contain,
),
),
const SizedBox(height: 14),
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.25),
border: Border.all(color: Colors.white, width: 1.5),
),
child: Text(
'$_seconds',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
),
],
),
),
],
),
),
);
}
}
-84
View File
@@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'main.dart'; // per VanguardApi
class SelectSchoolPage extends StatefulWidget {
final String token;
const SelectSchoolPage({super.key, required this.token});
@override
State<SelectSchoolPage> createState() => _SelectSchoolPageState();
}
class _SelectSchoolPageState extends State<SelectSchoolPage> {
bool loading = true;
String error = '';
List schools = [];
@override
void initState() {
super.initState();
_loadSchools();
}
Future<void> _loadSchools() async {
try {
final data = await VanguardApi.getUserSchools(token: widget.token);
if (data['success'] != true) {
throw Exception(data['message'] ?? 'Errore sconosciuto');
}
final List list = data['schools'] ?? [];
// Auto select
if (data['auto_select'] == true && list.length == 1) {
_enterSchool(list.first);
return;
}
setState(() {
schools = list;
loading = false;
});
} catch (e) {
setState(() {
error = e.toString();
loading = false;
});
}
}
void _enterSchool(Map school) {
// per ora debug
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => SuccessPage(token: widget.token)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Scegli la scuola')),
body: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? Center(child: Text(error))
: ListView.builder(
itemCount: schools.length,
itemBuilder: (context, i) {
final s = schools[i];
return ListTile(
leading: s['logo'] != null
? Image.network(s['logo'], width: 40)
: const Icon(Icons.school),
title: Text(s['name']),
subtitle: Text(s['address_full'] ?? ''),
onTap: () => _enterSchool(s),
);
},
),
);
}
}
+167
View File
@@ -0,0 +1,167 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:local_auth/error_codes.dart' as auth_error;
import 'package:flutter/services.dart';
/// Esito dello sblocco all'avvio.
enum UnlockResult {
/// Nessun login salvato: mostra la schermata di login normale.
noSavedLogin,
/// Token disponibile e pronto all'uso: entra diretto.
success,
/// C'è un login salvato ma la biometria è fallita/annullata:
/// resta sul login, l'utente può accedere con password.
biometricFailed,
/// Biometria richiesta ma non disponibile (niente impronte / hardware):
/// fallback a login con password (scelta B).
biometricUnavailable,
}
/// Custode unico di token + preferenze di accesso (ricordami + biometria).
///
/// Non è un provider: lo si interroga in momenti precisi
/// (avvio nello splash, login, logout). Vedi nota architetturale nel piano.
class AuthStorage {
AuthStorage._();
static final AuthStorage instance = AuthStorage._();
// Su Android forza l'uso di EncryptedSharedPreferences (più robusto).
static const _secure = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
final LocalAuthentication _localAuth = LocalAuthentication();
// Chiavi interne della cassaforte.
static const _kToken = 'auth_token';
static const _kRemember = 'pref_remember_me';
static const _kBiometric = 'pref_biometric';
// ---------------------------------------------------------------------------
// PREFERENZE
// ---------------------------------------------------------------------------
/// "Ricordami" attivo? (default: false)
Future<bool> isRememberMeEnabled() async {
return (await _secure.read(key: _kRemember)) == 'true';
}
/// Biometria attiva? (default: false)
Future<bool> isBiometricEnabled() async {
return (await _secure.read(key: _kBiometric)) == 'true';
}
/// Imposta "ricordami". Se lo spegni, cancella anche il token salvato:
/// senza ricordami non ha senso tenere un token in cassaforte.
Future<void> setRememberMe(bool value) async {
await _secure.write(key: _kRemember, value: value ? 'true' : 'false');
if (!value) {
await _secure.delete(key: _kToken);
// Biometria senza token salvato non ha nulla da proteggere: spegnila.
await _secure.write(key: _kBiometric, value: 'false');
}
}
/// Imposta la biometria. Ha senso solo se "ricordami" è attivo.
Future<void> setBiometric(bool value) async {
await _secure.write(key: _kBiometric, value: value ? 'true' : 'false');
}
// ---------------------------------------------------------------------------
// TOKEN
// ---------------------------------------------------------------------------
/// Salva il token SOLO se "ricordami" è attivo. Chiamato dopo il login.
Future<void> saveTokenIfRemember(String token) async {
if (await isRememberMeEnabled()) {
await _secure.write(key: _kToken, value: token);
}
}
/// Cancella tutto: token + preferenze. Chiamato al logout.
Future<void> clear() async {
await _secure.delete(key: _kToken);
await _secure.delete(key: _kRemember);
await _secure.delete(key: _kBiometric);
}
/// Lettura grezza del token (senza biometria). Uso interno.
Future<String?> _readToken() => _secure.read(key: _kToken);
// ---------------------------------------------------------------------------
// BIOMETRIA
// ---------------------------------------------------------------------------
/// Il dispositivo può fare biometria E ha almeno un metodo registrato?
Future<bool> canUseBiometrics() async {
try {
final supported = await _localAuth.isDeviceSupported();
if (!supported) return false;
final canCheck = await _localAuth.canCheckBiometrics;
if (!canCheck) return false;
final enrolled = await _localAuth.getAvailableBiometrics();
return enrolled.isNotEmpty;
} on PlatformException {
return false;
}
}
/// Chiede l'impronta/volto. true = passata, false = fallita/annullata.
Future<bool> _promptBiometric() async {
try {
return await _localAuth.authenticate(
localizedReason: 'Sblocca YogiBook per accedere al tuo account',
options: const AuthenticationOptions(
biometricOnly: true, // niente fallback al PIN del telefono (scelta B)
stickyAuth: true, // regge se l'app va in background durante il prompt
),
);
} on PlatformException catch (e) {
// Casi tipici: nessuna impronta registrata, hardware assente, troppi tentativi.
if (e.code == auth_error.notAvailable ||
e.code == auth_error.notEnrolled ||
e.code == auth_error.lockedOut ||
e.code == auth_error.permanentlyLockedOut) {
return false;
}
return false;
}
}
// ---------------------------------------------------------------------------
// SBLOCCO ALL'AVVIO (chiamato dallo splash)
// ---------------------------------------------------------------------------
/// Decide cosa fare all'avvio dell'app.
/// Restituisce l'esito; il token (se disponibile) è in [tokenOut].
Future<({UnlockResult result, String? token})> tryUnlockAtStartup() async {
// 1. Ricordami spento o nessun token → login normale.
if (!await isRememberMeEnabled()) {
return (result: UnlockResult.noSavedLogin, token: null);
}
final token = await _readToken();
if (token == null || token.isEmpty) {
return (result: UnlockResult.noSavedLogin, token: null);
}
// 2. Biometria spenta → entra diretto col token salvato.
if (!await isBiometricEnabled()) {
return (result: UnlockResult.success, token: token);
}
// 3. Biometria accesa ma non disponibile → fallback a password (scelta B).
if (!await canUseBiometrics()) {
return (result: UnlockResult.biometricUnavailable, token: null);
}
// 4. Biometria accesa e disponibile → chiedi l'impronta.
final ok = await _promptBiometric();
if (ok) {
return (result: UnlockResult.success, token: token);
}
return (result: UnlockResult.biometricFailed, token: null);
}
}
+3 -2
View File
@@ -7,8 +7,9 @@ import 'package:mime/mime.dart';
import '../config/api_config.dart';
class MedicalCertificatesApi {
static Uri _u(String path, [Map<String, String>? q]) =>
Uri.parse('${ApiConfig.phpApiBase}/$path').replace(queryParameters: q);
static Uri _u(String path, [Map<String, String>? q]) => Uri.parse(
'${ApiConfig.laravelApiBase}/$path',
).replace(queryParameters: q);
static Future<List<Map<String, dynamic>>> list({
required String token,
+67 -3
View File
@@ -1,15 +1,79 @@
import 'package:flutter/material.dart';
import '../models/school.dart';
import '../screens/home_page.dart';
import '../screens/lessons_page.dart';
import '../screens/account_page.dart';
import '../screens/meditation_page.dart';
/// Bottom navigation centralizzata.
/// Gestisce da sé la navigazione tra le 4 tab principali.
/// Ogni pagina passa solo il proprio [currentIndex] e i dati utente.
///
/// [onBeforeLeave] è opzionale: le pagine che devono fare pulizia prima di
/// uscire (es. Meditazione che ferma musica/timer) lo passano.
class AppBottomNav extends StatelessWidget {
final int currentIndex;
final ValueChanged<int> onTap;
final String token;
final School school;
final String? userFirstName;
final Future<void> Function()? onBeforeLeave;
const AppBottomNav({
super.key,
required this.currentIndex,
required this.onTap,
required this.token,
required this.school,
this.userFirstName,
this.onBeforeLeave,
});
Future<void> _go(BuildContext context, int i) async {
if (i == currentIndex) return;
// pulizia eventuale (musica/timer) prima di lasciare la pagina
if (onBeforeLeave != null) {
await onBeforeLeave!();
}
if (!context.mounted) return;
Widget page;
switch (i) {
case 0:
page = HomePage(
token: token,
school: school,
userFirstName: userFirstName,
);
break;
case 1:
page = LessonsPage(
token: token,
school: school,
userFirstName: userFirstName,
);
break;
case 2:
page = AccountPage(
token: token,
school: school,
userFirstName: userFirstName,
);
break;
case 3:
page = MeditationPage(
token: token,
school: school,
userFirstName: userFirstName,
);
break;
default:
return;
}
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page));
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
@@ -18,7 +82,7 @@ class AppBottomNav extends StatelessWidget {
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: currentIndex,
onTap: onTap,
onTap: (i) => _go(context, i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_rounded), label: 'Home'),
BottomNavigationBarItem(
+58 -1
View File
@@ -9,6 +9,8 @@ import '../screens/medical_certificates_page.dart';
import '../screens/lessons_page.dart';
import '../screens/orders_detail_page.dart';
import '../screens/account_page.dart';
import '../screens/settings_page.dart';
import 'package:url_launcher/url_launcher.dart';
class AppDrawer extends StatelessWidget {
final String token;
@@ -33,6 +35,16 @@ class AppDrawer extends StatelessWidget {
return '${ApiConfig.scheme}://${ApiConfig.host}/public/userarea/$raw';
}
Future<void> _openUrl(BuildContext context, String url) async {
final uri = Uri.parse(url);
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Impossibile aprire il link.')),
);
}
}
Future<void> _logout(BuildContext context) async {
final ok = await showDialog<bool>(
context: context,
@@ -127,7 +139,11 @@ class AppDrawer extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OrdersDetailPage(token: token),
builder: (_) => OrdersDetailPage(
token: token,
school: school,
userFirstName: userFirstName,
),
),
);
},
@@ -151,6 +167,47 @@ class AppDrawer extends StatelessWidget {
},
),
ListTile(
leading: const Icon(Icons.settings_rounded),
title: const Text('Impostazioni'),
onTap: () {
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SettingsPage(
token: token,
school: school,
userFirstName: userFirstName,
),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.privacy_tip_outlined),
title: const Text('Cookie e Privacy Policy'),
onTap: () {
Navigator.of(context).pop();
_openUrl(context, 'https://yogasoul.it/cookie-policy/');
},
),
ListTile(
leading: const Icon(Icons.description_outlined),
title: const Text('Condizioni generali'),
onTap: () {
Navigator.of(context).pop();
_openUrl(
context,
'https://yogasoul.it/condizioni-generali-di-vendita/',
);
},
),
const Divider(height: 1),
ListTile(
@@ -8,6 +8,7 @@
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+1
View File
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
flutter_secure_storage_linux
url_launcher_linux
)
@@ -8,7 +8,9 @@ import Foundation
import audioplayers_darwin
import file_picker
import file_selector_macos
import flutter_secure_storage_macos
import google_sign_in_ios
import local_auth_darwin
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
@@ -17,7 +19,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+176
View File
@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: be169cf6ac481e052c4538715d88841d567150dfe1df38aaec76461a4e7b39f2
url: "https://pub.dev"
source: hosted
version: "4.1.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -73,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
change_app_package_name:
dependency: "direct dev"
description:
name: change_app_package_name
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
characters:
dependency: transitive
description:
@@ -81,6 +105,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -198,6 +238,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -219,6 +267,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -293,6 +389,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
url: "https://pub.dev"
source: hosted
version: "4.9.2"
image_picker:
dependency: "direct main"
description:
@@ -365,6 +469,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
@@ -397,6 +517,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
url: "https://pub.dev"
source: hosted
version: "1.0.56"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev"
source: hosted
version: "1.6.1"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
url: "https://pub.dev"
source: hosted
version: "1.1.0"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
matcher:
dependency: transitive
description:
@@ -509,6 +669,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.2"
provider:
dependency: "direct main"
description:
@@ -754,6 +922,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.4 <4.0.0"
flutter: ">=3.35.0"
+16
View File
@@ -44,10 +44,14 @@ dependencies:
mime: ^2.0.0
provider: ^6.1.5+1
shared_preferences: ^2.2.3
flutter_secure_storage: ^9.2.2
local_auth: ^2.3.0
dev_dependencies:
flutter_test:
sdk: flutter
change_app_package_name: ^1.5.0
flutter_launcher_icons: ^0.14.4
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@@ -67,6 +71,8 @@ flutter:
uses-material-design: true
assets:
- assets/images/yogibook_logo.png
- assets/images/splash_bg.png
- assets/images/yogasoul_logo.png
- assets/audio/meditation.mp3
# To add assets to your application, add an assets section, like this:
@@ -99,3 +105,13 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icon/app_icon.png"
# Adaptive icon Android: sfondo bianco + soggetto in primo piano.
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/icon/app_icon.png"
# iOS non ammette trasparenza: rimuove il canale alpha automaticamente.
remove_alpha_ios: true
@@ -8,6 +8,8 @@
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
+2
View File
@@ -5,6 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
file_selector_windows
flutter_secure_storage_windows
local_auth_windows
url_launcher_windows
)