mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
This commit is contained in:
@@ -39,4 +39,33 @@ public sealed class FocusViewOptions
|
||||
[JsonPropertyName(nameof(EnergyDisplay))]
|
||||
[OptionName(Name = "Energy Display Mode", Description = "Sets how should the energy display show the information")]
|
||||
public PointsDisplay EnergyDisplay { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(GridColumns))]
|
||||
[OptionName(Name = "Layout Grid Columns", Description = "Number of columns in the focus view layout grid")]
|
||||
[OptionIgnore]
|
||||
public int GridColumns { get; set; } = DefaultGridColumns;
|
||||
|
||||
[JsonPropertyName(nameof(GridRows))]
|
||||
[OptionName(Name = "Layout Grid Rows", Description = "Number of rows in the focus view layout grid")]
|
||||
[OptionIgnore]
|
||||
public int GridRows { get; set; } = DefaultGridRows;
|
||||
|
||||
[JsonPropertyName(nameof(Layout))]
|
||||
[OptionName(Name = "Layout", Description = "Tile-based layout of the focus view components")]
|
||||
public List<FocusViewTile> Layout { get; set; } = DefaultLayout();
|
||||
|
||||
public const int DefaultGridColumns = 20;
|
||||
public const int DefaultGridRows = 16;
|
||||
|
||||
public static List<FocusViewTile> DefaultLayout() =>
|
||||
[
|
||||
new() { Component = FocusViewComponent.Character, Column = 1, Row = 1, ColumnSpan = 10, RowSpan = 3 },
|
||||
new() { Component = FocusViewComponent.PlayerResources, Column = 1, Row = 4, ColumnSpan = 10, RowSpan = 3 },
|
||||
new() { Component = FocusViewComponent.Vanquish, Column = 1, Row = 7, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.Title, Column = 1, Row = 8, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.CurrentMap, Column = 1, Row = 9, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.QuestLog, Column = 1, Row = 10, ColumnSpan = 10, RowSpan = 7 },
|
||||
new() { Component = FocusViewComponent.Build, Column = 11, Row = 1, ColumnSpan = 10, RowSpan = 5 },
|
||||
new() { Component = FocusViewComponent.Browser, Column = 11, Row = 6, ColumnSpan = 10, RowSpan = 11 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
@if (this.Context is not null)
|
||||
{
|
||||
<div class="container backdrop-panel">
|
||||
<div class="container backdrop-panel">
|
||||
@if (this.Context is null)
|
||||
{
|
||||
<SpinnerWidget IsLoading="true" />
|
||||
}
|
||||
else if (this.Context.Title is null)
|
||||
{
|
||||
<div class="title-track-container">
|
||||
<div class="title-label">
|
||||
Title Track
|
||||
</div>
|
||||
</div>
|
||||
<div class="title-bar-container">
|
||||
<div class="title-idle">
|
||||
No active title
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="title-track-container">
|
||||
<div class="title-label">
|
||||
Title Track
|
||||
@@ -18,8 +35,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -69,7 +86,7 @@
|
||||
|
||||
private string GetTitleDescription()
|
||||
{
|
||||
if (this.Context is null)
|
||||
if (this.Context?.Title is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -54,3 +54,15 @@
|
||||
color: var(--neutral-foreground-rest);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title-idle {
|
||||
width: 100%;
|
||||
height: var(--font-size-x-large);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--neutral-fill-rest);
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
font-size: var(--font-size-medium);
|
||||
color: var(--neutral-foreground-hint);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
@if (this.Context is not null &&
|
||||
this.Context.HardMode &&
|
||||
this.Context.Vanquishing)
|
||||
{
|
||||
<div class="container backdrop-panel">
|
||||
<div class="container backdrop-panel">
|
||||
@if (this.Context is null)
|
||||
{
|
||||
<SpinnerWidget IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="title">
|
||||
Vanquishing
|
||||
</div>
|
||||
<div class="vanquish-bar-container">
|
||||
<div class="vanquish-bar" @onclick="@(() => this.VanquishBarClicked())">
|
||||
<div class="vanquish-bar-fill" style="@(this.GetVanquishBarWidth())"></div>
|
||||
<div class="vanquish-bar-text">
|
||||
@this.GetVanquishBarText()
|
||||
@if (this.Context.HardMode && this.Context.Vanquishing)
|
||||
{
|
||||
<div class="vanquish-bar-container">
|
||||
<div class="vanquish-bar" @onclick="@(() => this.VanquishBarClicked())">
|
||||
<div class="vanquish-bar-fill" style="@(this.GetVanquishBarWidth())"></div>
|
||||
<div class="vanquish-bar-text">
|
||||
@this.GetVanquishBarText()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="vanquish-bar-container">
|
||||
<div class="vanquish-idle">
|
||||
Not vanquishing
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -37,6 +50,11 @@
|
||||
}
|
||||
|
||||
var totalFoes = this.Context.FoesToKill + this.Context.FoesKilled;
|
||||
if (totalFoes is 0)
|
||||
{
|
||||
return "width: 0%;";
|
||||
}
|
||||
|
||||
return $"width: {this.Context.FoesKilled * 100d / totalFoes}%;";
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,12 @@
|
||||
color: var(--neutral-foreground-rest);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vanquish-idle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: var(--font-size-medium);
|
||||
color: var(--neutral-foreground-hint);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,82 @@
|
||||
@using Daybreak.Views.Components.FocusView
|
||||
@using Daybreak.Shared.Models.FocusView
|
||||
@using Daybreak.Views.Components.FocusView
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@page "/focus"
|
||||
@inherits ViewBase<FocusView, FocusViewModel>
|
||||
|
||||
<div class="focus-dashboard">
|
||||
<div class="dashboard-half">
|
||||
<div class="component character-component">
|
||||
<CharacterComponent Context="@this.ViewModel.CharacterComponentContext"
|
||||
OnCharacterChanged="@this.ViewModel.OnCharacterChanged"
|
||||
OnExperienceBarClicked="@this.ViewModel.OnExperienceDisplayClicked" />
|
||||
</div>
|
||||
<div class="component resources-component">
|
||||
<PlayerResourcesComponent Context="@this.ViewModel.PlayerResourcesComponentContext"
|
||||
OnBalthazarBarClicked="@this.ViewModel.OnBalthazarBarClicked"
|
||||
OnKurzickBarClicked="@this.ViewModel.OnKurzickBarClicked"
|
||||
OnImperialBarClicked="@this.ViewModel.OnImperialBarClicked"
|
||||
OnLuxonBarClicked="@this.ViewModel.OnLuxonBarClicked" />
|
||||
</div>
|
||||
<div class="component vanquish-component">
|
||||
<VanquishComponent Context="@this.ViewModel.VanquishComponentContext"
|
||||
OnVanquishBarClicked="@this.ViewModel.OnVanquishingDisplayClicked" />
|
||||
</div>
|
||||
<div class="component title-component">
|
||||
<TitleInformationComponent Context="@this.ViewModel.TitleInformationComponentContext"
|
||||
OnTitleClicked="@this.ViewModel.OnTitleClicked" />
|
||||
</div>
|
||||
<div class="component map-component">
|
||||
<CurrentMapComponent Context="@this.ViewModel.CurrentMapComponentContext"
|
||||
OnCurrentMapClicked="@this.ViewModel.OnCurrentMapClicked"/>
|
||||
</div>
|
||||
<div class="component quest-component">
|
||||
<QuestLogComponent Context="@this.ViewModel.QuestLogComponentContext"
|
||||
OnQuestClicked="@this.ViewModel.OnQuestClicked"/>
|
||||
</div>
|
||||
<div class="focus-dashboard @(this.ViewModel.IsEditingLayout ? "editing" : string.Empty)">
|
||||
<div class="title-bar-additional-btns-left">
|
||||
@if (this.ViewModel.IsEditingLayout)
|
||||
{
|
||||
<button class="title-bar-btn" @onclick="@this.SaveLayoutAsync" title="Save layout">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Save())" />
|
||||
</button>
|
||||
<button class="title-bar-btn" @onclick="@this.ViewModel.ResetLayout" title="Reset layout to defaults">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.ArrowUndo())" />
|
||||
</button>
|
||||
<div class="add-component-wrapper">
|
||||
<button class="title-bar-btn"
|
||||
title="Add component"
|
||||
disabled="@(!this.ViewModel.HiddenComponents.Any())"
|
||||
@onclick="@this.ToggleAddDropdown">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Add())" />
|
||||
</button>
|
||||
@if (this.isAddDropdownOpen && this.ViewModel.HiddenComponents.Any())
|
||||
{
|
||||
<div class="add-component-menu">
|
||||
@foreach (var component in this.ViewModel.HiddenComponents)
|
||||
{
|
||||
<div class="add-component-item" @onclick="@(() => this.AddComponentAsync(component))">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Eye())" />
|
||||
<span>@TileName(component)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="title-bar-btn" @onclick="@this.EnterEditLayout" title="Edit layout">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.SquareMultiple())" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="dashboard-half">
|
||||
<div class="component build-component">
|
||||
<BuildComponent Context="@this.ViewModel.BuildComponentContext"
|
||||
OnBuildClicked="@this.ViewModel.OnBuildClicked"
|
||||
OnLoadPlayerSingleBuildClicked="@this.ViewModel.OnPlayerSingleBuildClicked"
|
||||
OnLoadPlayerTeamBuildClicked="@this.ViewModel.OnPlayerTeamBuildClicked" />
|
||||
</div>
|
||||
<div class="component browser-component">
|
||||
<iframe sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
style="height: 100%; width: 100%; margin: -2px;"
|
||||
src="@this.ViewModel.BrowserSource" />
|
||||
|
||||
<div class="focus-content">
|
||||
<div class="grid-stack" @ref="this.gridElement"
|
||||
style="--focus-cols: @this.ViewModel.GridColumns; --focus-rows: @this.ViewModel.GridRows;">
|
||||
@foreach (var tile in this.ViewModel.VisibleTiles)
|
||||
{
|
||||
<div class="grid-stack-item"
|
||||
@key="tile.Component"
|
||||
data-component="@tile.Component"
|
||||
gs-x="@(tile.Column - 1)"
|
||||
gs-y="@(tile.Row - 1)"
|
||||
gs-w="@tile.ColumnSpan"
|
||||
gs-h="@tile.RowSpan">
|
||||
<div class="grid-stack-item-content component component-@(tile.Component.ToString().ToLowerInvariant())">
|
||||
@if (this.ViewModel.IsEditingLayout)
|
||||
{
|
||||
<div class="focus-tile-placeholder">
|
||||
<span class="focus-tile-name">@TileName(tile.Component)</span>
|
||||
<button class="tile-remove-button"
|
||||
title="Hide @TileName(tile.Component)"
|
||||
@onclick="@(() => this.ToggleComponentAsync(tile.Component))">
|
||||
<FluentIcon Value="@(new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Dismiss())" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="focus-tile-body">
|
||||
@this.RenderTile(tile.Component)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,4 +86,127 @@
|
||||
public required string ConfigurationId { get; init; }
|
||||
[Parameter]
|
||||
public required string ProcessId { get; init; }
|
||||
|
||||
[Inject]
|
||||
public required IJSRuntime JSRuntime { get; init; }
|
||||
|
||||
private ElementReference gridElement;
|
||||
private int lastSyncedVersion = -1;
|
||||
private bool isAddDropdownOpen;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (this.ViewModel.Tiles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ViewModel.LayoutVersion != this.lastSyncedVersion)
|
||||
{
|
||||
this.lastSyncedVersion = this.ViewModel.LayoutVersion;
|
||||
this.ViewModel.MarkLayoutSynced();
|
||||
await this.JSRuntime.InvokeVoidAsync("focusGrid.init", this.gridElement, new
|
||||
{
|
||||
columns = this.ViewModel.GridColumns,
|
||||
rows = this.ViewModel.GridRows,
|
||||
editing = this.ViewModel.IsEditingLayout
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pulls the current geometry from Gridstack into the view model. Called only at meaningful
|
||||
// moments (save / show / hide) so live drag/resize is never interrupted by a round-trip.
|
||||
private async Task SyncFromGridAsync()
|
||||
{
|
||||
if (this.ViewModel.Tiles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var changes = await this.JSRuntime.InvokeAsync<FocusTileChange[]>("focusGrid.save");
|
||||
this.ViewModel.ApplyTileChanges(changes);
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
await this.SyncFromGridAsync();
|
||||
this.isAddDropdownOpen = false;
|
||||
this.ViewModel.SaveLayout();
|
||||
}
|
||||
|
||||
private void EnterEditLayout()
|
||||
{
|
||||
this.isAddDropdownOpen = false;
|
||||
this.ViewModel.ToggleEditLayout();
|
||||
}
|
||||
|
||||
private void ToggleAddDropdown() => this.isAddDropdownOpen = !this.isAddDropdownOpen;
|
||||
|
||||
private async Task ToggleComponentAsync(FocusViewComponent component)
|
||||
{
|
||||
await this.SyncFromGridAsync();
|
||||
this.ViewModel.ToggleTileVisibility(component);
|
||||
}
|
||||
|
||||
private async Task AddComponentAsync(FocusViewComponent component)
|
||||
{
|
||||
await this.SyncFromGridAsync();
|
||||
this.ViewModel.ToggleTileVisibility(component);
|
||||
if (!this.ViewModel.HiddenComponents.Any())
|
||||
{
|
||||
this.isAddDropdownOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private RenderFragment RenderTile(FocusViewComponent component) => component switch
|
||||
{
|
||||
FocusViewComponent.Character => @<CharacterComponent Context="@this.ViewModel.CharacterComponentContext"
|
||||
OnCharacterChanged="@this.ViewModel.OnCharacterChanged"
|
||||
OnExperienceBarClicked="@this.ViewModel.OnExperienceDisplayClicked" />,
|
||||
FocusViewComponent.PlayerResources => @<PlayerResourcesComponent Context="@this.ViewModel.PlayerResourcesComponentContext"
|
||||
OnBalthazarBarClicked="@this.ViewModel.OnBalthazarBarClicked"
|
||||
OnKurzickBarClicked="@this.ViewModel.OnKurzickBarClicked"
|
||||
OnImperialBarClicked="@this.ViewModel.OnImperialBarClicked"
|
||||
OnLuxonBarClicked="@this.ViewModel.OnLuxonBarClicked" />,
|
||||
FocusViewComponent.Vanquish => @<VanquishComponent Context="@this.ViewModel.VanquishComponentContext"
|
||||
OnVanquishBarClicked="@this.ViewModel.OnVanquishingDisplayClicked" />,
|
||||
FocusViewComponent.Title => @<TitleInformationComponent Context="@this.ViewModel.TitleInformationComponentContext"
|
||||
OnTitleClicked="@this.ViewModel.OnTitleClicked" />,
|
||||
FocusViewComponent.CurrentMap => @<CurrentMapComponent Context="@this.ViewModel.CurrentMapComponentContext"
|
||||
OnCurrentMapClicked="@this.ViewModel.OnCurrentMapClicked" />,
|
||||
FocusViewComponent.QuestLog => @<QuestLogComponent Context="@this.ViewModel.QuestLogComponentContext"
|
||||
OnQuestClicked="@this.ViewModel.OnQuestClicked" />,
|
||||
FocusViewComponent.Build => @<BuildComponent Context="@this.ViewModel.BuildComponentContext"
|
||||
OnBuildClicked="@this.ViewModel.OnBuildClicked"
|
||||
OnLoadPlayerSingleBuildClicked="@this.ViewModel.OnPlayerSingleBuildClicked"
|
||||
OnLoadPlayerTeamBuildClicked="@this.ViewModel.OnPlayerTeamBuildClicked" />,
|
||||
FocusViewComponent.Browser => @<iframe sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
style="height: 100%; width: 100%; margin: -2px;"
|
||||
src="@this.ViewModel.BrowserSource" />,
|
||||
_ => @<text></text>
|
||||
};
|
||||
|
||||
private static string TileName(FocusViewComponent component) => component switch
|
||||
{
|
||||
FocusViewComponent.Character => "Character",
|
||||
FocusViewComponent.PlayerResources => "Resources",
|
||||
FocusViewComponent.Vanquish => "Vanquish",
|
||||
FocusViewComponent.Title => "Title",
|
||||
FocusViewComponent.CurrentMap => "Current Map",
|
||||
FocusViewComponent.QuestLog => "Quest Log",
|
||||
FocusViewComponent.Build => "Builds",
|
||||
FocusViewComponent.Browser => "Browser",
|
||||
_ => component.ToString()
|
||||
};
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_ = this.JSRuntime.InvokeVoidAsync("focusGrid.destroy");
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,115 @@ public sealed class FocusViewModel(
|
||||
public BuildComponentContext? BuildComponentContext { get; private set; }
|
||||
public string? BrowserSource { get; private set; } = "https://wiki.guildwars.com/wiki/Main_Page";
|
||||
|
||||
private List<FocusViewTile> tiles = [];
|
||||
|
||||
public bool IsEditingLayout { get; private set; }
|
||||
public bool LayoutInitialized { get; private set; }
|
||||
public int LayoutVersion { get; private set; }
|
||||
public IReadOnlyList<FocusViewTile> Tiles => this.tiles;
|
||||
public IEnumerable<FocusViewTile> VisibleTiles => this.tiles.Where(t => t.Visible);
|
||||
public IEnumerable<FocusViewComponent> HiddenComponents => this.tiles.Where(t => !t.Visible).Select(t => t.Component);
|
||||
public int GridColumns => FocusViewOptions.DefaultGridColumns;
|
||||
public int GridRows => FocusViewOptions.DefaultGridRows;
|
||||
|
||||
public void ToggleEditLayout()
|
||||
{
|
||||
this.IsEditingLayout = !this.IsEditingLayout;
|
||||
this.LayoutVersion++;
|
||||
this.RefreshView();
|
||||
}
|
||||
|
||||
public void SaveLayout()
|
||||
{
|
||||
this.PersistLayout();
|
||||
this.IsEditingLayout = false;
|
||||
this.LayoutVersion++;
|
||||
this.RefreshView();
|
||||
}
|
||||
|
||||
public bool IsTileVisible(FocusViewComponent component) =>
|
||||
this.tiles.FirstOrDefault(t => t.Component == component)?.Visible ?? false;
|
||||
|
||||
public void ToggleTileVisibility(FocusViewComponent component)
|
||||
{
|
||||
var tile = this.tiles.FirstOrDefault(t => t.Component == component);
|
||||
if (tile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tile.Visible = !tile.Visible;
|
||||
this.LayoutVersion++;
|
||||
this.PersistLayout();
|
||||
this.RefreshView();
|
||||
}
|
||||
|
||||
public void ApplyTileChanges(IEnumerable<FocusTileChange> changes)
|
||||
{
|
||||
var columns = this.GridColumns;
|
||||
var rows = this.GridRows;
|
||||
foreach (var change in changes)
|
||||
{
|
||||
if (!Enum.TryParse<FocusViewComponent>(change.Component, out var component))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var tile = this.tiles.FirstOrDefault(t => t.Component == component);
|
||||
if (tile is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tile.ColumnSpan = Math.Clamp(change.ColumnSpan, 1, columns);
|
||||
tile.RowSpan = Math.Clamp(change.RowSpan, 1, rows);
|
||||
tile.Column = Math.Clamp(change.Column, 1, columns - tile.ColumnSpan + 1);
|
||||
tile.Row = Math.Clamp(change.Row, 1, rows - tile.RowSpan + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetLayout()
|
||||
{
|
||||
this.tiles = FocusViewOptions.DefaultLayout();
|
||||
this.IsEditingLayout = false;
|
||||
this.LayoutVersion++;
|
||||
this.PersistLayout();
|
||||
this.RefreshView();
|
||||
}
|
||||
|
||||
public void MarkLayoutSynced() => this.LayoutInitialized = true;
|
||||
|
||||
private void LoadLayout()
|
||||
{
|
||||
var savedLayout = this.options.CurrentValue.Layout ?? [];
|
||||
var normalized = new List<FocusViewTile>();
|
||||
var defaults = FocusViewOptions.DefaultLayout();
|
||||
var columns = this.GridColumns;
|
||||
var rows = this.GridRows;
|
||||
foreach (var component in Enum.GetValues<FocusViewComponent>())
|
||||
{
|
||||
var tile = savedLayout.FirstOrDefault(t => t.Component == component)
|
||||
?? defaults.First(t => t.Component == component);
|
||||
tile.ColumnSpan = Math.Clamp(tile.ColumnSpan, 1, columns);
|
||||
tile.RowSpan = Math.Clamp(tile.RowSpan, 1, rows);
|
||||
tile.Column = Math.Clamp(tile.Column, 1, columns - tile.ColumnSpan + 1);
|
||||
tile.Row = Math.Clamp(tile.Row, 1, rows - tile.RowSpan + 1);
|
||||
normalized.Add(tile);
|
||||
}
|
||||
|
||||
this.tiles = normalized;
|
||||
this.PersistLayout();
|
||||
}
|
||||
|
||||
private void PersistLayout()
|
||||
{
|
||||
var options = this.options.CurrentValue;
|
||||
options.GridColumns = this.GridColumns;
|
||||
options.GridRows = this.GridRows;
|
||||
options.Layout = this.tiles;
|
||||
this.optionsProvider.SaveOption(options);
|
||||
}
|
||||
|
||||
public override async ValueTask ParametersSet(FocusView view, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
@@ -291,6 +400,7 @@ public sealed class FocusViewModel(
|
||||
this.cancellationSource?.Dispose();
|
||||
this.cancellationSource = new CancellationTokenSource();
|
||||
var ct = this.cancellationSource.Token;
|
||||
this.LoadLayout();
|
||||
this.SetupDefaultValues();
|
||||
Task.Factory.StartNew(() => this.PeriodicallyFetchGameInformation(ct), ct, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
@@ -344,7 +454,10 @@ public sealed class FocusViewModel(
|
||||
else
|
||||
{
|
||||
retryCount = 0;
|
||||
await this.RefreshViewAsync();
|
||||
if (!this.IsEditingLayout)
|
||||
{
|
||||
await this.RefreshViewAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -602,7 +715,16 @@ public sealed class FocusViewModel(
|
||||
if (titleInfo is null ||
|
||||
!Title.TryParse((int)titleInfo.Id, out var title))
|
||||
{
|
||||
return default;
|
||||
return new TitleInformationComponentContext
|
||||
{
|
||||
Title = default,
|
||||
CurrentPoints = 0,
|
||||
IsPercentage = false,
|
||||
MaxTierNumber = 0,
|
||||
TierNumber = 0,
|
||||
PointsForCurrentRank = 0,
|
||||
PointsForNextRank = 0
|
||||
};
|
||||
}
|
||||
|
||||
return new TitleInformationComponentContext
|
||||
@@ -657,3 +779,12 @@ public sealed class FocusViewModel(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record FocusTileChange
|
||||
{
|
||||
public required string Component { get; init; }
|
||||
public required int Column { get; init; }
|
||||
public required int Row { get; init; }
|
||||
public required int ColumnSpan { get; init; }
|
||||
public required int RowSpan { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,56 +1,197 @@
|
||||
.focus-dashboard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: calc(100% - 55px);
|
||||
margin-top: 35px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-half {
|
||||
.focus-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
margin-top: 35px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.component {
|
||||
.grid-stack {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Gridstack's own rule (.grid-stack>.grid-stack-item>.grid-stack-item-content) forces
|
||||
overflow-y:auto with high specificity, which makes tiles scroll. Override it at equal
|
||||
specificity so tiles clip instead. */
|
||||
.grid-stack > .grid-stack-item > .grid-stack-item-content {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.character-component {
|
||||
flex-shrink: 0;
|
||||
overflow: visible !important;
|
||||
.focus-tile-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.focus-tile-body > *,
|
||||
.focus-tile-body ::deep > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* The character select dropdown overflows its tile, so it must not be clipped or scrolled.
|
||||
Needs to outrank gridstack's own .grid-stack>.grid-stack-item>.grid-stack-item-content rule. */
|
||||
.grid-stack > .grid-stack-item > .grid-stack-item-content.component-character {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.component-character .focus-tile-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.component-character {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.vanquish-component {
|
||||
flex-shrink: 0;
|
||||
/* In edit mode the live Character dropdown is not rendered (only the placeholder), so drop its
|
||||
raised z-index/overflow which would otherwise let it sit above the resize grabber. */
|
||||
.focus-dashboard.editing .grid-stack > .grid-stack-item > .grid-stack-item-content.component-character {
|
||||
overflow: clip;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.resources-component {
|
||||
flex-shrink: 0;
|
||||
/* Editing mode visuals */
|
||||
.focus-dashboard.editing .grid-stack {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--neutral-stroke-rest) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--neutral-stroke-rest) 1px, transparent 1px);
|
||||
background-size: calc(100% / var(--focus-cols)) calc(100% / var(--focus-rows));
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.title-component {
|
||||
flex-shrink: 0;
|
||||
.focus-dashboard.editing .grid-stack-item-content {
|
||||
outline: 1px dashed var(--accent-stroke-control-rest);
|
||||
outline-offset: -1px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.map-component {
|
||||
flex-shrink: 0;
|
||||
/* In edit mode each tile is replaced by a full-size name placeholder so the
|
||||
layout is not distorted by the live components (which require game data). */
|
||||
.focus-tile-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent-fill-rest);
|
||||
color: var(--foreground-on-accent-rest);
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quest-component {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.focus-tile-name {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.build-component {
|
||||
height: 35%;
|
||||
flex-shrink: 0;
|
||||
/* Larger, easier-to-grab resize handle in the bottom-right corner. z-index sits above any
|
||||
component (e.g. Character uses z-index:1000 for its dropdown) so the grabber is always visible. */
|
||||
.focus-dashboard.editing .grid-stack-item > .ui-resizable-se {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
background: transparent;
|
||||
border-right: 3px solid var(--foreground-on-accent-rest);
|
||||
border-bottom: 3px solid var(--foreground-on-accent-rest);
|
||||
border-bottom-right-radius: 4px;
|
||||
transform: none;
|
||||
cursor: se-resize;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.browser-component {
|
||||
height: 65%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Per-tile remove button (top-right) to hide a component while editing. */
|
||||
.tile-remove-button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1002;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tile-remove-button:hover {
|
||||
background: var(--error, #c50f1f);
|
||||
}
|
||||
|
||||
/* Add-component dropdown, anchored to the titlebar button (mirrors the character dropdown). */
|
||||
.add-component-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.title-bar-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.add-component-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 180px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
background: var(--neutral-fill-rest);
|
||||
border: 1px solid var(--neutral-stroke-rest);
|
||||
border-radius: 6px;
|
||||
z-index: 20002;
|
||||
}
|
||||
|
||||
.add-component-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
color: var(--neutral-foreground-rest);
|
||||
border-bottom: 1px solid var(--neutral-stroke-rest);
|
||||
}
|
||||
|
||||
.add-component-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.add-component-item:hover {
|
||||
background: var(--neutral-fill-hover);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Daybreak</title>
|
||||
<base href="/" />
|
||||
<link href="css/site.css" rel="stylesheet" />
|
||||
<link href="deps/gridstack/12.6.0/gridstack.min.css" rel="stylesheet" />
|
||||
<link href="Daybreak.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
@@ -14,9 +15,11 @@
|
||||
<div id="app"></div>
|
||||
<script src="_framework/blazor.webview.js"></script>
|
||||
<script src="deps/marked.js/15.0.12/marked.min.js"></script>
|
||||
<script src="deps/gridstack/12.6.0/gridstack-all.js"></script>
|
||||
<script src="js/wiki-renderer.js"></script>
|
||||
<script src="js/hover-delay-popup.js"></script>
|
||||
<script src="js/rendering.js"></script>
|
||||
<script src="js/focus-grid.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.grid-stack{position:relative}.grid-stack-rtl{direction:ltr}.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack-placeholder>.placeholder-content{background-color:rgba(0,0,0,.1);margin:0;position:absolute;width:auto;z-index:0!important}.grid-stack>.grid-stack-item{position:absolute;padding:0;top:0;width:var(--gs-column-width);height:var(--gs-cell-height)}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item.size-to-content:not(.size-to-content-max)>.grid-stack-item-content{overflow-y:hidden}.grid-stack:not(.grid-stack-rtl)>.grid-stack-item{left:0}.grid-stack.grid-stack-rtl>.grid-stack-item{right:0}.grid-stack>.grid-stack-item>.grid-stack-item-content,.grid-stack>.grid-stack-placeholder>.placeholder-content{top:var(--gs-item-margin-top);right:var(--gs-item-margin-right);bottom:var(--gs-item-margin-bottom);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack-item>.ui-resizable-ne,.grid-stack-item>.ui-resizable-nw,.grid-stack-item>.ui-resizable-se,.grid-stack-item>.ui-resizable-sw{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');background-repeat:no-repeat;background-position:center}.grid-stack-item>.ui-resizable-ne{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-sw{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-nw{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-se{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;top:var(--gs-item-margin-top);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:var(--gs-item-margin-top);left:25px;right:25px}.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;top:var(--gs-item-margin-top);right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;top:15px;bottom:15px;right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px;bottom:var(--gs-item-margin-bottom);right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:var(--gs-item-margin-bottom);right:25px}.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px;bottom:var(--gs-item-margin-bottom);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;top:15px;bottom:15px;left:var(--gs-item-margin-left)}.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack-item.ui-draggable-dragging{will-change:left,right,top}.grid-stack-item.ui-resizable-resizing{will-change:width,height}.ui-draggable-dragging,.ui-resizable-resizing{z-index:10000}.ui-draggable-dragging>.grid-stack-item-content,.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack-animate,.grid-stack-animate .grid-stack-item{transition:left .3s,right .3s,top .3s,height .3s,width .3s}.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack-animate .grid-stack-item.ui-resizable-resizing{transition:left 0s,right 0s,top 0s,height 0s,width 0s}.grid-stack>.grid-stack-item[gs-y="0"]{top:0}.grid-stack:not(.grid-stack-rtl)>.grid-stack-item[gs-x="0"]{left:0}.grid-stack.grid-stack-rtl>.grid-stack-item[gs-x="0"]{right:0}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Interop bridge between the Blazor FocusView and the vendored Gridstack.js library.
|
||||
//
|
||||
// Ownership model (kept deliberately simple to avoid the two engines fighting each other):
|
||||
// * Blazor owns the tile *markup* and renders the initial geometry as gs-x/gs-y/gs-w/gs-h.
|
||||
// * While editing, Gridstack owns drag/resize *entirely* and runs natively for the whole
|
||||
// session - there are no per-move callbacks into .NET, which is what kept the previous
|
||||
// implementation feeling janky.
|
||||
// * .NET pulls the final geometry on demand via save() (on Save / show / hide), never on
|
||||
// every mouse move.
|
||||
window.focusGrid = (() => {
|
||||
let grid = null;
|
||||
let resizeObserver = null;
|
||||
let gridElement = null;
|
||||
let gridRows = 0;
|
||||
|
||||
function computeCellHeight(element, rows) {
|
||||
const height = element.clientHeight;
|
||||
if (!height || !rows) {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
return Math.max(1, Math.floor(height / rows));
|
||||
}
|
||||
|
||||
function init(element, options) {
|
||||
if (!window.GridStack) {
|
||||
console.error('GridStack library is not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
destroy();
|
||||
gridElement = element;
|
||||
gridRows = options.rows;
|
||||
|
||||
grid = window.GridStack.init({
|
||||
column: options.columns,
|
||||
cellHeight: computeCellHeight(element, options.rows),
|
||||
margin: 2,
|
||||
// float:false gives "gravity" - tiles compact upwards into any free space above them.
|
||||
float: false,
|
||||
disableDrag: !options.editing,
|
||||
disableResize: !options.editing,
|
||||
staticGrid: !options.editing,
|
||||
draggable: { cancel: '.tile-remove-button' },
|
||||
resizable: { handles: 'se' },
|
||||
animate: true
|
||||
}, element);
|
||||
|
||||
// Keep the cells filling the available height as the window/container resizes.
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (!grid || !gridElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.cellHeight(computeCellHeight(gridElement, gridRows));
|
||||
});
|
||||
resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
// Returns the current geometry of every tile so .NET can persist it. 1-based coordinates
|
||||
// to match the FocusViewTile model.
|
||||
function save() {
|
||||
if (!grid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return grid.getGridItems().map(el => {
|
||||
const node = el.gridstackNode || {};
|
||||
return {
|
||||
component: el.getAttribute('data-component'),
|
||||
column: (node.x || 0) + 1,
|
||||
row: (node.y || 0) + 1,
|
||||
columnSpan: node.w || 1,
|
||||
rowSpan: node.h || 1
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
if (grid) {
|
||||
grid.destroy(false);
|
||||
grid = null;
|
||||
}
|
||||
|
||||
gridElement = null;
|
||||
}
|
||||
|
||||
return { init, save, destroy };
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Daybreak.Shared.Models.FocusView;
|
||||
|
||||
public enum FocusViewComponent
|
||||
{
|
||||
Character,
|
||||
PlayerResources,
|
||||
Vanquish,
|
||||
Title,
|
||||
CurrentMap,
|
||||
QuestLog,
|
||||
Build,
|
||||
Browser
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Shared.Models.FocusView;
|
||||
|
||||
public sealed class FocusViewTile
|
||||
{
|
||||
[JsonPropertyName(nameof(Component))]
|
||||
public FocusViewComponent Component { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(Visible))]
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
[JsonPropertyName(nameof(Column))]
|
||||
public int Column { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName(nameof(Row))]
|
||||
public int Row { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName(nameof(ColumnSpan))]
|
||||
public int ColumnSpan { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName(nameof(RowSpan))]
|
||||
public int RowSpan { get; set; } = 1;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Daybreak.Shared.Models.FocusView;
|
||||
public sealed class TitleInformationComponentContext
|
||||
{
|
||||
public required Title Title { get; init; }
|
||||
public required Title? Title { get; init; }
|
||||
public required bool IsPercentage { get; init; }
|
||||
public required uint CurrentPoints { get; init; }
|
||||
public required uint PointsForCurrentRank { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user