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>
126 lines
4.5 KiB
Dart
126 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:manager_app/Components/resource_input_container.dart';
|
|
import 'package:manager_app/constants.dart';
|
|
import 'package:manager_app/l10n/app_localizations.dart';
|
|
|
|
/// Le fond immersif d'un lieu, tel que le gestionnaire le choisit.
|
|
///
|
|
/// **Le type n'est pas demandé, il est déduit** de la ressource choisie : une
|
|
/// `Image360` donne un panorama, une `Video360` une vidéo, un `Model3D` une scène.
|
|
/// Demander « panorama ou vidéo ? » après avoir fait choisir une vidéo, c'est poser
|
|
/// une question dont la réponse est déjà sur l'écran — et ouvrir la possibilité d'y
|
|
/// répondre faux, ce qui donnerait un fond noir sans explication.
|
|
///
|
|
/// Le repli est un champ séparé, et il est proposé à côté plutôt que caché : trois
|
|
/// canaux sur quatre ne rendent pas un panorama, et c'est lui qu'ils afficheront.
|
|
class ImmersiveBackgroundPicker extends StatefulWidget {
|
|
const ImmersiveBackgroundPicker({
|
|
super.key,
|
|
required this.value,
|
|
required this.onChanged,
|
|
required this.title,
|
|
required this.hint,
|
|
});
|
|
|
|
final ImmersiveBackgroundDTO? value;
|
|
final ValueChanged<ImmersiveBackgroundDTO?> onChanged;
|
|
final String title;
|
|
final String hint;
|
|
|
|
@override
|
|
State<ImmersiveBackgroundPicker> createState() => _ImmersiveBackgroundPickerState();
|
|
}
|
|
|
|
class _ImmersiveBackgroundPickerState extends State<ImmersiveBackgroundPicker> {
|
|
static const _immersiveTypes = [
|
|
ResourceType.Image360,
|
|
ResourceType.Video360,
|
|
ResourceType.Model3D,
|
|
];
|
|
|
|
static ImmersiveBackgroundKind _kindOf(ResourceType? type) {
|
|
if (type == ResourceType.Video360) return ImmersiveBackgroundKind.Video360;
|
|
if (type == ResourceType.Model3D) return ImmersiveBackgroundKind.Scene3D;
|
|
return ImmersiveBackgroundKind.Pano;
|
|
}
|
|
|
|
String _kindLabel(AppLocalizations l, ImmersiveBackgroundKind? kind) {
|
|
if (kind == ImmersiveBackgroundKind.Video360) return l.backgroundKindVideo360;
|
|
if (kind == ImmersiveBackgroundKind.Scene3D) return l.backgroundKindScene3D;
|
|
return l.backgroundKindPano;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l = AppLocalizations.of(context)!;
|
|
final value = widget.value;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(widget.title,
|
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
|
const SizedBox(height: kSpace2),
|
|
Text(widget.hint, style: kTextHint),
|
|
const SizedBox(height: kSpace4),
|
|
ResourceInputContainer(
|
|
label: l.backgroundResourceLabel,
|
|
initialValue: value?.resourceId,
|
|
inResourceTypes: _immersiveTypes,
|
|
onChanged: (ResourceDTO resource) {
|
|
final updated = ImmersiveBackgroundDTO(
|
|
resourceId: resource.id,
|
|
kind: _kindOf(resource.type),
|
|
fallbackResourceId: value?.fallbackResourceId,
|
|
);
|
|
setState(() {});
|
|
widget.onChanged(updated);
|
|
},
|
|
),
|
|
if (value?.resourceId != null) ...[
|
|
const SizedBox(height: kSpace2),
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.info_outline, size: 14, color: kInk3),
|
|
const SizedBox(width: kSpace2),
|
|
Expanded(
|
|
child: Text(_kindLabel(l, value!.kind), style: kTextHint),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: kSpace5),
|
|
ResourceInputContainer(
|
|
label: l.backgroundFallbackLabel,
|
|
initialValue: value.fallbackResourceId,
|
|
inResourceTypes: const [ResourceType.Image],
|
|
onChanged: (ResourceDTO resource) {
|
|
final updated = ImmersiveBackgroundDTO(
|
|
resourceId: value.resourceId,
|
|
kind: value.kind,
|
|
fallbackResourceId: resource.id,
|
|
);
|
|
setState(() {});
|
|
widget.onChanged(updated);
|
|
},
|
|
),
|
|
const SizedBox(height: kSpace2),
|
|
Text(l.backgroundFallbackHint, style: kTextHint),
|
|
const SizedBox(height: kSpace4),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextButton.icon(
|
|
onPressed: () {
|
|
setState(() {});
|
|
widget.onChanged(null);
|
|
},
|
|
icon: const Icon(Icons.close, size: 16),
|
|
label: Text(l.backgroundRemove),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|