import 'dart:io'; import 'package:bus_infotainment/audio_cache.dart'; import 'package:bus_infotainment/backend/live_information.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; class InitialStartup extends StatefulWidget { @override State createState() => _InitialStartupState(); } class _InitialStartupState extends State { int _page = 0; @override Widget build(BuildContext context) { if (_page == 0 && !kIsWeb) { _page = 1; } return Scaffold( body: [ _page1(this), _page2(this), _page3(this) ][_page], ); } void setPage(int page) { setState(() { _page = page; }); } } abstract class InitialStartupPage extends StatefulWidget { _InitialStartupState parent; InitialStartupPage(this.parent); } // Cookies page - only for web class _page1 extends InitialStartupPage { _page1(super.parent); @override State<_page1> createState() => _page1State(); } class _page1State extends State<_page1> { @override Widget build(BuildContext context) { // TODO: implement build return Container( padding: EdgeInsets.all(32), alignment: Alignment.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Cookies", style: TextStyle( fontSize: 32, fontWeight: FontWeight.w600, ) ), Text( "This website uses first-party cookies for the storage of user data. These cookies are necessary for the functioning of the site and help us provide you with a better browsing experience. By using this website, you consent to the use of these cookies.", textAlign: TextAlign.center, ), SizedBox( height: 8, ), ShadButton( onPressed: () { widget.parent.setPage(1); }, text: Text( "I understand and agree" ), ) ], ) ); } } // Permission request page class _page2 extends InitialStartupPage { _page2(super.parent); @override State<_page2> createState() => _page2State(); } class _page2State extends State<_page2> { Future _allPermissionsGranted() async { List perms = []; perms.addAll([ await Permission.manageExternalStorage.isGranted, await Permission.location.isGranted ]); // if (defaultTargetPlatform == TargetPlatform.android) { // perms.add( // await Permission.nearbyWifiDevices.isGranted // ); // } return !perms.contains(false); } @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(16), alignment: Alignment.center, child: SizedBox( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Permissions", style: TextStyle( fontSize: 32, fontWeight: FontWeight.w600, ) ), SizedBox( height: 16, ), Container( height: 210, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( // mainAxisSize: MainAxisSize.min, // crossAxisAlignment: CrossAxisAlignment.start, children: [ ShadCard( width: 300, height: double.infinity, title: Text( "Location", ), description: Text( "Your location is required for automatically updating your nearest bus stop." ), content: Container( child: Column( children: [ SizedBox( height: 4, ), FutureBuilder( future: Permission.location.isGranted, builder: (context, val) { bool isEnabled = true; String text = "Request permission"; Color color = Colors.white; if (val.hasData) { isEnabled = !val.data!; } if (!isEnabled) { text = "Permission granted!"; color = Colors.green.shade400; } return ShadButton( text: Text(text), onPressed: () async { await Permission.location.request(); setState(() { }); }, enabled: isEnabled, backgroundColor: color, ); }, ), ], ), ), ), if (!kIsWeb) SizedBox( width: 16, ), if (!kIsWeb) ShadCard( width: 300, height: double.infinity, title: Text( "Storage", ), description: Text( "Storage access is required to access recorded announcements." ), content: Container( child: Column( children: [ SizedBox( height: 4, ), FutureBuilder( future: Permission.manageExternalStorage.isGranted, builder: (context, val) { bool isEnabled = true; String text = "Request permission"; Color color = Colors.white; if (val.hasData) { isEnabled = !val.data!; } if (!isEnabled) { text = "Permission granted!"; color = Colors.green.shade400; } return ShadButton( text: Text(text), onPressed: () async { await Permission.manageExternalStorage.request(); setState(() { }); }, enabled: isEnabled, backgroundColor: color, ); }, ) ], ), ), ), if (defaultTargetPlatform == TargetPlatform.android) SizedBox( width: 16, ), if (defaultTargetPlatform == TargetPlatform.android && false) ShadCard( width: 300, height: double.infinity, title: Text( "Nearby Devices", ), description: Text( "Nearby Devices access is required to find nearby devices, and to establish connections with them." ), content: Container( child: Column( children: [ SizedBox( height: 4, ), FutureBuilder( future: Permission.nearbyWifiDevices.isGranted, builder: (context, val) { bool isEnabled = true; String text = "Request permission"; Color color = Colors.white; if (val.hasData) { isEnabled = !val.data!; } if (!isEnabled) { text = "Permission granted!"; color = Colors.green.shade400; } return ShadButton( text: Text(text), onPressed: () async { await Permission.nearbyWifiDevices.request(); setState(() { }); }, enabled: isEnabled, backgroundColor: color, ); }, ) ], ), ), ), /*SizedBox( width: 16, ), ShadCard( width: 300, height: 200, title: Text( "Network", ), description: Text( "Network access is required for commincation between devices for multi mode." ), content: Container( child: Column( children: [ SizedBox( height: 4, ), FutureBuilder( future: Permission.nearbyWifiDevices.isGranted, builder: (context, val) { bool isEnabled = true; String text = "Request permission"; Color color = Colors.white; if (val.hasData) { isEnabled = !val.data!; } if (!isEnabled) { text = "Permission granted!"; color = Colors.green.shade400; } return ShadButton( text: Text(text), onPressed: () async { await Permission.manageExternalStorage.request(); setState(() { }); }, enabled: isEnabled, backgroundColor: color, ); }, ) ], ), ), ),*/ ], ), ), ), SizedBox( height: 16, ), FutureBuilder( future: _allPermissionsGranted(), builder: (context, val) { bool isEnabled = true; String text = "Continue"; Color color = Colors.white; if (val.hasData) { isEnabled = val.data!; } if (!isEnabled) { text = "Grant all permissions before continuing"; } return ShadButton( text: Text(text), onPressed: () async { widget.parent.setPage(2); }, enabled: isEnabled, backgroundColor: color, width: double.infinity, ); }, ) ], ), ) ); } } class _page3 extends InitialStartupPage { _page3(super.parent); @override State<_page3> createState() => _page3State(); } class _page3State extends State<_page3> { bool _loadingAudio = false; Future _announcementsUploaded() async { try { Uint8List bytes = await LiveInformation().announcementModule.getBundleBytes(); return true; } catch (e) { return false; } } @override Widget build(BuildContext context) { // TODO: implement build return Container( padding: EdgeInsets.all(16), alignment: Alignment.center, child: SizedBox( width: double.infinity, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Prerequisites", style: TextStyle( fontSize: 32, fontWeight: FontWeight.w600, ) ), SizedBox( height: 16, ), ShadCard( width: double.infinity, title: Text( "Announcement files", ), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "You are required to upload the London iBus announcement files. These files can be acquire by submitting a request to TfL under the Freedom of Information Act (2000)" ), SizedBox( height: 4, ), Text( "Please upload a zip file." ) ], ), content: Container( child: Column( children: [ SizedBox( height: 4, ), if (!_loadingAudio) FutureBuilder( future: _announcementsUploaded(), builder: (context, val) { bool isEnabled = true; String text = "Upload file"; Color color = Colors.white; if (val.hasData) { isEnabled = !val.data!; } if (!isEnabled) { text = "File uploaded!"; color = Colors.green.shade400; } return ShadButton( text: Text(text), onPressed: () async { setState(() { _loadingAudio = true; }); FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: [ "zip" ] ); if (result != null) { 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); AnnouncementCache cache = LiveInformation().announcementModule.announcementCache; if (!kIsWeb) { // Use shared preferences to store the file location SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString("AnnouncementsFileLocation", result.files.single.path!); } // load a random announcement to ensure that the file is usable await cache.loadAnnouncementsFromBytes(bytes, ["S_WALTHAMSTOW_CENTRAL_001.mp3"]); setState(() { _loadingAudio = false; }); } else { setState(() { _loadingAudio = false; }); } }, enabled: isEnabled, backgroundColor: color, ); }, ) else CircularProgressIndicator(), ], ), ), ), SizedBox( height: 16, ), FutureBuilder( future: _announcementsUploaded(), builder: (context, val) { bool isEnabled = true; String text = "Continue"; Color color = Colors.white; if (val.hasData) { isEnabled = val.data!; } if (!isEnabled) { text = "Complete the prerequisites to continue"; } return ShadButton( text: Text(text), onPressed: () async { LiveInformation().initTrackerModule(); showShadDialog( context: context, builder: (context) => ShadDialog.alert( title: Text("You're all setup"), description: Text("You can now continue to the application. \nTry not to annoy anyone on the bus! ;)"), actions: [ ShadButton( text: Text("Continue to application"), onPressed: () { Navigator.pushNamed(context, "/"); }, ) ], ), barrierDismissible: false ); }, enabled: isEnabled, backgroundColor: color, width: double.infinity, ); }, ) ], ), ) ); } }