diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4bf1084..fd75bdb 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -7,6 +7,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
- Settings → TV: console mode turns a Google TV / Android TV on over the network and switches to the PC's HDMI input; it can also put the TV in standby on restore. (#126)
### Fixes
+- Console interface: the System tab now supports custom controller shortcuts and desktop settings, including Playnite, Android TV, custom FPS, closing Steam, the tutorial and the data folder. (#128)
- Session menu: closing a window through the internal Alt+Tab now asks for confirmation; Cancel keeps the window open and restores focus to its card. (#127)
- Reverted PR #45: restored the previous behavior when stopping a session. (#125)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f111a83..64d6919 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
- Ajustes → TV: o modo console liga a Google TV / Android TV pela rede e troca para a entrada HDMI do PC; também pode colocá-la em espera ao restaurar. (#126)
### Correções
+- Interface Console: a aba Sistema agora permite configurar atalhos personalizados e acessar os ajustes do modo PC, incluindo Playnite, Android TV, FPS personalizado, fechar a Steam, tutorial e pasta de dados. (#128)
- Menu da sessão: fechar uma janela pelo Alt+Tab interno agora pede confirmação; Cancelar mantém a janela aberta e devolve o foco ao cartão. (#127)
- Revertida a PR #45: restaurado o comportamento anterior ao encerrar uma sessão. (#125)
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index 52fd7a6..d56a51e 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -12,6 +12,12 @@ Details that don't fit in the [README](../README.md). 🇧🇷 [Guia em portugu
You can also restore anytime from the tray (*Restore setup* / *Show window*). With black overlays, **ESC** dismisses the curtains.
+## Settings in Console mode
+
+The **System** tab provides the same settings as desktop mode, including controller shortcuts, Playnite, Android TV, custom FPS and the data folder. Both interfaces share the saved values, which remain after restarting the app.
+
+Open the shortcut action you want to configure, choose **Set** or **Change**, hold the desired combination on one controller and release the buttons. **Remove** disables that shortcut. During capture, controller buttons do not navigate the interface; use **Cancel** or **Esc** to stop capture. Outside capture, **B** leaves the editor. Address and custom FPS fields accept keyboard input.
+
## Local control API
`consolemode://` links fire and forget. Tools that need an answer — a remote-control agent running as a Windows service, a Stream Deck plugin showing whether console mode is on — can use the named pipe `\\.\pipe\ConsoleMode.Control` while the app is running: send one JSON line, get one back.
diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md
index 1fdeae2..40076b0 100644
--- a/docs/GUIDE.pt-BR.md
+++ b/docs/GUIDE.pt-BR.md
@@ -12,6 +12,12 @@ Detalhes que não cabem no [README](../README.pt-BR.md). 🇺🇸 [Guide in Engl
Também dá para restaurar a qualquer momento pela bandeja (*Restaurar setup* / *Mostrar janela*). Com cortinas pretas, **ESC** remove o overlay.
+## Configurações no modo Console
+
+A aba **Sistema** oferece os mesmos ajustes do modo PC, incluindo os atalhos do controle, Playnite, Android TV, FPS personalizado e pasta de dados. Os valores são compartilhados entre as duas interfaces e permanecem salvos ao reabrir o app.
+
+Abra a ação de atalho que deseja configurar, escolha **Definir** ou **Alterar**, segure a combinação desejada em um único controle e solte os botões. **Remover** desativa aquele atalho. Durante a captura, os botões não navegam pela interface; use **Cancelar** ou **Esc** para interrompê-la. Fora da captura, **B** volta do editor. Campos de endereço e FPS personalizado aceitam digitação por teclado.
+
## API de controle local
Os links `consolemode://` não dão resposta. Ferramentas que precisam de uma — um agente de controle remoto rodando como serviço do Windows, um plugin de Stream Deck que mostra se o modo console está ligado — podem usar o named pipe `\\.\pipe\ConsoleMode.Control` com o app aberto: envie uma linha JSON e receba outra.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 7b9e276..5ccaa57 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -257,6 +257,17 @@ Pré-condições: TV Google TV / Android TV na mesma rede do PC, com **Depuraç
- [ ] O arquivo `adbkey.pem` na pasta de dados começa com `dpapi:` (não com `-----BEGIN`). Copiar a pasta de dados para outro usuário do Windows: o log mostra "chave ADB de outro usuário/PC; criando outra" e a TV pede permissão de novo. Resultado: ______
- [ ] TV desligada da tomada: **Jogar agora** espera no máximo ~30 s pela TV e segue (o log mostra o tempo). Resultado: ______
+## 3d. Configurações no modo Console
+
+- [ ] Em Sistema, as configurações do modo PC também estão disponíveis: caminho do Playnite, fechar a Steam, Android TV, FPS personalizado, atalhos, diagnóstico do controle, tutorial e pasta de dados. Resultado: ______
+- [ ] Configurar, alterar e remover os atalhos de abrir o app, menu da sessão e voltar ao PC; uma combinação em conflito mostra o mesmo erro do modo PC. Durante a captura, os botões não navegam nem iniciam uma sessão. Resultado: ______
+- [ ] Na captura, apertar e soltar B/○ sozinho cancela sem alterar o atalho; B/○ de novo fecha o editor e devolve o foco à configuração. B/○ junto com outro botão ainda pode ser salvo como combinação, mas B/○ sozinho não é um atalho válido. Teclado e mouse também conseguem abrir e fechar o editor sem acionar opções atrás dele. Resultado: ______
+- [ ] Alterar uma configuração no modo Console, trocar para PC e reabrir o app: o valor permanece igual nas duas interfaces. Resultado: ______
+- [ ] Selecionar e limpar o caminho do Playnite; cancelar o seletor mantém o valor anterior. Resultado: ______
+- [ ] Configurar a Android TV no modo Console: endereço, MAC opcional, entrada HDMI, comando da entrada e espera ao restaurar; Testar agora mostra o resultado e libera o botão após erro ou timeout. Resultado: ______
+- [ ] Selecionar FPS personalizado e digitar o limite; o modo PC mostra o mesmo valor. Sem RTSS disponível, a configuração respeita a mesma indisponibilidade das duas interfaces. Resultado: ______
+- [ ] Copiar o diagnóstico do controle, abrir a pasta de dados e rever o tutorial no modo Console; ao voltar, o foco continua em uma opção visível. Resultado: ______
+
## 4. Regressões
- [ ] Interface Desktop: mapa de telas, `Segmented`, chips, tour de 3 passos e Ajustes continuam como antes. Resultado: ______
diff --git a/src/ConsoleMode/Controls/ShortcutPicker.cs b/src/ConsoleMode/Controls/ShortcutPicker.cs
index bd520a1..3f6effe 100644
--- a/src/ConsoleMode/Controls/ShortcutPicker.cs
+++ b/src/ConsoleMode/Controls/ShortcutPicker.cs
@@ -26,6 +26,13 @@ public sealed class ShortcutPicker : StackPanel
Foreground = new SolidColorBrush(Color.FromArgb(255, 232, 88, 88)),
Visibility = Visibility.Collapsed
};
+ private readonly TextBlock _captureHint = new()
+ {
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 460,
+ Opacity = 0.7,
+ Visibility = Visibility.Collapsed
+ };
private readonly Button _set = new();
private readonly Button _clear = new();
private readonly DispatcherQueueTimer _timer;
@@ -45,6 +52,7 @@ public ShortcutPicker(MainViewModel vm, ShortcutSlot slot, bool stacked = false)
row.Children.Add(_set);
row.Children.Add(_clear);
Children.Add(row);
+ Children.Add(_captureHint);
Children.Add(_error);
_timer = DispatcherQueue.GetForCurrentThread().CreateTimer();
@@ -77,6 +85,8 @@ private void Refresh()
_clear.Content = LocalizationService.Get("ShortcutClear");
_clear.Visibility = isSet && _capture is null ? Visibility.Visible : Visibility.Collapsed;
_value.Opacity = isSet || _capture is not null ? 1 : 0.6;
+ _captureHint.Text = LocalizationService.Get("ShortcutCaptureHint", _vm.HintBack);
+ _captureHint.Visibility = _capture is null ? Visibility.Collapsed : Visibility.Visible;
}
private void StartCapture()
@@ -100,6 +110,12 @@ private void StopCapture()
Refresh();
}
+ /// Stops the active capture when its containing console overlay is dismissed.
+ public void CancelCapture() => StopCapture();
+
+ /// Moves controller focus to this shortcut's Set/Change/Cancel action.
+ public bool FocusCaptureButton() => _set.Focus(FocusState.Keyboard);
+
private void Poll()
{
if (_capture is null) return;
@@ -110,6 +126,9 @@ private void Poll()
if (captured is not { } mask) return;
StopCapture();
+ // A lone B/Circle is never a valid shortcut, so use its release to cancel capture.
+ // Combinations containing B still reach TrySetShortcut unchanged.
+ if (mask == ControllerShortcuts.B) return;
var error = _vm.TrySetShortcut(_slot, mask);
_error.Text = error ?? "";
_error.Visibility = error is null ? Visibility.Collapsed : Visibility.Visible;
diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json
index a1f8075..1722536 100644
--- a/src/ConsoleMode/Resources/Strings.en-US.json
+++ b/src/ConsoleMode/Resources/Strings.en-US.json
@@ -115,6 +115,7 @@
"ShortcutClear": "Remove",
"ShortcutCancel": "Cancel",
"ShortcutPressButtons": "Hold the buttons, then let go…",
+ "ShortcutCaptureHint": "Release to save. Press {0} alone to cancel.",
"ShortcutNotAllowed": "Use two or more buttons together (like Select + Y), or the Home button alone. A single button would fire in the middle of a game.",
"ShortcutInUse": "{0} is already the shortcut for \"{1}\". Pick a different combination.",
"ShortcutsOnboardingTitle": "Set up your controller shortcuts",
diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json
index 875882a..2010049 100644
--- a/src/ConsoleMode/Resources/Strings.es-ES.json
+++ b/src/ConsoleMode/Resources/Strings.es-ES.json
@@ -115,6 +115,7 @@
"ShortcutClear": "Quitar",
"ShortcutCancel": "Cancelar",
"ShortcutPressButtons": "Mantén los botones y luego suéltalos…",
+ "ShortcutCaptureHint": "Suelta para guardar. Pulsa {0} solo para cancelar.",
"ShortcutNotAllowed": "Usa dos o más botones a la vez (como Select + Y), o solo el botón Home. Un botón suelto se activaría en mitad del juego.",
"ShortcutInUse": "{0} ya es el atajo de \"{1}\". Elige otra combinación.",
"ShortcutsOnboardingTitle": "Configura los atajos del mando",
diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json
index 1dcf2c5..ed7721a 100644
--- a/src/ConsoleMode/Resources/Strings.pt-BR.json
+++ b/src/ConsoleMode/Resources/Strings.pt-BR.json
@@ -115,6 +115,7 @@
"ShortcutClear": "Remover",
"ShortcutCancel": "Cancelar",
"ShortcutPressButtons": "Segure os botões e depois solte…",
+ "ShortcutCaptureHint": "Solte para salvar. Pressione {0} sozinho para cancelar.",
"ShortcutNotAllowed": "Use dois ou mais botões juntos (como Select + Y), ou só o botão Home. Um botão sozinho dispararia no meio do jogo.",
"ShortcutInUse": "{0} já é o atalho de \"{1}\". Escolha outra combinação.",
"ShortcutsOnboardingTitle": "Configure os atalhos do controle",
diff --git a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
index 2f207ae..defc95f 100644
--- a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
+++ b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
@@ -37,6 +37,9 @@ partial void OnIsConsoleUiChanged(bool value)
{
OnPropertyChanged(nameof(IsDesktopHome));
OnPropertyChanged(nameof(IsDesktopSettings));
+ OnPropertyChanged(nameof(IsTourMap));
+ OnPropertyChanged(nameof(IsTourRole));
+ OnPropertyChanged(nameof(IsTourPlay));
if (value) { IsRolePanelOpen = false; IsControllerToastOpen = false; }
else { ConsoleTabIndex = HomeTabIndex; IsPickerOpen = false; }
}
@@ -130,6 +133,10 @@ private void RefreshConsoleTexts()
OnPropertyChanged(nameof(HdrText));
OnPropertyChanged(nameof(VrrText));
OnPropertyChanged(nameof(BackgroundModeText));
+ OnPropertyChanged(nameof(ConsoleTutorialTitle));
+ OnPropertyChanged(nameof(ConsoleTutorialBody));
+ OnPropertyChanged(nameof(ConsoleTutorialAction));
+ if (IsShortcutEditorOpen) ShortcutEditorTitle = ShortcutName(_shortcutEditorSlot);
}
/// "desktop" / "console": the on-screen switch, remembered as an explicit choice.
@@ -142,6 +149,32 @@ private void SwitchUi(string mode)
}
[ObservableProperty] private bool _isConsoleSettingsOpen;
+ [ObservableProperty] private bool _isShortcutEditorOpen;
+ [ObservableProperty] private string _shortcutEditorTitle = "";
+ [ObservableProperty] private bool _isConsoleTutorialOpen;
+ private ShortcutSlot _shortcutEditorSlot;
+
+ public string ShortcutEditorSlot => _shortcutEditorSlot.ToString().ToLowerInvariant();
+ public string ConsoleTutorialTitle => LocalizationService.Get(TourStep switch
+ {
+ 2 => "TourRoleTitle",
+ 3 => "TourPlayTitle",
+ _ => "TourMapTitle"
+ });
+ public string ConsoleTutorialBody => TourStep switch
+ {
+ 2 => LocalizationService.Get("TourRoleDescription"),
+ 3 => LocalizationService.Get("TourPlayDescription"),
+ _ => TourMapText
+ };
+ public string ConsoleTutorialAction => LocalizationService.Get(TourStep >= 3 ? "GotIt" : "Next");
+
+ partial void OnTourStepChanged(int value)
+ {
+ OnPropertyChanged(nameof(ConsoleTutorialTitle));
+ OnPropertyChanged(nameof(ConsoleTutorialBody));
+ OnPropertyChanged(nameof(ConsoleTutorialAction));
+ }
/// x:Bind helper for on/off rows.
public string OnOff(bool value) => LocalizationService.Get(value ? "ToggleOn" : "ToggleOff");
@@ -195,9 +228,32 @@ private void CycleSetting(string spec)
case "startup": StartWithWindows = !StartWithWindows; break;
case "updates": CheckUpdates = !CheckUpdates; break;
case "betas": BetaUpdates = !BetaUpdates; break;
+ case "closest": CloseSteamOnRestore = !CloseSteamOnRestore; break;
+ case "tvpower": TvTurnOffOnRestore = !TvTurnOffOnRestore; break;
}
}
+ [RelayCommand]
+ private void OpenShortcutEditor(string slot)
+ {
+ if (IsConsoleActive) return;
+ var target = slot switch
+ {
+ "home" => ShortcutSlot.Home,
+ "menu" => ShortcutSlot.Menu,
+ "exit" => ShortcutSlot.Exit,
+ _ => (ShortcutSlot?)null
+ };
+ if (target is null) return;
+ _shortcutEditorSlot = target.Value;
+ ShortcutEditorTitle = ShortcutName(target.Value);
+ OnPropertyChanged(nameof(ShortcutEditorSlot));
+ IsShortcutEditorOpen = true;
+ }
+
+ [RelayCommand]
+ private void CloseShortcutEditor() => IsShortcutEditorOpen = false;
+
private static ComboOption? Next(IList options, ComboOption? current, int step)
{
var index = UiModeResolver.Cycle(options.Count, current is null ? -1 : options.IndexOf(current), step);
@@ -222,10 +278,12 @@ private void OpenPicker(string key)
{
"launch" => (LocalizationService.Get("LaunchCard"), LaunchOptions.Select(o => (o.Text, o.Value)), SelectedLaunch?.Value),
"audio" => (LocalizationService.Get("AudioOutputCard"), AudioOptions.Select(o => (o.Text, o.Value)), SelectedAudio?.Value),
- "fps" => (LocalizationService.Get("FpsCard"), FpsOptions.Where(o => o.Value != FpsCustomValue.ToString()).Select(o => (o.Text, o.Value)), SelectedFps?.Value),
+ "fps" => (LocalizationService.Get("FpsCard"), FpsOptions.Select(o => (o.Text, o.Value)), SelectedFps?.Value),
"hide" => (LocalizationService.Get("HideOtherScreensCard"), HideStrategies.Select(o => (o.Text, o.Value)), SelectedHideStrategy?.Value),
"language" => (LocalizationService.Get("LanguageCard"), LanguageOptions.Select(o => (o.Text, o.Value)), SelectedLanguage?.Value),
"ui" => (LocalizationService.Get("UiModeCard"), UiModeOptions.Select(o => (o.Text, o.Value)), SelectedUiMode?.Value),
+ "tvprovider" => (LocalizationService.Get("TvControlCard"), TvProviders.Select(o => (o.Text, o.Value)), SelectedTvProvider?.Value),
+ "tvhdmi" => (LocalizationService.Get("TvHdmiCard"), TvHdmiInputs.Select(o => (o.Text, o.Value)), SelectedTvHdmiInput?.Value),
"mode" when FocusRow is not null => (LocalizationService.Get("ResolutionRefreshCard"), FocusRow.Modes.Select(m => (m.Text, m.Key)), FocusRow.SelectedMode?.Key),
_ => (null, null, null)
};
@@ -252,6 +310,8 @@ private void PickOption(PickerItem? item)
case "hide": SelectedHideStrategy = HideStrategies.FirstOrDefault(o => o.Value == item.Value) ?? SelectedHideStrategy; break;
case "language": SelectedLanguage = LanguageOptions.FirstOrDefault(o => o.Value == item.Value) ?? SelectedLanguage; break;
case "ui": SelectedUiMode = UiModeOptions.FirstOrDefault(o => o.Value == item.Value) ?? SelectedUiMode; break;
+ case "tvprovider": SelectedTvProvider = TvProviders.FirstOrDefault(o => o.Value == item.Value) ?? SelectedTvProvider; break;
+ case "tvhdmi": SelectedTvHdmiInput = TvHdmiInputs.FirstOrDefault(o => o.Value == item.Value) ?? SelectedTvHdmiInput; break;
case "mode":
if (FocusRow?.Modes.FirstOrDefault(m => m.Key == item.Value) is { } mode)
{
diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs
index 397796a..98ba468 100644
--- a/src/ConsoleMode/ViewModels/MainViewModel.cs
+++ b/src/ConsoleMode/ViewModels/MainViewModel.cs
@@ -735,9 +735,9 @@ private async Task TestConfirmAsync()
[NotifyPropertyChangedFor(nameof(IsTourMap), nameof(IsTourRole), nameof(IsTourPlay))]
private int _tourStep;
- public bool IsTourMap => TourStep == 1;
- public bool IsTourRole => TourStep == 2;
- public bool IsTourPlay => TourStep == 3;
+ public bool IsTourMap => !IsConsoleUi && TourStep == 1;
+ public bool IsTourRole => !IsConsoleUi && TourStep == 2;
+ public bool IsTourPlay => !IsConsoleUi && TourStep == 3;
public string TourMapText => FocusRow is null
? LocalizationService.Get("TourMapGeneric")
@@ -747,6 +747,16 @@ private async Task TestConfirmAsync()
private void StartTour()
{
if (!HasMonitors || IsConsoleActive) return;
+ if (IsConsoleUi)
+ {
+ ConsoleTabIndex = HomeTabIndex;
+ IsConsoleTutorialOpen = true;
+ IsStatusOpen = false;
+ SelectedMonitor = FocusRow ?? SelectedMonitor;
+ OnPropertyChanged(nameof(TourMapText));
+ TourStep = 1;
+ return;
+ }
ShowPage(settings: false);
IsStatusOpen = false;
SelectedMonitor = FocusRow ?? SelectedMonitor;
@@ -771,6 +781,7 @@ public void OnTourTipClosed(TeachingTip sender, TeachingTipClosedEventArgs args)
private void EndTour()
{
TourStep = 0;
+ IsConsoleTutorialOpen = false;
if (!_loadedConfig.TourDone)
{
var config = BuildConfig();
diff --git a/src/ConsoleMode/Views/ConsoleHomeView.xaml b/src/ConsoleMode/Views/ConsoleHomeView.xaml
index 90a02f6..15ddd55 100644
--- a/src/ConsoleMode/Views/ConsoleHomeView.xaml
+++ b/src/ConsoleMode/Views/ConsoleHomeView.xaml
@@ -475,24 +475,58 @@
-