import 'dart:html'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Models/menu.dart'; import 'package:manager_app/Models/menuSection.dart'; import 'package:manager_app/Screens/ApiKeys/api_keys_screen.dart'; import 'package:manager_app/Screens/Audit/audit_screen.dart'; import 'package:manager_app/Screens/Configurations/configurations_screen.dart'; import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart'; import 'package:manager_app/Screens/Resources/resources_screen.dart'; import 'package:manager_app/Screens/Statistics/statistics_screen.dart'; import 'package:manager_app/Screens/GuideIa/guide_ia_screen.dart'; import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart'; import 'package:manager_app/Screens/Applications/web_app_screen.dart'; import 'package:manager_app/Screens/Billing/subscription_screen.dart'; import 'package:manager_app/Screens/Notifications/notifications_screen.dart'; import 'package:manager_app/Screens/Users/users_screen.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/Components/quota_bars_widget.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/main.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; class MainScreen extends StatefulWidget { final InstanceDTO instance; final String? view; MainScreen({Key? key, required this.instance, required this.view}) : super(key: key); @override _MainScreenState createState() => _MainScreenState(); } class _MainScreenState extends State { late MenuSection devices; late MenuSection configurations; late MenuSection resources; Menu menu = Menu(title: "MyInfoMate"); Widget? selectedElement = null; final ValueNotifier currentPosition = ValueNotifier(null); // Keys for nudge animation — one per section type final _nudgeKeys = >{}; GlobalKey<_NudgeIconState> _nudgeKey(String type) => _nudgeKeys.putIfAbsent(type, () => GlobalKey<_NudgeIconState>()); /// Construit le menu à partir des applications déclarées sur l'instance. /// /// Appelé à l'ouverture et à chaque changement d'instance : les entrées /// Applications / Configurations / Ressources dépendent de l'instance, et une /// instance sans application mobile n'a pas de sous-entrée « Mobile ». void _buildMenu(InstanceDTO instance) { devices = MenuSection(type: "devices", menuId: 0, subMenu: []); if (instance.isMobile == true) devices.subMenu.add(MenuSection(type: "mobile", menuId: 1, subMenu: [])); if (instance.isTablet == true) devices.subMenu.add(MenuSection(type: "kiosk", menuId: 2, subMenu: [])); if (instance.isWeb == true) devices.subMenu.add(MenuSection(type: "web", menuId: 3, subMenu: [])); if (instance.isVR == true) devices.subMenu.add(MenuSection(type: "vr", menuId: 4, subMenu: [])); configurations = MenuSection(type: "configurations", menuId: 5, subMenu: []); resources = MenuSection(type: "resources", menuId: 6, subMenu: []); menu.sections = [devices, configurations, resources]; if (instance.hasStats == true) { menu.sections!.add(MenuSection(type: "statistics", menuId: 7, subMenu: [])); } if (instance.isAssistant == true) { menu.sections!.add(MenuSection(type: "guide-ia", menuId: 12, subMenu: [])); } } /// Première application déclarée par l'instance, dans l'ordre du menu. int _defaultPosition(InstanceDTO instance) { if (instance.isMobile == true) return 1; if (instance.isTablet == true) return 2; if (instance.isWeb == true) return 3; return 4; } @override void initState() { super.initState(); _buildMenu(widget.instance); _renderedInstanceId = widget.instance.id; currentPosition.value ??= _defaultPosition(widget.instance); selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance); } /// L'instance sur laquelle le menu et l'écran courant ont été construits. String? _renderedInstanceId; /// Reconstruit le menu et recale la position quand on a changé d'instance. void _applyInstance(InstanceDTO instance) { _buildMenu(instance); _renderedInstanceId = instance.id; final positions = menu.sections! .expand((s) => [s.menuId, ...s.subMenu.map((sub) => sub.menuId)]) .toSet(); if (!positions.contains(currentPosition.value)) { currentPosition.value = _defaultPosition(instance); } } /// Le changement d'instance passe par la même route : Flutter réutilise cet /// State et `initState` ne rejoue pas. Sans cela le menu reste celui de /// l'instance précédente, et la position courante peut désigner une /// application que la nouvelle instance ne déclare pas. @override void didUpdateWidget(covariant MainScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.instance.id == widget.instance.id) return; _applyInstance(widget.instance); selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance); } Future _showAssignPlanDialog(BuildContext context, ManagerAppContext managerCtx, Instance inst, {void Function()? onSaved}) async { List? plans; InstanceDTO? instanceDetail; try { plans = await managerCtx.clientAPI!.subscriptionPlanApi!.subscriptionPlanGet(); instanceDetail = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id); } catch (_) {} if (!context.mounted) return; final planList = plans ?? []; String? selectedPlanId = instanceDetail?.subscriptionPlanId; showDialog( context: context, builder: (dialogContext) => StatefulBuilder( builder: (dialogContext, setDialogState) => AlertDialog( title: Text(AppLocalizations.of(context)!.planDialogTitle(inst.name ?? '')), content: SizedBox( width: 360, child: planList.isEmpty ? Text(AppLocalizations.of(context)!.noPlansAvailable) : Column( mainAxisSize: MainAxisSize.min, children: [ RadioListTile( title: Text(AppLocalizations.of(context)!.noPlan), value: null, groupValue: selectedPlanId, onChanged: (v) => setDialogState(() => selectedPlanId = v), ), ...planList.map((plan) => RadioListTile( title: Text(plan.name), subtitle: Text( '${_formatQuotaBytes(plan.storageQuotaBytes, unlimitedLabel: AppLocalizations.of(dialogContext)!.unlimitedStorage)} · ${plan.aiTokensPerMonth == 0 ? AppLocalizations.of(dialogContext)!.unlimitedAI : AppLocalizations.of(dialogContext)!.aiTokensPerMonth(_formatTokens(plan.aiTokensPerMonth))}', style: const TextStyle(fontSize: 11), ), value: plan.id, groupValue: selectedPlanId, onChanged: (v) => setDialogState(() => selectedPlanId = v), )), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext, rootNavigator: true).pop(), child: Text(AppLocalizations.of(dialogContext)!.cancel), ), TextButton( onPressed: () async { Navigator.of(dialogContext, rootNavigator: true).pop(); try { await managerCtx.clientAPI!.instanceApi!.instanceUpdateinstance( InstanceDTO( id: instanceDetail?.id, name: instanceDetail?.name, pinCode: instanceDetail?.pinCode, isPushNotification: instanceDetail?.isPushNotification, hasStats: instanceDetail?.hasStats, hasAdvancedStats: instanceDetail?.hasAdvancedStats, isMobile: instanceDetail?.isMobile, isTablet: instanceDetail?.isTablet, isWeb: instanceDetail?.isWeb, isVR: instanceDetail?.isVR, isAssistant: instanceDetail?.isAssistant, aiTokensThisMonth: instanceDetail?.aiTokensThisMonth, aiUsageMonthKey: instanceDetail?.aiUsageMonthKey, subscriptionPlanId: selectedPlanId ?? '', ), ); onSaved?.call(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.planUpdated))); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorMessage(e.toString())))); } } }, child: Text(AppLocalizations.of(dialogContext)!.save), ), ], ), ), ); } static String _formatQuotaBytes(int? bytes, {required String unlimitedLabel}) { if (bytes == null || bytes == 0) return unlimitedLabel; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(0)} MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } static String _formatTokens(int? tokens) { if (tokens == null || tokens == 0) return '0'; if (tokens < 1000) return '$tokens'; if (tokens < 1000000) return '${(tokens / 1000).toStringAsFixed(0)}K'; return '${(tokens / 1000000).toStringAsFixed(1)}M'; } Future _showSwitchInstanceDialog(BuildContext context, ManagerAppContext managerCtx) async { List? instances; try { instances = await managerCtx.clientAPI!.instanceApi!.instanceGet(); } catch (_) {} if (!context.mounted) return; final instanceList = instances ?? []; showDialog( context: context, builder: (_) => AlertDialog( title: Text(AppLocalizations.of(context)!.switchInstance), content: SizedBox( width: 400, child: instanceList.isEmpty ? Text(AppLocalizations.of(context)!.noInstanceFound) : ListView.builder( shrinkWrap: true, itemCount: instanceList.length, itemBuilder: (_, i) { final inst = instanceList[i]; final isCurrent = inst.id == managerCtx.instanceId; return ListTile( leading: Icon( Icons.business, color: isCurrent ? kPrimaryColor : kBodyTextColor, ), title: Text( inst.name, style: TextStyle( color: isCurrent ? kPrimaryColor : kBodyTextColor, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal, ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (isCurrent) const Icon(Icons.check, color: Colors.green), IconButton( icon: const Icon(Icons.edit_outlined, size: 18), tooltip: AppLocalizations.of(context)!.configurePlan, onPressed: () => _showAssignPlanDialog(context, managerCtx, inst, onSaved: () { Navigator.of(context, rootNavigator: true).pop(); }), ), ], ), onTap: isCurrent ? null : () async { Navigator.of(context, rootNavigator: true).pop(); final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id); if (newInstance != null && context.mounted) { setState(() { managerCtx.instanceId = newInstance.id; managerCtx.instanceDTO = newInstance; // Une configuration ou une section restée // sélectionnée appartient à l'instance qu'on // vient de quitter : l'écran de détail la // réaffichait telle quelle. managerCtx.selectedConfiguration = null; managerCtx.selectedSection = null; managerCtx.selectedSubSection = null; managerCtx.selectedSubSectionRawData = null; }); Provider.of(context, listen: false) .setContext(managerCtx); final view = newInstance.isMobile! ? 'mobile' : newInstance.isTablet! ? 'kiosk' : newInstance.isWeb! ? 'web' : 'vr'; context.go('/main/$view'); } }, ); }, ), ), actions: [ TextButton(onPressed: () => Navigator.of(context, rootNavigator: true).pop(), child: Text(AppLocalizations.of(context)!.close)), ], ), ); } static String _localizedSectionName(BuildContext context, String type) { final l = AppLocalizations.of(context)!; switch (type) { case 'devices': return l.menuApplications; case 'configurations': return l.menuConfigurations; case 'resources': return l.menuResources; case 'statistics': return l.menuStatistics; case 'guide-ia': return l.menuGuideIa; case 'notifications': return l.menuNotifications; case 'users': return l.menuUsers; case 'apikeys': return l.menuApiKeys; case 'audit': return l.menuAudit; case 'subscription': return l.menuSubscription; case 'mobile': return l.menuMobile; case 'kiosk': return l.menuKiosk; case 'web': return l.menuWeb; case 'vr': return l.menuVr; default: return type; } } static IconData _sectionIcon(String type) { switch (type) { case 'devices': return Icons.apps; case 'configurations': return Icons.settings_outlined; case 'resources': return Icons.folder_open_outlined; case 'statistics': return Icons.bar_chart; case 'guide-ia': return Icons.record_voice_over_outlined; case 'notifications': return Icons.notifications_none; case 'users': return Icons.people_outline; case 'apikeys': return Icons.vpn_key_outlined; case 'audit': return Icons.history; case 'subscription': return Icons.workspace_premium_outlined; default: return Icons.circle_outlined; } } Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) { return Container( width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width child: Card( color: kWhite, margin: const EdgeInsets.symmetric(vertical: 8), elevation: 0, child: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Container( height: 150, width: 250, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( constraints: BoxConstraints(maxHeight: 60, minHeight: 40), child: SvgPicture.asset('assets/images/MyInfoMate_logo_only.svg') ), SizedBox(height: 16), Text( menu.title, style: TextStyle( color: kPrimaryColor, fontSize: 25, fontWeight: FontWeight.w400, fontFamily: "Helvetica", ), ), ], ), ), ), SizedBox(height: 25), Expanded( child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ListView( padding: EdgeInsets.zero, children: menu.sections!.map((section) { final router = GoRouter.of(context); final routeMatchList = router.routerDelegate.currentConfiguration; var currentPath = routeMatchList.isNotEmpty ? routeMatchList.matches.first.matchedLocation : null; if (section.subMenu.isEmpty) { return Container( key: ValueKey(section.type), decoration: currentPath!.contains(section.type) ? BoxDecoration( border: Border( right: BorderSide( color: kPrimaryColor, width: 2, ), ), ) : null, child: ListTile( leading: _NudgeIcon( key: _nudgeKey(section.type), icon: _sectionIcon(section.type), isActive: currentPath.contains(section.type), color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, size: 22, ), title: Text(_localizedSectionName(context, section.type), style: TextStyle(color: currentPath.contains(section.type) ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: currentPath.contains(section.type) ? FontWeight.w500 : FontWeight.w100)), selected: currentPosition == section.menuId, onTap: () { WidgetsBinding.instance.addPostFrameCallback((_) => _nudgeKey(section.type).currentState?.nudge()); context.go('/main/${section.type}'); if (isDrawer) Navigator.of(context).pop(); }, ), ); } else { final isAppActive = currentPath!.contains("mobile") || currentPath.contains("kiosk") || currentPath.contains("web") || currentPath.contains("vr"); return Container( key: ValueKey(section.type), child: ExpansionTile( leading: _NudgeIcon( key: _nudgeKey(section.type), icon: _sectionIcon(section.type), isActive: isAppActive, color: isAppActive ? kPrimaryColor : kBodyTextColor, size: 22, ), iconColor: isAppActive ? kPrimaryColor : kBodyTextColor, collapsedIconColor: isAppActive ? kPrimaryColor : kBodyTextColor, title: Text(_localizedSectionName(context, section.type), style: TextStyle(color: isAppActive ? kPrimaryColor : kBodyTextColor, fontSize: 22, fontWeight: isAppActive ? FontWeight.w500 : FontWeight.w100)), children: section.subMenu.map((subSection) { return Container( decoration: currentPath.contains(subSection.type) ? BoxDecoration( border: Border( right: BorderSide( color: kPrimaryColor, width: 2, ), ), ) : null, child: ListTile( title: Padding( padding: const EdgeInsets.only(left: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ subSection.type == "mobile" ? Icon(Icons.phone_iphone, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) : subSection.type == "kiosk" ? Icon(Icons.tablet_rounded, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) : subSection.type == "web" ? Icon(Icons.public_outlined, color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, size: 20) : subSection.type == "vr" ? Icon(Icons.panorama_photosphere, color: currentPath.contains(subSection.type)? kPrimaryColor : kBodyTextColor, size: 20) : SizedBox(), Padding( padding: const EdgeInsets.all(8.0), child: Text(_localizedSectionName(context, subSection.type), style: TextStyle(color: currentPath.contains(subSection.type) ? kPrimaryColor : kBodyTextColor, fontSize: 18)), ) ], ), ), selected: currentPosition.value == subSection.menuId, onTap: () { if (currentPath != null && currentPath.contains(subSection.type)) { // already on this page } else { WidgetsBinding.instance.addPostFrameCallback((_) => _nudgeKey(section.type).currentState?.nudge()); context.go('/main/${subSection.type}'); } if (isDrawer) Navigator.of(context).pop(); }, ), ); }).toList(), ), ); } }).toList(), ), ), ), // Footer: Quota + Language + Email + Switch instance (SuperAdmin) + Logout Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ const QuotaBarsWidget(), Row( children: [ DropdownButton( value: managerAppContext.locale, underline: const SizedBox(), isDense: true, items: const [ DropdownMenuItem(value: Locale('fr'), child: Text('🇫🇷 FR')), DropdownMenuItem(value: Locale('en'), child: Text('🇬🇧 EN')), DropdownMenuItem(value: Locale('nl'), child: Text('🇧🇪 NL')), ], onChanged: (locale) { if (locale != null) appContext.setLocale(locale); }, ), const SizedBox(width: 8), Expanded( child: AutoSizeText( (appContext.getContext() as ManagerAppContext).email ?? "", style: TextStyle(color: kBodyTextColor, fontSize: 14, fontWeight: FontWeight.w300, fontFamily: "Helvetica"), maxLines: 1, ), ), if ((appContext.getContext() as ManagerAppContext).role == UserRole.SuperAdmin) IconButton( icon: Icon(Icons.swap_horiz, color: kPrimaryColor), tooltip: AppLocalizations.of(context)!.tooltipSwitchInstance, onPressed: () => _showSwitchInstanceDialog(context, appContext.getContext() as ManagerAppContext), ), IconButton( icon: Icon(Icons.logout, color: kPrimaryColor), tooltip: AppLocalizations.of(context)!.tooltipLogout, onPressed: () async { await loadJsonSessionFile(); setState(() { Storage localStorage = window.localStorage; localStorage.clear(); ManagerAppContext managerAppContext = appContext.getContext(); managerAppContext.accessToken = null; managerAppContext.instanceId = null; managerAppContext.instanceDTO = null; appContext.setContext(managerAppContext); context.go('/login'); }); }, ), ], ) ], ), ), ], ), ), ); } @override Widget build(BuildContext context) { final appContext = Provider.of(context); ManagerAppContext managerAppContext = appContext.getContext(); // `widget.instance` est figée à la construction de la route. Or changer // d'instance en restant sur la même vue ne change pas l'URL, donc GoRouter // ne rejoue pas son builder et `didUpdateWidget` ne se déclenche jamais : // l'écran continuait d'afficher l'instance précédente jusqu'à ce qu'on // change d'onglet. C'est le contexte qui fait foi, pas le widget. final instance = managerAppContext.instanceDTO ?? widget.instance; if (instance.id != _renderedInstanceId) { _applyInstance(instance); } // Synchronise les items de menu sensibles au rôle à chaque rebuild final role = managerAppContext.role; final hasAdminItems = menu.sections!.any((s) => s.menuId == 8); final hasNotifItem = menu.sections!.any((s) => s.menuId == 10); final hasSubscriptionItem = menu.sections!.any((s) => s.menuId == 11); if (managerAppContext.instanceDTO?.subscriptionPlanId == "plan-essentiel" && !hasSubscriptionItem) { menu.sections!.add(MenuSection(type: "subscription", menuId: 11, subMenu: [])); } else if (managerAppContext.instanceDTO?.subscriptionPlanId != "plan-essentiel" && hasSubscriptionItem) { menu.sections!.removeWhere((s) => s.menuId == 11); } if (role != null && role.value <= 1 && managerAppContext.instanceDTO?.isPushNotification == true && !hasNotifItem) { menu.sections!.add(MenuSection(type: "notifications", menuId: 10, subMenu: [])); } else if ((role == null || role.value > 1 || managerAppContext.instanceDTO?.isPushNotification != true) && hasNotifItem) { menu.sections!.removeWhere((s) => s.menuId == 10); } if (role != null && role.value <= 1 && !hasAdminItems) { menu.sections!.add(MenuSection(type: "users", menuId: 8, subMenu: [])); menu.sections!.add(MenuSection(type: "apikeys", menuId: 9, subMenu: [])); } else if ((role == null || role.value > 1) && hasAdminItems) { menu.sections!.removeWhere((s) => s.menuId == 8 || s.menuId == 9); } // Le journal d'activité couvre toutes les instances et expose les valeurs avant/après : // l'endpoint est sous policy SuperAdmin, l'entrée de menu suit la même règle. final hasAuditItem = menu.sections!.any((s) => s.menuId == 13); if (role != null && role.value == 0 && !hasAuditItem) { menu.sections!.add(MenuSection(type: "audit", menuId: 13, subMenu: [])); } else if ((role == null || role.value != 0) && hasAuditItem) { menu.sections!.removeWhere((s) => s.menuId == 13); } Size size = MediaQuery.of(context).size; bool isMobile = size.width < 850; return Scaffold( appBar: isMobile ? AppBar( title: Text(menu.title, style: TextStyle(color: kWhite)), backgroundColor: kPrimaryColor, leading: Builder( builder: (context) => IconButton( icon: Icon(Icons.menu, color: kWhite), onPressed: () => Scaffold.of(context).openDrawer(), ), ), ) : null, drawer: isMobile ? ValueListenableBuilder( valueListenable: currentPosition, builder: (context, value, _) { return Drawer(child: buildMenu(context, appContext, managerAppContext, true)); } ) : null, body: Row( children: [ if (!isMobile) ValueListenableBuilder( valueListenable: currentPosition, builder: (context, value, _) { return buildMenu(context, appContext, managerAppContext, false); } ), Expanded( child: ValueListenableBuilder( valueListenable: currentPosition, builder: (context, value, _) { selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, instance); // La clé porte l'instance : sans elle les écrans en dessous // gardent leur State, donc leurs futures déjà résolues sur // l'instance précédente. return Padding( padding: const EdgeInsets.all(8.0), child: KeyedSubtree( key: ValueKey(instance.id), child: selectedElement!, ), ); } ), ), ], ), ); } } initElementToShow(BuildContext context, String? view, int currentPosition, Menu menu, InstanceDTO instanceDTO) { if(view != null) { switch (view) { case "mobile": currentPosition = 1; break; case "kiosk": currentPosition = 2; break; case "web": currentPosition = 3; break; case "vr": currentPosition = 4; break; case "configurations": currentPosition = 5; break; case "resources": currentPosition = 6; break; case "statistics": currentPosition = 7; break; case "users": currentPosition = 8; break; case "apikeys": currentPosition = 9; break; case "notifications": currentPosition = 10; break; case "subscription": currentPosition = 11; break; case "guide-ia": currentPosition = 12; break; case "audit": currentPosition = 13; break; } } MenuSection? elementToShow = menu.sections!.firstWhereOrNull((s) => s.menuId == currentPosition); elementToShow ??= menu.sections! .expand((s) => s.subMenu) .firstWhereOrNull((s) => s.menuId == currentPosition); // Position sans entrée correspondante : on retombe sur la première entrée // disponible plutôt que de laisser l'écran planter. elementToShow ??= menu.sections!.firstWhereOrNull((s) => s.subMenu.isNotEmpty)?.subMenu.first ?? menu.sections!.firstOrNull; if (elementToShow == null) { return Center(child: Text(AppLocalizations.of(context)!.noAppConfigured)); } switch (elementToShow.type) { case 'mobile' : var applicationInstanceMobile = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.Mobile); if (applicationInstanceMobile == null) return Center(child: Text(AppLocalizations.of(context)!.mobileAppNotConfigured)); return Padding( padding: const EdgeInsets.all(8.0), child: AppConfigurationLinkScreen(applicationInstanceDTO: applicationInstanceMobile) ); case 'kiosk' : var applicationInstanceTablet = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.Tablet); if (applicationInstanceTablet == null) return Center(child: Text(AppLocalizations.of(context)!.kioskAppNotConfigured)); return Padding( padding: const EdgeInsets.all(8.0), child: KioskScreen(applicationInstanceDTO: applicationInstanceTablet) ); case 'web' : return const WebInstanceLoader(); case 'vr' : var applicationInstanceVR = instanceDTO.applicationInstanceDTOs?.firstWhereOrNull((ai) => ai.appType == AppType.VR); if (applicationInstanceVR == null) return Center(child: Text(AppLocalizations.of(context)!.vrAppNotConfigured)); return Padding( padding: const EdgeInsets.all(8.0), child: Text(AppLocalizations.of(context)!.vrAppNotConfigured) ); case 'configurations' : return Padding( padding: const EdgeInsets.all(8.0), child: ConfigurationsScreen() ); case 'resources' : return Padding( padding: const EdgeInsets.all(8.0), child: ResourcesScreen( resourceTypes: [ ResourceType.Audio, ResourceType.Image, ResourceType.ImageUrl, ResourceType.Video, ResourceType.VideoUrl, ResourceType.Pdf, ResourceType.Json, ResourceType.JsonUrl ] ) ); case 'statistics' : return const Padding( padding: EdgeInsets.all(8.0), child: StatisticsScreen() ); case 'guide-ia' : return const Padding( padding: EdgeInsets.all(8.0), child: GuideIaScreen() ); case 'users': return const Padding( padding: EdgeInsets.all(8.0), child: UsersScreen() ); case 'apikeys': return const Padding( padding: EdgeInsets.all(8.0), child: ApiKeysScreen() ); case 'audit': return const Padding( padding: EdgeInsets.all(8.0), child: AuditScreen() ); case 'notifications': return const Padding( padding: EdgeInsets.all(8.0), child: NotificationsScreen() ); case 'subscription': return const Padding( padding: EdgeInsets.all(8.0), child: SubscriptionScreen() ); default: return Center(child: Text(AppLocalizations.of(context)!.noData)); } } class _NudgeIcon extends StatefulWidget { final IconData icon; final bool isActive; final Color color; final double size; const _NudgeIcon({Key? key, required this.icon, required this.isActive, required this.color, required this.size}) : super(key: key); @override State<_NudgeIcon> createState() => _NudgeIconState(); } class _NudgeIconState extends State<_NudgeIcon> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; late final Animation _dx; @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); _dx = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 0.0, end: 5.0), weight: 35), TweenSequenceItem(tween: Tween(begin: 5.0, end: 0.0), weight: 65), ]).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut)); } void nudge() => _ctrl.forward(from: 0); @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _dx, builder: (_, child) => Transform.translate( offset: Offset(_dx.value, 0), child: child, ), child: Icon(widget.icon, color: widget.color, size: widget.size), ); } }