UI/SpawnMenu/Spawnlists/SpawnlistView.razor
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

@{
    var data = GetData();
    var workshopId = Entry.GetMeta( "_workshopId", 0ul );
    var isPublished = workshopId > 0;
    var isInstalled = Entry.Files.IsReadOnly;
}

<SpawnMenuContent>
    <Header>
        <div class="spawnlist-header">
            <SpawnMenuToolbar>
                <Left>
                    <div class="spawnlist-identity">
                        <div class="folder-mark">
                            <div class="folder-tab"></div>
                            <IconPanel Text="collections_bookmark" />
                        </div>
                        <div class="identity-copy">
                            <h2>@data.Name</h2>
                            <div class="meta">
                                <span><IconPanel Text="widgets" /> @data.Items.Count( item => item.IsContent ) @Game.Language.GetPhrase( "spawnmenu.spawnlist.items" )</span>
                                <span class="dot"></span>
                                <span><IconPanel Text="@( isInstalled ? "cloud_done" : isPublished ? "public" : "lock" )" /> @Game.Language.GetPhrase( isInstalled ? "spawnmenu.spawnlist.installed" : isPublished ? "spawnmenu.spawnlist.published" : "spawnmenu.spawnlist.local" )</span>
                            </div>
                        </div>
                    </div>
                </Left>
                <Right>
                    @if ( isPublished )
                    {
                        <Button Icon="link" Tooltip="#spawnmenu.spawnlist.copy_url" class="icon-button" @onclick=@( () => Clipboard.SetText( $"https://steamcommunity.com/sharedfiles/filedetails/?id={workshopId}" ) ) />
                        @if ( !Entry.Files.IsReadOnly )
                        {
                            <Button Icon="sync" Tooltip="#spawnmenu.spawnlist.sync" class="icon-button" @onclick=@OnPublish />
                        }
                    }
                    else if ( !Entry.Files.IsReadOnly )
                    {
                        <Button Icon="cloud_upload" Text="#spawnmenu.spawnlist.publish" Tooltip="#spawnmenu.spawnlist.publish" class="menu-button" @onclick=@OnPublish />
                    }
                    @if ( !Entry.Files.IsReadOnly )
                    {
                        @if ( _editMode && CanRename )
                        {
                            <Button Icon="drive_file_rename_outline" Text="#spawnmenu.spawnlist.rename" class="menu-button" @onclick=@OnRename />
                        }
                        <Button Icon="edit" Tooltip="#spawnmenu.spawnlist.edit" class="icon-button" Active=@_editMode @onclick=@ToggleEditMode />
                        <Button Icon="delete" Tooltip="#spawnmenu.spawnlist.delete" class="icon-button" @onclick=@OnDelete />
                    }
                    else
                    {
                        <Button Icon="delete" Tooltip="#spawnmenu.spawnlist.remove" class="icon-button" @onclick=@OnUninstall />
                    }
                </Right>
            </SpawnMenuToolbar>

            @if ( _editMode )
            {
                <div class="edit-toolbar">
                    <Button class="menu-action" Icon="label" Text="#spawnmenu.spawnlist.add_title" @onclick=@AddTitle />
                    <Button class="menu-action" Icon="horizontal_rule" Text="#spawnmenu.spawnlist.add_divider" @onclick=@AddDivider />
                    <div class="edit-instructions">
                        <IconPanel Text="drag_indicator" />
                        <label>@Game.Language.GetPhrase( "spawnmenu.spawnlist.edit_instructions" )</label>
                    </div>
                </div>
            }
        </div>
    </Header>

    <Body>
        @if ( data.Items.Count == 0 )
        {
            <SpawnMenuEmptyState Icon="format_list_bulleted" Title="#spawnmenu.spawnlist.empty_title" Subtitle="#spawnmenu.spawnlist.empty_instructions" />
        }
        else
        {
            <MixedVirtualGrid class="@( _editMode ? "editing" : "" )" [email protected] ItemSize=@(new Vector2( 160, 120 )) CanAcceptDrop=@CanAcceptDrop OnDropIndex=@OnDropIndex>
                <Item Context="item">
                    @if ( item is SpawnlistItem spawnItem )
                    {
                        @if ( spawnItem.IsContent )
                        {
                            <SpawnMenuIcon Ident="@spawnItem.Ident" Title="@spawnItem.Title" Icon="@spawnItem.Icon" [email protected] />
                        }
                        else
                        {
                            <SpawnlistLayoutItem Item=@spawnItem OnRename=@RenameItem OnRemove=@RemoveItem />
                        }
                    }
                </Item>
            </MixedVirtualGrid>
        }
    </Body>
</SpawnMenuContent>

