manager-app/lib/Screens/Resources/resource_formatting.dart
Thomas Fransolet c8947880ba Ecran VR, section Scene3D et medias immersifs
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>
2026-09-13 16:54:13 +02:00

130 lines
4.7 KiB
Dart

import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_api_new/api.dart';
/// Extension réelle d'une ressource, dans l'ordre : le nom de fichier stocké s'il
/// existe, sinon celle que le type impose. Le téléchargement forçait `.json` pour
/// tout le monde — un PNG arrivait en `label.json`, illisible sans renommage.
const Map<ResourceType, String> _extensionByType = {
ResourceType.Image: '.jpg',
ResourceType.ImageUrl: '.jpg',
ResourceType.Audio: '.mp3',
ResourceType.Video: '.mp4',
ResourceType.VideoUrl: '.mp4',
ResourceType.Pdf: '.pdf',
ResourceType.Json: '.json',
ResourceType.JsonUrl: '.json',
ResourceType.Image360: '.jpg',
ResourceType.Video360: '.mp4',
ResourceType.Model3D: '.glb',
};
/// Type d'une ressource locale, déduit de son extension. Un seul endroit : le
/// dialogue d'ajout l'affiche en pastille, `create()` l'enregistre. Avant, le
/// dialogue posait `ResourceType.Image` pour tout fichier et `create()` le
/// recalculait — à la lecture, un MP3 semblait parti pour être une image.
ResourceType? resourceTypeForExtension(String? extension) {
switch (extension?.toLowerCase()) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return ResourceType.Image;
case 'mp3':
return ResourceType.Audio;
case 'mp4':
case 'webm':
return ResourceType.Video;
case 'pdf':
return ResourceType.Pdf;
case 'json':
return ResourceType.Json;
// Un .glb n'est jamais autre chose qu'un modèle 3D — contrairement à un .jpg,
// qui peut être plat ou équirectangulaire sans que le fichier le dise.
case 'glb':
return ResourceType.Model3D;
default:
return null;
}
}
String mimeTypeForExtension(String? extension) {
switch (extension?.toLowerCase()) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'mp3':
return 'audio/mpeg';
case 'mp4':
return 'video/mp4';
case 'webm':
return 'video/webm';
case 'pdf':
return 'application/pdf';
case 'json':
return 'application/json';
case 'glb':
return 'model/gltf-binary';
default:
return 'application/octet-stream';
}
}
/// Libellé lisible d'un type, dans la langue du manager.
String resourceTypeLabel(AppLocalizations l, ResourceType? type) {
switch (type) {
case ResourceType.Image: return l.resourceTypeImage;
case ResourceType.ImageUrl: return l.resourceTypeImageUrl;
case ResourceType.Video: return l.resourceTypeVideo;
case ResourceType.VideoUrl: return l.resourceTypeVideoUrl;
case ResourceType.Audio: return l.resourceTypeAudio;
case ResourceType.Pdf: return l.resourceTypePdf;
case ResourceType.Json: return l.resourceTypeJson;
case ResourceType.JsonUrl: return l.resourceTypeJsonUrl;
case ResourceType.Image360: return l.resourceTypeImage360;
case ResourceType.Video360: return l.resourceTypeVideo360;
case ResourceType.Model3D: return l.resourceTypeModel3D;
default: return '';
}
}
String downloadFileNameFor(ResourceDTO resource) {
final fileName = resource.fileName;
if (fileName != null && fileName.trim().isNotEmpty) {
return fileName;
}
final label = (resource.label ?? 'ressource').trim();
final extension = _extensionByType[resource.type] ?? '';
return label.toLowerCase().endsWith(extension) ? label : '$label$extension';
}
/// L'unité suit la langue : « Ko » en français, « KB » en anglais.
String formatBytes(BuildContext context, int? bytes) {
final l = AppLocalizations.of(context)!;
if (bytes == null || bytes <= 0) return '';
if (bytes < 1024) return l.mediaSizeBytes('$bytes');
if (bytes < 1024 * 1024) return l.mediaSizeKilobytes((bytes / 1024).toStringAsFixed(0));
return l.mediaSizeMegabytes((bytes / (1024 * 1024)).toStringAsFixed(1));
}
/// Mois et dates passent par `intl` avec la locale courante — une liste de mois
/// écrite en dur ne se traduit pas et ne suit pas l'ordre jour/mois de l'anglais.
String formatMonth(BuildContext context, DateTime? date) {
if (date == null) return AppLocalizations.of(context)!.mediaNoDate;
final locale = Localizations.localeOf(context).toString();
final label = DateFormat.yMMMM(locale).format(date);
return label.isEmpty ? label : label[0].toUpperCase() + label.substring(1);
}
String formatDate(BuildContext context, DateTime? date) => date == null
? ''
: DateFormat.yMd(Localizations.localeOf(context).toString()).format(date);
String monthKey(DateTime? date) =>
date == null ? '0000-00' : '${date.year}-${date.month.toString().padLeft(2, '0')}';