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
81 changes: 79 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
name: Release to Microsoft Store
name: Release

# Dispara ao publicar uma tag "v*" (ex.: v1.2.3) ou manualmente pela aba Actions.
# Pré-requisitos (configurar uma única vez em Settings > Secrets and variables > Actions):
#
# Dois canais, propositalmente independentes:
# github - CANAL OFICIAL. Publica o instalador nos Releases e é de onde o aplicativo se
# atualiza sozinho (ver ServicoAtualizacao). Não depende de secret nenhum.
# store - Microsoft Store. Passa por análise e costuma sair depois. Um MSIX da Store e um
# pacote assinado localmente têm identidades diferentes, então um NUNCA atualiza o
# outro: quem instalou pela Store é apenas avisado da versão nova.
#
# Pré-requisitos do job da Store (Settings > Secrets and variables > Actions):
# AZURE_AD_TENANT_ID - Tenant do Microsoft Entra ID associado ao Partner Center
# AZURE_AD_APPLICATION_CLIENT_ID - Client ID do app do Entra ID
# AZURE_AD_APPLICATION_SECRET - Client secret desse app
Expand All @@ -26,6 +34,75 @@ concurrency:
cancel-in-progress: false

jobs:
github:
name: Publish installer to GitHub Releases
runs-on: windows-latest
# O canal oficial anexa arquivos ao release — o único job que escreve no repositório.
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Resolve version
id: ver
shell: pwsh
run: |
$raw = '${{ github.event.inputs.version }}'
if ([string]::IsNullOrWhiteSpace($raw)) { $raw = '${{ github.ref_name }}' }
$raw = $raw.TrimStart('v','V')
if ($raw -notmatch '^\d+\.\d+(\.\d+){0,2}$') {
throw "Versão inválida: '$raw'. Use algo como v1.2.3 ou 1.2.3."
}
"version=$raw" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Host "Versão: $raw"

- name: Install vpk
run: dotnet tool install -g vpk

# SEM PublishSingleFile: o Velopack empacota o conteúdo da pasta e monta atualizações
# incrementais comparando arquivo a arquivo. Num executável único, qualquer mudança
# reescreve o binário inteiro e todo update volta a baixar ~150 MB.
- name: Publish (win-x64)
run: >
dotnet publish src/ClefExplorer.csproj
-c Release
-r win-x64
--self-contained
-p:PublishSingleFile=false
-o publish

# Traz os pacotes já publicados para o vpk conseguir gerar o delta desta versão.
# Na primeira execução não há o que baixar e o comando segue sem erro.
- name: Download previous releases
continue-on-error: true
run: vpk download github --repoUrl https://github.com/${{ github.repository }} --token ${{ secrets.GITHUB_TOKEN }}

- name: Build installer
run: >
vpk pack
--packId ClefExplorer
--packVersion ${{ steps.ver.outputs.version }}
--packDir publish
--mainExe ClefExplorer.exe
--packAuthors "Anderson Fernandes do Nascimento"
--icon src/app.ico

# Anexa Setup.exe, .nupkg e releases.win.json ao release da tag. O manifesto é o que o
# aplicativo instalado lê para descobrir que existe versão nova.
- name: Upload to GitHub Release
run: >
vpk upload github
--repoUrl https://github.com/${{ github.repository }}
--token ${{ secrets.GITHUB_TOKEN }}
--tag ${{ github.ref_name }}
--releaseName "ClefExplorer ${{ steps.ver.outputs.version }}"
--publish

store:
name: Package MSIX & publish to Store
runs-on: windows-latest
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@ O **ClefExplorer** é uma aplicação desktop moderna desenvolvida para facilita
![screenshot](screenshot.png?raw=true "Screenshot")

## Download
As versões estão disponíveis neste repositório do GitHub junto com a Windows Store

### Windows
### Instalador (recomendado)

