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
5 changes: 2 additions & 3 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing.

## [Unreleased]
### What's new
- Settings → TV: console mode turns the TV on and switches to the PC's HDMI input, and can put it in standby on restore. First route: Google TV / Android TV over the network (ADB), nothing to install on the PC. (#93)
### Fixes
- `consolemode://stop` and the configurable Back to PC shortcut now close Playnite fullscreen before restoring; an in-progress restore finishes before another attempt starts. (#45)

## [1.6.0-alpha.3]
### What's new
Expand All @@ -15,7 +15,6 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
- Sounds in the Console interface when you move the focus, pick and go back (made by the app itself, at a low volume). Turn them off in Settings → Interface sounds. (#117)
- Controller shortcuts of your choice: the button that opens Console Mode, the session menu's and the one that goes back to the PC are now set in Settings (hold the buttons you want and let go on the same controller). Capture never combines buttons from different controllers. None is on by default: the first time you open this version the app shows the setup, with a suggestion for each action (Home, Select + Y and Start + Select). One button can't serve two actions. If you used the Home button or Start + Select before, choose them again. (#116)
- Settings → "Receive test versions (alpha and beta)": anyone can join the tests of upcoming versions from the app. Off by default; turning it on shows a warning that test versions can have bugs.
- Settings → TV: console mode turns the TV on and switches it to the PC's HDMI input, and can put it in standby on restore. First route: Google TV / Android TV over the network (ADB), nothing to install on the PC. (#93)

### Fixes
- Session menu: if Windows cannot activate the selected window, the menu stays open so you can try again. (#120)
Expand Down
5 changes: 2 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma.

## [Unreleased]
### Novidades
- Ajustes → TV: o modo console liga a TV e troca para a entrada HDMI do PC, e pode colocá-la em espera ao restaurar. Primeiro caminho: Google TV / Android TV pela rede (ADB), sem instalar nada no PC. (#93)
### Correcoes
- `consolemode://stop` e o atalho configuravel de voltar ao PC agora fecham o Playnite em tela cheia antes de restaurar; uma restauracao em andamento termina antes de uma nova tentativa. (#45)

## [1.6.0-alpha.3]
### Novidades
Expand All @@ -15,7 +15,6 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
- Sons na interface Console ao mover o foco, escolher e voltar (sons criados pelo próprio app, em volume baixo). Dá para desligar em Ajustes → Sons da interface. (#117)
- Atalhos do controle à sua escolha: o botão que abre o Console Mode, o do menu da sessão e o de voltar ao PC agora são configuráveis em Ajustes (basta segurar os botões que você quer e soltar no mesmo controle). A captura não mistura botões de controles diferentes. Nenhum vem ligado: na primeira abertura desta versão o app mostra a configuração, com uma sugestão para cada ação (Home, Select + Y e Start + Select). Um mesmo botão não pode servir para duas ações. Quem usava o botão Home ou Start + Select precisa escolher de novo. (#116)
- Ajustes → "Receber versões de teste (alpha e beta)": qualquer pessoa pode entrar nos testes das próximas versões pelo próprio app. Desligado por padrão; ao ligar, o app avisa que a versão pode ter bugs.
- Ajustes → TV: o modo console liga a TV e troca para a entrada HDMI do PC, e pode colocá-la em espera ao restaurar. Primeiro caminho: Google TV / Android TV pela rede (ADB), sem instalar nada no PC. (#93)

### Correções
- Menu da sessão: se o Windows não conseguir ativar a janela escolhida, o menu continua aberto para tentar de novo. (#120)
Expand Down
2 changes: 2 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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.

`consolemode://stop`, `ConsoleMode.exe --stop` and the configured **Back to PC** controller shortcut close Playnite fullscreen before restoring. The session menu's **Back to the PC** and **Exit Console Mode** actions, and the tray's **Restore setup**, keep their restore behavior; in the menu preview, **Back to the PC** only closes the preview.

## 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
2 changes: 2 additions & 0 deletions docs/GUIDE.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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.

`consolemode://stop`, `ConsoleMode.exe --stop` e o atalho configuravel **Voltar ao PC** fecham o Playnite em tela cheia antes de restaurar. As acoes **Voltar ao PC** e **Sair do Console Mode** do menu da sessao, e **Restaurar setup** da bandeja, mantem o comportamento de restauracao; na previa do menu, **Voltar ao PC** apenas fecha a previa.

## 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
7 changes: 7 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,13 @@ 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: ______

Pre-condicoes para Playnite: iniciador = Playnite tela cheia; sessao ativa na TV; atalho configuravel **Voltar ao PC** definido.

- [ ] `start consolemode://stop` e `ConsoleMode.exe --stop` com o app aberto: o Playnite fecha antes de a mesa voltar; o app fica na bandeja. Resultado: ______
- [ ] Atalho configuravel **Voltar ao PC** durante Playnite: fecha o Playnite antes de restaurar. Resultado: ______
- [ ] Fechar o Playnite por conta propria continua restaurando automaticamente. Resultado: ______
- [ ] Menu da sessao: **Voltar ao PC** restaura sem encerrar o Playnite; **Sair do Console Mode** restaura e fecha o app. Na previa sem sessao, **Voltar ao PC** apenas fecha o menu. Resultado: ______
- [ ] Bandeja > **Restaurar setup** continua sendo restauracao manual; **Modo Xbox** nao fecha um front-end. Resultado: ______
## 4. Regressões

- [ ] Interface Desktop: mapa de telas, `Segmented`, chips, tour de 3 passos e Ajustes continuam como antes. Resultado: ______
Expand Down
4 changes: 2 additions & 2 deletions src/ConsoleMode/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private void ListenForSignals()
_window?.DispatcherQueue.TryEnqueue(() =>
{
if (ViewModel is null) return;
if (ViewModel.IsConsoleActive) _ = ViewModel.RestoreNowAsync();
if (ViewModel.IsConsoleActive) _ = ViewModel.StopConsoleAsync();
else _tray?.ShowWindow();
}), null, Timeout.Infinite, executeOnlyOnce: false));
}
Expand Down Expand Up @@ -167,7 +167,7 @@ private void WatchGuideButton()
_ = HandleStartRequestAsync();
};
_exitChord = new ControllerHoldWatcher(_window.DispatcherQueue, 0, "atalho Voltar ao PC");
_exitChord.Held += () => { if (ViewModel?.IsConsoleActive == true) _ = ViewModel.RestoreNowAsync(); };
_exitChord.Held += () => { if (ViewModel?.IsConsoleActive == true) _ = ViewModel.StopConsoleAsync(); };
_menuChord = new ControllerHoldWatcher(_window.DispatcherQueue, 0, "atalho Menu da sessão")
{
HoldDuration = TimeSpan.FromMilliseconds(250)
Expand Down
34 changes: 34 additions & 0 deletions src/ConsoleMode/Services/LaunchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ public bool IsXboxActive(ConsoleRuntimeState state)
return false;
}

/// <summary>
/// Asks the fullscreen front-end to quit, so leaving console mode from outside doesn't leave
/// Big Picture / Playnite sitting on the desk monitor. Xbox mode has no window of its own.
/// Returns once it is gone, or after <paramref name="timeoutMs"/>.
/// </summary>
public bool CloseFrontEnd(string mode, ConsoleRuntimeState state, int timeoutMs = 8000)
{
if (mode == "xboxMode" || !IsFullscreenActive(mode, state)) return true;
if (mode == "bigPicture") return CloseBigPicture(state, timeoutMs);
if (mode != "playnite") return true;

var handles = GetFullscreenHandles(mode);
foreach (var p in Process.GetProcessesByName("Playnite.FullscreenApp"))
{
try { p.CloseMainWindow(); } catch { /* best effort */ }
}

var deadline = Environment.TickCount64 + timeoutMs;
var nudged = false;
while (Environment.TickCount64 < deadline)
{
Thread.Sleep(500);
if (!IsFullscreenActive(mode, state)) return true;
// halfway through and still there: close the window itself
if (!nudged && Environment.TickCount64 > deadline - timeoutMs / 2)
{
nudged = true;
foreach (var h in handles.Concat(GetFullscreenHandles(mode)).Distinct())
NativeWindows.PostMessage(h, NativeWindows.WmClose, 0, 0);
}
}
return !IsFullscreenActive(mode, state);
}

public bool IsFullscreenActive(string mode, ConsoleRuntimeState state) => mode switch
{
"bigPicture" => IsBigPictureActive(state),
Expand Down
11 changes: 11 additions & 0 deletions src/ConsoleMode/Services/RestoreWaitPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ConsoleMode.Services;

/// <summary>Bounds how long an explicit restore waits for another restore already in progress.</summary>
public static class RestoreWaitPolicy
{
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
public static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(100);

public static bool ShouldWait(bool restoreInProgress, TimeSpan elapsed) =>
restoreInProgress && elapsed < Timeout;
}
73 changes: 55 additions & 18 deletions src/ConsoleMode/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public partial class MainViewModel : ObservableObject
private readonly DispatcherQueue _dispatcher = DispatcherQueue.GetForCurrentThread();
private CancellationTokenSource? _loopCts;
private volatile bool _busy;
private bool _restoreRequestActive;
private bool _applying;

/// <summary>Last config read from disk; monitor fields survive a failed monitor listing.</summary>
Expand Down Expand Up @@ -780,33 +781,69 @@ private void EndTour()
RequestShortcutOnboarding();
}

/// <summary>
/// Leave console mode on request from outside the session (consolemode://stop, the
/// controller exit chord): quit Big Picture / Playnite first — the loop then restores the
/// desk on its own, as when the user exits it — and restore explicitly if still active.
/// The tray's Restore keeps restoring only.
/// </summary>
public async Task StopConsoleAsync()
{
if (!IsConsoleActive) return;
var mode = Engine.State.FullscreenMode;
var closed = await Task.Run(() => Engine.Launch.CloseFrontEnd(mode, Engine.State));
AppLog.Write($"Stop-ConsoleMode: {mode} {(closed ? "fechado" : "ainda aberto")}");
if (!await WaitForRestoreToFinishAsync()) return;
if (IsConsoleActive) await RestoreNowAsync();
}

private async Task<bool> WaitForRestoreToFinishAsync()
{
var timer = Stopwatch.StartNew();
while (RestoreWaitPolicy.ShouldWait(Engine.State.RestoreInProgress, timer.Elapsed))
await Task.Delay(RestoreWaitPolicy.PollInterval);

if (!Engine.State.RestoreInProgress) return true;
AppLog.Write("Stop-ConsoleMode: restauração anterior ainda em andamento após 30 segundos; nova restauração adiada");
return false;
}

[RelayCommand]
public async Task RestoreNowAsync()
{
if (_busy && !Engine.State.IsActive) return;
StopLoop();
_busy = true;
IsRestoring = true;
if (_busy || _restoreRequestActive) return;
_restoreRequestActive = true;
try
{
await Task.Run(() => Engine.Stop());
IsConsoleActive = false;
SetStatus(LocalizationService.Get("RestoreSuccess"), InfoBarSeverity.Success);
}
catch (Exception ex)
{
AppLog.Write($"Restore: {ex}");
SetStatus(LocalizationService.Get("RestoreFailure", ex.Message), InfoBarSeverity.Error);
if (!await WaitForRestoreToFinishAsync()) return;
StopLoop();
_busy = true;
IsRestoring = true;
try
{
await Task.Run(() => Engine.Stop());
IsConsoleActive = false;
SetStatus(LocalizationService.Get("RestoreSuccess"), InfoBarSeverity.Success);
}
catch (Exception ex)
{
AppLog.Write($"Restore: {ex}");
SetStatus(LocalizationService.Get("RestoreFailure", ex.Message), InfoBarSeverity.Error);
}
finally
{
IsRestoring = false;
_busy = false;
}

// Screens were renumbered/re-enabled; refresh names and resolutions.
Engine.Monitors.ClearCache();
await ReloadAsync();
}
finally
{
IsRestoring = false;
_busy = false;
_restoreRequestActive = false;
}

// Screens were renumbered/re-enabled; refresh names and resolutions.
Engine.Monitors.ClearCache();
await ReloadAsync();
}

public bool TryCloseToTray() => IsConsoleActive;
Expand Down
1 change: 1 addition & 0 deletions tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="../../src/ConsoleMode/Services/AltTabRules.cs" Link="AltTabRules.cs" />
<Compile Include="../../src/ConsoleMode/Services/ControlFsLocator.cs" Link="ControlFsLocator.cs" />
<Compile Include="../../src/ConsoleMode/Services/SteamShutdown.cs" Link="SteamShutdown.cs" />
<Compile Include="../../src/ConsoleMode/Services/RestoreWaitPolicy.cs" Link="RestoreWaitPolicy.cs" />
<Compile Include="../../src/ConsoleMode/Models/Models.cs" Link="Models.cs" />
<Compile Include="../../src/ConsoleMode/Services/Tv/AdbProtocol.cs" Link="Tv/AdbProtocol.cs" />
<Compile Include="../../src/ConsoleMode/Services/Tv/AdbShellOutput.cs" Link="Tv/AdbShellOutput.cs" />
Expand Down
20 changes: 20 additions & 0 deletions tests/ConsoleMode.Tests/RestoreWaitPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using ConsoleMode.Services;

namespace ConsoleMode.Tests;

public sealed class RestoreWaitPolicyTests
{
[Fact]
public void KeepsWaitingWhileAnotherRestoreIsActiveAndWithinTheBound()
{
Assert.True(RestoreWaitPolicy.ShouldWait(true, RestoreWaitPolicy.Timeout - TimeSpan.FromMilliseconds(1)));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void StopsWaitingWhenRestoreFinishedOrBoundWasReached(bool restoreInProgress)
{
Assert.False(RestoreWaitPolicy.ShouldWait(restoreInProgress, RestoreWaitPolicy.Timeout));
}
}
Loading