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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,6 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

# Capturas do Playwright MCP durante a inspeção da UI — depuração, nunca fonte.
.playwright-mcp/
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
tempo de empacotamento pelo script e pelo workflow, ambos a partir deste valor.
-->
<PropertyGroup>
<ClefVersion>1.1.0</ClefVersion>
<ClefVersion>1.2.0</ClefVersion>

<!-- Ignorado quando a build recebe /p:Version=... (propriedade global vence), que é
como o release aplica a versão da tag. -->
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Utilize a barra lateral esquerda para:
- **Windows Forms** (Host nativo)
- **Blazor Hybrid** (Interface de usuário web dentro do desktop)
- **Microsoft.AspNetCore.Components.WebView.WindowsForms**
- **Serilog** & **Serilog.Formatting.Compact.Reader** (Parsing de logs)
- **Serilog** (renderização de message templates; o parsing CLEF é próprio, sobre `Utf8JsonReader`)
- **Omni.Blazor** (Biblioteca de componentes / design system - pacote NuGet `AndersonN.Omni.Blazor`)

## 📄 Licença
Expand Down
17 changes: 11 additions & 6 deletions src/ClefExplorer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AndersonN.Omni.Blazor" Version="0.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="10.0.11" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<!-- Mínimo 0.5.0: a 0.4.0 achatava a árvore de grupos inteira de uma vez e congelava
ao agrupar um log grande. A 0.5.0 virtualiza o modo agrupado e traz o
GroupHierarchy de data que a coluna Data/hora usa. -->
<PackageReference Include="AndersonN.Omni.Blazor" Version="0.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="10.0.90" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4078.44" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.10" />
<!-- Só o Serilog: o LeitorClef substituiu o Serilog.Formatting.Compact.Reader (e o
Newtonsoft que vinha com ele). Do Serilog continuam em uso o MessageTemplate/
MessageTemplateParser, os LogEventPropertyValue e o JsonValueFormatter da exportação. -->
<PackageReference Include="Serilog" Version="4.4.0" />
</ItemGroup>

<ItemGroup>
Expand Down
183 changes: 160 additions & 23 deletions src/Components/LogFileTree.razor
Original file line number Diff line number Diff line change
@@ -1,44 +1,75 @@
@using System.IO
@using System.Text.RegularExpressions
@using ClefExplorer.Helpers

@if (RootNodes.Any())
@inject ContextMenuService ContextMenu
@inject ExploradorArquivos Explorador
@inject NotificationService Notifications
@inject IJSRuntime JS