Baixe o **ClefExplorer-win-Setup.exe** na [página de releases](https://github.com/afernandes/ClefExplorer/releases/latest).
Instala para o usuário atual (sem pedir administrador) e **se mantém atualizado sozinho**: a
cada abertura o aplicativo procura versão nova, baixa em segundo plano e aplica no reinício.

Também há o `ClefExplorer-win-Portable.zip`, para rodar sem instalar — este não se atualiza
sozinho, apenas avisa quando sai versão nova.

### Microsoft Store

<a href='https://www.microsoft.com/store/apps/9MVZN1HVJ230?cid=storebadge&ocid=badge'><img src='English_get-it-from-MS_InvariantCulture_Default.png' alt='English badge' style='height: 38px;' height="38" /></a>

A versão da Store é atualizada pelo próprio Windows, mas cada envio passa por análise e
costuma sair alguns dias depois do GitHub — que é o canal oficial. Por isso o aplicativo
avisa quando há versão nova publicada aqui, com um link para o release.


## 🚀 Funcionalidades Principais

Expand Down
4 changes: 4 additions & 0 deletions src/ClefExplorer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
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" />
<!-- Auto-atualização pelo canal oficial (Releases do GitHub). Só tem efeito em quem
instalou pelo Setup: no pacote da Store e no executável avulso o app apenas avisa
que há versão nova — ver ServicoAtualizacao. -->
<PackageReference Include="Velopack" Version="1.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
69 changes: 69 additions & 0 deletions src/Components/LogViewer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
@inject NotificationService Notifications
@inject UiPreferencesService UiPreferences
@inject ConsultaLogs Consulta
@inject ServicoAtualizacao Atualizacao
@inject ExploradorArquivos Explorador
@inject IJSRuntime JS

<div class="clef-root">
Expand All @@ -34,6 +36,29 @@
</OmniBanner>
}

@if (_atualizacao is { } atualizacao && !_avisoAtualizacaoDispensado)
{
<OmniBanner Severity="NotificationSeverity.Info" Dismissible="true" OnClosed="DispensarAvisoAtualizacao">
<ChildContent>
A versão @atualizacao.Versao do Clef Explorer está disponível.
@if (atualizacao.PodeReiniciar)
{
<text> Ela já foi baixada e será aplicada ao reiniciar.</text>
}
</ChildContent>
<Actions>
@if (atualizacao.PodeReiniciar)
{
<OmniButton Text="Reiniciar agora" Size="ComponentSize.Sm" Variant="ButtonVariant.Primary" OnClick="AplicarAtualizacao" />
}
else
{
<OmniButton Text="Ver no GitHub" Size="ComponentSize.Sm" Variant="ButtonVariant.Primary" OnClick="AbrirPaginaDaAtualizacao" />
}
</Actions>
</OmniBanner>
}

<div class="clef-body">
<OmniSplitter Orientation="Orientation.Horizontal" Variant="SplitterVariant.Line">
<OmniSplitterPane Size="260px" Min="200px" Max="600px">
Expand Down Expand Up @@ -230,6 +255,8 @@
private DateTime? _dataInicial;
private DateTime? _dataFinal;
private bool _showDefaultAppNotification;
private InfoAtualizacao? _atualizacao;
private bool _avisoAtualizacaoDispensado;
private bool _isExporting;
private int _eventosExportados;
private int _totalExportacao;
Expand Down Expand Up @@ -395,6 +422,11 @@
AplicarFiltros();
}

Atualizacao.Changed += OnAtualizacaoEncontrada;
// Uma verificação que já tenha terminado antes deste componente existir (o serviço
// é singleton e sobrevive a recargas da UI) não dispara o evento de novo.
_atualizacao = Atualizacao.Disponivel;

CheckDefaultApp();
ReportarErrosDeInicializacao();
}
Expand Down Expand Up @@ -524,6 +556,10 @@
// mas o resto do app não é afetado.
AppLog.Warning("Não foi possível registrar o arraste interno", ex);
}

// Depois do primeiro render, sem await: a janela já está na tela e a faixa aparece
// quando (e se) a resposta chegar. O serviço trata as próprias exceções.
_ = Atualizacao.VerificarAsync();
}

/// <summary>
Expand Down Expand Up @@ -606,6 +642,7 @@
Store.Changed -= OnStoreChanged;
Store.PathsLoaded -= OnPathsLoaded;
GroupService.Changed -= OnGroupsChanged;
Atualizacao.Changed -= OnAtualizacaoEncontrada;

Consulta.CancelarConsultaAtual();
foreach (var campo in new[]
Expand Down Expand Up @@ -647,6 +684,38 @@
_showDefaultAppNotification = false;
}

/// <summary>
/// Procura versão nova em segundo plano, depois que a janela já apareceu. Nunca no
/// caminho de abertura: é uma chamada de rede, e o aplicativo tem que abrir na mesma
/// velocidade com ou sem internet.
/// </summary>
private void OnAtualizacaoEncontrada() => DespacharNaUi(() =>
{
_atualizacao = Atualizacao.Disponivel;
StateHasChanged();
});

