diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 24b1785..c0e190c 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -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
@@ -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)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a75d083..5a25339 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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)
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index 52fd7a6..807776d 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -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.
diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md
index 1fdeae2..859dfd6 100644
--- a/docs/GUIDE.pt-BR.md
+++ b/docs/GUIDE.pt-BR.md
@@ -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.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 53fd908..3dec0af 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -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: ______
diff --git a/src/ConsoleMode/App.xaml.cs b/src/ConsoleMode/App.xaml.cs
index 0ff4335..285420b 100644
--- a/src/ConsoleMode/App.xaml.cs
+++ b/src/ConsoleMode/App.xaml.cs
@@ -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));
}
@@ -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)
diff --git a/src/ConsoleMode/Services/LaunchService.cs b/src/ConsoleMode/Services/LaunchService.cs
index 32f46d7..b6b50b2 100644
--- a/src/ConsoleMode/Services/LaunchService.cs
+++ b/src/ConsoleMode/Services/LaunchService.cs
@@ -226,6 +226,40 @@ public bool IsXboxActive(ConsoleRuntimeState state)
return false;
}
+ ///
+ /// 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 .
+ ///
+ 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),
diff --git a/src/ConsoleMode/Services/RestoreWaitPolicy.cs b/src/ConsoleMode/Services/RestoreWaitPolicy.cs
new file mode 100644
index 0000000..d41bd74
--- /dev/null
+++ b/src/ConsoleMode/Services/RestoreWaitPolicy.cs
@@ -0,0 +1,11 @@
+namespace ConsoleMode.Services;
+
+/// Bounds how long an explicit restore waits for another restore already in progress.
+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;
+}
diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs
index 397796a..5320388 100644
--- a/src/ConsoleMode/ViewModels/MainViewModel.cs
+++ b/src/ConsoleMode/ViewModels/MainViewModel.cs
@@ -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;
/// Last config read from disk; monitor fields survive a failed monitor listing.
@@ -780,33 +781,69 @@ private void EndTour()
RequestShortcutOnboarding();
}
+ ///
+ /// 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.
+ ///
+ 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 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;
diff --git a/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj b/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj
index 3d0fa4b..f008914 100644
--- a/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj
+++ b/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj
@@ -39,6 +39,7 @@
+
diff --git a/tests/ConsoleMode.Tests/RestoreWaitPolicyTests.cs b/tests/ConsoleMode.Tests/RestoreWaitPolicyTests.cs
new file mode 100644
index 0000000..0910bcb
--- /dev/null
+++ b/tests/ConsoleMode.Tests/RestoreWaitPolicyTests.cs
@@ -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));
+ }
+}