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 createState() => _pages_SettingsState(); } class _pages_SettingsState extends State { 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 createState() => _AnnouncementUploadState(); } class _AnnouncementUploadState extends State { Future 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 createState() => _PermissionsSetupState(); } class _PermissionsSetupState extends State { List _hasPermissions = []; bool get hasPermissions { return !_hasPermissions.contains(false) && _hasPermissions.length > 0; } Future 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 createState() => _LoginFormState(); } class _LoginFormState extends State { @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 createState() => _ConsoleState(); } class _ConsoleState extends State { 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 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 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() ), ), ) ], ) ) ], ) ); } }