Files
Bus-Infotainment--IBus-/lib/pages/settings.dart
2024-05-01 12:29:59 +01:00

1469 lines
37 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:bus_infotainment/audio_cache.dart';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/backend/modules/commands.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:text_scroll/text_scroll.dart';
import 'package:url_launcher/url_launcher_string.dart';
class pages_Settings extends StatefulWidget {
@override
State<pages_Settings> createState() => _pages_SettingsState();
}
class _pages_SettingsState extends State<pages_Settings> {
late final Widget _loginPage;
@override
void initState() {
// TODO: implement initState
super.initState();
if (!LiveInformation().auth.isAuthenticated()){
_loginPage = _LoginPage(
onLogin: () {
setState(() {});
},
);
} else {
_loginPage = Container(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: (){
setState(() {});
LiveInformation().auth.deleteSession();
},
// make the corner radius 4, background color match the theme, and text colour white, fill to width of parent
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
minimumSize: Size(double.infinity, 48)
),
child: Text(
"Sign out",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5
)
)
),
);
}
}
@override
Widget build(BuildContext context) {
return Container(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 8
),
AnnouncementUpload(),
SizedBox(
height: 8
),
Container(
margin: EdgeInsets.symmetric(
horizontal: 8
),
child: SettingsField(
label: "Announce distance",
defaultValue: "150m",
)
),
SizedBox(
height: 8
),
Container(
margin: EdgeInsets.symmetric(
horizontal: 8
),
child: SettingsField(
label: "Announce time",
defaultValue: "10s",
)
),
Container(
margin: EdgeInsets.only(
left: 8,
top: 2
),
child: Text(
"Console",
style: GoogleFonts.teko(
fontSize: 24
),
),
),
Container(
margin: const EdgeInsets.symmetric(
horizontal: 8
),
child: Console()
),
if (false)
Container(
height: 2,
width: double.infinity,
color: Colors.white70,
),
if (false)
Container(
padding: const EdgeInsets.all(8),
child: _loginPage,
)
],
),
)
);
}
}
class AnnouncementUpload extends StatefulWidget {
Function onUploaded = () {};
AnnouncementUpload({Key? key, this.onUploaded = _defaultOnUploaded}) : super(key: key);
static void _defaultOnUploaded() {}
@override
State<AnnouncementUpload> createState() => _AnnouncementUploadState();
}
class _AnnouncementUploadState extends State<AnnouncementUpload> {
Future<void> UploadButtonPressed() async {
// Pick the file
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
print("Got file");
AnnouncementCache cache = LiveInformation().announcementModule.announcementCache;
late Uint8List bytes;
if (kIsWeb) {
bytes = result.files.single.bytes!;
} else {
File file = File(result.files.single.path!);
bytes = file.readAsBytesSync();
}
LiveInformation().announcementModule.setBundleBytes(bytes);
// load a random announcement to ensure that the file is usable
await cache.loadAnnouncementsFromBytes(bytes, ["S_WALTHAMSTOW_CENTRAL_001.mp3"]);
print("Loaded announcements");
setState(() {
});
if (!kIsWeb) {
// Use shared preferences to store the file location
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString("AnnouncementsFileLocation", result.files.single.path!);
}
widget.onUploaded();
} else {
// User canceled the picker
}
}
@override
void initState() {
// TODO: implement initState
super.initState();
if (!kIsWeb) {
SharedPreferences.getInstance().then((prefs) async {
String FileLocation = prefs.getString("AnnouncementsFileLocation")!;
File file = File(FileLocation);
Uint8List bytes = file.readAsBytesSync();
LiveInformation().announcementModule.setBundleBytes(bytes);
AnnouncementCache cache = LiveInformation().announcementModule.announcementCache;
await cache.loadAnnouncementsFromBytes(bytes, ["S_WALTHAMSTOW_CENTRAL_001.mp3"]);
setState(() {
});
print("Loaded announcemends from SharedPrefs");
widget.onUploaded();
});
}
}
@override
Widget build(BuildContext context) {
bool checkPassed = LiveInformation().announcementModule.announcementCache.keys.length != 0;
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white70,
width: 2
),
),
margin: EdgeInsets.symmetric(
horizontal: 8
),
padding: EdgeInsets.all(8),
// height: 100,
child: Column(
children: [
if (!checkPassed)
Row(
children: [
Icon(
Icons.error,
color: Colors.red,
size: 18,
),
SizedBox(
width: 4,
),
Transform.translate(
offset: Offset(0, 0),
child: Text(
"NO ANNOUNCEMENTS LOADED",
style: GoogleFonts.interTight(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white70,
letterSpacing: 0.1,
),
),
)
],
)
else
Row(
children: [
Icon(
Icons.check_box,
color: Colors.green,
size: 18,
),
SizedBox(
width: 4,
),
Transform.translate(
offset: Offset(0, 0),
child: Text(
"ANNOUNCEMENTS LOADED",
style: GoogleFonts.interTight(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white70,
letterSpacing: 0.1,
),
),
)
],
),
SizedBox(
height: 8,
),
Text(
"Disclaimer: It is illegal to redistribute Transport for London's intellectual property. Even if it were permissible, the files are too large to be packaged into a website...",
style: GoogleFonts.interTight(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white70,
letterSpacing: 0.1,
height: 1
),
),
SizedBox(
height: 8,
),
Text(
"...because of these reasons, you will have to provide the announcement files yourself.",
style: GoogleFonts.interTight(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white70,
letterSpacing: 0.1,
height: 1
),
),
SizedBox(
height: 8,
),
Text(
"A ZIP file should be provided containg audio files with the correct naming scheme (e.g. S_WALTHAMSTOW_CENTRAL_001.mp3).",
style: GoogleFonts.interTight(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white70,
letterSpacing: 0.1,
height: 1
),
),
SizedBox(
height: 8,
),
Text(
"No specific folder structure is required. The files will be sorted and indexed.",
style: GoogleFonts.interTight(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white70,
letterSpacing: 0.1,
height: 1
),
),
if (kIsWeb)
SizedBox(
height: 8,
),
if (kIsWeb)
Text(
"Announcements uploaded on web are not persistent, and will need to be re-uploaded each time the site is loaded.",
style: GoogleFonts.interTight(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white70,
letterSpacing: 0.1,
height: 1
),
),
SizedBox(
height: 8,
),
SizedBox(
height: 32,
width: double.infinity,
child: ElevatedButton(
onPressed: (){
UploadButtonPressed();
},
// make the corner radius 4, background color match the theme, and text colour white, fill to width of parent
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
),
child: Text(
"Upload file",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5
)
)
),
),
],
)
);
}
}
class PermissionsSetup extends StatefulWidget {
@override
State<PermissionsSetup> createState() => _PermissionsSetupState();
}
class _PermissionsSetupState extends State<PermissionsSetup> {
List<bool> _hasPermissions = [];
bool get hasPermissions {
return !_hasPermissions.contains(false) && _hasPermissions.length > 0;
}
Future<void> requestPermission() async {
_hasPermissions = [];
print("Requesting location permission");
if (!await Permission.location.isGranted){
PermissionStatus locationStatus = await Permission.location.request();
if (locationStatus.isGranted) {
_hasPermissions.add(true);
LiveInformation().initTrackerModule();
} else {
_hasPermissions.add(false);
}
} else {
_hasPermissions.add(true);
}
print("Gotten result for location permission");
if (!kIsWeb){
print("Requesting storage permissions");
PermissionStatus fileStatus = await Permission.manageExternalStorage.request();
if (fileStatus.isGranted) {
_hasPermissions.add(true);
} else {
_hasPermissions.add(false);
}
} else {
_hasPermissions.add(true);
}
print("Permissions: $_hasPermissions");
setState(() {
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white70,
width: 2
),
),
margin: EdgeInsets.symmetric(
horizontal: 8
),
padding: EdgeInsets.all(8),
child: Column(
children: [
Row(
children: [
Icon(
hasPermissions ? Icons.check_box : Icons.error,
color: hasPermissions ? Colors.green : Colors.red,
size: 18,
),
SizedBox(
width: 4,
),
Transform.translate(
offset: Offset(0, 0),
child: Text(
hasPermissions ? "PERMISSIONS GRANTED" : "MISSING PERMISSIONS",
style: GoogleFonts.interTight(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white70,
letterSpacing: 0.1,
),
),
),
],
),
SizedBox(
height: 8,
),
SizedBox(
height: 32,
width: double.infinity,
child: ElevatedButton(
onPressed: (){
requestPermission();
},
// make the corner radius 4, background color match the theme, and text colour white, fill to width of parent
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
),
child: Text(
"Request permissions",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5
)
)
),
)
],
),
);
}
}
enum _LoginType {
login,
signup
}
class _LoginPage extends StatefulWidget {
_LoginType type = _LoginType.login;
final Function() onLogin;
_LoginPage({super.key, required this.onLogin, });
@override
State<_LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<_LoginPage> {
@override
Widget build(BuildContext context) {
return Container(
color: Color.fromRGBO(19, 19, 19, 1),
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Login form
widget.type == _LoginType.login ?
LoginForm(
handleSubmit: (form) {
print("Login form submitted");
LiveInformation().auth.createEmailSession(
email: form.emailController.text,
password: form.passwordController.text
).then((value) {
widget.onLogin();
});
},
requestSignup: () {
setState(() {
widget.type = _LoginType.signup;
});
},
) :
SignupForm(
handleSubmit: (form) {
print("Signup form submitted");
LiveInformation().auth.createUser(
displayName: form.dispnameController.text,
username: form.usernameController.text,
email: form.emailController.text,
password: form.passwordController.text
).then((value) {
// login
LiveInformation().auth.createEmailSession(
email: form.emailController.text,
password: form.passwordController.text
).then((value) {
widget.onLogin();
});
});
},
requestSignin: () {
setState(() {
widget.type = _LoginType.login;
});
},
),
],
),
);
}
}
class LoginForm extends StatefulWidget {
final Function(LoginForm) handleSubmit;
final Function() requestSignup;
/* TextControllers */
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
LoginForm({super.key, required this.handleSubmit, required this.requestSignup});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Sign In",
style: GoogleFonts.montserrat(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: -1
)
),
SizedBox(height: 20),
PW_TextField(
title: "Email",
controller: widget.emailController,
handleSubmit: (form) {
print("Signup form submitted");
widget.handleSubmit(widget);
}
),
SizedBox(height: 20),
PW_TextField(
title: "Password",
obscure: true,
controller: widget.passwordController,
handleSubmit: (form) {
print("Signup form submitted");
widget.handleSubmit(widget);
}
),
SizedBox(height: 20),
ElevatedButton(
onPressed: (){
widget.handleSubmit(widget);
},
// make the corner radius 4, background color match the theme, and text colour white, fill to width of parent
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
minimumSize: Size(double.infinity, 48)
),
child: Text(
"Sign in",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5
)
)
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: (){
},
// Make border radius 4, background transparent, and text colour white
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
backgroundColor: Colors.transparent
),
child: Text(
"Forgot password?",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5
),
)
),
Container(
width: 1,
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: Colors.grey.shade800,
),
TextButton(
onPressed: (){
widget.requestSignup();
},
// Make border radius 4, background transparent, and text colour white
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
backgroundColor: Colors.transparent
),
child: Text(
"Sign Up",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5
),
)
),
],
),
],
)
);
}
}
class SignupForm extends StatelessWidget {
final Function(SignupForm) handleSubmit;
final Function() requestSignin;
/* TextControllers */
final TextEditingController dispnameController = TextEditingController();
final TextEditingController usernameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
SignupForm({super.key, required this.handleSubmit, required this.requestSignin});
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Sign Up",
style: GoogleFonts.montserrat(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: -1
)
),
SizedBox(height: 20),
PW_TextField(
title: "Display Name",
controller: dispnameController,
handleSubmit: (form) {
print("Signup form submitted");
handleSubmit(this);
}
),
SizedBox(height: 20),
PW_TextField(
title: "Username",
controller: usernameController,
handleSubmit: (form) {
print("Signup form submitted");
handleSubmit(this);
}
),
SizedBox(height: 20),
PW_TextField(
title: "Email",
controller: emailController,
handleSubmit: (form) {
print("Signup form submitted");
handleSubmit(this);
}
),
SizedBox(height: 20),
PW_TextField(
title: "Password",
obscure: true,
controller: passwordController,
handleSubmit: (form) {
print("Signup form submitted");
handleSubmit(this);
}
),
SizedBox(height: 20),
PW_TextField(
title: "Confirm Password",
obscure: true,
controller: confirmPasswordController,
handleSubmit: (form) {
print("Signup form submitted");
handleSubmit(this);
}
),
SizedBox(height: 20),
// Terms and conditions with hyperlink to terms and conditions, with checkbox
// use TextSpan
// use check box
Row(
children: [
Checkbox(
value: false,
onChanged: (value) {
print("Checkbox changed to $value");
},
),
Expanded(
child: RichText(
// wrap text
text: TextSpan(
children: [
TextSpan(
text: "By registering, you agree to our ",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5,
),
),
TextSpan(
text: "Terms and Conditions",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5,
decoration: TextDecoration.underline,
),
recognizer: new TapGestureRecognizer()
..onTap = () {
launchUrlString("https://google.co.uk/");
},
),
],
),
),
),
],
),
SizedBox(height: 20),
ElevatedButton(
onPressed: (){
handleSubmit(this);
},
// make the corner radius 4, background color match the theme, and text colour white, fill to width of parent
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
minimumSize: Size(double.infinity, 48)
),
child: Text(
"Sign up",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5
)
)
),
SizedBox(height: 20),
// Already have an account? Sign in
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Already have an account? ",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5,
),
),
TextButton(
onPressed: (){
requestSignin();
},
// Make border radius 4, background transparent, and text colour white
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)
),
backgroundColor: Colors.transparent
),
child: Text(
"Sign in",
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
letterSpacing: 0.5
),
)
),
],
),
],
)
);
}
}
class PW_TextField extends StatelessWidget {
String title = "field";
bool obscure = false;
bool longText = false;
late TextEditingController controller;
Function(String)? handleTapOutside;
Function(String)? handleEditingComplete;
Function(String)? handleChanged;
Function(String)? handleSubmit;
PW_TextField({super.key, this.title = "field", required this.controller, this.obscure = false, this.longText = false, this.handleSubmit, this.handleTapOutside, this.handleEditingComplete, this.handleChanged});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade300,
letterSpacing: 0.1
)
),
SizedBox(height: 4),
// field with uniform padding and margin
SizedBox(
child: TextField(
onEditingComplete: () {
if (handleEditingComplete != null) {
handleEditingComplete!(controller.text);
}
},
onChanged: (value) {
if (handleChanged != null) {
handleChanged!(controller.text);
}
},
onSubmitted: (value) {
if (handleSubmit != null) {
handleSubmit!(controller.text);
}
},
onTapOutside: (value) {
if (handleTapOutside != null) {
handleTapOutside!(controller.text);
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(12),
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
),
filled: true,
// fillColor: STUDIOS_DEFAULT_BACKGROUND_COLOR,
hintText: title,
hintStyle: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade700,
letterSpacing: 0.1
),
),
// wrap text
minLines: longText ? 3 : 1,
maxLines: longText ? null : 1,
keyboardType: TextInputType.multiline,
obscureText: obscure,
style: GoogleFonts.interTight(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade300,
letterSpacing: 0.1,
),
controller: controller,
),
)
],
);
}
}
// Console widget
class Console extends StatefulWidget {
Console({super.key});
@override
State<Console> createState() => _ConsoleState();
}
class _ConsoleState extends State<Console> {
ListenerReceipt? _listenerReceipt;
@override
void initState() {
// TODO: implement initState
super.initState();
_listenerReceipt = LiveInformation().commandModule.onCommandReceived.addListener((p0) {
print("Command received, updating console");
setState(() {});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> commands = [];
TextEditingController commandController = TextEditingController();
commands.add(
Text("Command History:")
);
for (int i = 0; i < LiveInformation().commandModule.commandHistory.length; i++){
CommandInfo command = LiveInformation().commandModule.commandHistory[i];
commands.add(
TextScroll(
command.command,
style: GoogleFonts.teko(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade300,
letterSpacing: 0.1,
),
mode: TextScrollMode.bouncing,
pauseBetween: const Duration(seconds: 2),
pauseOnBounce: const Duration(seconds: 1),
)
);
}
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white70, width: 2),
color: Colors.black,
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
height: 300,
child: ListView(
children: commands,
),
),
Container(
height: 2,
width: double.infinity,
color: Colors.white70,
),
Container(
height: 50,
padding: const EdgeInsets.all(8),
child: TextField(
controller: commandController,
decoration: const InputDecoration(
hintText: ">",
),
style: GoogleFonts.teko(
height: 1,
),
onSubmitted: (value) {
commandController.clear();
LiveInformation().commandModule.executeCommand(value);
},
),
)
],
),
);
}
}
class SettingsField<T> extends StatelessWidget {
String label = "Untitled Field";
T defaultValue;
SettingsField({super.key, this.label = "Untitled Field", required this.defaultValue});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white70,
width: 2
),
),
height: 50,
padding: EdgeInsets.all(4),
child: Row(
children: [
SizedBox(
width: 4
),
Text(
label,
style: GoogleFonts.teko(
fontSize: 25,
height: 1,
letterSpacing: 0.02,
color: Colors.white70
),
),
SizedBox(
width: 8
),
Expanded(
child: Container(
height: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white70,
width: 2
)
),
child: Text(
defaultValue.toString(),
style: GoogleFonts.teko(
fontSize: 25,
height: 1
)
)
),
),
SizedBox(
width: 4
),
AspectRatio(
aspectRatio: 1,
child: Stack(
children: [
Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white70,
width: 2
)
),
child: Icon(
Icons.edit,
color: Colors.white70,
size: 20,
)
),
Container(
padding: EdgeInsets.all(2),
child: Positioned.fill(
child: ElevatedButton(
onPressed: () {
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: Container()
),
),
)
],
)
)
],
)
);
}
}