@code
{
    public Storage.Entry Entry { get; set; }

    SpawnlistData _cachedData;
    RealTimeSince _lastRefresh;
    bool _editMode;
    public bool CanReorder => _editMode && Entry?.Files.IsReadOnly == false;
    bool CanRename => Entry?.Files.IsReadOnly == false && Entry.GetMeta( "_workshopId", 0ul ) == 0;

    SpawnlistData GetData()
    {
        if ( _cachedData == null )
        {
            _cachedData = SpawnlistData.Load( Entry );
            _lastRefresh = 0;
        }

        return _cachedData;
    }

    public void RefreshCache()
    {
        _cachedData = SpawnlistData.Load( Entry );
        _lastRefresh = 0;
        StateHasChanged();
    }

    public override void Tick()
    {
        base.Tick();

        // Periodically check for changes from other tabs
        if ( _lastRefresh > 1f )
        {
            var fresh = SpawnlistData.Load( Entry );
            if ( fresh.Name != _cachedData?.Name || !ItemsMatch( fresh.Items, _cachedData?.Items, !Entry.Files.IsReadOnly ) )
            {
                _cachedData = fresh;
                StateHasChanged();
            }
            _lastRefresh = 0;
        }
    }

    protected override int BuildHash() => HashCode.Combine( _cachedData?.Items?.Count, _cachedData?.Name, _editMode, CanRename );

    void OnRename()
    {
        if ( !_editMode || !CanRename ) return;

        _ = new StringQueryPopup
        {
            Title = "#spawnmenu.spawnlist.rename_title",
            Prompt = "#spawnmenu.spawnlist.rename_prompt",
            Placeholder = "#spawnmenu.spawnlist.name_placeholder",
            ConfirmLabel = "#spawnmenu.spawnlist.rename_button",
            InitialValue = GetData().Name,
            OnConfirm = RenameSpawnlist,
            Parent = FindPopupPanel()
        };
    }

    void RenameSpawnlist( string name )
    {
        if ( !CanRename ) return;

        SpawnlistData.Rename( Entry, name );
        RefreshCache();
        Ancestors.OfType<SpawnlistsPage>().FirstOrDefault()?.RefreshList();
    }

    void ToggleEditMode()
    {
        if ( Entry?.Files.IsReadOnly != false ) return;
        _editMode = !_editMode;
    }

    void AddTitle()
    {
        SpawnlistData.AddMarker( Entry, SpawnlistItemKind.Title, Game.Language.GetPhrase( "spawnmenu.spawnlist.new_section" ) );
        RefreshCache();
    }

    void AddDivider()
    {
        SpawnlistData.AddMarker( Entry, SpawnlistItemKind.Divider );
        RefreshCache();
    }

    public bool CanAcceptDrop( PanelEvent e )
    {
        if ( !CanReorder ) return false;
        var drag = DragHandler.Current ?? e?.Target?.UserData as DragData;
        return drag is not null && drag.SpawnlistItemId != Guid.Empty;
    }

    void OnDropIndex( int index, PanelEvent e )
    {
        var drag = DragHandler.Current ?? e.Target?.UserData as DragData;
        if ( drag is null || drag.SpawnlistItemId == Guid.Empty ) return;

        drag.Handled = true;
        if ( drag.Source is not null ) drag.Source.UserData = null;
        SpawnlistData.MoveItem( Entry, drag.SpawnlistItemId, index );
        RefreshCache();
    }

    void RenameItem( Guid id, string title )
    {
        title = title?.Trim() ?? "";
        SpawnlistData.RenameItem( Entry, id, title );
        var item = _cachedData?.Items.FirstOrDefault( item => item.Id == id );
        if ( item is not null ) item.Title = title;
        StateHasChanged();
    }

    static bool ItemsMatch( IReadOnlyList<SpawnlistItem> left, IReadOnlyList<SpawnlistItem> right, bool compareIds )
    {
        if ( left is null || right is null ) return left == right;
        if ( left.Count != right.Count ) return false;

        for ( var i = 0; i < left.Count; i++ )
        {
            var a = left[i];
            var b = right[i];
            if ( (compareIds && a.Id != b.Id) || a.Kind != b.Kind || a.Ident != b.Ident || a.Title != b.Title || a.Icon != b.Icon )
                return false;
        }

        return true;
    }

    void RemoveItem( Guid id )
    {
        SpawnlistData.RemoveItem( Entry, id );
        RefreshCache();
    }

    void OnPublish()
    {
        if ( Entry is null ) return;
        SpawnlistData.Publish( Entry );
    }

    void OnDelete()
    {
        if ( Entry is null ) return;
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Delete( Entry );
    }

    void OnUninstall()
    {
        if ( Entry is null ) return;
        var workshopId = Entry.GetMeta( "_workshopId", 0ul );
        var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
        page?.Collection.Uninstall( workshopId );
    }
}