diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cce69c3..46d4107 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,7 @@ jobs: /p:AppxBundlePlatforms=x64 /p:UapAppxPackageBuildMode=StoreUpload /p:AppxPackageSigningEnabled=false + /p:Version=${{ steps.ver.outputs.version }} - name: Locate upload package id: pkg diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..077b652 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,27 @@ + + + + + 1.1.0 + + $(ClefVersion) + + $(ClefVersion).0 + $(ClefVersion).0 + + + diff --git a/ROADMAP.md b/ROADMAP.md index 1736c9c..e4885e0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -55,12 +55,12 @@ O app está publicado na Store (product `9MVZN1HVJ230`, gratuito) e o CI está v | # | Item | Tipo | Esforço | Status | |---|---|---|---|---| -| 18 | Virtualização da lista (alternativa/complemento à paginação) | ⚡ Perf | 🟡 | ⬜ | +| 18 | Virtualização da lista (alternativa/complemento à paginação) | ⚡ Perf | 🟡 | ✅ (+ páginas de até 1000) | | 19 | Painel de estatísticas / timeline | ✨ Feature | 🔴 | ⬜ | -| 20 | Acessibilidade (teclado, ARIA, foco) | ♿ A11y | 🟡 | ⬜ | -| 21 | Versionamento coerente + auto-update da versão standalone | 🧱 Débito | 🟡 | ⬜ | -| 22 | Internacionalização (i18n) | ✨ Feature | 🔴 | ⬜ | -| 23 | Higiene geral do código e nomenclatura | 🧱 Débito | 🟢 | 🟨 parcial (avisos `CS*` zerados) | +| 20 | Acessibilidade (teclado, ARIA, foco) | ♿ A11y | 🟡 | ✅ (anel de foco não verificado na tela) | +| 21 | Versionamento coerente + auto-update da versão standalone | 🧱 Débito | 🟡 | 🟨 versão unificada; auto-update pendente | +| 22 | Internacionalização (i18n) | ✨ Feature | 🔴 | ⬜ (depende de decisão de alcance) | +| 23 | Higiene geral do código e nomenclatura | 🧱 Débito | 🟢 | ✅ | --- diff --git a/publish-store-package.ps1 b/publish-store-package.ps1 index 8f50191..0b55679 100644 --- a/publish-store-package.ps1 +++ b/publish-store-package.ps1 @@ -39,7 +39,7 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$Version, + [string]$Version = '', [string]$Platform = 'x64', [string]$Configuration = 'Release', @@ -60,6 +60,18 @@ foreach ($p in @($srcProj, $pkgProj, $manifest)) { if (-not (Test-Path $p)) { throw "Não encontrei: $p (rode o script da raiz do repositório)." } } +# ---------- Versão: parâmetro, ou a do Directory.Build.props ---------- +if ([string]::IsNullOrWhiteSpace($Version)) { + $propsPath = Join-Path $root 'Directory.Build.props' + if (Test-Path $propsPath) { + $Version = ([xml](Get-Content $propsPath)).Project.PropertyGroup.ClefVersion + } + if ([string]::IsNullOrWhiteSpace($Version)) { + throw "Informe -Version ou defina em Directory.Build.props." + } + Write-Host "Versão herdada do Directory.Build.props: $Version" -ForegroundColor DarkGray +} + # ---------- Normaliza a versão para x.y.z.0 ---------- $v = $Version.Trim().TrimStart('v', 'V') if ($v -notmatch '^\d+\.\d+(\.\d+){0,2}$') { diff --git a/src/ClefExplorer.csproj b/src/ClefExplorer.csproj index aa1fd79..3a7d63b 100644 --- a/src/ClefExplorer.csproj +++ b/src/ClefExplorer.csproj @@ -11,7 +11,9 @@ true true - win-x64;win-x86 + + win-x64 true true app.ico @@ -24,9 +26,7 @@ https://github.com/afernandes/ClefExplorer https://github.com/afernandes/ClefExplorer.git log;serilog;clef;viewer;blazor;winforms;structured-logging - 1.0.0 - 1.0.0.0 - 1.0.0.0 + diff --git a/src/Components/LogGroupManager.razor b/src/Components/LogGroupManager.razor index 7743a66..e25eee3 100644 --- a/src/Components/LogGroupManager.razor +++ b/src/Components/LogGroupManager.razor @@ -11,10 +11,16 @@ @foreach (var group in GroupService.Groups) { var g = group; -
+ @*
+ } diff --git a/src/Components/LogHeader.razor b/src/Components/LogHeader.razor index 6bc245e..af99720 100644 --- a/src/Components/LogHeader.razor +++ b/src/Components/LogHeader.razor @@ -42,7 +42,9 @@ [Parameter] public EventCallback OnReload { get; set; } - private readonly int[] _qtdOptions = { 30, 50, 100 }; + // Páginas grandes só passaram a fazer sentido depois que a lista virou virtualizada: + // antes, 1000 itens significavam 1000 nós no DOM. + private readonly int[] _qtdOptions = { 30, 50, 100, 250, 500, 1000 }; private static string FormatQtd(int v) => $"{v} itens"; diff --git a/src/Components/LogList.razor b/src/Components/LogList.razor index 633497a..ee91237 100644 --- a/src/Components/LogList.razor +++ b/src/Components/LogList.razor @@ -1,7 +1,9 @@ @using ClefExplorer.Models @using ClefExplorer.Helpers -
+@* role=listbox + option: os itens são
por causa do layout, mas precisam ser + anunciados como uma lista de opções selecionáveis e alcançáveis pelo teclado. *@ +
@if (IsBusy) {
@@ -9,29 +11,38 @@ Carregando logs...
} - else if (Eventos != null && Eventos.Any()) + else if (_itens.Count > 0) { - @foreach (var e in Eventos) - { - var ev = e; - var selected = ReferenceEquals(SelectedEvent, ev); -
-
-
- - @ev.Timestamp?.ToString("HH:mm:ss") -
-
@ev.Message
-
- @if (!string.IsNullOrEmpty(ev.Exception)) - { - Exception - } - @ObterResumoProps(ev) + @* Virtualizado: só as linhas visíveis (mais um pequeno overscan) vão ao DOM. + Height=null porque o pai (.clef-list) já limita a altura e faz o scroll — + sem altura limitada o Virtualize renderizaria tudo. É o que permite páginas + grandes (500/1000) sem a UI travar. *@ + + +
+
+
+ + @ev.Timestamp?.ToString("HH:mm:ss") +
+
@ev.Message
+
+ @if (!string.IsNullOrEmpty(ev.Exception)) + { + Exception + } + @ObterResumoProps(ev) +
-
- } + + } else { @@ -47,6 +58,35 @@ [Parameter] public ClefEvent? SelectedEvent { get; set; } [Parameter] public EventCallback OnSelect { get; set; } + /// + /// O OmniVirtualize exige ICollection: materializa uma vez por mudança de + /// parâmetros, em vez de a cada render. + /// + private ICollection _itens = Array.Empty(); + + protected override void OnParametersSet() => + _itens = Eventos as ICollection ?? Eventos?.ToList() ?? (ICollection)Array.Empty(); + + /// Enter/Espaço ativam o item, como num botão. + private async Task AoTeclar(KeyboardEventArgs e, ClefEvent ev) + { + if (e.Key is "Enter" or " ") + { + await OnSelect.InvokeAsync(ev); + } + } + + /// + /// Nome anunciado pelo leitor de tela. Sem ele, o item seria lido como um amontoado de + /// textos soltos — e o nível, que visualmente é um badge colorido, se perderia. + /// + private static string RotuloAcessivel(ClefEvent e) + { + var hora = e.Timestamp?.ToString("HH:mm:ss") ?? ""; + var excecao = string.IsNullOrEmpty(e.Exception) ? "" : ", com exceção"; + return $"{e.Level} às {hora}: {e.Message}{excecao}"; + } + private string ObterResumoProps(ClefEvent e) { if (e.Properties == null) return ""; diff --git a/src/Components/LogToolbar.razor b/src/Components/LogToolbar.razor index d072cae..f5bb27e 100644 --- a/src/Components/LogToolbar.razor +++ b/src/Components/LogToolbar.razor @@ -42,8 +42,11 @@ Title="@(DetailAtBottom ? "Mover o painel de detalhes para a direita" : "Mover o painel de detalhes para baixo")" OnClick="OnToggleDetailPosition" /> } - diff --git a/src/Services/FileAssociationService.cs b/src/Services/FileAssociationService.cs index 0c32768..bb0c038 100644 --- a/src/Services/FileAssociationService.cs +++ b/src/Services/FileAssociationService.cs @@ -101,7 +101,9 @@ private void RegisterAppId(string exePath) using var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{AppId}"); if (key != null) { - key.SetValue(null, "Reader Log File"); + // Mesmo rótulo declarado no Package.appxmanifest, para o tipo de arquivo + // aparecer igual na versão instalada e na avulsa. + key.SetValue(null, "Clef Log File"); key.SetValue("Icon", $"\"{exePath}\",0"); using var shell = key.CreateSubKey("shell"); diff --git a/src/wwwroot/css/app.css b/src/wwwroot/css/app.css index b82b505..3a9226f 100644 --- a/src/wwwroot/css/app.css +++ b/src/wwwroot/css/app.css @@ -552,15 +552,21 @@ span.omni-tree-text.clef-tree-backup { font-weight: 600; } flex-direction: column; gap: 8px; } +/* É um