private void AplicarAtualizacao() => Atualizacao.AplicarEReiniciar();

private void AbrirPaginaDaAtualizacao()
{
if (_atualizacao is null) return;
if (!Explorador.AbrirUrl(_atualizacao.Url))
{
Notifications.Warning(
"Não foi possível abrir a página da versão nova",
ServicoAtualizacao.UrlDosReleases);
}
}

private void DispensarAvisoAtualizacao()
{
// Só até a próxima abertura: a verificação roda a cada início, então a faixa
// reaparece se a versão continuar disponível — sem virar insistência dentro da
// mesma sessão.
_avisoAtualizacaoDispensado = true;
}

private void Carregar()
{
AplicarFiltros();
Expand Down
12 changes: 12 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using ClefExplorer.Services;
using Omni.Blazor;
using Velopack;

namespace ClefExplorer
{
Expand All @@ -10,6 +11,13 @@ internal static class Program
[STAThread]
static void Main(string[] args)
{
// PRIMEIRA linha do Main, antes de qualquer outra coisa: é aqui que o Velopack
// trata os hooks de instalação/atualização (o instalador reexecuta o próprio
// executável com argumentos próprios e espera que ele saia). Qualquer código
// antes disto — inclusive o mutex de instância única — rodaria durante a
// instalação e poderia travá-la.
VelopackApp.Build().Run();

Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", Path.GetTempPath() + @"ClefExplorer");

// Preserve a origem dos caminhos relativos antes de mudar o diretório para a
Expand Down Expand Up @@ -50,6 +58,10 @@ static void Main(string[] args)
services.AddSingleton<IFilePickerService, WinFormsFilePickerService>();
services.AddSingleton<FileAssociationService>();
services.AddSingleton<ExploradorArquivos>();
services.AddSingleton<IAtualizadorLocal, AtualizadorVelopack>();
services.AddSingleton<IConsultorReleases>(
_ => new ConsultorReleasesGithub(ConsultorReleasesGithub.CriarCliente()));
services.AddSingleton<ServicoAtualizacao>();
Comment on lines +61 to +64

#if DEBUG
services.AddBlazorWebViewDeveloperTools();
Expand Down
68 changes: 68 additions & 0 deletions src/Services/AtualizadorVelopack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Velopack;
using Velopack.Sources;

namespace ClefExplorer.Services
{
/// <summary>
/// Atualização automática para quem instalou pelo canal oficial (o Setup publicado nos
/// Releases do GitHub).
/// </summary>
/// <remarks>
/// <para>
/// <see cref="PodeAplicar"/> é falso em duas situações legítimas, e nas duas o
/// <see cref="ServicoAtualizacao"/> cai no caminho de apenas avisar:
/// </para>
/// <list type="bullet">
/// <item>
/// pacote da Microsoft Store — fica em <c>Program Files\WindowsApps</c>, somente
/// leitura, e a identidade de um MSIX não pode ser substituída pelo próprio
/// aplicativo; quem atualiza é a Store;
/// </item>
/// <item>
/// executável avulso — não existe estrutura de instalação para receber o pacote.
/// </item>
/// </list>
/// </remarks>
public sealed class AtualizadorVelopack : IAtualizadorLocal
{
private const string RepositorioOficial = "https://github.com/afernandes/ClefExplorer";

private readonly UpdateManager? _gerenciador;
private UpdateInfo? _preparada;

public AtualizadorVelopack()
{
try
{
_gerenciador = new UpdateManager(new GithubSource(RepositorioOficial, null, prerelease: false));
}
catch (Exception ex)
{
// Sem gerenciador o aplicativo segue no modo "apenas avisa".
AppLog.Warning("Não foi possível iniciar o atualizador automático", ex);
_gerenciador = null;
}
}

public bool PodeAplicar => _gerenciador?.IsInstalled ?? false;

public async Task<string?> PrepararAsync(CancellationToken cancellationToken)
{
if (_gerenciador is null) return null;

var novidade = await _gerenciador.CheckForUpdatesAsync().ConfigureAwait(false);
if (novidade is null) return null;

await _gerenciador.DownloadUpdatesAsync(novidade, cancelToken: cancellationToken).ConfigureAwait(false);
_preparada = novidade;
return novidade.TargetFullRelease.Version.ToString();
}

public void AplicarEReiniciar()
{
if (_gerenciador is null || _preparada is null) return;

_gerenciador.ApplyUpdatesAndRestart(_preparada.TargetFullRelease);
}
}
}
Loading
Loading