Feature
ein paar feature aber datenbank macht probleme wenn man aufträge speichern möchge
This commit is contained in:
55
screens/app_logged_in_gate.dart
Normal file
55
screens/app_logged_in_gate.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/app_preferences.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'onboarding/onboarding_screen.dart';
|
||||
import 'shell/main_shell_screen.dart';
|
||||
|
||||
/// Zeigt einmalig das Onboarding, danach die Haupt-App.
|
||||
class AppLoggedInGate extends StatefulWidget {
|
||||
const AppLoggedInGate({super.key, required this.onLoggedOut});
|
||||
|
||||
final VoidCallback onLoggedOut;
|
||||
|
||||
@override
|
||||
State<AppLoggedInGate> createState() => _AppLoggedInGateState();
|
||||
}
|
||||
|
||||
class _AppLoggedInGateState extends State<AppLoggedInGate> {
|
||||
bool _prefsReady = false;
|
||||
bool _onboardingDone = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPrefs();
|
||||
}
|
||||
|
||||
Future<void> _loadPrefs() async {
|
||||
final done = await AppPreferences.isOnboardingDone();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_onboardingDone = done;
|
||||
_prefsReady = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_prefsReady) {
|
||||
return const Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (!_onboardingDone) {
|
||||
return OnboardingScreen(
|
||||
onComplete: () async {
|
||||
await AppPreferences.setOnboardingDone();
|
||||
if (mounted) setState(() => _onboardingDone = true);
|
||||
},
|
||||
);
|
||||
}
|
||||
return MainShellScreen(onLoggedOut: widget.onLoggedOut);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,14 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:appwrite/appwrite.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Screen 1: Login & Registrierung (E-Mail / Passwort) über Firebase Auth.
|
||||
import '../../appwrite_config.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
/// Screen 1: Login & Registrierung (E-Mail / Passwort) über Appwrite.
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
const AuthScreen({super.key, required this.onLoggedIn});
|
||||
|
||||
final VoidCallback onLoggedIn;
|
||||
|
||||
@override
|
||||
State<AuthScreen> createState() => _AuthScreenState();
|
||||
@@ -12,6 +17,7 @@ class AuthScreen extends StatefulWidget {
|
||||
class _AuthScreenState extends State<AuthScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
final _account = Account(appwriteClient);
|
||||
|
||||
final _loginEmail = TextEditingController();
|
||||
final _loginPassword = TextEditingController();
|
||||
@@ -44,23 +50,27 @@ class _AuthScreenState extends State<AuthScreen>
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
|
||||
}
|
||||
|
||||
String _authMessage(FirebaseAuthException e) {
|
||||
switch (e.code) {
|
||||
case 'user-not-found':
|
||||
case 'wrong-password':
|
||||
case 'invalid-credential':
|
||||
return 'E-Mail oder Passwort ist falsch.';
|
||||
case 'email-already-in-use':
|
||||
return 'Diese E-Mail ist bereits registriert.';
|
||||
case 'weak-password':
|
||||
return 'Passwort ist zu schwach (mindestens 6 Zeichen).';
|
||||
case 'invalid-email':
|
||||
return 'Ungültige E-Mail-Adresse.';
|
||||
case 'network-request-failed':
|
||||
return 'Netzwerkfehler. Internet prüfen.';
|
||||
default:
|
||||
return e.message ?? 'Fehler: ${e.code}';
|
||||
String _authMessage(AppwriteException e) {
|
||||
final t = e.type ?? '';
|
||||
final m = (e.message ?? '').toLowerCase();
|
||||
if (t == 'user_invalid_credentials' ||
|
||||
m.contains('invalid credentials') ||
|
||||
m.contains('wrong password')) {
|
||||
return 'E-Mail oder Passwort ist falsch.';
|
||||
}
|
||||
if (t == 'user_already_exists' || m.contains('already exists')) {
|
||||
return 'Diese E-Mail ist bereits registriert.';
|
||||
}
|
||||
if (m.contains('password') && m.contains('short')) {
|
||||
return 'Passwort ist zu schwach (mindestens 8 Zeichen in Appwrite).';
|
||||
}
|
||||
if (t == 'general_argument_invalid' && m.contains('email')) {
|
||||
return 'Ungültige E-Mail-Adresse.';
|
||||
}
|
||||
if (e.code == 0 && m.contains('network')) {
|
||||
return 'Netzwerkfehler. Internet prüfen.';
|
||||
}
|
||||
return e.message?.isNotEmpty == true ? e.message! : 'Fehler: $t';
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
@@ -72,11 +82,12 @@ class _AuthScreenState extends State<AuthScreen>
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await FirebaseAuth.instance.signInWithEmailAndPassword(
|
||||
await _account.createEmailPasswordSession(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (mounted) widget.onLoggedIn();
|
||||
} on AppwriteException catch (e) {
|
||||
_snack(_authMessage(e));
|
||||
} catch (e) {
|
||||
_snack('Unerwarteter Fehler: $e');
|
||||
@@ -93,20 +104,29 @@ class _AuthScreenState extends State<AuthScreen>
|
||||
_snack('E-Mail und Passwort eingeben.');
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
_snack('Passwort mindestens 6 Zeichen (Firebase).');
|
||||
if (password.length < 8) {
|
||||
_snack('Passwort mindestens 8 Zeichen (Appwrite-Standard).');
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final cred = await FirebaseAuth.instance.createUserWithEmailAndPassword(
|
||||
await _account.create(
|
||||
userId: ID.unique(),
|
||||
email: email,
|
||||
password: password,
|
||||
name: name.isEmpty ? null : name,
|
||||
);
|
||||
await _account.createEmailPasswordSession(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
if (name.isNotEmpty && cred.user != null) {
|
||||
await cred.user!.updateDisplayName(name);
|
||||
if (name.isNotEmpty) {
|
||||
try {
|
||||
await _account.updateName(name: name);
|
||||
} catch (_) {}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (mounted) widget.onLoggedIn();
|
||||
} on AppwriteException catch (e) {
|
||||
_snack(_authMessage(e));
|
||||
} catch (e) {
|
||||
_snack('Unerwarteter Fehler: $e');
|
||||
@@ -120,27 +140,29 @@ class _AuthScreenState extends State<AuthScreen>
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Icon(Icons.construction, size: 48, color: scheme.primary),
|
||||
Icon(Icons.handyman_rounded, size: 48, color: scheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Handwerksapp',
|
||||
'HandwerkPro',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Nach dem Login: Aufträge anlegen, Fotos, Kunden-Unterschrift, PDF teilen.\n'
|
||||
'Zuerst: Konto anlegen oder anmelden.',
|
||||
'Aufträge, Fotos, Unterschrift, PDF – alles in einer App. '
|
||||
'Anmelden oder registrieren (Appwrite).',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
color: const Color(0xFFB0B0B0),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -269,7 +291,9 @@ class _RegisterForm extends StatelessWidget {
|
||||
controller: password,
|
||||
obscureText: true,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: const InputDecoration(labelText: 'Passwort (min. 6 Zeichen)'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Passwort (min. 8 Zeichen)',
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Wird angezeigt, wenn Firebase.initializeApp fehlschlägt (z. B. Platzhalter-Keys).
|
||||
class FirebaseSetupRequiredScreen extends StatelessWidget {
|
||||
const FirebaseSetupRequiredScreen({super.key, required this.error});
|
||||
|
||||
final Object error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Firebase einrichten',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'1. In der Firebase Console ein Projekt anlegen.\n'
|
||||
'2. Android-App (Package com.example.handwerksapp) und iOS-App '
|
||||
'(Bundle com.example.handwerksapp) registrieren.\n'
|
||||
'3. Authentication → E-Mail/Passwort aktivieren.\n'
|
||||
'4. Im Projektordner im Terminal:\n\n'
|
||||
' dart pub global activate flutterfire_cli\n'
|
||||
' flutterfire configure\n\n'
|
||||
' Das überschreibt lib/firebase_options.dart und die '
|
||||
'google-services / GoogleService-Info Dateien.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Technische Meldung:\n$error',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/auftrag.dart';
|
||||
import '../../services/auftrag_repository.dart';
|
||||
import '../auftrag/auftrag_bearbeiten_screen.dart';
|
||||
|
||||
class AuftraegeHomeScreen extends StatelessWidget {
|
||||
const AuftraegeHomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final repo = AuftragRepository();
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
final name = user?.displayName?.trim();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Meine Rechnungen'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: 'Abmelden',
|
||||
onPressed: () => FirebaseAuth.instance.signOut(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Card(
|
||||
color: scheme.primaryContainer.withValues(alpha: 0.35),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: scheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Nach dem Login bist du hier. Tippe unten auf '
|
||||
'„Neue Rechnung“: Daten eintragen, Fotos & Unterschrift, '
|
||||
'dann PDF erzeugen oder per E-Mail senden.\n\n'
|
||||
'Wichtig: Nach großen Code-Änderungen App neu starten '
|
||||
'(Stop ▶), kein reines Hot Reload.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
|
||||
child: Text(
|
||||
name != null && name.isNotEmpty ? 'Hallo, $name' : 'Übersicht',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: StreamBuilder<List<Auftrag>>(
|
||||
stream: repo.watchAuftraege(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'Rechnungen konnten nicht geladen werden.\n'
|
||||
'Firestore aktiviert und Sicherheitsregeln gesetzt?\n\n'
|
||||
'${snapshot.error}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final list = snapshot.data!;
|
||||
if (list.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
'Noch keine Rechnungen.\n'
|
||||
'Unten auf „Neue Rechnung“ tippen.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 88),
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (context, i) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final a = list[i];
|
||||
final datum = a.createdAt != null
|
||||
? DateFormat('dd.MM.yyyy').format(a.createdAt!)
|
||||
: '';
|
||||
final nr = a.rechnungsnummer.isNotEmpty
|
||||
? a.rechnungsnummer
|
||||
: 'ohne Nr.';
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
a.titel.isEmpty ? '(Ohne Titel)' : a.titel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
'$nr · ${a.kundenName.isEmpty ? "Kunde —" : a.kundenName}'
|
||||
'${datum.isNotEmpty ? " · $datum" : ""}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) =>
|
||||
AuftragBearbeitenScreen(auftragId: a.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const AuftragBearbeitenScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Neue Rechnung'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
screens/legal/legal_document_screen.dart
Normal file
59
screens/legal/legal_document_screen.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
class LegalDocumentScreen extends StatelessWidget {
|
||||
const LegalDocumentScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.showDisclaimer = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final bool showDisclaimer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
if (showDisclaimer) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.statusGeplant.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.statusGeplant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Hinweis: Unverbindliche Vorlage – bitte rechtlich prüfen lassen.',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFCC80),
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Text(
|
||||
body.trim(),
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade300,
|
||||
height: 1.45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
screens/onboarding/onboarding_screen.dart
Normal file
164
screens/onboarding/onboarding_screen.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
class OnboardingScreen extends StatefulWidget {
|
||||
const OnboardingScreen({super.key, required this.onComplete});
|
||||
|
||||
final Future<void> Function() onComplete;
|
||||
|
||||
@override
|
||||
State<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
final _pageController = PageController();
|
||||
int _page = 0;
|
||||
static const _total = 4;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _next() async {
|
||||
if (_page < _total - 1) {
|
||||
await _pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 320),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
} else {
|
||||
await widget.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => widget.onComplete(),
|
||||
child: const Text('Überspringen'),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (i) => setState(() => _page = i),
|
||||
children: const [
|
||||
_OnboardingPage(
|
||||
icon: Icons.handyman_rounded,
|
||||
title: 'Willkommen bei HandwerkPro',
|
||||
text:
|
||||
'Erfasse Aufträge mit Fotos, Kundenadresse und '
|
||||
'Unterschrift – alles strukturiert an einem Ort.',
|
||||
),
|
||||
_OnboardingPage(
|
||||
icon: Icons.cloud_done_outlined,
|
||||
title: 'Deine Daten',
|
||||
text:
|
||||
'Nach dem Login werden Aufträge sicher gespeichert. '
|
||||
'Im Profil findest du Impressum, Datenschutz und einen '
|
||||
'JSON-Export deiner Auftragsdaten.',
|
||||
),
|
||||
_OnboardingPage(
|
||||
icon: Icons.picture_as_pdf_outlined,
|
||||
title: 'PDF & Teilen',
|
||||
text:
|
||||
'Aus jedem Auftrag erzeugst du eine PDF-Rechnung und '
|
||||
'teilst sie oder sendest sie per E-Mail – ohne '
|
||||
'Steuerberatung, aber dokumentationsfest.',
|
||||
),
|
||||
_OnboardingPage(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
title: 'Workflow fürs Handwerk',
|
||||
text:
|
||||
'Angebot, Leistung, Rechnung, Mahnung – ein Vorgang, '
|
||||
'ohne doppelte Kundendaten. Kleinunternehmer-Hinweis, '
|
||||
'Skonto, SEPA-QR und CSV-Export für dein Steuerbüro.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
_total,
|
||||
(i) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: CircleAvatar(
|
||||
radius: 4,
|
||||
backgroundColor: i == _page
|
||||
? AppTheme.accentCyan
|
||||
: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: _next,
|
||||
child: Text(_page < _total - 1 ? 'Weiter' : 'Los geht’s'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OnboardingPage extends StatelessWidget {
|
||||
const _OnboardingPage({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 72, color: AppTheme.accentCyan),
|
||||
const SizedBox(height: 28),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade400,
|
||||
height: 1.45,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1153
screens/shell/main_shell_screen.dart
Normal file
1153
screens/shell/main_shell_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user