first commit

This commit is contained in:
atseirjo
2026-01-23 07:33:20 +01:00
commit aeca07a5a3
31 changed files with 1831 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
class Environment {
static const String appwritePublicEndpoint = 'https://appwrite.joshihomeserver.ipv64.net/v1';
static const String appwriteProjectId = '6894f2b0001f127bab72';
static const String appwriteProjectName = 'Flutter Projects';
static const String appwriteRealtimeCollectionId = '68a22f520035a95d6666';
static const String appwriteDatabaseId = '68a22ef90021b90f0f43';
}

View File

@@ -0,0 +1,85 @@
import 'package:get/get.dart';
import '../models/tank_model.dart';
import '../services/appwrite_service.dart';
class HomeController extends GetxController {
final isLoading = false.obs;
final listTankModel = <TankModel>[].obs;
final appwriteService = AppwriteService();
@override
void onInit() {
_loadListDocument();
super.onInit();
}
@override
void onReady() {}
@override
void onClose() {}
Future<void> _loadListDocument() async {
isLoading.value = true;
if(listTankModel.isNotEmpty){
listTankModel.clear();
}
var dateYear = DateTime.now().year;
var userId = await appwriteService.getCurrentUserId();
if (userId == null) {
//User nicht eingeloggt, evtl. zur Login-Seite navigieren
update();
return;
}
var resultList = await appwriteService.getDocumentsFromCollection(userId);
if (resultList.isEmpty) {
//Dokumente erfolgreich geladen, hier können Sie die Liste verarbeiten
print('Dokumente wurden nicht geladen: ${resultList.length}');
} else {
for (var doc in resultList) {
var tankModel = TankModel.fromMap(doc.data);
listTankModel.add(tankModel);
}
if (listTankModel.isNotEmpty) {
var sortList = listTankModel.where((tank) {
var year = (DateTime.tryParse(tank.szDate)!).year;
return year == dateYear;
}).toList();
listTankModel.clear();
listTankModel.addAll(sortList);
listTankModel.sort((a, b) {
var dateA = DateTime.tryParse(a.szDate)!;
var dateB = DateTime.tryParse(b.szDate)!;
return dateB.compareTo(dateA);
});
for (var tank in listTankModel) {
print(
'SortTankModel: ${tank.szDate} - ${tank.szLiters}L - ${tank.szPricePerLiter}€/L - Total: ${tank.szPriceTotal}',
);
}
}
}
isLoading.value = false;
update();
}
Future<void> logout() async {
var logoutSuccess = await appwriteService.logout();
if (logoutSuccess) {
Get.snackbar(
'Logout erfolgreich',
'Sie wurden abgemeldet.',
snackPosition: SnackPosition.BOTTOM,
);
Get.offAllNamed('/login-page');
} else {
Get.snackbar(
'Logout fehlgeschlagen',
'Bitte versuchen Sie es erneut.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../pages/home_view.dart';
import '../pages/signin_view.dart';
import '../services/appwrite_service.dart';
class LoginController extends GetxController {
final emailController = TextEditingController();
final passwordController = TextEditingController();
final appwriteService = AppwriteService();
@override
void onReady() {}
@override
void onClose() {
emailController.dispose();
passwordController.dispose();
}
void login() async {
var email = emailController.text;
var password = passwordController.text;
// Hier können Sie die Login-Logik implementieren
print('Login mit E-Mail: $email, Passwort: $password');
var loginSuccess = await appwriteService.login(email, password);
if (loginSuccess) {
Get.snackbar(
'Login erfolgreich',
'Willkommen zurück!',
snackPosition: SnackPosition.BOTTOM,
);
Get.offAndToNamed(HomePage.namedRoute);
} else {
Get.snackbar(
'Login fehlgeschlagen',
'Bitte überprüfen Sie Ihre E-Mail und Ihr Passwort und versuchen Sie es erneut.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void goToSignInPage() {
Get.offAndToNamed(SigninPage.namedRoute);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../pages/home_view.dart';
import '../pages/login_view.dart';
import '../services/appwrite_service.dart';
class SigninController extends GetxController {
final userNameController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final appwriteService = AppwriteService();
@override
void onReady() {}
@override
void onClose() {
userNameController.dispose();
emailController.dispose();
passwordController.dispose();
}
void register() async {
//Eingaben überprüfen
if (!_checkInputs()) return;
//Registrierungslogik
var userName = userNameController.text;
var email = emailController.text;
var password = passwordController.text;
// Testausgabe
print(
'Registrieren mit Benutzername: $userName, E-Mail: $email, Passwort: $password',
);
//appwrite Registrierung und login Logik hier einfügen
var registrationSuccess = await appwriteService.register(
userName,
email,
password,
);
//Nach erfolgreicher Registrierung Meldung und zur Startseite navigieren
if (registrationSuccess) {
Get.snackbar(
'Registrierung erfolgreich, sie werden weitergeleitet',
'Willkommen, $userName!',
snackPosition: SnackPosition.BOTTOM,
);
Get.offAndToNamed(HomePage.namedRoute);
} else {
Get.snackbar(
'Registrierung fehlgeschlagen',
'Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void goToLoginPage() {
Get.offAndToNamed(LoginPage.namedRoute);
}
bool _checkInputs() {
var isOk = false;
if (userNameController.text.isEmpty ||
emailController.text.isEmpty ||
passwordController.text.isEmpty) {
Get.snackbar(
'Fehler',
'Bitte alle Felder ausfüllen',
snackPosition: SnackPosition.BOTTOM,
);
isOk = false;
} else {
isOk = true;
}
if (passwordController.text.length < 6) {
Get.snackbar(
'Fehler',
'Passwort muss mindestens 6 Zeichen lang sein',
snackPosition: SnackPosition.BOTTOM,
);
isOk = false;
} else {
isOk = true;
}
return isOk;
}
}

10
lib/helper/helper.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'package:intl/intl.dart';
/// Retrieves the current date in the format "yyyy-MM-dd".
///
/// @return [String] A formatted date.
String getCurrentDate(DateTime date) {
return DateFormat("yyyy-MM-dd").format(date);
}

View File

@@ -0,0 +1,19 @@
import 'package:get/get.dart';
import '../controller/home_controller.dart';
import '../controller/login_controller.dart';
import '../controller/signin_controller.dart';
class SampleBindings extends Bindings {
@override
void dependencies() {
// Define your dependencies here no permanent Binding
Get.lazyPut<LoginController>(() => LoginController());
Get.lazyPut<SigninController>(() => SigninController());
Get.lazyPut<HomeController>(() => HomeController());
}
}

View File

@@ -0,0 +1,27 @@
import 'package:get/get.dart';
import 'sample_bindings.dart';
import '../pages/home_view.dart';
import '../pages/signin_view.dart';
import '../pages/login_view.dart';
class SampleRouts {
static final sampleBindings = SampleBindings();
static List<GetPage<dynamic>> samplePages = [
GetPage(
name: LoginPage.namedRoute,
page: () => const LoginPage(),
binding: sampleBindings,
),
GetPage(
name: SigninPage.namedRoute,
page: () => const SigninPage(),
binding: sampleBindings,
),
GetPage(
name: HomePage.namedRoute,
page: () => const HomePage(),
binding: sampleBindings,
),
];
}

28
lib/main.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'helper/sample_bindings.dart';
import 'helper/sample_routes.dart';
import 'pages/login_view.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
initialBinding: SampleBindings(),
initialRoute: LoginPage.namedRoute,
getPages: SampleRouts.samplePages,
);
}
}

View File

@@ -0,0 +1,82 @@
import 'dart:convert';
class TankModel {
final String szDocumentId;
final String szUserId;
final String szDate;
final String szOdometer;
final String szLiters;
final String szPricePerLiter;
final String szLocation;
final String szPriceTotal;
TankModel({
required this.szDocumentId,
required this.szUserId,
required this.szDate,
required this.szOdometer,
required this.szLiters,
required this.szPricePerLiter,
required this.szLocation,
required this.szPriceTotal,
});
TankModel copyWith({
String? szDocumentId,
String? szUserId,
String? szDate,
String? szOdometer,
String? szLiters,
String? szPricePerLiter,
String? szLocation,
String? szPriceTotal,
}) {
return TankModel(
szDocumentId: szDocumentId ?? this.szDocumentId,
szUserId: szUserId ?? this.szUserId,
szDate: szDate ?? this.szDate,
szOdometer: szOdometer ?? this.szOdometer,
szLiters: szLiters ?? this.szLiters,
szPricePerLiter: szPricePerLiter ?? this.szPricePerLiter,
szLocation: szLocation ?? this.szLocation,
szPriceTotal: szPriceTotal ?? this.szPriceTotal,
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'\$id': szDocumentId});
result.addAll({'userId': szUserId});
result.addAll({'date': szDate});
result.addAll({'odometer': szOdometer});
result.addAll({'liters': szLiters});
result.addAll({'pricePerLiter': szPricePerLiter});
result.addAll({'location': szLocation});
result.addAll({'priceTotal': szPriceTotal});
return result;
}
factory TankModel.fromMap(Map<String, dynamic> map) {
return TankModel(
szDocumentId: map['\$id'] ?? '',
szUserId: map['userId'] ?? '',
szDate: map['date'] ?? '',
szOdometer: map['odometer'] ?? '',
szLiters: map['liters'] ?? '',
szPricePerLiter: map['pricePerLiter'] ?? '',
szLocation: map['location'] ?? '',
szPriceTotal: (double.parse(map['liters']?.toString() ?? '0') * double.parse(map['pricePerLiter']?.toString() ?? '0')).toStringAsFixed(2),
);
}
String toJson() => json.encode(toMap());
factory TankModel.fromJson(String source) => TankModel.fromMap(json.decode(source));
@override
String toString() {
return 'TankModel(szDocumentId: $szDocumentId, szUserId: $szUserId, szDate: $szDate, szOdometer: $szOdometer, szLiters: $szLiters, szPricePerLiter: $szPricePerLiter, szLocation: $szLocation, szPriceTotal: $szPriceTotal)';
}
}

56
lib/pages/home_view.dart Normal file
View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/home_controller.dart';
class HomePage extends GetView<HomeController> {
static const String namedRoute = '/home-page';
const HomePage({super.key});
@override
Widget build(BuildContext context) {
var homCtrl = controller;
return PopScope(
canPop: false,
child: SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
title: const Text('Tank List'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
controller.onInit();
},
),
IconButton(
onPressed: () => homCtrl.logout(),
icon: Icon(Icons.logout),
),
],
),
body: Obx(
() => homCtrl.isLoading.value == false
? ListView.builder(
itemBuilder: (context, index) {
var tank = homCtrl.listTankModel[index];
return ListTile(
title: Text(
'${tank.szDate} - ${tank.szLiters}L - ${tank.szPricePerLiter}€/L',
),
subtitle: Text(
'Total: ${tank.szPriceTotal}€ - Odometer: ${tank.szOdometer}km',
),
);
},
itemCount: homCtrl.listTankModel.length,
)
: Center(child: CircularProgressIndicator()),
),
),
),
);
}
}

36
lib/pages/login_view.dart Normal file
View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/login_controller.dart';
import '../widgets/my_login_widget.dart';
class LoginPage extends GetView<LoginController> {
static const String namedRoute = '/login-page';
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
var logCtrl = controller;
var displayWidth = MediaQuery.of(context).size.width;
var displayHeight = MediaQuery.of(context).size.height;
return SafeArea(
child: Scaffold(
body: Container(
height: displayHeight,
width: displayWidth,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 130.0,
),
color: Colors.blue.shade100,
child: SingleChildScrollView(
child: MyLoginWidget(
onButtonPressed: () => logCtrl.login(),
signInOnTab: () => logCtrl.goToSignInPage(),
logCtrl: logCtrl,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/signin_controller.dart';
import '../widgets/my_signin_widget.dart';
class SigninPage extends GetView<SigninController> {
static const String namedRoute = '/signin-page';
const SigninPage({super.key});
@override
Widget build(BuildContext context) {
var sigCtrl = controller;
var displayWidth = MediaQuery.of(context).size.width;
var displayHeight = MediaQuery.of(context).size.height;
return SafeArea(
child: Scaffold(
body: Container(
height: displayHeight,
width: displayWidth,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 100.0,
),
color: Colors.blue.shade100,
child: SingleChildScrollView(
child: MySigninWidget(
signCtrl: sigCtrl,
onButtonPressed: () => sigCtrl.register(),
logInOnTab: () => sigCtrl.goToLoginPage(),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,168 @@
import 'package:appwrite/models.dart';
import 'package:appwrite/appwrite.dart';
import '../config/environment.dart';
class AppwriteService {
static final String endpoint = Environment.appwritePublicEndpoint;
static final String projectId = Environment.appwriteProjectId;
static final String projectName = Environment.appwriteProjectName;
static final String realtimeCollectionId =
Environment.appwriteRealtimeCollectionId;
static final String databaseId = Environment.appwriteDatabaseId;
final Client _client = Client().setProject(projectId).setEndpoint(endpoint);
// ignore: unused_field
late final Account _account;
// ignore: unused_field
late final Databases _databases;
AppwriteService._internal() {
_account = Account(_client);
_databases = Databases(_client);
}
static final AppwriteService _instance = AppwriteService._internal();
/// Singleton instance getter
factory AppwriteService() => _instance;
// register new user with userName, e-Mail and password
Future<bool> register(String userName, String email, String password) async {
try {
final user = await _account.create(
userId: ID.unique(),
email: email,
password: password,
name: userName,
);
print('Registrierung erfolgreich: ${user.$id}');
return true;
} catch (e) {
print('Registrierung fehlgeschlagen: $e');
return false;
}
}
// login with e-Mail and password
Future<bool> login(String email, String password) async {
await logout();
try {
final session = await _account.createEmailPasswordSession(
email: email,
password: password,
);
print('Login erfolgreich: ${session.$id}');
return true;
} catch (e) {
print('Login fehlgeschlagen: $e');
return false;
}
}
// logout current user
Future<bool> logout() async {
try {
await _account.deleteSession(sessionId: 'current');
print('Logout erfolgreich');
return true;
} catch (e) {
print('Logout fehlgeschlagen: $e');
return false;
}
}
//Get current user ID
Future<String?> getCurrentUserId() async {
try {
final user = await _account.get();
return user.$id;
} catch (e) {
print('Fehler beim Abrufen der Benutzer-ID: $e');
return null;
}
}
// Get List<Document> from Realtime Collection
Future<List<Document>> getDocumentsFromCollection(String userId) async {
try {
final documents = await _databases.listDocuments(
databaseId: databaseId,
collectionId: realtimeCollectionId,
queries: [Query.equal('userId', userId), Query.orderDesc('date')],
);
return documents.documents;
} catch (e) {
print('Fehler beim Abrufen der Dokumente: $e');
return [];
}
}
// Get Document per Id from Realtime Collection
Future<Document?> getDocumentById(String documentId) async {
try {
final document = await _databases.getDocument(
databaseId: databaseId,
collectionId: realtimeCollectionId,
documentId: documentId,
);
return document;
} catch (e) {
print('Fehler beim Abrufen des Dokuments: $e');
return null;
}
}
// Save a new document to Realtime Collection
Future<bool> createDocumentInCollection(Map<String, dynamic> data) async {
try {
await _databases.createDocument(
databaseId: databaseId,
collectionId: realtimeCollectionId,
documentId: ID.unique(),
data: data,
);
print('Dokument erfolgreich erstellt');
return true;
} catch (e) {
print('Fehler beim Erstellen des Dokuments: $e');
return false;
}
}
// Update an existing document in Realtime Collection
Future<bool> updateDocumentInCollection(
String documentId,
Map<String, dynamic> data,
) async {
try {
await _databases.updateDocument(
databaseId: databaseId,
collectionId: realtimeCollectionId,
documentId: documentId,
data: data,
);
print('Dokument erfolgreich aktualisiert');
return true;
} catch (e) {
print('Fehler beim Aktualisieren des Dokuments: $e');
return false;
}
}
// Delete a document from Realtime Collection
Future<bool> deleteDocumentFromCollection(String documentId) async {
try {
await _databases.deleteDocument(
databaseId: databaseId,
collectionId: realtimeCollectionId,
documentId: documentId,
);
print('Dokument erfolgreich gelöscht');
return true;
} catch (e) {
print('Fehler beim Löschen des Dokuments: $e');
return false;
}
}
}

View File

@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart' show GoogleFonts;
import '../controller/login_controller.dart';
class MyLoginWidget extends StatelessWidget {
final void Function()? onButtonPressed;
final void Function()? signInOnTab;
final LoginController logCtrl;
const MyLoginWidget({
super.key,
this.onButtonPressed,
this.signInOnTab,
required this.logCtrl,
});
@override
Widget build(BuildContext context) {
final googleFont = GoogleFonts.righteous().fontFamily;
return Column(
children: [
Stack(
children: [
// Umriss (Outline)
Text(
"Login Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
letterSpacing: 5.0,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5
..color = Colors.blue.shade900,
),
),
// Füllung
Text(
"Login Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
color: Colors.orange.shade300,
letterSpacing: 5.0,
),
),
],
),
SizedBox(height: 20),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.orange.shade400, width: 8),
borderRadius: BorderRadius.circular(30),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Image.asset(
'assets/images/guru.png',
width: 400,
height: 400,
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: logCtrl.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'E-Mail',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
obscureText: true,
controller: logCtrl.passwordController,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
),
),
SizedBox(height: 30),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
Colors.orange.shade500,
),
foregroundColor: WidgetStatePropertyAll<Color>(
Colors.blue.shade900,
),
minimumSize: WidgetStatePropertyAll<Size>(Size(430, 60)),
),
onPressed: onButtonPressed,
child: Text(
'Login',
style: TextStyle(fontSize: 34, fontFamily: googleFont),
),
),
SizedBox(height: 10),
RichText(
text: TextSpan(
style: TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(text: 'Zum registrieren '),
WidgetSpan(
child: GestureDetector(
onTap: signInOnTab,
child: Text(
'Sign in',
style: TextStyle(
fontSize: 16,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
),
),
TextSpan(text: ' clicken!!'),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart' show GoogleFonts;
import '../controller/signin_controller.dart';
class MySigninWidget extends StatelessWidget {
final void Function()? onButtonPressed;
final void Function()? logInOnTab;
final SigninController signCtrl;
const MySigninWidget({
super.key,
this.onButtonPressed,
this.logInOnTab,
required this.signCtrl,
});
@override
Widget build(BuildContext context) {
final googleFont = GoogleFonts.righteous().fontFamily;
return Column(
children: [
Stack(
children: [
// Umriss (Outline)
Text(
"Signin Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
letterSpacing: 5.0,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5
..color = Colors.blue.shade900,
),
),
// Füllung
Text(
"Signin Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
color: Colors.orange.shade300,
letterSpacing: 5.0,
),
),
],
),
SizedBox(height: 20),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.orange.shade400, width: 8),
borderRadius: BorderRadius.circular(30),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Image.asset(
'assets/images/guru01.png',
width: 400,
height: 400,
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.userNameController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
obscureText: true,
controller: signCtrl.passwordController,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
),
),
SizedBox(height: 30),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
Colors.orange.shade500,
),
foregroundColor: WidgetStatePropertyAll<Color>(
Colors.blue.shade900,
),
minimumSize: WidgetStatePropertyAll<Size>(Size(430, 60)),
),
onPressed: onButtonPressed,
child: Text(
'Register',
style: TextStyle(fontSize: 34, fontFamily: googleFont),
),
),
SizedBox(height: 10),
RichText(
text: TextSpan(
style: TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(text: 'Zum login '),
WidgetSpan(
child: GestureDetector(
onTap: logInOnTab,
child: Text(
'Log in',
style: TextStyle(
fontSize: 16,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
),
),
TextSpan(text: ' clicken!!'),
],
),
),
],
);
}
}