Les tuiles suivent maintenant les mêmes colSpan × rowSpan que les autres canaux, sur six colonnes — environ 83° d'ouverture, dans le confort du regard — avec un écart serré assumé : espacées, les tuiles se lisent comme des panneaux sans rapport plutôt que comme une planche. Le pas angulaire se déduit de la largeur de cellule et du rayon de l'arc, plutôt que d'être posé en dur. S'y ajoute le sélecteur de langue avec ses drapeaux, et les libellés de l'interface sortis du code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
7.2 KiB
C#
173 lines
7.2 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Threading.Tasks;
|
||
using MyInfoMate.Vr.Net;
|
||
using UnityEngine;
|
||
|
||
namespace MyInfoMate.Vr.Menu
|
||
{
|
||
/// <summary>
|
||
/// Remplit une <see cref="PagedView"/> à partir d'une section — item <b>E8</b> du
|
||
/// lot XR-4. C'est ici, et nulle part ailleurs, que vit ce qui distingue un Slider
|
||
/// d'une Map ou d'un Event.
|
||
///
|
||
/// <b>Trois types sur les quatre retenus au §8.</b> Le quatrième, la Video 360, attend
|
||
/// la ressource 360° côté backend. Le Parcours est volontairement rendu comme une Map :
|
||
/// « chaque étape = un lieu autour de soi » demande de la 3D, donc `SectionScene3D`.
|
||
/// </summary>
|
||
public static class SectionPages
|
||
{
|
||
/// <summary>Les types qu'on sait ouvrir aujourd'hui.</summary>
|
||
public static bool CanRender(ConfigurationExport.SectionType type) =>
|
||
type == ConfigurationExport.SectionType.Slider
|
||
|| type == ConfigurationExport.SectionType.Map
|
||
|| type == ConfigurationExport.SectionType.Parcours
|
||
|| type == ConfigurationExport.SectionType.Event;
|
||
|
||
public static async Task FillAsync(PagedView view,
|
||
ConfigurationExport.SectionSummary section,
|
||
ApiClient client,
|
||
string language)
|
||
{
|
||
switch (section.Type)
|
||
{
|
||
case ConfigurationExport.SectionType.Slider:
|
||
await FillSlider(view, section, client, language);
|
||
break;
|
||
|
||
case ConfigurationExport.SectionType.Map:
|
||
case ConfigurationExport.SectionType.Parcours:
|
||
await FillMap(view, section, client, language);
|
||
break;
|
||
|
||
case ConfigurationExport.SectionType.Event:
|
||
FillEvent(view, section, language);
|
||
break;
|
||
}
|
||
|
||
if (view != null && view.PageCount == 0)
|
||
view.ShowEmpty(Ui.Strings.Get(Ui.Strings.EmptyContent, language));
|
||
}
|
||
|
||
static async Task FillSlider(PagedView view,
|
||
ConfigurationExport.SectionSummary section,
|
||
ApiClient client,
|
||
string language)
|
||
{
|
||
foreach (var content in section.OrderedContents())
|
||
{
|
||
var texture = await LoadImage(client, content.Resource?.Url);
|
||
if (view == null) return;
|
||
if (texture == null) continue;
|
||
|
||
view.AddPage(new PagedView.Page
|
||
{
|
||
Heading = ConfigurationExport.Translate(content.Title, language) ?? "",
|
||
Body = ConfigurationExport.Translate(content.Description, language),
|
||
Image = texture
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Une Map devient une <b>liste de points d'intérêt</b>, un par page.
|
||
///
|
||
/// ⚠️ Ce n'est pas la Map que le plan promet. Sa valeur en VR, c'est « le plan posé
|
||
/// devant soi comme une maquette » — et ça demande un modèle 3D, donc
|
||
/// `SectionScene3D` (§9). Une carte Google affichée sur un panneau flottant serait
|
||
/// <i>moins</i> bonne qu'un téléphone : autant montrer les lieux eux-mêmes, avec
|
||
/// leur photo et leurs horaires, ce que le `GeoPoint` porte déjà.
|
||
/// </summary>
|
||
static async Task FillMap(PagedView view,
|
||
ConfigurationExport.SectionSummary section,
|
||
ApiClient client,
|
||
string language)
|
||
{
|
||
foreach (var point in section.Points)
|
||
{
|
||
var texture = await LoadImage(client, point.ImageUrl);
|
||
if (view == null) return;
|
||
|
||
var parts = new List<string>();
|
||
|
||
var description = ConfigurationExport.Translate(point.Description, language);
|
||
if (!string.IsNullOrEmpty(description)) parts.Add(description);
|
||
|
||
var schedules = ConfigurationExport.Translate(point.Schedules, language);
|
||
if (!string.IsNullOrEmpty(schedules)) parts.Add(schedules);
|
||
|
||
view.AddPage(new PagedView.Page
|
||
{
|
||
Heading = ConfigurationExport.Translate(point.Title, language) ?? "",
|
||
Body = string.Join("\n", parts),
|
||
Image = texture
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Un Event devient le programme, une page par bloc.
|
||
///
|
||
/// <b>« Ce qui se passe aujourd'hui »</b> est l'usage visé (§8) : un casque en
|
||
/// écran d'attente dans un office de tourisme. On ne montre donc que la journée en
|
||
/// cours — et seulement s'il s'y passe quelque chose : un programme vide le jour
|
||
/// même vaut mieux rempli par les dates suivantes que pas rempli du tout.
|
||
/// </summary>
|
||
static void FillEvent(PagedView view,
|
||
ConfigurationExport.SectionSummary section,
|
||
string language)
|
||
{
|
||
var blocks = new List<ConfigurationExport.ProgrammeBlock>(section.Programme);
|
||
blocks.Sort((a, b) => Nullable.Compare(a.StartTime, b.StartTime));
|
||
|
||
var today = new List<ConfigurationExport.ProgrammeBlock>();
|
||
var upcoming = new List<ConfigurationExport.ProgrammeBlock>();
|
||
|
||
foreach (var block in blocks)
|
||
{
|
||
if (block.StartTime == null) continue;
|
||
|
||
var start = block.StartTime.Value.ToLocalTime();
|
||
if (start.Date == DateTime.Now.Date) today.Add(block);
|
||
else if (start > DateTime.Now) upcoming.Add(block);
|
||
}
|
||
|
||
var shown = today.Count > 0 ? today : upcoming;
|
||
|
||
foreach (var block in shown)
|
||
{
|
||
var start = block.StartTime?.ToLocalTime();
|
||
var end = block.EndTime?.ToLocalTime();
|
||
|
||
var when = start == null ? "" :
|
||
today.Count > 0
|
||
? (end == null ? $"{start:HH:mm}" : $"{start:HH:mm} – {end:HH:mm}")
|
||
: $"{start:dd/MM} · {start:HH:mm}";
|
||
|
||
var description = ConfigurationExport.Translate(block.Description, language);
|
||
|
||
view.AddPage(new PagedView.Page
|
||
{
|
||
Heading = ConfigurationExport.Translate(block.Title, language) ?? "",
|
||
Body = string.IsNullOrEmpty(description) ? when : $"{when}\n{description}"
|
||
});
|
||
}
|
||
|
||
if (view.PageCount == 0)
|
||
view.ShowEmpty(Ui.Strings.Get(Ui.Strings.NothingToday, language));
|
||
}
|
||
|
||
static async Task<Texture2D> LoadImage(ApiClient client, string url)
|
||
{
|
||
if (string.IsNullOrEmpty(url)) return null;
|
||
|
||
var path = await ContentCache.MediaPathAsync(client, url);
|
||
if (path == null) return null;
|
||
|
||
var texture = new Texture2D(2, 2);
|
||
return texture.LoadImage(File.ReadAllBytes(path)) ? texture : null;
|
||
}
|
||
}
|
||
}
|