home_screen.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import 'package:flutter/material.dart';
  2. import 'package:permission_handler/permission_handler.dart';
  3. import 'package:gemini_live_app/services/permission_service.dart';
  4. import 'package:gemini_live_app/application/live_screen.dart';
  5. /// The initial screen of the application.
  6. ///
  7. /// This screen is responsible for checking and requesting necessary permissions
  8. /// before allowing the user to proceed to the [LiveScreen].
  9. class HomeScreen extends StatefulWidget {
  10. const HomeScreen({super.key});
  11. @override
  12. State<HomeScreen> createState() => _HomeScreenState();
  13. }
  14. class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
  15. final _permissionService = PermissionService();
  16. Map<Permission, PermissionStatus> _permissionStatuses = {};
  17. bool _checking = true;
  18. @override
  19. void initState() {
  20. super.initState();
  21. WidgetsBinding.instance.addObserver(this);
  22. _checkPermissions();
  23. }
  24. @override
  25. void dispose() {
  26. WidgetsBinding.instance.removeObserver(this);
  27. super.dispose();
  28. }
  29. /// Checks the status of all required permissions.
  30. Future<void> _checkPermissions() async {
  31. setState(() => _checking = true);
  32. final statuses = await _permissionService.checkAllPermissions();
  33. setState(() {
  34. _permissionStatuses = statuses;
  35. _checking = false;
  36. });
  37. }
  38. /// Requests a single permission from the user.
  39. ///
  40. /// After the permission is requested, it re-checks all permissions to update
  41. /// the UI.
  42. Future<void> _requestSinglePermission(Permission permission) async {
  43. final status = await _permissionService.requestPermission(permission);
  44. // Add a small delay to let iOS update permission state
  45. await Future.delayed(const Duration(milliseconds: 500));
  46. // Recheck all permissions after delay
  47. final recheckStatuses = await _permissionService.checkAllPermissions();
  48. setState(() {
  49. _permissionStatuses = recheckStatuses;
  50. });
  51. if (status.isGranted) {
  52. if (mounted) {
  53. ScaffoldMessenger.of(context).showSnackBar(
  54. SnackBar(
  55. content: Text(
  56. '${_permissionService.getPermissionName(permission)} permission granted!',
  57. ),
  58. backgroundColor: Colors.green,
  59. duration: const Duration(seconds: 2),
  60. ),
  61. );
  62. }
  63. } else if (status.isDenied) {
  64. await _permissionService.requestPermission(permission);
  65. } else if (status.isPermanentlyDenied) {
  66. if (mounted) {
  67. ScaffoldMessenger.of(context).showSnackBar(
  68. SnackBar(
  69. content: Text(
  70. '${_permissionService.getPermissionName(permission)} permission denied. Please enable it in Settings.',
  71. ),
  72. backgroundColor: Colors.orange,
  73. action: SnackBarAction(
  74. label: 'Open Settings',
  75. textColor: Colors.white,
  76. onPressed: () {
  77. _permissionService.openSettings();
  78. },
  79. ),
  80. duration: const Duration(seconds: 6),
  81. ),
  82. );
  83. }
  84. }
  85. }
  86. /// Navigates to the [LiveScreen].
  87. ///
  88. /// This method is called when all required permissions have been granted.
  89. /// After returning from the [LiveScreen], it refreshes the permission statuses.
  90. void _navigateToLive() async {
  91. await Navigator.of(
  92. context,
  93. ).push(MaterialPageRoute(builder: (context) => const LiveScreen()));
  94. // Refresh permissions when returning
  95. _checkPermissions();
  96. }
  97. /// Returns `true` if all required permissions are granted.
  98. bool get _allPermissionsGranted {
  99. if (_permissionStatuses.isEmpty) return false;
  100. final allGranted = _permissionStatuses.values.every(
  101. (status) => status.isGranted,
  102. );
  103. return allGranted;
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. return Scaffold(
  108. body: SafeArea(
  109. child: Padding(
  110. padding: const EdgeInsets.all(24.0),
  111. child: Column(
  112. crossAxisAlignment: CrossAxisAlignment.start,
  113. children: [
  114. const SizedBox(height: 40),
  115. const Text(
  116. 'Gemini Live',
  117. style: TextStyle(
  118. fontSize: 32,
  119. fontWeight: FontWeight.bold,
  120. color: Colors.white,
  121. ),
  122. ),
  123. const SizedBox(height: 16),
  124. Text(
  125. 'Experience real-time voice conversations with AI',
  126. style: TextStyle(fontSize: 16, color: Colors.grey[400]),
  127. ),
  128. const SizedBox(height: 48),
  129. const Text(
  130. 'Required Permissions',
  131. style: TextStyle(
  132. fontSize: 20,
  133. fontWeight: FontWeight.w600,
  134. color: Colors.white,
  135. ),
  136. ),
  137. const SizedBox(height: 24),
  138. if (_checking)
  139. const Center(child: CircularProgressIndicator())
  140. else
  141. ..._buildPermissionItems(),
  142. const Spacer(),
  143. const SizedBox(height: 16),
  144. SizedBox(
  145. width: double.infinity,
  146. height: 56,
  147. child: ElevatedButton(
  148. onPressed: _allPermissionsGranted ? _navigateToLive : null,
  149. style: ElevatedButton.styleFrom(
  150. backgroundColor: _allPermissionsGranted
  151. ? const Color(0xFFA8C7FA)
  152. : Colors.grey[800],
  153. foregroundColor: _allPermissionsGranted
  154. ? Colors.black
  155. : Colors.grey[600],
  156. shape: RoundedRectangleBorder(
  157. borderRadius: BorderRadius.circular(12),
  158. ),
  159. ),
  160. child: const Text(
  161. 'Start Conversation',
  162. style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  163. ),
  164. ),
  165. ),
  166. const SizedBox(height: 24),
  167. ],
  168. ),
  169. ),
  170. ),
  171. );
  172. }
  173. /// Builds the list of permission items.
  174. List<Widget> _buildPermissionItems() {
  175. final permissions = _permissionService.getRequiredPermissions();
  176. return permissions.map((permission) {
  177. final status = _permissionStatuses[permission];
  178. final granted = status?.isGranted ?? false;
  179. return Padding(
  180. padding: const EdgeInsets.only(bottom: 12.0),
  181. child: _buildPermissionItem(
  182. icon: _getIconData(permission),
  183. title: _permissionService.getPermissionName(permission),
  184. description: _permissionService.getPermissionDescription(permission),
  185. granted: granted,
  186. permission: permission,
  187. ),
  188. );
  189. }).toList();
  190. }
  191. /// Returns the appropriate icon for a given [Permission].
  192. IconData _getIconData(Permission permission) {
  193. switch (permission) {
  194. case Permission.microphone:
  195. return Icons.mic;
  196. case Permission.camera:
  197. return Icons.camera_alt;
  198. case Permission.photos:
  199. return Icons.photo_library;
  200. case Permission.notification:
  201. return Icons.notifications;
  202. default:
  203. return Icons.settings;
  204. }
  205. }
  206. /// Builds a single permission item widget.
  207. Widget _buildPermissionItem({
  208. required IconData icon,
  209. required String title,
  210. required String description,
  211. required bool granted,
  212. required Permission permission,
  213. }) {
  214. return Container(
  215. padding: const EdgeInsets.all(16),
  216. decoration: BoxDecoration(
  217. color: Colors.white.withAlpha(12),
  218. borderRadius: BorderRadius.circular(12),
  219. border: Border.all(
  220. color: granted
  221. ? const Color(0xFFA8C7FA).withAlpha(76)
  222. : Colors.grey.withAlpha(51),
  223. width: 1,
  224. ),
  225. ),
  226. child: Row(
  227. children: [
  228. Container(
  229. padding: const EdgeInsets.all(12),
  230. decoration: BoxDecoration(
  231. color: granted
  232. ? const Color(0xFFA8C7FA).withAlpha(51)
  233. : Colors.grey.withAlpha(25),
  234. borderRadius: BorderRadius.circular(8),
  235. ),
  236. child: Icon(
  237. icon,
  238. color: granted ? const Color(0xFFA8C7FA) : Colors.grey,
  239. size: 24,
  240. ),
  241. ),
  242. const SizedBox(width: 16),
  243. Expanded(
  244. child: Column(
  245. crossAxisAlignment: CrossAxisAlignment.start,
  246. children: [
  247. Text(
  248. title,
  249. style: const TextStyle(
  250. fontSize: 16,
  251. fontWeight: FontWeight.w600,
  252. color: Colors.white,
  253. ),
  254. ),
  255. const SizedBox(height: 4),
  256. Text(
  257. description,
  258. style: TextStyle(fontSize: 14, color: Colors.grey[400]),
  259. ),
  260. ],
  261. ),
  262. ),
  263. const SizedBox(width: 12),
  264. if (granted)
  265. const Icon(Icons.check_circle, color: Colors.green, size: 24)
  266. else
  267. ElevatedButton(
  268. onPressed: () => _requestSinglePermission(permission),
  269. style: ElevatedButton.styleFrom(
  270. backgroundColor: const Color(0xFFA8C7FA),
  271. foregroundColor: Colors.black,
  272. padding: const EdgeInsets.symmetric(
  273. horizontal: 16,
  274. vertical: 8,
  275. ),
  276. minimumSize: const Size(70, 36),
  277. shape: RoundedRectangleBorder(
  278. borderRadius: BorderRadius.circular(8),
  279. ),
  280. ),
  281. child: const Text(
  282. 'Grant',
  283. style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
  284. ),
  285. ),
  286. ],
  287. ),
  288. );
  289. }
  290. }