@if (!RootNodes.Any())
{
<div class="clef-center" style="height:auto; padding:16px;">
<span style="font-size:12px;">Nenhum arquivo carregado</span>
</div>
}
else if (_raizesVisiveis.Count == 0)
{
<div class="clef-center" style="height:auto; padding:16px;">
<span style="font-size:12px;">Nenhum arquivo corresponde à busca</span>
</div>
}
else
{
<OmniTree Data="RootNodes"
@* Data/Children entregam os nós FILTRADOS, mas as MESMAS instâncias da árvore
completa — é o que permite a marcação (que o OmniTree guarda por referência)
sobreviver a uma busca. *@
<OmniTree Data="_raizesVisiveis"
AllowCheckboxes="true"
CheckedValues="_checkedNodes"
CheckedValuesChanged="OnCheckedChanged">
<OmniTreeLevel Text="@(o => ((FileTreeNode)o).Name)"
Children="@(o => ((FileTreeNode)o).Children)"
HasChildren="@(o => ((FileTreeNode)o).Children.Count > 0)"
Children="@(o => FilhosVisiveis((FileTreeNode)o))"
HasChildren="@(o => FilhosVisiveis((FileTreeNode)o).Count > 0)"
Expanded="@(o => true)"
Icon="@(o => NodeIcon((FileTreeNode)o))">
<Template Context="item">
@{ var bk = item.Value is FileTreeNode fn && fn.IsBackup; }
@if (!string.IsNullOrEmpty(item.Icon))
{
<OmniIcon Name="@item.Icon" Size="ComponentSize.Sm" Class="@(bk ? "omni-tree-icon clef-tree-backup" : "omni-tree-icon")" />
@{
var node = item.Value as FileTreeNode;
var bk = node?.IsBackup == true;
}
<span class="@(bk ? "omni-tree-text clef-tree-backup" : "omni-tree-text")">@item.Text</span>
@* display:contents — o wrapper existe só para capturar o botão direito
sobre o nó inteiro (ícone + texto) sem entrar no layout do OmniTree. *@
<span class="clef-tree-node"
@oncontextmenu="@(e => AbrirMenu(e, node))"
@oncontextmenu:preventDefault="true">
@if (!string.IsNullOrEmpty(item.Icon))
{
<OmniIcon Name="@item.Icon" Size="ComponentSize.Sm" Class="@(bk ? "omni-tree-icon clef-tree-backup" : "omni-tree-icon")" />
}
<span class="@(bk ? "omni-tree-text clef-tree-backup" : "omni-tree-text")">@item.Text</span>
</span>
</Template>
</OmniTreeLevel>
</OmniTree>
}
else
{
<div class="clef-center" style="height:auto; padding:16px;">
<span style="font-size:12px;">Nenhum arquivo carregado</span>
</div>
}

@code {
[Parameter] public IEnumerable<string> Files { get; set; } = Enumerable.Empty<string>();
[Parameter] public IEnumerable<string> CheckedFiles { get; set; } = Enumerable.Empty<string>();
[Parameter] public EventCallback<HashSet<string>> OnSelectionChanged { get; set; }

/// <summary>Termo digitado no campo de busca da barra lateral; vazio mostra tudo.</summary>
[Parameter] public string? Filtro { get; set; }

private List<FileTreeNode> RootNodes { get; set; } = new();
private List<object> _checkedNodes = new();
private List<string>? _lastFiles;

private HashSet<FileTreeNode> _visiveis = new();
private List<FileTreeNode> _raizesVisiveis = new();
private string? _ultimoFiltro;

protected override void OnParametersSet()
{
var currentFiles = Files?.OrderBy(f => f).ToList() ?? new List<string>();
Expand All @@ -57,8 +88,25 @@ else
BuildTree();
_checkedNodes = ComputeChecked(currentChecked);
}

if (filesChanged || Filtro != _ultimoFiltro)
{
_ultimoFiltro = Filtro;
AplicarFiltro();
}
}

// ─── Busca na árvore ────────────────────────────────────────────────────────

private void AplicarFiltro()
{
_visiveis = FiltroArvoreArquivos.Visiveis(RootNodes, Filtro);
_raizesVisiveis = RootNodes.Where(_visiveis.Contains).ToList();
}

private List<FileTreeNode> FilhosVisiveis(FileTreeNode node) =>
node.Children.Where(_visiveis.Contains).ToList();

private static string NodeIcon(FileTreeNode node)
{
if (node.IsBackup) return "archive";
Expand Down Expand Up @@ -111,6 +159,103 @@ else
await OnSelectionChanged.InvokeAsync(set);
}

// ─── Menu de contexto ───────────────────────────────────────────────────────

private void AbrirMenu(MouseEventArgs e, FileTreeNode? node)
{
if (node is null) return;
ContextMenu.Open(e, ConstruirMenu(node));
}

private List<ContextMenuItem> ConstruirMenu(FileTreeNode node)
{
var caminhos = node.CaminhosDescendentes().ToList();
var arquivo = node.FullPath;
var itens = new List<ContextMenuItem>();

// Um nó de agrupamento não tem pasta própria: a hierarquia vem do NOME do
// arquivo, então não há o que revelar no Explorer.
if (!string.IsNullOrEmpty(arquivo))
{
itens.Add(new ContextMenuItem
{
Text = "Mostrar no Explorer",
Icon = "folder",
OnClick = () => MostrarNoExplorer(arquivo),
});
}

if (caminhos.Count > 0)
{
itens.Add(new ContextMenuItem
{
Text = caminhos.Count == 1 ? "Copiar caminho" : $"Copiar {caminhos.Count} caminhos",
Icon = "copy",
OnClick = () => CopiarCaminhos(caminhos),
});

itens.Add(ContextMenuItem.Separator());

// Com dezenas de arquivos carregados, isolar um deles pelo checkbox significa
// desmarcar todos os outros a mão.
itens.Add(new ContextMenuItem
{
Text = caminhos.Count == 1 ? "Exibir somente este" : $"Exibir somente estes {caminhos.Count}",
Icon = "filter",
OnClick = () => ExibirSomente(caminhos),
});
}

itens.Add(new ContextMenuItem
{
Text = "Exibir todos os arquivos",
Icon = "list",
OnClick = ExibirTodos,
});

return itens;
}

private Task MostrarNoExplorer(string caminho)
{
if (!Explorador.Revelar(caminho))
{
Notifications.Warning("Arquivo não encontrado", caminho);
}
return Task.CompletedTask;
}

private async Task CopiarCaminhos(IReadOnlyList<string> caminhos)
{
try
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", string.Join(Environment.NewLine, caminhos));
Notifications.Info(caminhos.Count == 1 ? "Caminho copiado" : $"{caminhos.Count} caminhos copiados");
}
catch (Exception ex)
{
AppLog.Warning("Não foi possível copiar para a área de transferência", ex);
Notifications.Warning("Não foi possível copiar o caminho");
}
}

private Task ExibirSomente(IEnumerable<string> caminhos) =>
AplicarSelecao(new HashSet<string>(caminhos, StringComparer.OrdinalIgnoreCase));

private Task ExibirTodos() =>
AplicarSelecao(new HashSet<string>(_lastFiles ?? new List<string>(), StringComparer.OrdinalIgnoreCase));

/// <summary>
/// Troca a marcação inteira de uma vez. É a exceção à regra de que o OmniTree é a fonte
/// única durante a interação: aqui quem manda é uma ação explícita do usuário, e o
/// estado precisa ser reescrito nos dois lados (checkboxes e arquivos visíveis).
/// </summary>
private async Task AplicarSelecao(HashSet<string> caminhos)
{
_checkedNodes = ComputeChecked(caminhos);
await OnSelectionChanged.InvokeAsync(caminhos);
}

// ─── Construção da árvore virtual (lógica preservada da versão Bootstrap) ───

private void BuildTree()
Expand Down Expand Up @@ -347,12 +492,4 @@ else
}
}

public class FileTreeNode
{
public string Name { get; set; } = "";
public string? FullPath { get; set; }
public List<FileTreeNode> Children { get; set; } = new();
public FileTreeNode? Parent { get; set; }
public bool IsBackup { get; set; }
}
}
Loading
Loading