Canal VR - L'entree VR affiche VrScreen au lieu de « non configuree » : sous-onglet Configuration (AppConfigurationLinkScreen reutilise) et sous-onglet Casques (pincode d'appairage, etat, batterie, version, dernier vu). - Le dialogue de plan SuperAdmin porte aussi les canaux et les add-ons (assistant, contenu immersif). Activer un canal cree son ApplicationInstance s'il n'existe pas ; le desactiver ne supprime rien. Medias immersifs - Types Image360, Video360, Model3D : icones, libelles, extensions, apercu dans la Mediatheque et facettes. - Au depot, la pastille de type bascule Image <-> Image 360 et Video <-> Video 360 : rien dans un .jpg ne dit qu'il est equirectangulaire. Un .glb est reconnu seul. - ImageCompressor ne touche plus aux 360 ni aux GLB, sur les deux chemins d'upload : ramenee a 2560 px, une equirectangulaire devient illisible dans un casque. Scene3D - Nouveau type de section (famille des lieux) et son ecran de configuration : modele, mode objet/decor, points d'interet. - Fond immersif d'une configuration, visible si l'instance a l'add-on : le type est deduit de la ressource choisie, avec une image de repli. Client API edite a la main en miroir du backend : SectionScene3DApi, Scene3DDTO, ImmersiveBackgroundDTO, Position3D, AppType sur les devices, ResourceType etendu. Libelles FR/EN/NL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
4.2 KiB
Dart
129 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
|
|
import 'package:manager_app/Screens/Vr_devices/vr_devices_tab.dart';
|
|
import 'package:manager_app/Components/immersive_background_picker.dart';
|
|
import 'package:manager_app/Components/message_notification.dart';
|
|
import 'package:manager_app/app_context.dart';
|
|
import 'package:manager_app/constants.dart';
|
|
import 'package:manager_app/l10n/app_localizations.dart';
|
|
|
|
/// Le canal VR a les deux faces des autres canaux — ce qu'on diffuse et sur quoi
|
|
/// on le diffuse — mais contrairement au kiosk la flotte porte des informations
|
|
/// propres au casque (batterie, version, dernier vu) qui n'ont pas leur place
|
|
/// dans l'écran de configuration.
|
|
class VrScreen extends StatefulWidget {
|
|
final ApplicationInstanceDTO applicationInstanceDTO;
|
|
const VrScreen({super.key, required this.applicationInstanceDTO});
|
|
|
|
@override
|
|
State<VrScreen> createState() => _VrScreenState();
|
|
}
|
|
|
|
class _VrScreenState extends State<VrScreen> {
|
|
int _tab = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l = AppLocalizations.of(context)!;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_tabs(l),
|
|
const SizedBox(height: kSpace7),
|
|
Expanded(
|
|
child: _tab == 0
|
|
? Column(
|
|
children: [
|
|
Expanded(
|
|
child: AppConfigurationLinkScreen(
|
|
applicationInstanceDTO: widget.applicationInstanceDTO,
|
|
),
|
|
),
|
|
_backgroundCard(l),
|
|
],
|
|
)
|
|
: VrDevicesTab(
|
|
applicationInstanceDTO: widget.applicationInstanceDTO,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Le fond du menu d'accueil du casque. Il vit ici et pas dans l'écran de
|
|
/// configuration partagé : celui-ci sert aussi au mobile, au web et au kiosk, qui
|
|
/// n'ont pas de menu à décorer.
|
|
///
|
|
/// Enregistré à chaque changement, comme l'image principale juste au-dessus — c'est
|
|
/// la convention de cet écran, et deux comportements d'enregistrement sur la même
|
|
/// page se paient en contenus perdus.
|
|
Widget _backgroundCard(AppLocalizations l) {
|
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(kSpace5),
|
|
decoration: BoxDecoration(
|
|
color: kSurface,
|
|
border: Border(top: BorderSide(color: kLine)),
|
|
),
|
|
child: ImmersiveBackgroundPicker(
|
|
value: widget.applicationInstanceDTO.immersiveBackground,
|
|
title: l.backgroundTitle,
|
|
hint: l.backgroundHintMenu,
|
|
onChanged: (value) async {
|
|
setState(() => widget.applicationInstanceDTO.immersiveBackground = value);
|
|
|
|
final updated = await updateApplicationInstance(
|
|
appContext, widget.applicationInstanceDTO);
|
|
|
|
if (updated != null && mounted) {
|
|
showNotification(kSuccess, kWhite, l.appUpdatedSuccess, context, null);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tabs(AppLocalizations l) {
|
|
return Container(
|
|
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: kLine))),
|
|
child: Row(
|
|
children: [
|
|
_tabButton(l.vrTabConfig, 0),
|
|
_tabButton(l.vrTabHeadsets, 1),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tabButton(String label, int index) {
|
|
final selected = _tab == index;
|
|
return InkWell(
|
|
onTap: () => setState(() => _tab = index),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: selected ? kBrand : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: selected ? kInk : kInk3,
|
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|