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 @@ -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)

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions docs/GUIDE.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ______
Expand Down
19 changes: 19 additions & 0 deletions src/ConsoleMode/Controls/ShortcutPicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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()
Expand All @@ -100,6 +110,12 @@ private void StopCapture()
Refresh();
}

/// <summary>Stops the active capture when its containing console overlay is dismissed.</summary>
public void CancelCapture() => StopCapture();

/// <summary>Moves controller focus to this shortcut's Set/Change/Cancel action.</summary>
public bool FocusCaptureButton() => _set.Focus(FocusState.Keyboard);

private void Poll()
{
if (_capture is null) return;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/ConsoleMode/Resources/Strings.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/ConsoleMode/Resources/Strings.es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/ConsoleMode/Resources/Strings.pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 61 additions & 1 deletion src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down Expand Up @@ -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);
}

/// <summary>"desktop" / "console": the on-screen switch, remembered as an explicit choice.</summary>
Expand All @@ -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));
}

/// <summary>x:Bind helper for on/off rows.</summary>
public string OnOff(bool value) => LocalizationService.Get(value ? "ToggleOn" : "ToggleOff");
Expand Down Expand Up @@ -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<ComboOption> options, ComboOption? current, int step)
{
var index = UiModeResolver.Cycle(options.Count, current is null ? -1 : options.IndexOf(current), step);
Expand All @@ -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)
};
Expand All @@ -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)
{
Expand Down
17 changes: 14 additions & 3 deletions src/ConsoleMode/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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;
Expand All @@ -771,6 +781,7 @@ public void OnTourTipClosed(TeachingTip sender, TeachingTipClosedEventArgs args)
private void EndTour()
{
TourStep = 0;
IsConsoleTutorialOpen = false;
if (!_loadedConfig.TourDone)
{
var config = BuildConfig();
Expand Down
Loading
Loading