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
2 changes: 2 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the release workflow uses the section matching the tag and fails if either is missing.

## [Unreleased]
### What's new
- **Create RAR** (#258): in Compress, **Formato** gained **RAR**, created by the **WinRAR you already have installed** (ControlFS checks `Rar.exe` and its signature and never bundles, downloads or reimplements RAR, which is a proprietary format). Without WinRAR the option appears greyed out saying to install it. The archive is written to a temporary file and only appears at the end, overwriting nothing; ControlFS opens the RAR it creates. All items must be in the same folder, and there's no password or volumes.
### Fixes
- **The app no longer freezes after copying files:** the engine reported progress for every 80 KB block and each report redrew the screen on the UI thread; on a large copy the UI queue filled up and the app stopped responding during and right after the copy. The engine now limits its reports and the UI receives at most about 10 updates per second, always the latest one (#257).

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` **nos dois arquivos**. O workflow de release usa a seção da tag e falha se faltar alguma.

## [Unreleased]
### Novidades
- **Criar RAR** (#258): em Compactar, o **Formato** ganhou **RAR**, criado pelo **WinRAR que você já tem instalado** (o ControlFS confere o `Rar.exe` e a assinatura dele e nunca embute, baixa nem reimplementa o RAR, que é um formato proprietário). Sem o WinRAR, a opção aparece apagada dizendo para instalá-lo. O arquivo é gravado num temporário e só aparece no fim, sem sobrescrever nada; o ControlFS abre o RAR criado. Todos os itens precisam estar na mesma pasta, e não há senha nem volumes.
### Correções
- **O app não trava mais depois de copiar arquivos:** o motor relatava o andamento a cada bloco de 80 KB e cada relato redesenhava a tela na thread de interface; numa cópia grande a fila da interface enchia e o app parava de responder durante e logo depois da cópia. Agora o motor limita os relatos e a interface recebe no máximo cerca de 10 atualizações por segundo, sempre a mais recente (#257).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Buttons follow **physical position**, so a Nintendo layout doesn't flip confirm

**Archives**
- **Browse and extract** ZIP (including ZIP64 and AES), 7z, RAR4/RAR5, TAR, TAR.GZ and GZ without unpacking first; **split volumes** (`.7z.001`, `.part1.rar`, `.z01`) open from any part; encrypted archives with password; several archives at once, each into its own folder; **integrity test**
- **Create** ZIP, TAR.GZ and 7z from marked items (RAR can't be created: proprietary) ([matrix](docs/archive-support.md))
- **Create** ZIP, TAR.GZ and 7z from marked items, and **RAR** when WinRAR is installed on your PC (ControlFS runs your own WinRAR; RAR is proprietary, so it never bundles or reimplements it) ([matrix](docs/archive-support.md))

**Previews**
- **Images** (JPG, PNG, GIF, BMP, WebP) with zoom, pan and next/previous; **text** (logs, notes, configs, code) with **light editing**; **PDF** without Edge
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Os botões seguem a **posição física**, então um controle Nintendo não inve

**Compactados**
- **Navegar e extrair** ZIP (inclusive ZIP64 e AES), 7z, RAR4/RAR5, TAR, TAR.GZ e GZ sem descompactar antes; **volumes divididos** (`.7z.001`, `.part1.rar`, `.z01`) abrem a partir de qualquer parte; compactados com senha; vários de uma vez, cada um na sua pasta; **teste de integridade**
- **Criar** ZIP, TAR.GZ e 7z a partir dos itens marcados (RAR não pode ser criado: formato proprietário) ([matriz](docs/archive-support.md))
- **Criar** ZIP, TAR.GZ e 7z a partir dos itens marcados, e **RAR** quando o WinRAR está instalado no seu PC (o ControlFS usa o seu próprio WinRAR; o RAR é proprietário, então ele nunca embute nem reimplementa esse formato) ([matriz](docs/archive-support.md))

**Visualização**
- **Imagens** (JPG, PNG, GIF, BMP, WebP) com zoom, deslocamento e anterior/próxima; **texto** (logs, notas, configurações, código) com **edição leve**; **PDF** sem o Edge
Expand Down
2 changes: 1 addition & 1 deletion docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Blocked entries (unsafe names like `../`, links, reserved Windows names) show a

## Compressing

Mark items with West (or focus one) → North → **Compress…**. Choose the name (on-screen keyboard), the **Formato** (format) — activating it opens a picker with ZIP, TAR.GZ and 7z (smaller, slower to create; opens in 7-Zip and in the current Windows 11 File Explorer), each with a line about it and the current one marked — and the **Compressão** (compression level, also a picker), then **Compress**. Choosing in a picker returns to the same window; Back leaves the value unchanged. The archive is written to a temporary file and only appears when finished; an existing file is never overwritten (the name gets "(2)"). Links and junctions inside folders are skipped and listed in the result. RAR can't be created (proprietary format).
Mark items with West (or focus one) → North → **Compress…**. Choose the name (on-screen keyboard), the **Formato** (format) — activating it opens a picker with ZIP, TAR.GZ, 7z (smaller, slower to create; opens in 7-Zip and in the current Windows 11 File Explorer) and RAR (created by the WinRAR you already have installed; without WinRAR it shows greyed out and says so — ControlFS never bundles or downloads anything RAR; all items must be in the same folder), each with a line about it and the current one marked — and the **Compressão** (compression level, also a picker), then **Compress**. Choosing in a picker returns to the same window; Back leaves the value unchanged. The archive is written to a temporary file and only appears when finished; an existing file is never overwritten (the name gets "(2)"). Links and junctions inside folders are skipped and listed in the result. RAR can't be created (proprietary format).

## Operations center

Expand Down
2 changes: 1 addition & 1 deletion docs/GUIDE.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Entradas bloqueadas (nomes inseguros como `../`, links, nomes reservados do Wind

## Compactando

Marque itens com Oeste (ou foque um) → Norte → **Compactar…**. Escolha o nome (teclado virtual), o **Formato** — ativá-lo abre um seletor com ZIP, TAR.GZ e 7z (menor e mais lento para criar; abre no 7-Zip e no Explorador do Windows 11 atual), cada um com uma linha sobre ele e o atual marcado — e a **Compressão** (nível, também um seletor), e depois **Compactar**. Escolher no seletor volta à mesma janela; Voltar deixa o valor como estava. O arquivo é gravado num temporário e só aparece quando termina; um arquivo existente nunca é sobrescrito (o nome ganha "(2)"). Links e junctions dentro das pastas são ignorados e listados no resultado. RAR não pode ser criado (formato proprietário).
Marque itens com Oeste (ou foque um) → Norte → **Compactar…**. Escolha o nome (teclado virtual), o **Formato** — ativá-lo abre um seletor com ZIP, TAR.GZ, 7z (menor e mais lento para criar; abre no 7-Zip e no Explorador do Windows 11 atual) e RAR (criado pelo WinRAR que você já tem instalado; sem o WinRAR aparece apagado e diz o motivo — o ControlFS nunca inclui nem baixa nada do RAR; todos os itens precisam estar na mesma pasta), cada um com uma linha sobre ele e o atual marcado — e a **Compressão** (nível, também um seletor), e depois **Compactar**. Escolher no seletor volta à mesma janela; Voltar deixa o valor como estava. O arquivo é gravado num temporário e só aparece quando termina; um arquivo existente nunca é sobrescrito (o nome ganha "(2)"). Links e junctions dentro das pastas são ignorados e listados no resultado. RAR não pode ser criado (formato proprietário).

## Central de operações

Expand Down
2 changes: 1 addition & 1 deletion docs/ui-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Inventory (every modal type; the render report proves the rule for the marked on
| Option picker (#261: Formato, Compressão, Tema, Destaque, Ordenar por, Legendas, Tamanho, Modo…) | Medium | every choice and its description always visible (no description area, no per-focus growth); the current one is marked | Screens `p1`/`p1b` (group `picker-theme`, width and height), `p3`/`p3b` (`picker-format`), all pickers same width (`picker-width`), `p1c`/`p3c` (the modal underneath returns at its size) |
| Menu (Início), item actions, path menu, drive/tab/operation menus, folder picker menu, "Locais" | Medium (with grid) / Compact | reserved description area; body scrolls | Screens `4`/`4a` (group `menu-app`), `m1`/`m1b` (`menu-actions`) |
| Configurações | Medium | reserved description area; tiles and rows measure bold | Screens `4b`–`4e` (group `settings`: tile, long row, long value, wrapped label) |
| Compactar (ZIP / TAR.GZ / 7z) | Standard | `LineReserve`: identical for every format and compression | Screens `q1`, `q1b`, `q1c`, `p3c` (group `compress`, width and height) |
| Compactar (ZIP / TAR.GZ / 7z / RAR — RAR only enabled with the user's WinRAR, #258) | Standard | `LineReserve`: identical for every format and compression | Screens `q1`, `q1b`, `q1c`, `p3c` (group `compress`, width and height) |
| Extrair (summary) | Standard | `LineReserve` for the destination | Screens `m3`/`m3b` (group `extract-summary`) |
| Confirmations (delete, exit, run, terminal, unmount, discard…) | Compact | content only; long names wrap | Screens `m2`/`m2b`/`q2` (group `confirm-delete`, width) |
| Results, conflict, batch, history and operation details, errors, disk usage, "Continuar o vídeo?" | Standard (Compact when short) | content only | Screens `m5b`, `m5c`, `m7` |
Expand Down
4 changes: 2 additions & 2 deletions src/ControlFS.Application/AppController.Choices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace ControlFS.Application;

/// <summary>Uma alternativa de um valor com várias opções (#261): o valor, o texto e, opcionalmente, uma linha que explica.</summary>
public sealed record Choice<T>(T Value, string Label, string? Description = null);
public sealed record Choice<T>(T Value, string Label, string? Description = null, string? DisabledReason = null);

/// <summary>
/// Seletor de opções (#261): um padrão só para todo valor que tem várias alternativas escondidas (formato e compressão de
Expand Down Expand Up @@ -60,7 +60,7 @@ internal void ShowChoicePicker<T>(string label, T current, IReadOnlyList<Choice<
{
var index = Math.Max(0, choices.ToList().FindIndex(c => EqualityComparer<T>.Default.Equals(c.Value, current)));
var items = choices.Select((choice, i) => new MenuItem(choice.Label, () => ApplyChoice(choice.Value, onPick),
Detail: choice.Description, Icon: i == index ? ActionIcon.RadioOn : ActionIcon.RadioOff, Value: i == index ? "atual" : null)).ToList();
choice.DisabledReason, choice.Description, i == index ? ActionIcon.RadioOn : ActionIcon.RadioOff, Value: i == index ? "atual" : null)).ToList();
PushModal(new MenuModal(label, items) { Icon = icon, Subtitle = context, PickerCurrent = index, FocusIndex = index });
}

Expand Down
10 changes: 9 additions & 1 deletion src/ControlFS.Application/AppController.Compress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void Refill()
Refill();
}), icon: ActionIcon.Rename);
// Formato e compressão têm várias alternativas escondidas: abrem o seletor (#261) em vez de alternar às cegas.
var format = ChoiceOption("Formato", () => plan.Format, CompressFormats, picked =>
var format = ChoiceOption("Formato", () => plan.Format, AvailableCompressFormats(), picked =>
{
plan = plan with { Format = picked };
Refill();
Expand Down Expand Up @@ -167,8 +167,16 @@ private void OnCompressionCompleted(OperationItem item, CompressPlan plan, Opera
new(CompressionFormat.Zip, "ZIP", "abre em qualquer Windows"),
new(CompressionFormat.TarGZip, "TAR.GZ", "comum em Linux/macOS"),
new(CompressionFormat.SevenZip, "7z", "menor; abre no 7-Zip e no Explorador do Windows 11 atual (mais lento para criar)"),
new(CompressionFormat.Rar, "RAR", "abre no WinRAR; criado pelo WinRAR que você já tem instalado (o ControlFS não inclui nem baixa nada do RAR)"),
];

/// <summary>
/// Os formatos como o seletor os mostra agora: o RAR só fica disponível se o serviço achar o WinRAR do usuário; sem ele
/// aparece desativado com o motivo (docs/decisions/0011). Os demais formatos sempre estão disponíveis.
/// </summary>
private IReadOnlyList<Choice<CompressionFormat>> AvailableCompressFormats() =>
[.. CompressFormats.Select(c => _archives.GetCreationAvailability(c.Value) is { IsAvailable: false } none ? c with { DisabledReason = none.Reason ?? "Indisponível neste PC." } : c)];

internal static IReadOnlyList<Choice<CompressionStrength>> StrengthChoices { get; } =
[
new(CompressionStrength.Fast, "rápida", "Comprime menos e termina antes."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using ControlFS.Core.Models;
using ControlFS.Core.Policies;
using ControlFS.Infrastructure.Archives;
using ControlFS.Infrastructure.Archives.Creation;
using ControlFS.UnitTests.Support;

namespace ControlFS.UnitTests.Application;
Expand All @@ -23,10 +24,11 @@ private sealed class FakeShell : IShellService
public void RevealInExplorer(string path) => Calls.Add(("reveal", path));
}

private (Driver Driver, FakeShell Shell) Boot()
/// <summary><paramref name="rar"/> falso: o WinRAR "instalado" para o teste; null: sem WinRAR (não depende da máquina).</summary>
private (Driver Driver, FakeShell Shell) Boot(RarTool? rar = null)
{
var shell = new FakeShell();
var app = new AppController(new TestFileSystem(_tmp.Path), new ArchiveService(), shell: shell);
var app = new AppController(new TestFileSystem(_tmp.Path), new ArchiveService(rarLocator: () => rar), shell: shell);
app.Start();
return (new Driver(app), shell);
}
Expand Down Expand Up @@ -153,14 +155,14 @@ public void Format_picker_lists_every_format_marks_the_current_and_returns_to_th
var picker = await d.WaitMenu();
Assert.True(picker.IsPicker);
Assert.Equal("Formato", picker.Title);
Assert.Equal(["ZIP", "TAR.GZ", "7z"], picker.Items.Select(i => i.Label));
Assert.Equal(["ZIP", "TAR.GZ", "7z", "RAR"], picker.Items.Select(i => i.Label));
Assert.All(picker.Items, i => Assert.False(string.IsNullOrEmpty(i.Detail)));
Assert.Equal(0, picker.FocusIndex);
Assert.Equal((ActionIcon.RadioOn, "atual"), (picker.Items[0].Icon, picker.Items[0].Value));
Assert.All(picker.Items.Skip(1), i => Assert.Equal((ActionIcon.RadioOff, (string?)null), (i.Icon, i.Value)));
Assert.Contains("opção 1 de 3, selecionada", d.App.DescribeFocus().Item, StringComparison.Ordinal);
Assert.Contains("opção 1 de 4, selecionada", d.App.DescribeFocus().Item, StringComparison.Ordinal);
d.Press(InputAction.NavigateDown);
Assert.Contains("opção 2 de 3", d.App.DescribeFocus().Item, StringComparison.Ordinal);
Assert.Contains("opção 2 de 4", d.App.DescribeFocus().Item, StringComparison.Ordinal);
Assert.DoesNotContain("selecionada", d.App.DescribeFocus().Item, StringComparison.Ordinal);

// Voltar deixa tudo como estava.
Expand All @@ -183,6 +185,38 @@ public void Format_picker_lists_every_format_marks_the_current_and_returns_to_th
Assert.Contains(dialog.Lines, l => l is { Label: "Compressão", Value: "normal" });
});

[Fact]
public void Rar_is_listed_disabled_with_the_reason_without_winrar_and_selectable_with_it() => UiContext.Run(async () =>
{
File.WriteAllText(_tmp.Sub("x.txt"), "x");

// Sem WinRAR: a alternativa aparece, desativada, dizendo o que fazer; escolher não muda o formato.
var (d, _) = Boot();
d.Press(InputAction.Confirm);
await d.FocusItem("x.txt");
d.Press(InputAction.OpenContextMenu);
await d.ChooseMenu("Compactar…");
var dialog = await d.WaitDialog("Compactar");
d.ChooseOption(dialog, "Formato: ZIP");
var picker = await d.WaitMenu();
var rar = picker.Items.Single(i => i.Label == "RAR");
Assert.False(rar.IsEnabled);
Assert.Contains("WinRAR", rar.DisabledReason, StringComparison.Ordinal);
Assert.All(picker.Items.Where(i => i.Label != "RAR"), i => Assert.True(i.IsEnabled));
d.Press(InputAction.Back);
Assert.Equal("Formato: ZIP", dialog.Options.First(o => o.Label.StartsWith("Formato", StringComparison.Ordinal)).Label);

// Com o WinRAR do usuário: dá para escolher e o arquivo ganha a extensão .rar.
var (d2, _) = Boot(new RarTool("C:\\WinRAR\\Rar.exe"));
d2.Press(InputAction.Confirm);
await d2.FocusItem("x.txt");
d2.Press(InputAction.OpenContextMenu);
await d2.ChooseMenu("Compactar…");
var dialog2 = await d2.WaitDialog("Compactar");
await d2.PickOption(dialog2, "Formato", "RAR");
Assert.Contains(dialog2.Lines, l => l.Label == "Arquivo" && l.Value.EndsWith(".rar", StringComparison.Ordinal));
});

[Fact]
public void Compress_name_is_typed_with_the_virtual_keyboard_and_validated() => UiContext.Run(async () =>
{
Expand Down
Loading