Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37781a610f | ||
|
|
ec9aa415f6 | ||
|
|
24d2f97eae | ||
|
|
c9b8f666cd | ||
|
|
35b6c835fd | ||
|
|
39a9f09f04 |
@@ -438,3 +438,9 @@ jobs:
|
|||||||
echo "winget source serving ${GITHUB_REF_NAME#v}"
|
echo "winget source serving ${GITHUB_REF_NAME#v}"
|
||||||
env:
|
env:
|
||||||
GITHUB_REF_NAME: ${{ gitea.ref_name }}
|
GITHUB_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
# `env:` populates the RUNNER's environment; this action runs `script` on the REMOTE
|
||||||
|
# host, which inherits nothing from it. `envs:` is the action's own allow-list of names
|
||||||
|
# to forward into the remote shell — without it `set -u` aborted at the first expansion
|
||||||
|
# ("GITHUB_REF_NAME: unbound variable") and the step failed on every tag, after the
|
||||||
|
# catalogue had already shipped correctly.
|
||||||
|
envs: GITHUB_REF_NAME
|
||||||
|
|||||||
@@ -179,6 +179,23 @@ shutdown — and only forces the issue after ten seconds of being ignored.
|
|||||||
> (5 minutes). A display set to **Forever** stays up regardless of what happens to the game — a
|
> (5 minutes). A display set to **Forever** stays up regardless of what happens to the game — a
|
||||||
> pinned display is a deliberate "this box is a game host" choice, and closing a game doesn't undo it.
|
> pinned display is a deliberate "this box is a game host" choice, and closing a game doesn't undo it.
|
||||||
|
|
||||||
|
### On a gamescope session, the display has the final say
|
||||||
|
|
||||||
|
When a launch gets its **own gamescope** — a dedicated game session, the usual setup on a Steam Deck
|
||||||
|
or a Bazzite couch box — the game runs *inside* the streamed display. So it lives exactly as long as
|
||||||
|
that display does, and **Keep alive decides that, not the setting above**:
|
||||||
|
|
||||||
|
| you disconnect by | what happens to the game |
|
||||||
|
|---|---|
|
||||||
|
| pressing **Stop** (or the console's stop) | the display tears down at once — keep-alive is deliberately skipped for a real stop — and the game goes with it, even on *Leave it running* |
|
||||||
|
| dropping out (network, sleep) | the display lingers for your keep-alive window, then tears down; the game ends with it |
|
||||||
|
| dropping out, keep-alive **Forever** | the display is pinned, so the game genuinely survives — and *Always close it* still ends it when the reconnect window closes |
|
||||||
|
|
||||||
|
So on a gamescope box, "leave the game running after I disconnect" means **keep-alive Forever** (or a
|
||||||
|
window long enough to come back in), not just this setting. On a desktop session — KWin, GNOME, Sway —
|
||||||
|
the game is an ordinary process next to your desktop and none of this applies; the setting above is
|
||||||
|
the whole story.
|
||||||
|
|
||||||
### Automation
|
### Automation
|
||||||
|
|
||||||
The host publishes `game.running` and `game.exited` events (the latter says whether the player quit
|
The host publishes `game.running` and `game.exited` events (the latter says whether the player quit
|
||||||
|
|||||||
@@ -36,13 +36,22 @@ Every library entry can now carry **platform, description, developer, publisher,
|
|||||||
|
|
||||||
## New: install and update the Windows host with winget
|
## New: install and update the Windows host with winget
|
||||||
|
|
||||||
Windows had **no update path at all** — no self-update, no package manager — so keeping a host current meant noticing that a release had happened and re-running an installer by hand. Now, after adding Punktfunk's package source once:
|
Windows had **no update path at all** — no self-update, no package manager — so keeping a host current meant noticing that a release had happened and re-running an installer by hand. Now it is two commands.
|
||||||
|
|
||||||
|
Punktfunk lives in **its own package source**, not the public winget catalogue, so add that first. Once per machine, from an **Administrator** terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
winget source add -n punktfunk https://winget.punktfunk.unom.io -t Microsoft.Rest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install, and from then on upgrade:
|
||||||
|
|
||||||
```
|
```
|
||||||
winget install unom.PunktfunkHost
|
winget install unom.PunktfunkHost
|
||||||
|
winget upgrade unom.PunktfunkHost
|
||||||
```
|
```
|
||||||
|
|
||||||
and `winget upgrade` from then on. The source is ours rather than the public catalogue, and it knows about every release, so you can pin an older version or upgrade from one.
|
**If you skip the first command, `winget install unom.PunktfunkHost` will tell you no package was found** — winget only searches the sources it knows about, and Punktfunk is not in the public one. The source knows about every release, so you can also pin an older version with `--version` or upgrade from one.
|
||||||
|
|
||||||
A silent install shows you the same disclosures the wizard does — including where the bundled virtual audio device comes from — and takes the same defaults the wizard offers, so installing without a screen doesn't quietly get you a different machine.
|
A silent install shows you the same disclosures the wizard does — including where the bundled virtual audio device comes from — and takes the same defaults the wizard offers, so installing without a screen doesn't quietly get you a different machine.
|
||||||
|
|
||||||
|
|||||||
@@ -354,15 +354,31 @@ begin
|
|||||||
Result := ExpandConstant('{commonappdata}\punktfunk\host.env');
|
Result := ExpandConstant('{commonappdata}\punktfunk\host.env');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ True if another Moonlight-compatible streaming host is installed - by its SCM service key or its
|
{ True if another Moonlight-compatible streaming host is not merely present but will actually RUN.
|
||||||
Program Files directory. Sunshine and its forks all register a "<Name>Service" and install under
|
Sunshine and its forks register a "<Name>Service"; `Start` is that service's start type
|
||||||
Program Files, so a registry + directory probe catches them without enumerating processes (which
|
(REG_DWORD): 0 boot, 1 system, 2 automatic, 3 manual, 4 disabled. Only 0-2 come up on their own,
|
||||||
pure Pascal can't do cleanly). }
|
and only a host that comes up can take the GameStream ports or load a second virtual-display
|
||||||
function StreamHostPresent(SvcKey, DirName: String): Boolean;
|
driver - which is the entire content of the warning below.
|
||||||
|
|
||||||
|
DELIBERATELY NARROWER than it was. The old probe also counted the service key existing at ANY
|
||||||
|
start type, plus a bare Program Files\<Name> directory - so a disabled service, or a leftover folder
|
||||||
|
from an uninstall, read as a live conflict. Combined with the silent-install default of IDNO that
|
||||||
|
aborted setup, and a field report followed within hours of 0.20.0: `winget install
|
||||||
|
unom.PunktfunkHost` failed with exit code 1 (0x8A150006) on a box whose Sunshine was not running.
|
||||||
|
Nothing about a dormant install can clash, and the tray reached the same conclusion in this same
|
||||||
|
release (3e782852 dropped its always-on warning over a merely-INSTALLED Sunshine as a false
|
||||||
|
alarm) - the two surfaces now agree.
|
||||||
|
|
||||||
|
A host running as a plain user process rather than a service is not detected here, by choice:
|
||||||
|
that cannot be seen from pure Pascal, it is a runtime condition rather than an install-time one,
|
||||||
|
and the host already reports it where it belongs (the `punktfunk::detect` startup warning, the
|
||||||
|
`detect-conflicts` subcommand, and /api/v1/local/summary). }
|
||||||
|
function StreamHostEnabled(SvcKey: String): Boolean;
|
||||||
|
var
|
||||||
|
StartType: Cardinal;
|
||||||
begin
|
begin
|
||||||
Result := RegKeyExists(HKLM, 'SYSTEM\CurrentControlSet\Services\' + SvcKey)
|
Result := RegQueryDWordValue(HKLM, 'SYSTEM\CurrentControlSet\Services\' + SvcKey, 'Start', StartType)
|
||||||
or DirExists(ExpandConstant('{commonpf}\' + DirName))
|
and (StartType <= 2);
|
||||||
or DirExists(ExpandConstant('{commonpf32}\' + DirName));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Runs before any wizard page - the earliest point we can warn. Detect a conflicting host and let
|
{ Runs before any wizard page - the earliest point we can warn. Detect a conflicting host and let
|
||||||
@@ -375,11 +391,11 @@ begin
|
|||||||
{ Record the fresh-vs-upgrade verdict while host.env still reflects the PREVIOUS run. }
|
{ Record the fresh-vs-upgrade verdict while host.env still reflects the PREVIOUS run. }
|
||||||
FreshHostInstall := not FileExists(HostEnvPath);
|
FreshHostInstall := not FileExists(HostEnvPath);
|
||||||
Found := '';
|
Found := '';
|
||||||
if StreamHostPresent('SunshineService', 'Sunshine') then Found := Found + ' - Sunshine' + #13#10;
|
if StreamHostEnabled('SunshineService') then Found := Found + ' - Sunshine' + #13#10;
|
||||||
if StreamHostPresent('ApolloService', 'Apollo') then Found := Found + ' - Apollo' + #13#10;
|
if StreamHostEnabled('ApolloService') then Found := Found + ' - Apollo' + #13#10;
|
||||||
if StreamHostPresent('VibeshineService', 'Vibeshine') then Found := Found + ' - Vibeshine' + #13#10;
|
if StreamHostEnabled('VibeshineService') then Found := Found + ' - Vibeshine' + #13#10;
|
||||||
if StreamHostPresent('VibepolloService', 'Vibepollo') then Found := Found + ' - Vibepollo' + #13#10;
|
if StreamHostEnabled('VibepolloService') then Found := Found + ' - Vibepollo' + #13#10;
|
||||||
if StreamHostPresent('LuminalShineService', 'LuminalShine') then Found := Found + ' - LuminalShine' + #13#10;
|
if StreamHostEnabled('LuminalShineService') then Found := Found + ' - LuminalShine' + #13#10;
|
||||||
if Found <> '' then
|
if Found <> '' then
|
||||||
{ SuppressibleMsgBox, NOT MsgBox: a plain MsgBox ignores /SUPPRESSMSGBOXES and displays even
|
{ SuppressibleMsgBox, NOT MsgBox: a plain MsgBox ignores /SUPPRESSMSGBOXES and displays even
|
||||||
under /VERYSILENT - i.e. an unattended install (winget) would block on a modal dialog with no
|
under /VERYSILENT - i.e. an unattended install (winget) would block on a modal dialog with no
|
||||||
@@ -387,12 +403,15 @@ begin
|
|||||||
install onto a box that already runs Sunshine/Apollo ABORTS (Setup exits non-zero) instead of
|
install onto a box that already runs Sunshine/Apollo ABORTS (Setup exits non-zero) instead of
|
||||||
proceeding into the unsupported dual-host state the message describes. }
|
proceeding into the unsupported dual-host state the message describes. }
|
||||||
Result := SuppressibleMsgBox(
|
Result := SuppressibleMsgBox(
|
||||||
'Another game-streaming host is already installed on this PC:' + #13#10#13#10 + Found + #13#10 +
|
{ NB: keep #13#10 off the START of a line - ISPP reads a leading '#' as a preprocessor
|
||||||
|
directive and aborts the compile with "Unknown preprocessor directive". }
|
||||||
|
'Another game-streaming host is installed on this PC and set to start automatically:' + #13#10#13#10 + Found + #13#10 +
|
||||||
'Running Punktfunk alongside Sunshine / Apollo / other Moonlight-compatible hosts is NOT ' +
|
'Running Punktfunk alongside Sunshine / Apollo / other Moonlight-compatible hosts is NOT ' +
|
||||||
'supported. They bind the same GameStream network ports (47984, 47989, 47998-48010) and ' +
|
'supported. They bind the same GameStream network ports (47984, 47989, 47998-48010) and ' +
|
||||||
'install a conflicting virtual-display driver, which causes pairing failures, "address ' +
|
'install a conflicting virtual-display driver, which causes pairing failures, "address ' +
|
||||||
'already in use" errors and capture glitches.' + #13#10#13#10 +
|
'already in use" errors and capture glitches.' + #13#10#13#10 +
|
||||||
'Stop and uninstall the other host before using Punktfunk.' + #13#10#13#10 +
|
'Stop and disable its service (or uninstall it) before using Punktfunk. A host that is ' +
|
||||||
|
'installed but disabled does not clash and is not reported here.' + #13#10#13#10 +
|
||||||
'Continue with the installation anyway?',
|
'Continue with the installation anyway?',
|
||||||
mbConfirmation, MB_YESNO or MB_DEFBUTTON2, IDNO) = IDYES;
|
mbConfirmation, MB_YESNO or MB_DEFBUTTON2, IDNO) = IDYES;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+2
-9
@@ -45,6 +45,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
"brace-expansion": "^5.0.8",
|
||||||
"dompurify": "^3.4.12",
|
"dompurify": "^3.4.12",
|
||||||
"fast-uri": "^3.1.4",
|
"fast-uri": "^3.1.4",
|
||||||
"immutable": "^4.3.9",
|
"immutable": "^4.3.9",
|
||||||
@@ -1116,7 +1117,7 @@
|
|||||||
|
|
||||||
"body-scroll-lock": ["body-scroll-lock@4.0.0-beta.0", "", {}, "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ=="],
|
"body-scroll-lock": ["body-scroll-lock@4.0.0-beta.0", "", {}, "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
"brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
@@ -2544,8 +2545,6 @@
|
|||||||
|
|
||||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="],
|
|
||||||
|
|
||||||
"rollup-plugin-visualizer/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"rollup-plugin-visualizer/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||||
|
|
||||||
"sass/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"sass/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
@@ -2556,14 +2555,8 @@
|
|||||||
|
|
||||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="],
|
|
||||||
|
|
||||||
"archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
|
|
||||||
"sass/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"sass/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,5 +413,6 @@
|
|||||||
"session_game_grace": "Zeitfenster für die Rückkehr",
|
"session_game_grace": "Zeitfenster für die Rückkehr",
|
||||||
"session_game_grace_help": "Wie lange ein verschwundener Client Zeit hat zurückzukommen, bevor sein Spiel geschlossen wird. Die Konsole zeigt den Countdown; eine neue Verbindung bricht ihn ab.",
|
"session_game_grace_help": "Wie lange ein verschwundener Client Zeit hat zurückzukommen, bevor sein Spiel geschlossen wird. Die Konsole zeigt den Countdown; eine neue Verbindung bricht ihn ab.",
|
||||||
"session_game_saved": "Sitzungs- und Spieleinstellungen gespeichert",
|
"session_game_saved": "Sitzungs- und Spieleinstellungen gespeichert",
|
||||||
"session_game_inert": "Dieser Host kann keine Spiele starten – hier bewirken diese Einstellungen nichts"
|
"session_game_inert": "Dieser Host kann keine Spiele starten – hier bewirken diese Einstellungen nichts",
|
||||||
|
"session_game_nested_note": "In einer gamescope-Spielsitzung läuft das Spiel *innerhalb* der gestreamten Anzeige und lebt daher genau so lange wie diese – das entscheidet „Offen halten“ oben, nicht diese Einstellung. Ein bewusstes Stoppen reißt die Anzeige sofort ab und nimmt das Spiel mit, egal was du hier wählst."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,5 +413,6 @@
|
|||||||
"session_game_grace": "Reconnect window",
|
"session_game_grace": "Reconnect window",
|
||||||
"session_game_grace_help": "How long a client that vanished has to come back before its game is closed. The console shows the countdown, and reconnecting cancels it.",
|
"session_game_grace_help": "How long a client that vanished has to come back before its game is closed. The console shows the countdown, and reconnecting cancels it.",
|
||||||
"session_game_saved": "Session and game settings saved",
|
"session_game_saved": "Session and game settings saved",
|
||||||
"session_game_inert": "This host has no way to launch games, so these settings do nothing here"
|
"session_game_inert": "This host has no way to launch games, so these settings do nothing here",
|
||||||
|
"session_game_nested_note": "On a gamescope game session the game runs *inside* the streamed display, so it lives exactly as long as that display does — which is what Keep alive above decides, not this setting. A deliberate Stop tears that display down at once and takes the game with it, whichever option you pick here."
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -65,6 +65,7 @@
|
|||||||
"immutable": "^4.3.9",
|
"immutable": "^4.3.9",
|
||||||
"undici": "^7.28.0",
|
"undici": "^7.28.0",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.10",
|
||||||
"js-yaml": "^4.3.0"
|
"js-yaml": "^4.3.0",
|
||||||
|
"brace-expansion": "^5.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,20 @@ const GameRow: FC<{
|
|||||||
const waiting = game.state === "grace";
|
const waiting = game.state === "grace";
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-16 w-11 shrink-0 overflow-hidden rounded bg-muted">
|
{/* Fixed slot so rows line up whether or not a title has a cover. Plenty won't: an
|
||||||
{art && (
|
operator-typed command has no catalog entry behind it, a custom entry may carry no
|
||||||
|
art, and nothing does until `/library` has loaded — an empty box reads as broken, so
|
||||||
|
the placeholder says "game" instead of nothing. */}
|
||||||
|
<div className="flex h-16 w-11 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
|
||||||
|
{art ? (
|
||||||
<img
|
<img
|
||||||
src={art}
|
src={art}
|
||||||
alt=""
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
className="size-full object-cover"
|
className="size-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Gamepad2 className="size-5 text-muted-foreground/60" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ export const SessionGameCard: FC = () => {
|
|||||||
{m.session_game_always_warning()}
|
{m.session_game_always_warning()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* Shown for every option, including "leave it running": on a nested
|
||||||
|
gamescope launch the game IS inside the streamed display, so the
|
||||||
|
display's own keep-alive outranks anything chosen here — verified
|
||||||
|
on glass (.41), where a deliberate stop ended the game under
|
||||||
|
`keep`. Worded so a non-gamescope host reads it and moves on. */}
|
||||||
|
<p className="max-w-prose text-xs text-muted-foreground">
|
||||||
|
{m.session_game_nested_note()}
|
||||||
|
</p>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{(server.game_on_session_end ?? "keep") === "always" && (
|
{(server.game_on_session_end ?? "keep") === "always" && (
|
||||||
|
|||||||
Reference in New Issue
Block a user