The police computer/tablet overlay UI. Renders three shells: a full desktop PC with lock screen, taskbar and draggable windows, a rugged tablet, and an in-car mobile data computer, all hosting MdtView for the MDT pages.
@using Sandbox;
@using Sandbox.UI;
@namespace HorizonRP
@inherits PanelComponent
@*
Les ordinateurs de police de l'UCPD : un seul moteur (UI/Mdt/MdtView.razor, le MDT), trois
habillages, portés par ce composant déjà présent dans la scène :
- le poste du commissariat (E devant un point « police_ordinateur ») : un vrai PC plein écran,
écran de verrouillage et ouverture de session au matricule, bureau au fond UCPD, icônes,
barre des tâches (menu Démarrer, applis ouvertes, réseau, horloge de la ville, date),
fenêtres déplaçables, réductibles, agrandissables, plusieurs à la fois. La salle des scellés
(point « police_scelles ») ouvre le même poste, en borne, sur le registre ;
- la tablette durcie à pied (F7) : coque renforcée, coins protégés, poignée, boutons physiques,
caméra, barre d'état (réseau, batterie, heure) ;
- l'ordinateur de bord (F7 assis dans un véhicule de police) : PC portable durci monté sur la
console (bras de montage, clavier), avec le statut de l'unité et le lecteur de plaques.
La gestion des fenêtres est dans PoliceTablet.Desktop.cs. Rien n'est décidé ici : l'hôte
revérifie chaque demande (Police/PoliceService.*.cs).
*@
<root>
@if ( Shell is null && Prompt is { } prompt )
{
<div class="pt-prompt-row">
<div class="pt-prompt">
<div class="pt-keycap">@HorizonHud.KeyName( "use" )</div>
<i>@prompt.Icon</i>
<div class="pt-prompt-text">@prompt.Text</div>
</div>
</div>
}
@if ( Shell == "pc" && Me is not null )
{
<div class="pc-screen" @ref="Desktop" onmouseup=@(() => EndDrag())>
@if ( !LoggedIn )
{
<div class="pc-lock">
<div class="pc-wallpaper"><div class="wp-badge"><i>local_police</i></div></div>
<div class="lock-clock">
<div class="lock-time">@GameClock.Text</div>
<div class="lock-date">@DateText</div>
</div>
<div class="lock-card">
<div class="lock-avatar"><i>@PoliceRules.GradeOf( Me.PoliceGrade ).Insignia</i></div>
<div class="lock-name">@($"{(PoliceRules.GradeOf( Me.PoliceGrade ).Name)} {(Me.RpName)}")</div>
<div class="lock-sub">@PoliceRules.Department</div>
<div class="lock-field">
<i>badge</i>
<TextEntry class="lock-input" Placeholder="Matricule" Value=@LoginBadge OnTextEdited=@((string v) => { LoginBadge = v; LoginError = null; }) onsubmit=@(() => Login())></TextEntry>
<div class="lock-go" onclick=@(() => Login())><i>arrow_forward</i></div>
</div>
@if ( LoginError is not null )
{
<div class="lock-error">@LoginError</div>
}
<div class="lock-btn" onclick=@(() => Login())>Ouvrir la session</div>
<div class="lock-hint">Identifiant : votre matricule de l'UCPD</div>
</div>
<div class="lock-foot">
<div class="lock-foot-left">@($"{(StationName)} · Réseau sécurisé CJIS")</div>
<div class="lock-foot-right">
<i>lan</i>
<i>accessibility_new</i>
<div class="lock-power" onclick=@(() => CloseShell())><i>power_settings_new</i></div>
</div>
</div>
</div>
}
else
{
<div class="pc-desktop">
<div class="pc-wallpaper">
<div class="wp-badge"><i>local_police</i></div>
<div class="wp-title">UNION CITY POLICE DEPARTMENT</div>
<div class="wp-motto">TO PROTECT AND TO SERVE</div>
</div>
<div class="pc-icons">
@foreach ( var app in MdtCatalog.Apps )
{
<div class="pc-icon" onclick=@(() => OpenApp( app.Id ))>
<div class="pc-icon-tile" style="@($"background-color: {app.Color};")"><i>@app.Icon</i></div>
<div class="pc-icon-name">@app.Name</div>
</div>
}
</div>
<div class="pc-widget">
<div class="widget-head"><i>radio</i>Central UCPD</div>
<div class="widget-row"><div class="widget-value @(PendingCalls > 0 ? "hot" : "")">@PendingCalls</div><div class="widget-text">appel(s) 911 en attente</div></div>
<div class="widget-row"><div class="widget-value">@UnitsOnDuty</div><div class="widget-text">unité(s) en service</div></div>
<div class="widget-link" onclick=@(() => OpenApp( "mdt", "appels" ))>Ouvrir le central 911</div>
</div>
@foreach ( var w in Windows )
{
if ( w.Minimized )
continue;
var app = MdtCatalog.App( w.App ) ?? MdtCatalog.Apps[0];
<div class="pc-window @(w == Focused ? "focused" : "") @(w.Maximized ? "max" : "")" style="@WindowStyle( w )" onmousedown=@(() => Focus( w ))>
<div class="pc-titlebar" onmousedown=@(() => StartDrag( w ))>
<div class="pc-app-icon" style="@($"background-color: {app.Color};")"><i>@app.Icon</i></div>
<div class="pc-title">@WindowTitle( w, app )</div>
<div class="pc-ctrl" onclick=@(() => Minimize( w ))><i>remove</i></div>
<div class="pc-ctrl" onclick=@(() => ToggleMaximize( w ))><i>@(w.Maximized ? "filter_none" : "crop_square")</i></div>
<div class="pc-ctrl close" onclick=@(() => CloseWindow( w ))><i>close</i></div>
</div>
@if ( app.Tabs.Length > 1 )
{
<div class="pc-ribbon">
@foreach ( var t in app.Tabs )
{
var tab = MdtCatalog.Tab( t );
<div class="pc-ribbon-tab @(w.State.Tab == t ? "on" : "")" onclick=@(() => SetTab( w.State, t ))>
<i>@tab.Icon</i>@tab.Name
@if ( TabCount( t ) is { } n and > 0 )
{
<div class="pt-count @(t == "appels" ? "hot" : "")">@n</div>
}
</div>
}
<div class="pc-ribbon-hint">@MdtCatalog.Tab( w.State.Tab ).Hint</div>
</div>
}
<div class="pc-body">
<MdtView Tablet=@this [email protected]></MdtView>
</div>
</div>
}
@if ( StartOpen )
{
<div class="pc-start-catcher" onclick=@(() => StartOpen = false)></div>
<div class="pc-start">
<div class="start-search"><i>search</i><span>Applications de l'UCPD</span></div>
<div class="start-section">Épinglées</div>
<div class="start-grid">
@foreach ( var app in MdtCatalog.Apps )
{
<div class="start-app" onclick=@(() => OpenApp( app.Id ))>
<div class="pc-icon-tile small" style="@($"background-color: {app.Color};")"><i>@app.Icon</i></div>
<div class="start-app-name">@app.Name</div>
</div>
}
</div>
<div class="start-section">Recommandé</div>
<div class="start-reco" onclick=@(() => OpenApp( "mdt", "appels" ))><i>emergency</i><div class="row-main"><div class="start-reco-title">Central 911</div><div class="start-reco-sub">@($"{(PendingCalls)} appel(s) en attente")</div></div></div>
<div class="start-reco" onclick=@(() => OpenApp( "scelles" ))><i>inventory_2</i><div class="row-main"><div class="start-reco-title">Registre des scellés</div><div class="start-reco-sub">Pièces saisies, restitutions, destructions</div></div></div>
<div class="start-foot">
<div class="start-user">
<div class="start-avatar"><i>@PoliceRules.GradeOf( Me.PoliceGrade ).Insignia</i></div>
<div class="start-user-name">@($"{(PoliceRules.GradeOf( Me.PoliceGrade ).Name)} {(Me.RpName)}")</div>
</div>
<div class="start-power" onclick=@(() => LockSession())><i>lock</i>Verrouiller</div>
<div class="start-power" onclick=@(() => CloseShell())><i>power_settings_new</i>Quitter le poste</div>
</div>
</div>
}
<div class="pc-taskbar">
<div class="tb-left">
<i>shield</i>
<div class="tb-left-text">
<div class="tb-left-title">@StationName</div>
<div class="tb-left-sub">@(Kiosk ? "Salle des scellés" : "Commissariat central")</div>
</div>
</div>
<div class="tb-center">
<div class="tb-btn start @(StartOpen ? "on" : "")" onclick=@(() => StartOpen = !StartOpen)><i>local_police</i></div>
@foreach ( var app in MdtCatalog.Apps )
{
var open = Windows.Any( x => x.App == app.Id );
var focused = Focused is { } fw && fw.App == app.Id && !fw.Minimized;
<div class="tb-btn @(open ? "open" : "") @(focused ? "focused" : "")" onclick=@(() => TaskbarClick( app.Id ))>
<i style="@($"color: {app.Color};")">@app.Icon</i>
<div class="tb-dot"></div>
</div>
}
</div>
<div class="tb-right">
<div class="tb-tray">
<i>@(Backend.IsOnline ? "lan" : "portable_wifi_off")</i>
<i>volume_up</i>
<div class="tb-bell" onclick=@(() => OpenApp( "mdt", "appels" ))>
<i>notifications</i>
@if ( PendingCalls > 0 )
{
<div class="tb-bell-dot">@PendingCalls</div>
}
</div>
</div>
<div class="tb-clock">
<div class="tb-time">@GameClock.Text</div>
<div class="tb-date">@DateTime.Now.ToString( "dd/MM/yyyy" )</div>
</div>
</div>
</div>
</div>
}
</div>
}
else if ( (Shell == "tablette" || Shell == "bord") && Me is not null )
{
<div class="pt-backdrop">
@if ( Shell == "tablette" )
{
<div class="tough">
<div class="tough-handle"><div class="tough-handle-grip"></div></div>
<div class="tough-case">
<div class="bumper tl"><div class="screw"></div></div>
<div class="bumper tr"><div class="screw"></div></div>
<div class="bumper bl"><div class="screw"></div></div>
<div class="bumper br"><div class="screw"></div></div>
<div class="tough-top">
<div class="tough-brand">UCPD · RUGGED TABLET</div>
<div class="tough-cam"><div class="tough-lens"></div></div>
<div class="tough-leds"><div class="led green"></div><div class="led amber"></div></div>
</div>
<div class="tough-row">
<div class="tough-screen">
<div class="pt-statusbar">
<div class="sb-left"><i>local_police</i><span>@PoliceRules.Short</span><b>·</b><span>MDT</span></div>
<div class="sb-center">@GameClock.Text</div>
<div class="sb-right">
<span>LTE</span>
<i>@(Backend.IsOnline ? "signal_cellular_alt" : "signal_cellular_off")</i>
<i>@(Backend.IsOnline ? "wifi" : "wifi_off")</i>
<span>87 %</span>
<div class="battery"><div class="battery-fill"></div></div>
</div>
</div>
@MobileContent( MdtCatalog.TabletTabs )
</div>
<div class="tough-keys">
<div class="hw-btn power" onclick=@(() => CloseShell())><i>power_settings_new</i></div>
<div class="hw-btn" onclick=@(() => SetTab( Mobile, "accueil" ))><i>home</i></div>
<div class="hw-btn" onclick=@(() => SetTab( Mobile, "appels" ))><i>emergency</i></div>
<div class="hw-btn small">A1</div>
<div class="hw-btn small">A2</div>
<div class="hw-vol"><div class="hw-vol-key">+</div><div class="hw-vol-key">−</div></div>
</div>
</div>
<div class="tough-bottom">
<div class="tough-port"></div>
<div class="tough-hint"><span class="pt-kbd">@HorizonHud.KeyName( "PoliceTablet" )</span><span>Ranger la tablette</span></div>
<div class="tough-port"></div>
</div>
</div>
</div>
}
else
{
<div class="rig">
<div class="rig-lid">
<div class="bumper tl"></div>
<div class="bumper tr"></div>
<div class="rig-top"><div class="tough-cam"><div class="tough-lens"></div></div></div>
<div class="rig-screen">
<div class="rig-bar">
<div class="rig-unit">
<div class="rig-callsign">@(string.IsNullOrWhiteSpace( Me.PoliceCallsign ) ? "UNITÉ" : Me.PoliceCallsign)</div>
<div class="rig-car">@CarText</div>
</div>
<div class="rig-status">
@foreach ( var s in PoliceService.UnitStates )
{
var on = PoliceService.UnitStateOf( Me ).Id == s.Id;
<div class="rig-st @(on ? "on" : "")" style="@(on ? $"background-color: {s.Color}; border-color: {s.Color};" : $"border-color: {s.Color};")" onclick=@(() => SetStatus( s.Id ))>
<div class="rig-st-code">@s.Code</div>
<div class="rig-st-name">@s.Name</div>
</div>
}
</div>
<div class="rig-right">
<div class="rig-gps"><i>satellite_alt</i>GPS</div>
<i>@(Backend.IsOnline ? "signal_cellular_alt" : "signal_cellular_off")</i>
<div class="rig-clock">@GameClock.Text</div>
</div>
</div>
@MobileContent( MdtCatalog.CarTabs )
<div class="rig-anpr">
<i>document_scanner</i>
<div class="anpr-slot">@ReaderLine( "AV", FrontRead )</div>
<div class="anpr-slot">@ReaderLine( "AR", RearRead )</div>
<div class="anpr-open" onclick=@(() => SetTab( Mobile, "lecteur" ))>Lecteur</div>
</div>
</div>
<div class="rig-label">UCPD · MOBILE DATA COMPUTER</div>
</div>
<div class="rig-hinge"><div class="hinge-knuckle"></div><div class="hinge-knuckle"></div></div>
<div class="rig-base">
<div class="rig-keys">
@for ( var row = 0; row < 4; row++ )
{
<div class="key-row">
@for ( var k = 0; k < KeysInRow( row ); k++ )
{
<div class="key @(row == 3 && k == 3 ? "space" : "")"></div>
}
</div>
}
</div>
<div class="rig-pad"></div>
<div class="rig-hint"><span class="pt-kbd">@HorizonHud.KeyName( "PoliceTablet" )</span><span>Rabattre l'écran</span></div>
</div>
<div class="rig-arm">
<div class="arm-plate"><div class="arm-knob"></div><div class="arm-knob"></div></div>
<div class="arm-pole"></div>
<div class="arm-foot"></div>
</div>
</div>
}
</div>
}
</root>
@code
{
/// <summary>L'habillage ouvert : « pc », « tablette », « bord », ou null (fermé).</summary>
public string Shell { get; private set; }
public bool Open => Shell is not null;
RoleplayPlayer Me => RoleplayPlayer.Local;
/// <summary>L'état du MDT de la tablette et de l'ordinateur de bord (le poste a un état par fenêtre).</summary>
MdtState Mobile { get; } = new();
int PendingCalls => PoliceService.Current?.Dispatch.Count( x => x.Service == "police" && x.Status == "attente" ) ?? 0;
int UnitsOnDuty => RoleplayPlayer.All.Count( x => x.IsCop );
int? TabCount( string id ) => id switch
{
"appels" => PendingCalls,
"avis" => PoliceTabletClient.Warrants?.Count( x => x.Active ),
"vehicules" => PoliceService.Current?.Flags.Count,
_ => null,
};
static string[] DayNames => new[] { "dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi" };
static string[] MonthNames => new[] { "janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre" };
/// <summary>« jeudi 29 octobre 2026 » : la date réelle (la ville n'a que son heure).</summary>
static string DateText
{
get
{
var d = DateTime.Now;
return $"{DayNames[(int)d.DayOfWeek]} {d.Day} {MonthNames[d.Month - 1]} {d.Year}";
}
}
// ------------------------------------------------------------------ tablette et ordinateur de bord
/// <summary>Le rail des onglets et l'écran du MDT, communs à la tablette et à l'ordinateur de bord.</summary>
RenderFragment MobileContent( string[] tabs ) => @<text>
<div class="pt-mdt">
<div class="pt-rail">
<div class="pt-brand">
<div class="pt-brand-badge"><i>local_police</i></div>
<div class="pt-brand-text">
<div class="pt-brand-title">UCPD</div>
<div class="pt-brand-sub">@(Shell == "bord" ? "Mobile Data Computer" : "Mobile Data Terminal")</div>
</div>
</div>
@foreach ( var id in tabs )
{
var entry = MdtCatalog.Tab( id );
<div class="pt-nav @(Mobile.Tab == id ? "active" : "")" onclick=@(() => SetTab( Mobile, id ))>
<i>@entry.Icon</i>
<div class="pt-nav-label">@entry.Name</div>
@if ( TabCount( id ) is { } count and > 0 )
{
<div class="pt-count @(id == "appels" ? "hot" : "")">@count</div>
}
</div>
}
<div class="pt-rail-foot">
<div class="pt-me">
<div class="pt-me-insignia"><i>@PoliceRules.GradeOf( Me.PoliceGrade ).Insignia</i></div>
<div class="pt-me-text">
<div class="pt-me-name">@Me.RpName</div>
<div class="pt-me-sub">@($"{(PoliceRules.GradeOf( Me.PoliceGrade ).Name)} · {(string.IsNullOrWhiteSpace( Me.PoliceCallsign ) ? "sans indicatif" : Me.PoliceCallsign)}")</div>
</div>
</div>
</div>
</div>
<div class="pt-main">
<div class="pt-head">
<div class="pt-head-title"><i>@MdtCatalog.Tab( Mobile.Tab ).Icon</i>@MdtCatalog.Tab( Mobile.Tab ).Name</div>
<div class="pt-head-sub">@MdtCatalog.Tab( Mobile.Tab ).Hint</div>
<div class="pt-head-close" onclick=@(() => CloseShell())><i>close</i></div>
</div>
<div class="pt-body">
<MdtView Tablet=@this State=@Mobile></MdtView>
</div>
</div>
</div>
</text>;
/// <summary>Une ligne du bandeau du lecteur de plaques, en bas de l'ordinateur de bord.</summary>
RenderFragment ReaderLine( string side, PlateReading reading ) => @<text>
<div class="anpr-side">@side</div>
@if ( reading is null )
{
<div class="anpr-none">—</div>
}
else
{
<div class="anpr-plate @(reading.Flagged ? "hot" : "")">@reading.Plate</div>
<div class="anpr-model">@($"{(reading.Model)} · {(reading.Kmh):0} km/h")</div>
@if ( reading.Flagged )
{
<div class="anpr-flag">@reading.Reason</div>
}
}
</text>;
static int KeysInRow( int row ) => row switch { 0 => 13, 1 => 12, 2 => 11, _ => 7 };
string CarText
{
get
{
var car = Vehicle.OfLocalPlayer;
return car.IsValid() ? $"{(car.Info?.Name ?? "Véhicule")} · {car.Plate}" : "—";
}
}
void SetStatus( string id )
{
Sounds.Play( "tap", 0.25f );
PoliceService.RequestUnitStatus( id );
}
void SetTab( MdtState state, string tab )
{
Sounds.Play( "tap", 0.25f );
state.Tab = tab;
state.Confirm = null;
}
}