From c4a8d5f5559c65de7b5996a10d039ff0a5b67ecb Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:42:35 -0300 Subject: [PATCH 1/2] feat: theme choice (dark, light or automatic) in the welcome New step with instant preview; the theme toggle left the basics step. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.en-US.md | 2 ++ CHANGELOG.md | 2 ++ docs/GUIDE.md | 2 +- docs/GUIDE.pt-BR.md | 2 +- docs/input-and-focus.md | 2 +- .../Diagnostics/ScreenRenderer.cs | 2 ++ src/ControlFS.App/Views/OnboardingView.cs | 1 + .../AppController.Onboarding.cs | 25 +++++++++++++----- src/ControlFS.Application/State/Onboarding.cs | 1 + .../Application/OnboardingJourneyTests.cs | 26 ++++++++++++++----- 10 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 4b46504b..11c7cb33 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 a1a27694..c460aa42 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 9989827f..8f528cf3 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 38ebf0a0..614b46f1 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 5d7c8f53..15483ed3 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 167c1f85..43a07002 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 30ae211c..231db3ab 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 ebadf770..14fa822e 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(); diff --git a/src/ControlFS.Application/State/Onboarding.cs b/src/ControlFS.Application/State/Onboarding.cs index de943cc6..d6c31742 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 09bcc194..1b686381 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 From 060376bda6443c4e87c47fb6affc475963574d04 Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:45:20 -0300 Subject: [PATCH 2/2] test: welcome now has six steps; focus the current theme on the theme step Co-Authored-By: Claude Sonnet 5.5 --- src/ControlFS.Application/AppController.Onboarding.cs | 2 +- tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ControlFS.Application/AppController.Onboarding.cs b/src/ControlFS.Application/AppController.Onboarding.cs index 14fa822e..588a23cc 100644 --- a/src/ControlFS.Application/AppController.Onboarding.cs +++ b/src/ControlFS.Application/AppController.Onboarding.cs @@ -222,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/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs b/tests/ControlFS.UnitTests/Application/PromoJourneyTests.cs index 404f79b3..f641e474 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);