Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
/p:AppxBundlePlatforms=x64
/p:UapAppxPackageBuildMode=StoreUpload
/p:AppxPackageSigningEnabled=false
/p:Version=${{ steps.ver.outputs.version }}
Comment on lines 94 to +96

- name: Locate upload package
id: pkg
Expand Down
27 changes: 27 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project>

<!--
Fonte única da versão do produto.

Antes o número vivia solto no ClefExplorer.csproj e ficou em 1.0.0 enquanto a Store
já estava em 1.0.2 — o título da janela, que lê a versão do assembly, mostrava um
número diferente do publicado.

Quem consome:
- ClefExplorer.csproj → Version/FileVersion/AssemblyVersion (título da janela);
- publish-store-package.ps1 → padrão quando -Version não é informado;
- .github/workflows/release.yml → sobrescreve com a versão da tag (a tag manda no release).

A <Identity Version> do Package.appxmanifest NÃO é lida daqui: é XML carimbado em
tempo de empacotamento pelo script e pelo workflow, ambos a partir deste valor.
-->
<PropertyGroup>
<ClefVersion>1.1.0</ClefVersion>

<Version>$(ClefVersion)</Version>
<!-- A Store reserva a 4ª parte (revisão) e só aceita 0. -->
<FileVersion>$(ClefVersion).0</FileVersion>
<AssemblyVersion>$(ClefVersion).0</AssemblyVersion>
</PropertyGroup>

</Project>
10 changes: 5 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | 🟢 | |

---

Expand Down
14 changes: 13 additions & 1 deletion publish-store-package.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Version,
[string]$Version = '',
Comment on lines 41 to +42

[string]$Platform = 'x64',
[string]$Configuration = 'Release',
Expand All @@ -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 <ClefVersion> 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}$') {
Expand Down
8 changes: 4 additions & 4 deletions src/ClefExplorer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<!-- Single-file publish defaults (can be overridden via CLI) -->
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
<!-- Só x64: é o que o CI publica, o que o pacote da Store contém e o que o README
anuncia como requisito. Declarar win-x86 sugeria um suporte que não existe. -->
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<InvariantGlobalization>true</InvariantGlobalization>
<ApplicationIcon>app.ico</ApplicationIcon>
Expand All @@ -24,9 +26,7 @@
<PackageProjectUrl>https://github.com/afernandes/ClefExplorer</PackageProjectUrl>
<RepositoryUrl>https://github.com/afernandes/ClefExplorer.git</RepositoryUrl>
<PackageTags>log;serilog;clef;viewer;blazor;winforms;structured-logging</PackageTags>
<Version>1.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<!-- Version/FileVersion/AssemblyVersion vêm do Directory.Build.props (fonte única). -->
</PropertyGroup>

<ItemGroup>
Expand Down
10 changes: 8 additions & 2 deletions src/Components/LogGroupManager.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
@foreach (var group in GroupService.Groups)
{
var g = group;
<div class="clef-group-item @(_editingGroup?.Id == g.Id ? "is-active" : "")" @key="g.Id" @onclick="@(() => EditGroup(g))">
@* <button> em vez de <div @onclick>: recebe foco, ativa por Enter/Espaço e é
anunciado como botão. Não há nada interativo aninhado, então a troca é segura. *@
<button type="button"
class="clef-group-item @(_editingGroup?.Id == g.Id ? "is-active" : "")"
@key="g.Id"
aria-pressed="@((_editingGroup?.Id == g.Id).ToString().ToLowerInvariant())"
@onclick="@(() => EditGroup(g))">
<span class="clef-mono" style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">@g.Name</span>
<OmniIcon Name="chevron-right" Size="ComponentSize.Sm" />
</div>
</button>
}
</div>

Expand Down
4 changes: 3 additions & 1 deletion src/Components/LogHeader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
82 changes: 61 additions & 21 deletions src/Components/LogList.razor
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
@using ClefExplorer.Models
@using ClefExplorer.Helpers

