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 CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
- **Hold to repeat** on the on-screen keyboard: holding West (backspace), LB/RB (cursor) or South on `⌫ ◀ ▶` repeats with acceleration; Done never repeats. (#42)
- **Favorites**: pin folders from the actions menu (North → "Add to favorites"). They come first on the home screen and in the folder picker's "Go to another place", can be reordered from the home screen and are saved with your settings (portable mode included). A favorite that went missing shows as unavailable and is only removed when you say so. (#48)
- **Retry failed items**: from the result of a copy, move, delete or extraction, run again only the items that failed or were not processed; what already succeeded is never redone. Items interrupted by a cancel are now reported as "not processed". (#19)
- **Native Windows icons** in the list (home, folders, picker and archives): file types, folders, special folders and drives. Icons load in the background without stalling scrolling and follow the display scale; no emoji left in the list. (#24)

### Improvements
- **Clearer focus that always lands somewhere valid:** the list uses the same highlight ring as menus and dialogs; after deleting or moving items focus goes to the next remaining item (or the previous one); opening a folder scrolls the focused item into view right away; and the physical keyboard keeps working right after a menu or dialog closes. (#31)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
- **Segurar para repetir** no teclado virtual: manter Oeste (apagar), LB/RB (cursor) ou Sul sobre `⌫ ◀ ▶` repete com aceleração; Concluir nunca repete. (#42)
- **Favoritos**: fixe pastas pelo menu de ações (Norte → "Adicionar aos favoritos"). Elas aparecem primeiro na tela inicial e em "Ir para outro local" do seletor de pastas, podem ser reordenadas no início e ficam salvas nas preferências (inclusive no modo portátil). Uma favorita que sumiu aparece como indisponível e só sai da lista se você mandar. (#48)
- **Tentar de novo só as falhas**: no resultado de uma cópia, movimentação, exclusão ou extração, refaz apenas os itens que falharam ou não foram processados; o que já deu certo nunca é refeito. Itens interrompidos por cancelamento agora aparecem como "não processados". (#19)
- **Ícones nativos do Windows** na lista (início, pastas, seletor e compactados): tipos de arquivo, pastas, pastas especiais e unidades. Os ícones carregam em segundo plano, sem travar a rolagem, e acompanham a escala da tela; nenhum emoji na lista. (#24)

### Melhorias
- **Foco mais claro e sempre num lugar válido:** a lista usa o mesmo anel de destaque dos menus e diálogos; ao excluir ou mover itens o foco vai para o próximo item que sobrou (ou o anterior); ao abrir uma pasta o item focado já aparece rolado na tela; e o teclado físico volta a responder logo depois de fechar um menu ou diálogo. (#31)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Buttons follow **physical position**, so a Nintendo layout doesn't flip confirm
## Features

- Real folders and drives, history, sorting, hidden items, marking, properties
- **Native Windows icons** for files, folders, special folders and drives, loaded in the background and sized for the screen's scale
- **File operations:** rename, copy, cut, paste, move and delete to the Recycle Bin, with conflict handling (skip / keep both / replace / merge folders) and per-item results
- Own **on-screen keyboard** (Portuguese/English, accents, symbols, cursor, masked passwords) usable with only directions + confirm + back
- **Create folder** with Windows naming rules
Expand All @@ -55,7 +56,7 @@ Buttons follow **physical position**, so a Nintendo layout doesn't flip confirm

## Roadmap

**Next:** native Windows icons, controller button prompts, virtual keyboard redesign, search, favorites ([roadmap](https://github.com/nextestudios/ControlFS/issues/95)) · **Later:** two panes, search, favorites, split volumes, unknown-controller wizard, light theme. Details in [docs/roadmap.md](docs/roadmap.md).
**Next:** controller button prompts, virtual keyboard redesign, search, favorites ([roadmap](https://github.com/nextestudios/ControlFS/issues/95)) · **Later:** two panes, search, favorites, split volumes, unknown-controller wizard, light theme. Details in [docs/roadmap.md](docs/roadmap.md).

## More

Expand Down
3 changes: 2 additions & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Os botões seguem a **posição física**, então um controle Nintendo não inve
## Recursos

- Pastas e unidades reais, histórico, ordenação, itens ocultos, marcação, propriedades
- **Ícones nativos do Windows** para arquivos, pastas, pastas especiais e unidades, carregados em segundo plano e no tamanho da escala da tela
- **Operações de arquivo:** renomear, copiar, recortar, colar, mover e excluir para a Lixeira, com conflitos (pular / manter ambos / substituir / mesclar pastas) e resultado por item
- **Teclado virtual** próprio (português/inglês, acentos, símbolos, cursor, senha mascarada) usável só com direções + confirmar + voltar
- **Criar pasta** com as regras de nomes do Windows
Expand All @@ -55,7 +56,7 @@ Os botões seguem a **posição física**, então um controle Nintendo não inve

## Roadmap

**Próximos:** ícones nativos do Windows, botões do controle no rodapé, novo teclado virtual, busca, favoritos ([roadmap](https://github.com/nextestudios/ControlFS/issues/95)) · **Depois:** dois painéis, busca, favoritos, volumes divididos, assistente para controles desconhecidos, tema claro. Detalhes em [docs/roadmap.md](docs/roadmap.md).
**Próximos:** botões do controle no rodapé, novo teclado virtual, busca, favoritos ([roadmap](https://github.com/nextestudios/ControlFS/issues/95)) · **Depois:** dois painéis, busca, favoritos, volumes divididos, assistente para controles desconhecidos, tema claro. Detalhes em [docs/roadmap.md](docs/roadmap.md).

## Mais

Expand Down
11 changes: 11 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ versão, Windows, controle e conexão. Resultados de controles vão para `contro
- [ ] Com controle real: segurar Oeste por 2 s apaga vários caracteres e para na hora ao soltar; segurar LB/RB move o cursor continuamente; segurar Sul em Concluir não repete.
- [ ] Favoritar uma pasta num pendrive, fechar o app, remover o pendrive e reabrir: o favorito aparece primeiro como indisponível (⚠); reconectar e voltar ao início: volta a abrir.

## Ícones do Windows (#24)

- [ ] Início: Downloads, Documentos, Área de trabalho, Imagens, Vídeos e Músicas com o ícone próprio; unidade fixa, pendrive,
leitor óptico e unidade de rede com ícones diferentes.
- [ ] Pasta com .txt, .pdf, .docx, .zip, .exe e .ico: cada um com o ícone do programa associado; .exe com o ícone do próprio programa.
- [ ] Dentro de um ZIP: ícones por extensão (nenhum emoji na lista).
- [ ] Pasta com 10.000 arquivos: rolar com o gatilho e o analógico; a rolagem não trava enquanto os ícones aparecem.
- [ ] 1080p 100%, 4K 200% e portátil (1280×800 a 150%): ícones nítidos, sem serrilhado; mover a janela para um monitor
com outra escala troca os ícones para o novo tamanho.
- [ ] Anel de foco e marcação continuam legíveis sobre os ícones.

## Formatos, compactar e abrir com o Windows

- [ ] Abrir e extrair um RAR e um 7z reais baixados da internet (conferir a marca de origem nos extraídos).
Expand Down
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Monólito modular desktop. O núcleo não depende de WinUI, SDL3 nem SharpCompre

## Onde está cada contrato pedido

`IFileSystemProvider`, `IArchiveService`, `IInputBackend`, `ISettingsStore` em `Core/Contracts`; `IArchiveEngine` em
`IFileSystemProvider`, `IArchiveService`, `IInputBackend`, `ISettingsStore`, `IIconProvider` em `Core/Contracts`
(ícones do Shell: `Infrastructure.Windows/Shell/ShellIconProvider`, numa thread STA própria; cache LRU por tipo e
tamanho em `App/Controls/IconLoader`); `IArchiveEngine` em
`Infrastructure.Archives/Engines` (detalhe de adaptador). `IFileOperationService`, `IControllerProfileStore`,
`IThumbnailService` e uma interface pública para a fila **ainda não existem**: serão criados quando houver implementação
real (Etapas 2–3), não como classes vazias.
39 changes: 31 additions & 8 deletions src/ControlFS.App/Controls/EntryRowTemplate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ControlFS.App.Resources;
using ControlFS.Core.Contracts;
using ControlFS.Core.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
Expand All @@ -19,7 +20,10 @@ public static class EntryRowTemplate
"<Border x:Name=\"Ring\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" BorderThickness=\"3\" CornerRadius=\"6\">" +
"<Grid Padding=\"12,8\" ColumnSpacing=\"12\">" +
"<Grid.ColumnDefinitions><ColumnDefinition Width=\"36\"/><ColumnDefinition Width=\"*\"/><ColumnDefinition Width=\"Auto\"/></Grid.ColumnDefinitions>" +
"<TextBlock x:Name=\"Icon\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" FontSize=\"22\" VerticalAlignment=\"Center\"/>" +
"<Grid Width=\"32\" Height=\"32\" VerticalAlignment=\"Center\">" +
"<TextBlock x:Name=\"Icon\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" FontFamily=\"Segoe Fluent Icons, Segoe MDL2 Assets\" FontSize=\"26\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"/>" +
"<Image x:Name=\"IconImage\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" Width=\"32\" Height=\"32\" Stretch=\"Uniform\" Visibility=\"Collapsed\"/>" +
"</Grid>" +
"<StackPanel Grid.Column=\"1\" VerticalAlignment=\"Center\">" +
"<TextBlock x:Name=\"Title\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" FontSize=\"20\" TextTrimming=\"CharacterEllipsis\"/>" +
"<TextBlock x:Name=\"Detail\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" FontSize=\"14\" TextTrimming=\"CharacterEllipsis\"/>" +
Expand All @@ -29,7 +33,7 @@ public static class EntryRowTemplate

public static DataTemplate Create() => (DataTemplate)XamlReader.Load(Xaml);

public static void Fill(SelectorItem container, FileEntry entry, bool focused, bool selected, bool cut = false)
public static void Fill(SelectorItem container, FileEntry entry, bool focused, bool selected, bool cut, IconLoader icons, IReadOnlySet<string>? specialFolders)
{
if (container.ContentTemplateRoot is not FrameworkElement root) return;
SetFocused(container, focused);
Expand All @@ -38,25 +42,44 @@ public static void Fill(SelectorItem container, FileEntry entry, bool focused, b
var detail = (TextBlock)root.FindName("Detail");
var mark = (TextBlock)root.FindName("Mark");

icon.Text = entry.IsBlocked ? "⚠" : entry.Kind switch
// Símbolo de reserva (fonte de ícones do Windows, nunca emoji) até o ícone do Shell chegar.
icon.Text = entry.IsBlocked ? Glyphs.Warning : entry.Kind switch
{
EntryKind.Drive => "🖴",
EntryKind.KnownFolder or EntryKind.Directory or EntryKind.ArchiveDirectory => "📁",
_ when IsArchiveName(entry.Name) => "📦",
_ => "📄",
EntryKind.Drive => Glyphs.Drive,
EntryKind.KnownFolder or EntryKind.Directory or EntryKind.ArchiveDirectory => Glyphs.Folder,
_ when IsArchiveName(entry.Name) => Glyphs.Archive,
_ => Glyphs.File,
};
icon.Foreground = entry.IsBlocked ? Theme.Danger : Theme.TextMuted;
icons.Load((Image)root.FindName("IconImage"), icon, IconRequest.For(entry, specialFolders));
title.Text = entry.Name;
title.Foreground = entry.IsBlocked ? Theme.Danger : entry.IsHidden ? Theme.TextMuted : Theme.Text;
detail.Text = entry.IsBlocked ? "Bloqueado: " + entry.BlockedReason : Describe(entry);
detail.Foreground = entry.IsBlocked ? Theme.Danger : Theme.TextMuted;
mark.Text = selected ? "✔ marcado" : cut ? "✂ recortado" : entry.IsEncrypted ? "🔒" : string.Empty;
mark.Text = selected ? "✔ marcado" : cut ? "✂ recortado" : entry.IsEncrypted ? "com senha" : string.Empty;
// Recortado: conteúdo esmaecido (e indicado em texto, não só visualmente); o anel de foco continua nítido.
if (root is Border { Child: UIElement content }) content.Opacity = cut ? 0.5 : 1.0;
mark.Foreground = selected ? Theme.Selected : Theme.TextMuted;
var state = (selected ? ", marcado" : string.Empty) + (cut ? ", recortado" : string.Empty);
AutomationProperties.SetName(container, entry.IsBlocked ? $"{entry.Name}, bloqueado: {entry.BlockedReason}" : $"{entry.Name}{state}");
}

/// <summary>Linha voltou para a fila de reciclagem: cancela o ícone pendente.</summary>
public static void Recycle(SelectorItem container, IconLoader icons)
{
if (container.ContentTemplateRoot is FrameworkElement root && root.FindName("IconImage") is Image image) icons.Cancel(image);
}

/// <summary>Segoe Fluent Icons / Segoe MDL2 Assets.</summary>
private static class Glyphs
{
public const string Folder = "\uE8B7";
public const string File = "\uE8A5";
public const string Drive = "\uEDA2";
public const string Archive = "\uE7B8";
public const string Warning = "\uE7BA";
}

/// <summary>Liga/desliga o anel de foco de uma linha já preenchida (sem refazer o conteúdo).</summary>
public static void SetFocused(SelectorItem container, bool focused)
{
Expand Down
131 changes: 131 additions & 0 deletions src/ControlFS.App/Controls/IconLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.WindowsRuntime;
using ControlFS.Core.Contracts;
using ControlFS.Core.Icons;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;

namespace ControlFS.App.Controls;

/// <summary>
/// Carrega ícones do sistema para as linhas da lista sem travar a UI: o símbolo aparece na hora e a imagem entra quando
/// chega. Pedidos iguais (mesma extensão) viram um só; linhas recicladas cancelam o pedido. Cache limitado, por tipo e
/// tamanho em pixels; trocar a escala (DPI) esvazia o cache. Usar somente na thread de UI.
/// </summary>
public sealed class IconLoader(IIconProvider provider)
{
/// <summary>Tamanho do ícone na linha, em pixels independentes de dispositivo.</summary>
public const double IconSize = 32;

private readonly LruCache<string, ImageSource?> _cache = new(512);
private readonly Dictionary<string, Flight> _inflight = [];
private readonly ConditionalWeakTable<Image, CancellationTokenSource> _pending = [];

private sealed class Flight(Task<IconImage?> task, CancellationTokenSource cancellation)
{
public Task<IconImage?> Task { get; } = task;
public CancellationTokenSource Cancellation { get; } = cancellation;
public int Waiters { get; set; }
}

public int SizePx { get; private set; } = (int)IconSize;

/// <summary>A escala mudou (outro monitor, outro DPI): os ícones em cache ficaram do tamanho errado.</summary>
public event Action? Invalidated;

public void SetScale(double scale)
{
var px = Math.Max(16, (int)Math.Round(IconSize * scale));
if (px == SizePx) return;
SizePx = px;
_cache.Clear();
_inflight.Clear();
Invalidated?.Invoke();
}

/// <summary>Mostra o símbolo de reserva e, quando houver, troca pelo ícone do sistema.</summary>
public void Load(Image image, UIElement fallback, IconRequest? request)
{
Cancel(image);
if (request is null)
{
Show(image, fallback, null, null);
return;
}
var key = request.Key + "@" + SizePx;
image.Tag = key;
if (_cache.TryGet(key, out var cached))
{
Show(image, fallback, cached, key);
return;
}
Show(image, fallback, null, key);
var cts = new CancellationTokenSource();
_pending.AddOrUpdate(image, cts);
_ = LoadAsync(image, fallback, request, key, cts.Token);
}

/// <summary>Linha reciclada pela virtualização: o pedido dela não interessa mais.</summary>
public void Cancel(Image image)
{
if (_pending.TryGetValue(image, out var previous))
{
previous.Cancel();
previous.Dispose();
_pending.Remove(image);
}
image.Tag = null;
}

private async Task LoadAsync(Image image, UIElement fallback, IconRequest request, string key, CancellationToken cancellationToken)
{
if (!_inflight.TryGetValue(key, out var flight))
{
var cts = new CancellationTokenSource();
flight = new Flight(provider.GetIconAsync(request, SizePx, cts.Token), cts);
_inflight[key] = flight;
}
flight.Waiters++;
IconImage? pixels;
try
{
pixels = await flight.Task.WaitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Ninguém mais espera este ícone: o provedor pode pular o trabalho.
if (--flight.Waiters == 0 && _inflight.TryGetValue(key, out var current) && current == flight)
{
_inflight.Remove(key);
flight.Cancellation.Cancel();
}
return;
}
flight.Waiters--;
if (_inflight.TryGetValue(key, out var same) && same == flight) _inflight.Remove(key);
if (!_cache.TryGet(key, out var source))
{
source = pixels is null ? null : ToBitmap(pixels);
if (key.EndsWith("@" + SizePx, StringComparison.Ordinal)) _cache.Set(key, source); // não guarda tamanho antigo
}
if (Equals(image.Tag, key)) Show(image, fallback, source, key);
}

private static void Show(Image image, UIElement fallback, ImageSource? source, string? key)
{
image.Source = source;
image.Visibility = source is null ? Visibility.Collapsed : Visibility.Visible;
fallback.Visibility = source is null ? Visibility.Visible : Visibility.Collapsed;
image.Tag = key;
}

private static WriteableBitmap ToBitmap(IconImage icon)
{
var bitmap = new WriteableBitmap(icon.Width, icon.Height);
using (var stream = bitmap.PixelBuffer.AsStream()) stream.Write(icon.Pixels.Span);
bitmap.Invalidate();
return bitmap;
}
}
Loading
Loading