diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4b46504..11c7cb3 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -4,6 +4,8 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
## [Unreleased]
### What's new
+- **Theme choice in the welcome**: a new step, "Escuro ou claro?" (dark or light), with **Escuro** (dark), **Claro** (light) and **Automático (segue o Windows)** (follows Windows). The screen changes at once so you can see it, focus starts on the current choice, and you can change it later in Menu → Configurações → Tema. The welcome goes from five to six steps (the theme setting moved out of "O básico").
+### What's new
- **Controller sounds** (#276): Menu → Configurações → **Sons do controle** (off, low, medium, high, max). Short, soft cues synthesized inside the app, distinct for moving focus, confirming/opening, going back and marking. They play only for controller actions (never the keyboard), don't machine-gun when you hold the D-pad, use the Windows audio output and don't change any button or navigation. Off by default; without a sound device nothing happens.
### What's new
- **Automation and `controlfs://` protocol**: support for `controlfs://start`, `controlfs://stop`, and `controlfs://show` links, as well as command-line arguments (`--start`, `--stop`, `--show`, `--close`), enabling seamless external integration with game launchers and frontends (such as Console Mode), Stream Deck buttons, and automation scripts. Invoking `controlfs://start` when the app is already open restores and brings the window to the foreground without launching duplicate instances; `controlfs://stop` cleanly exits the app; with copies or other operations running it doesn't cancel them by itself and shows the exit confirmation. The protocol is registered by the installer; the portable version writes nothing to the registry (the command-line flags work there). After an update, the app relaunched by the installer waits for the old instance to leave.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1a2769..c460aa4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
## [Unreleased]
### Novidades
+- **Escolha do tema nas boas-vindas**: um passo novo, "Escuro ou claro?", com **Escuro**, **Claro** e **Automático (segue o Windows)**. A tela muda na hora para você ver, o foco já começa na escolha atual e dá para trocar depois em Menu → Configurações → Tema. As boas-vindas passam de cinco para seis passos (o ajuste de tema saiu do passo "O básico").
+### Novidades
- **Sons do controle** (#276): Menu → Configurações → **Sons do controle** (desligados, baixo, médio, alto, máximo). Toques curtos e suaves, desenhados no próprio app, distintos para mover o foco, confirmar/abrir, voltar e marcar. Tocam só nas ações do controle (nunca do teclado), não repetem feito metralhadora ao segurar o direcional, usam a saída de áudio do Windows e não mudam nenhum botão nem a navegação. Vem desligado; sem placa de som, nada acontece.
### Novidades
- **Automação e protocolo `controlfs://`**: suporte aos links `controlfs://start`, `controlfs://stop` e `controlfs://show`, bem como argumentos de linha de comando (`--start`, `--stop`, `--show`, `--close`), facilitando a integração externa com launchers e frontends (como o Console Mode), botões de Stream Deck e scripts. Acionar `controlfs://start` quando o app já está aberto restaura e traz a janela para o primeiro plano sem abrir instâncias duplicadas; `controlfs://stop` encerra o app de forma limpa; havendo cópias ou outras operações em andamento, ele não as cancela sozinho e mostra a confirmação de saída. O protocolo é registrado pelo instalador; a versão portátil não escreve nada no registro (as opções de linha de comando funcionam nela). Depois de uma atualização, o app reaberto pelo instalador espera a instância antiga sair.
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index 9989827..8f528cf 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -82,7 +82,7 @@ Important confirmations (delete, replace, undo) are answered on the PC: from the
## First launch: welcome and guided tutorial
-The first time ControlFS opens, a full-screen **welcome** walks you through five steps: welcome; how the controller works (the real buttons of the controller in use — Open, Back, Actions, Menu, Search, Mark, L1/R1 for the top bar, L2/R2 for tabs, R3 for the view; press any button on your controller and the prompts switch to it); the basics, applied at once (confirm with the bottom or right button, button labels, theme, list/grid, Fluidez); privacy (everything stays on this PC; the update check is optional; the phone link is off until you open it); and **Quer fazer o tutorial guiado?** — **Começar tutorial** or **Agora não**.
+The first time ControlFS opens, a full-screen **welcome** walks you through six steps: welcome; how the controller works (the real buttons of the controller in use — Open, Back, Actions, Menu, Search, Mark, L1/R1 for the top bar, L2/R2 for tabs, R3 for the view; press any button on your controller and the prompts switch to it); **dark or light?** (Escuro, Claro or Automático, which follows Windows; the screen changes at once so you can see it); the basics, applied at once (confirm with the bottom or right button, button labels, list/grid, Fluidez); privacy (everything stays on this PC; the update check is optional; the phone link is off until you open it); and **Quer fazer o tutorial guiado?** — **Começar tutorial** or **Agora não**.
| Control | Welcome |
|---|---|
diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md
index 38ebf0a..614b46f 100644
--- a/docs/GUIDE.pt-BR.md
+++ b/docs/GUIDE.pt-BR.md
@@ -82,7 +82,7 @@ Confirmações importantes (excluir, substituir, desfazer) são respondidas no P
## Primeira vez: boas-vindas e tutorial guiado
-Na primeira vez que o ControlFS abre, as **boas-vindas** em tela cheia mostram cinco passos: boas-vindas; como o controle funciona (os botões de verdade do controle em uso — Abrir, Voltar, Ações, Menu, Buscar, Marcar, L1/R1 da barra superior, L2/R2 das abas, R3 da exibição; aperte qualquer botão do seu controle e as legendas mudam para ele); o básico, que vale na hora (confirmar com o botão inferior ou direito, legendas, tema, lista/grade, Fluidez); privacidade (tudo fica neste PC; procurar atualizações é opcional; o celular como controle fica desligado até você abrir); e **Quer fazer o tutorial guiado?** — **Começar tutorial** ou **Agora não**.
+Na primeira vez que o ControlFS abre, as **boas-vindas** em tela cheia mostram seis passos: boas-vindas; como o controle funciona (os botões de verdade do controle em uso — Abrir, Voltar, Ações, Menu, Buscar, Marcar, L1/R1 da barra superior, L2/R2 das abas, R3 da exibição; aperte qualquer botão do seu controle e as legendas mudam para ele); **escuro ou claro?** (Escuro, Claro ou Automático, que segue o Windows; a tela muda na hora para você ver); o básico, que vale na hora (confirmar com o botão inferior ou direito, legendas, lista/grade, Fluidez); privacidade (tudo fica neste PC; procurar atualizações é opcional; o celular como controle fica desligado até você abrir); e **Quer fazer o tutorial guiado?** — **Começar tutorial** ou **Agora não**.
| Controle | Boas-vindas |
|---|---|
diff --git a/docs/input-and-focus.md b/docs/input-and-focus.md
index 5d7c8f5..15483ed 100644
--- a/docs/input-and-focus.md
+++ b/docs/input-and-focus.md
@@ -233,7 +233,7 @@ com foco em "Extrair para \"nome\"". O rótulo usa a extensão (rápido); a aç
analógico) move o foco entre as opções do passo dando a volta; Sul escolhe ou troca o ajuste em foco (o foco fica
nele); Leste e L1 voltam um passo (no primeiro, nada); R1 avança; Start/Menu pula tudo sem confirmação. Trocar
"Confirmar com" vale na hora: o próximo Confirmar já é o outro botão. Mouse: clique numa opção (`PointerChooseModalOption`)
- ou numa legenda (a mesma ação). Narrador: "Boas-vindas, passo N de 5: título. texto" e a opção em foco com a descrição.
+ ou numa legenda (a mesma ação). Narrador: "Boas-vindas, passo N de 6: título. texto" e a opção em foco com a descrição.
- Só aparecem na primeira execução da janela real (`AppController.OfferOnboarding` e `AppSettings.OnboardingCompleted`
false, gravado pelo `JsonSettingsStore` ao criar as preferências). Preferências de versões anteriores (sem o campo) contam
como vistas; testes, `--render-screens` e `--no-onboarding` nunca as mostram sem pedir.
diff --git a/src/ControlFS.App/Diagnostics/ScreenRenderer.cs b/src/ControlFS.App/Diagnostics/ScreenRenderer.cs
index 167c1f8..43a0700 100644
--- a/src/ControlFS.App/Diagnostics/ScreenRenderer.cs
+++ b/src/ControlFS.App/Diagnostics/ScreenRenderer.cs
@@ -593,6 +593,8 @@ private static async Task CaptureOnboardingAsync(AppController app, MainWindow w
app.Handle(InputAction.NextRegion);
await CaptureAsync(stage, target, dir, "o2-onboarding-controls", window);
app.Handle(InputAction.NextRegion);
+ await CaptureAsync(stage, target, dir, "o2b-onboarding-theme", window);
+ app.Handle(InputAction.NextRegion);
app.Handle(InputAction.NavigateDown); // foco em "Legendas", com a descrição
await CaptureAsync(stage, target, dir, "o3-onboarding-basics", window);
app.Handle(InputAction.NextRegion);
diff --git a/src/ControlFS.App/Views/OnboardingView.cs b/src/ControlFS.App/Views/OnboardingView.cs
index 30ae211..231db3a 100644
--- a/src/ControlFS.App/Views/OnboardingView.cs
+++ b/src/ControlFS.App/Views/OnboardingView.cs
@@ -149,6 +149,7 @@ private static StackPanel OnboardingIntro(OnboardingModal modal, bool compact)
var icon = modal.Step switch
{
OnboardingStep.Controls => ActionIcon.Controller,
+ OnboardingStep.Theme => ActionIcon.Theme,
OnboardingStep.Basics => ActionIcon.Settings,
OnboardingStep.Privacy => ActionIcon.Password,
OnboardingStep.Tutorial => ActionIcon.Tutorial,
diff --git a/src/ControlFS.Application/AppController.Onboarding.cs b/src/ControlFS.Application/AppController.Onboarding.cs
index ebadf77..588a23c 100644
--- a/src/ControlFS.Application/AppController.Onboarding.cs
+++ b/src/ControlFS.Application/AppController.Onboarding.cs
@@ -1,3 +1,4 @@
+using ControlFS.Core.Appearance;
using ControlFS.Application.State;
using ControlFS.Core.Actions;
using ControlFS.Core.Contracts;
@@ -118,12 +119,17 @@ private void LoadOnboardingStep(OnboardingModal modal, int index, bool keepFocus
modal.ControlLegend = [];
void Reload() => LoadOnboardingStep(modal, modal.StepIndex, keepFocus: true);
var next = new OnboardingOption("Continuar", () => NextOnboardingStep(modal), ActionIcon.Resume);
+ OnboardingOption ThemeOption(string label, ThemeMode mode, string detail) => new(label, () =>
+ {
+ SetTheme(mode); // vale na hora: a janela recebe SettingsChanged e se redesenha
+ Reload();
+ }, Settings.Theme == mode ? ActionIcon.RadioOn : ActionIcon.RadioOff, Detail: detail);
switch (modal.Step)
{
case OnboardingStep.Welcome:
modal.StepTitle = "Boas-vindas ao ControlFS";
modal.StepBody = "Um gerenciador de arquivos feito para o controle: navegue, abra, copie e extraia do sofá, sem mouse nem teclado. "
- + "São cinco passos curtos; Menu pula tudo quando quiser.";
+ + "São seis passos curtos; Menu pula tudo quando quiser.";
modal.Options = [new("Começar", () => NextOnboardingStep(modal), ActionIcon.Resume)];
break;
case OnboardingStep.Controls:
@@ -148,6 +154,18 @@ private void LoadOnboardingStep(OnboardingModal modal, int index, bool keepFocus
];
modal.Options = [next];
break;
+ case OnboardingStep.Theme:
+ modal.StepTitle = "Escuro ou claro?";
+ modal.StepBody = "A tela muda na hora para você ver. Dá para trocar depois em Menu → Configurações → Tema.";
+ modal.Options =
+ [
+ ThemeOption("Escuro", ThemeMode.Dark, "Fundo escuro, bom para ambientes com pouca luz e para jogar à noite."),
+ ThemeOption("Claro", ThemeMode.Light, "Fundo claro, bom com a sala iluminada ou no sol."),
+ ThemeOption("Automático (segue o Windows)", ThemeMode.System, "Acompanha o modo claro ou escuro que o Windows está usando."),
+ next,
+ ];
+ if (!keepFocus) focus = Math.Max(0, (int)(Settings.Theme switch { ThemeMode.Dark => 0, ThemeMode.Light => 1, _ => 2 }));
+ break;
case OnboardingStep.Basics:
var south = Settings.Convention == ConfirmBackConvention.SouthConfirms;
modal.StepTitle = "O básico, do seu jeito";
@@ -164,11 +182,6 @@ private void LoadOnboardingStep(OnboardingModal modal, int index, bool keepFocus
UpdateSettings(s => s with { LabelStyle = (ButtonLabelStyle)(((int)s.LabelStyle + 1) % 5) });
Reload();
}, ActionIcon.Labels, LabelStyleName(Settings.LabelStyle), "Automáticas seguem o controle em uso (Xbox, PlayStation, Nintendo)."),
- new($"Tema: {ThemeName(Settings.Theme)}", () =>
- {
- CycleTheme();
- Reload();
- }, ActionIcon.Theme, ThemeName(Settings.Theme), "Automático segue o modo claro ou escuro do Windows."),
new($"Exibição: {ViewName(Settings.View)}", () =>
{
ToggleView();
@@ -209,7 +222,7 @@ private void LoadOnboardingStep(OnboardingModal modal, int index, bool keepFocus
];
break;
}
- modal.FocusIndex = keepFocus ? Math.Clamp(focus, 0, Math.Max(0, modal.Options.Count - 1)) : 0;
+ modal.FocusIndex = keepFocus || modal.Step == OnboardingStep.Theme ? Math.Clamp(focus, 0, Math.Max(0, modal.Options.Count - 1)) : 0;
}
/// Ajuda (Menu → Ajuda): tutorial guiado, boas-vindas e Sobre.
diff --git a/src/ControlFS.Application/State/Onboarding.cs b/src/ControlFS.Application/State/Onboarding.cs
index de943cc..d6c3174 100644
--- a/src/ControlFS.Application/State/Onboarding.cs
+++ b/src/ControlFS.Application/State/Onboarding.cs
@@ -7,6 +7,7 @@ public enum OnboardingStep
{
Welcome,
Controls,
+ Theme,
Basics,
Privacy,
Tutorial,
diff --git a/tests/ControlFS.UnitTests/Application/OnboardingJourneyTests.cs b/tests/ControlFS.UnitTests/Application/OnboardingJourneyTests.cs
index 09bcc19..1b68638 100644
--- a/tests/ControlFS.UnitTests/Application/OnboardingJourneyTests.cs
+++ b/tests/ControlFS.UnitTests/Application/OnboardingJourneyTests.cs
@@ -37,7 +37,7 @@ public void First_run_walks_the_steps_with_the_controller_applies_settings_at_on
var onboarding = Assert.IsType(app.TopModal);
Assert.Equal(OnboardingStep.Welcome, onboarding.Step);
Assert.Contains(app.Hints, h => h is { Action: InputAction.OpenAppMenu, Label: "Pular" });
- Assert.StartsWith("Boas-vindas, passo 1 de 5", app.DescribeFocus().Context, StringComparison.Ordinal); // Narrador
+ Assert.StartsWith("Boas-vindas, passo 1 de 6", app.DescribeFocus().Context, StringComparison.Ordinal); // Narrador
d.Press(InputAction.Confirm); // Começar
Assert.Equal(OnboardingStep.Controls, onboarding.Step);
@@ -46,17 +46,29 @@ public void First_run_walks_the_steps_with_the_controller_applies_settings_at_on
Assert.Equal(OnboardingStep.Welcome, onboarding.Step);
d.Press(InputAction.NextRegion);
d.Press(InputAction.Confirm); // Continuar
- Assert.Equal(OnboardingStep.Basics, onboarding.Step);
+ Assert.Equal(OnboardingStep.Theme, onboarding.Step);
- // Ajuste com efeito imediato: o tema troca na hora, a janela recebe e o foco continua no ajuste.
- d.Press(InputAction.NavigateDown);
- d.Press(InputAction.NavigateDown);
- Assert.StartsWith("Tema:", onboarding.FocusedOption!.Label, StringComparison.Ordinal);
+ // Escolha de tema: automático de início (foco nele); Claro e Escuro valem na hora, a janela recebe e o passo continua.
+ Assert.Equal("Automático (segue o Windows)", onboarding.FocusedOption!.Label);
+ d.Press(InputAction.NavigateUp);
+ d.Press(InputAction.NavigateUp);
+ Assert.Equal("Escuro", onboarding.FocusedOption!.Label);
d.Press(InputAction.Confirm);
Assert.Equal(ThemeMode.Dark, app.Settings.Theme);
Assert.Equal(ThemeMode.Dark, themes[^1]);
- Assert.Equal("Tema: escuro", onboarding.FocusedOption!.Label);
+ Assert.Equal(ActionIcon.RadioOn, onboarding.FocusedOption!.Icon);
+ Assert.Equal(ActionIcon.RadioOff, onboarding.Options[1].Icon);
Assert.Same(onboarding, app.TopModal);
+ d.Press(InputAction.NavigateDown);
+ d.Press(InputAction.Confirm); // Claro
+ Assert.Equal(ThemeMode.Light, app.Settings.Theme);
+ d.Press(InputAction.NavigateUp);
+ d.Press(InputAction.Confirm); // Escuro de novo
+ Assert.Equal(ThemeMode.Dark, app.Settings.Theme);
+
+ d.Press(InputAction.NextRegion);
+ Assert.Equal(OnboardingStep.Basics, onboarding.Step);
+ Assert.DoesNotContain(onboarding.Options, o => o.Label.StartsWith("Tema:", StringComparison.Ordinal)); // o tema tem passo próprio
d.Press(InputAction.NextRegion); // privacidade
d.Press(InputAction.NextRegion); // convite para o tutorial
diff --git a/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs b/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs
index 404f79b..f641e47 100644
--- a/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs
+++ b/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs
@@ -49,7 +49,7 @@ public void Shows_once_after_the_welcome_and_the_tutorial_opens_links_only_on_co
// Primeiro as boas-vindas; a tela da equipe não aparece por cima delas.
Assert.IsType(app.TopModal);
- for (var i = 0; i < 4; i++) d.Press(InputAction.NextRegion);
+ for (var i = 0; i < 5; i++) d.Press(InputAction.NextRegion);
Assert.Equal(OnboardingStep.Tutorial, ((OnboardingModal)app.TopModal!).Step);
d.Press(InputAction.Confirm); // "Começar tutorial": a tela da equipe espera o tutorial terminar
Assert.Null(app.TopModal);