<div class="clef-list">
@* role=listbox + option: os itens são <div> por causa do layout, mas precisam ser
anunciados como uma lista de opções selecionáveis e alcançáveis pelo teclado. *@
<div class="clef-list" role="listbox" aria-label="Eventos de log" aria-busy="@IsBusy.ToString().ToLowerInvariant()">
@if (IsBusy)
{
<div class="clef-center">
<OmniSpinner Size="ComponentSize.Lg" />
<span>Carregando logs...</span>
</div>
}
else if (Eventos != null && Eventos.Any())
else if (_itens.Count > 0)
{
@foreach (var e in Eventos)
{
var ev = e;
var selected = ReferenceEquals(SelectedEvent, ev);
<div class="clef-list-item @(selected ? "is-selected" : "")" @onclick="@(() => OnSelect.InvokeAsync(ev))">
<div class="clef-list-item-body">
<div class="clef-list-item-row">
<OmniBadge Text="@ev.Level" Variant="LogLevelStyles.Variant(ev.Level)" />
<span class="clef-list-item-time">@ev.Timestamp?.ToString("HH:mm:ss")</span>
</div>
<div class="clef-list-item-title" title="@ev.Message">@ev.Message</div>
<div class="clef-list-item-meta">
@if (!string.IsNullOrEmpty(ev.Exception))
{
<span class="clef-exception-flag"><OmniIcon Name="zap" Size="ComponentSize.Sm" /> Exception</span>
}
<span>@ObterResumoProps(ev)</span>
@* 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. *@
<OmniVirtualize TItem="ClefEvent" Items="_itens" Height="null" ItemSize="76">
<ItemContent Context="ev">
<div class="clef-list-item @(ReferenceEquals(SelectedEvent, ev) ? "is-selected" : "")"
role="option"
tabindex="0"
aria-selected="@ReferenceEquals(SelectedEvent, ev).ToString().ToLowerInvariant()"
aria-label="@RotuloAcessivel(ev)"
@onclick="@(() => OnSelect.InvokeAsync(ev))"
@onkeydown="@(e => AoTeclar(e, ev))">
<div class="clef-list-item-body">
<div class="clef-list-item-row">
<OmniBadge Text="@ev.Level" Variant="LogLevelStyles.Variant(ev.Level)" />
<span class="clef-list-item-time">@ev.Timestamp?.ToString("HH:mm:ss")</span>
</div>
<div class="clef-list-item-title" title="@ev.Message">@ev.Message</div>
<div class="clef-list-item-meta">
@if (!string.IsNullOrEmpty(ev.Exception))
{
<span class="clef-exception-flag"><OmniIcon Name="zap" Size="ComponentSize.Sm" /> Exception</span>
}
<span>@ObterResumoProps(ev)</span>
</div>
</div>
</div>
</div>
}
</ItemContent>
</OmniVirtualize>
}
else
{
Expand All @@ -47,6 +58,35 @@
[Parameter] public ClefEvent? SelectedEvent { get; set; }
[Parameter] public EventCallback<ClefEvent> OnSelect { get; set; }

/// <summary>
/// O <c>OmniVirtualize</c> exige <c>ICollection</c>: materializa uma vez por mudança de
/// parâmetros, em vez de a cada render.
/// </summary>
private ICollection<ClefEvent> _itens = Array.Empty<ClefEvent>();

protected override void OnParametersSet() =>
_itens = Eventos as ICollection<ClefEvent> ?? Eventos?.ToList() ?? (ICollection<ClefEvent>)Array.Empty<ClefEvent>();

/// <summary>Enter/Espaço ativam o item, como num botão.</summary>
private async Task AoTeclar(KeyboardEventArgs e, ClefEvent ev)
{
if (e.Key is "Enter" or " ")
{
await OnSelect.InvokeAsync(ev);
}
}

/// <summary>
/// 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.
/// </summary>
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 "";
Expand Down
7 changes: 5 additions & 2 deletions src/Components/LogToolbar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@
Title="@(DetailAtBottom ? "Mover o painel de detalhes para a direita" : "Mover o painel de detalhes para baixo")"
OnClick="OnToggleDetailPosition" />
}
<OmniPagination CurrentPage="CurrentPage"
CurrentPageChanged="CurrentPageChanged"
@* OmniPagination é zero-based; o app conta a partir de 1 (é o que aparece no
"X-Y de Z"). Sem a conversão, a página 1 era exibida como "2" e a última
ficava inalcançável. A tradução fica aqui, na fronteira com o componente. *@
<OmniPagination CurrentPage="@(CurrentPage - 1)"
CurrentPageChanged="@(i => CurrentPageChanged.InvokeAsync(i + 1))"
TotalCount="TotalCount"
PageSize="PageSize"
Compact="true" />
Expand Down
4 changes: 3 additions & 1 deletion src/Services/FileAssociationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
19 changes: 19 additions & 0 deletions src/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -552,15 +552,21 @@ span.omni-tree-text.clef-tree-backup { font-weight: 600; }
flex-direction: column;
gap: 8px;
}
/* É um <button> (foco + teclado); daí o reset de aparência e o width/text-align. */
.clef-group-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 8px 10px;
border-radius: var(--omni-radius-sm);
cursor: pointer;
border: 1px solid transparent;
background: transparent;
color: var(--omni-fg);
font: inherit;
text-align: left;
}
.clef-group-item:hover { background: var(--omni-bg-sunken); }
.clef-group-item.is-active {
Expand Down Expand Up @@ -660,3 +666,16 @@ span.omni-tree-text.clef-tree-backup { font-weight: 600; }

.clef-col-item:hover { background: var(--omni-bg-sunken); }
.clef-col-item input { cursor: pointer; }

/* ---- Foco visível --------------------------------------------------------- */
/* Nenhum dos elementos clicáveis próprios (clef-*) tinha indicador de foco: quem
navega por teclado não sabia onde estava. :focus-visible mostra o anel apenas na
navegação por teclado, sem poluir o clique de mouse. */
.clef-list-item:focus-visible,
.clef-filter:focus-visible,
.clef-group-item:focus-visible,
.clef-regex-toggle:focus-visible,
.clef-col-item:focus-within {
outline: 2px solid var(--omni-accent);
outline-offset: -2px;
}
Comment on lines +674 to +681
Loading