UI/SpawnMenu/SaveMenu.razor
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel
@attribute [SpawnMenuHost.SpawnMenuMode<SpawnMenuHost.HostOnly>]
@attribute [Icon( "💾" )]
@attribute [Title( "#spawnmenu.tab.saves" )]
@attribute [Order( 150 )]
<root>
<div class="save-layout">
<div class="save-tabs">
<div class="save-tab active">
<div class="save-tab-icon">
<IconPanel Text="save" />
</div>
<label>#spawnmenu.saves.header</label>
</div>
</div>
<div class="save-window">
<div class="save-list">
@if ( !SaveFiles.Any() )
{
<div class="empty-state">
<IconPanel Text="bookmark_border" />
<label>#spawnmenu.saves.empty</label>
</div>
}
@foreach ( var save in SaveFiles )
{
var s = save;
<div class="save-card @(!s.IsCompatible ? "outdated" : "")"
@onclick="@(() => ConfirmLoad( s ))"
@onmousedown="@((PanelEvent e) => OnCardMouseDown( s, e ))">
<div class="save-preview">
@if ( s.Thumbnail != null )
{
<Image class="save-thumb" Texture="@s.Thumbnail" />
}
else
{
<IconPanel class="save-preview-placeholder" Text="image" />
}
</div>
<div class="save-info">
<label class="save-name">@s.DisplayName</label>
<div class="save-meta">
@if ( !string.IsNullOrEmpty( s.Timestamp ) )
{
<label class="save-timestamp">@s.Timestamp</label>
}
@if ( !s.IsCompatible )
{
<label class="save-outdated-badge">#spawnmenu.saves.outdated_badge</label>
}
</div>
</div>
<div class="save-dots">
<IconPanel Text="more_vert" />
</div>
</div>
}
</div>
<div class="footer">
<TextEntry @ref="SaveNameEntry" Placeholder="#spawnmenu.saves.name_placeholder" @onsubmit="@OnSave"></TextEntry>
<button @onclick="@OnSave">
<IconPanel class="save-button-icon" Text="save" />
<label>#spawnmenu.saves.save_button</label>
</button>
</div>
</div>
</div>
</root>
@code
{
TextEntry SaveNameEntry { get; set; }
List<SaveFileEntry> SaveFiles = new();
record SaveFileEntry( string FileName, string DisplayName, string Timestamp, Texture Thumbnail, bool IsCompatible );
static string SavesPath => "saves";
protected override void OnAfterTreeRender ( bool firstTime )
{
if ( firstTime )
{
RefreshSaveList();
}
}
void RefreshSaveList()
{
SaveFiles.Clear();
if ( !FileSystem.Data.DirectoryExists( SavesPath ) )
{
FileSystem.Data.CreateDirectory( SavesPath );
}
foreach ( var file in FileSystem.Data.FindFile( SavesPath, "*.sav" ) )
{
var filePath = $"{SavesPath}/{file}";
var displayName = file;
string timestamp = null;
Texture thumbnail = null;
var metadata = SaveSystem.GetFileMetadata( filePath );
if ( metadata != null )
{
if ( metadata.TryGetValue( "Title", out var name ) )
displayName = name;
if ( metadata.TryGetValue( "Timestamp", out var ts ) )
timestamp = ts;
}
var isCompatible = SaveSystem.GetFileSaveVersion( filePath ) == SaveSystem.SaveVersion;
var thumbPath = $"{SavesPath}/{System.IO.Path.GetFileNameWithoutExtension( file )}.thumb.png";
if ( FileSystem.Data.FileExists( thumbPath ) )
{
thumbnail = Texture.LoadFromFileSystem( thumbPath, FileSystem.Data );
}
SaveFiles.Add( new SaveFileEntry( file, displayName, timestamp, thumbnail, isCompatible ) );
}
StateHasChanged();
}
void OnCardMouseDown( SaveFileEntry save, PanelEvent e )
{
if ( e is MousePanelEvent { MouseButton: MouseButtons.Right } me )
{
me.StopPropagation();
var menu = new Sandbox.UI.Menu();
menu.AddOption( "#spawnmenu.saves.load_context", "download", () => ConfirmLoad( save ) );
menu.AddOption( "#spawnmenu.saves.delete_context", "delete", () => ConfirmDelete( save ) );
menu.Open( this, Popup.PositionMode.UnderMouse );
}
}
void ConfirmLoad( SaveFileEntry save )
{
if ( !save.IsCompatible )
{
_ = new StringQueryPopup
{
Title = "#spawnmenu.saves.incompatible_title",
Prompt = Game.Language.GetPhrase( "spawnmenu.saves.incompatible_message", new() { { "name", save.DisplayName } } ),
ConfirmLabel = "#spawnmenu.common.ok",
ShowInput = false,
Parent = FindPopupPanel()
};
return;
}
_ = new StringQueryPopup
{
Title = "#spawnmenu.saves.load_title",
Prompt = Game.Language.GetPhrase( "spawnmenu.saves.load_confirm", new() { { "name", save.DisplayName } } ),
ConfirmLabel = "#spawnmenu.saves.load_button",
ShowInput = false,
OnConfirm = _ => LoadSave( save.FileName ),
Parent = FindPopupPanel()
};
}
void ConfirmDelete( SaveFileEntry save )
{
var popup = new StringQueryPopup
{
Title = "#spawnmenu.saves.delete_title",
Prompt = Game.Language.GetPhrase( "spawnmenu.saves.delete_confirm", new() { { "name", save.DisplayName } } ),
ConfirmLabel = "#spawnmenu.saves.delete_button",
ShowInput = false,
OnConfirm = _ => DeleteSave( save.FileName ),
Parent = FindPopupPanel()
};
}
void OnSave()
{
var saveName = SaveNameEntry?.Text?.Trim();
if ( string.IsNullOrEmpty( saveName ) )
return;
var now = DateTime.Now;
var timeString = now.ToString( "yyyy-MM-dd HH:mm:ss" );
var baseName = now.ToString( "yyyy-MM-dd-HH-mm-ss" );
var fileName = $"{SavesPath}/{baseName}.sav";
// Capture a 256x144 (16:9) thumbnail from the scene camera
var camera = Game.ActiveScene?.Camera;
if ( camera.IsValid() )
{
var bitmap = new Bitmap( 256, 144 );
camera.RenderToBitmap( bitmap );
FileSystem.Data.WriteAllBytes( $"{SavesPath}/{baseName}.thumb.png", bitmap.ToPng() );
}
SaveSystem.Current.SetMetadata( "Timestamp", timeString );
SaveSystem.Current.SetMetadata( "Title", saveName );
SaveSystem.Current.Save( fileName );
SaveNameEntry.Text = "";
RefreshSaveList();
}
void LoadSave( string fileName )
{
_ = SaveSystem.Current.Load( $"{SavesPath}/{fileName}" );
}
void DeleteSave( string fileName )
{
FileSystem.Data.DeleteFile( $"{SavesPath}/{fileName}" );
var thumbPath = $"{SavesPath}/{System.IO.Path.GetFileNameWithoutExtension( fileName )}.thumb.png";
if ( FileSystem.Data.FileExists( thumbPath ) )
FileSystem.Data.DeleteFile( thumbPath );
RefreshSaveList();
}
}