Code/CharacterKit/CloudClothingBrowser.razor

A Blazor Razor UI component for browsing cloud-hosted clothing items. It lets the user search the cloud package catalog for clothing, view purchased inventory, add an item by reference, and load selected clothing into the session wardrobe and character appearance.

NetworkingFile Access
@using Sandbox
@using Sandbox.UI
@using System
@using System.Threading.Tasks
@namespace CharacterEditor

<root class="cloudwardrobe">
 <div class="optionheading">Cloud clothing</div>
 <div class="optiondescription">Find clothing and hair published by the community. Add an item to your session wardrobe, then wear it like any other item. Human-compatible models are required.</div>
 <TextEntry Value=@Query OnTextEdited=@((string value)=>{Query=value;}) Placeholder="Search cloud clothing" />
 <div class="choicegrid">
  <button class="creatorbutton" disabled=@Busy onclick=@(()=>Search(false))>Search cloud</button>
  <button class="creatorbutton quiet" disabled=@Busy onclick=@ShowInventory>Your purchased cosmetics</button>
 </div>
 <TextEntry Value=@AssetReference OnTextEdited=@((string value)=>{AssetReference=value;}) Placeholder="Clothing asset path or cloud:creator.item" />
 <button class="creatorbutton quiet" disabled=@Busy onclick=@(()=>AddClothing(AssetReference.Trim()))>Add by reference</button>
 @if(!string.IsNullOrEmpty(Message)){<div class="optiondescription">@Message</div>}
 @if(InventoryVisible)
 {
  <div class="choicegrid thumbnailgrid">
  @foreach(var item in Sandbox.Services.Inventory.Definitions.Where(x=>Sandbox.Services.Inventory.HasItem(x.Id) && !string.IsNullOrEmpty(x.PackageIdent) && (string.IsNullOrWhiteSpace(Query)||x.Name.Contains(Query,StringComparison.OrdinalIgnoreCase))))
  {
   <button class=@TileClass("cloud:"+item.PackageIdent) disabled=@Busy onclick=@(()=>AddClothing("cloud:"+item.PackageIdent))>
    <div class="thumbnailframe"><div class="itemthumbnail" style=@($"background-image: url('{item.IconUrl}');")></div>@if(LoadingReference=="cloud:"+item.PackageIdent){<div class="clothingloading"><span class="material-icons loadingspinner">autorenew</span><span>Loading…</span></div>}</div><span class="itemlabel">@item.Name</span>
   </button>
  }
  </div>
 }
 else
 {
  <div class="choicegrid thumbnailgrid">
  @foreach(var package in Results)
  {
   <button class=@TileClass("cloud:"+package.FullIdent) disabled=@Busy onclick=@(()=>AddClothing("cloud:"+package.FullIdent))>
    <div class="thumbnailframe"><div class="itemthumbnail" style=@($"background-image: url('{package.ThumbWide}');")></div>@if(LoadingReference=="cloud:"+package.FullIdent){<div class="clothingloading"><span class="material-icons loadingspinner">autorenew</span><span>Loading…</span></div>}</div><span class="itemlabel">@package.Title</span>
   </button>
  }
  </div>
  @if(Results.Count>0 && Results.Count<Total){<button class="creatorbutton quiet" disabled=@Busy onclick=@(()=>Search(true))>Load more</button>}
 }
</root>

@code {
 public Action<WardrobeItem> Selected { get; set; }
 public CharacterAppearanceData Appearance { get; set; }
 string Query="", AssetReference="", Message;
 string LoadingReference;
 string ActiveQuery;
 bool Busy, InventoryVisible;
 int Generation;
 long Total;
 List<Package> Results=new();
 IEnumerable<string> Equipped => Appearance is null ? Enumerable.Empty<string>() : WardrobeCatalog.Paths(Appearance).Concat(new[] { Appearance.HairPath, Appearance.FacialHairPath }).Where(p=>!string.IsNullOrEmpty(p));
 bool IsEquipped(string reference) => Equipped.Any(path=>string.Equals(path,reference,StringComparison.OrdinalIgnoreCase) || (ClothingResources.Get(reference) is { } item && ClothingResources.Get(path)==item));
 string TileClass(string reference) => $"creatorbutton wardrobechoice {(IsEquipped(reference)?"chosen":"")} {(LoadingReference==reference?"loading":"")}";
 void ShowInventory()
 {
  if(Busy)return;
  InventoryVisible=true;
  Message="Purchased items use your s&box inventory. Items without a human model cannot be worn here.";
 }
 async Task Search(bool more)
 {
  if(Busy)return;
  Busy=true;
  InventoryVisible=false;
  var generation=++Generation;
  Message="Searching…";
  StateHasChanged();
  try
  {
   if(!more){Results.Clear();ActiveQuery=Query;}
   // Escape filters in user input so results remain clothing-only.
   var words=string.Join(" ", (ActiveQuery??"").Split(' ',StringSplitOptions.RemoveEmptyEntries).Where(x=>!x.Contains(':')));
   var result=await Package.FindAsync($"{words} type:clothing sort:newest",24,Results.Count);
   if(!IsValid || generation!=Generation)return;
   Results.AddRange(result.Packages);
   Total=result.TotalCount;
   Message=Total==0?"No clothing found.":$"{Total} cloud items. Click a preview to add it.";
  }
  catch(Exception e){if(IsValid)Message="Cloud search failed: "+e.Message;}
  finally{Busy=false;if(IsValid)StateHasChanged();}
 }
 async Task AddClothing(string reference)
 {
  if(Busy)return;
  Busy=true;LoadingReference=reference;Message="Loading clothing…";StateHasChanged();
  try
  {
   var clothing=await ClothingResources.Load(reference);
   if(!IsValid)return;
   if(ClothingResources.IsBaseSkin(clothing))throw new InvalidOperationException("Choose a clothing or hair item; base skins are selected in the Skin tab.");
   Selected?.Invoke(ClothingResources.Describe(reference,clothing));
   StateHasChanged();
   if(Appearance is not null) await CharacterVisual.Load(Appearance.DeepCopy());
   if(!IsValid)return;
   Message=(IsEquipped(reference)?"Equipped ":"Removed ")+clothing.Title+".";
  }
  catch(Exception e){if(IsValid)Message=e.Message;}
  finally{Busy=false;LoadingReference=null;if(IsValid)StateHasChanged();}
 }
 protected override async Task OnParametersSetAsync()
 {
  if(ActiveQuery is null && !Busy) await Search(false);
 }
 protected override int BuildHash()=>HashCode.Combine(Query,Message,Busy,InventoryVisible,Results.Count,Total,HashCode.Combine(Sandbox.Services.Inventory.Items?.Count,AssetReference,LoadingReference,ClothingResources.Revision),string.Join("|",Equipped));
}