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
38 changes: 37 additions & 1 deletion clients/deck/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ ApplicationWindow {
property var selectedGameForPreview: novaSelectedGameCard
property string selectedLaunchPreviewText: novaSelectedLaunchPreviewText
property var launchPreviewCopyAction: novaLaunchPreviewCopyAction
property var launchIntentPreview: novaLaunchIntentPreview
property string selectedLaunchPublicCopy: launchIntentPreview.publicCopy
property string selectedStreamLifecycleCopy: launchIntentPreview.streamLifecycleCopy

function selectedHostSubtitle() {
return "Read-only host detail only — not discovered from the network."
Expand All @@ -42,14 +45,31 @@ ApplicationWindow {
const hostId = selectedHostForPreview && selectedHostForPreview.id
? selectedHostForPreview.id
: "host-empty-state"
const hostName = selectedHostForPreview && selectedHostForPreview.displayName
? selectedHostForPreview.displayName
: "No host selected"
const gameTitle = selectedGameForPreview && selectedGameForPreview.title
? selectedGameForPreview.title
: "No game selected"
const launchModeLabel = selectedGameForPreview && selectedGameForPreview.launchModeLabel
? selectedGameForPreview.launchModeLabel
: "Stream: preview · Steam: direct"
const streamMode = launchModeLabel.indexOf("virtual_display") >= 0 ? "virtual_display"
: launchModeLabel.indexOf("headless") >= 0 ? "headless"
: "preview"
const steamMode = launchModeLabel.indexOf("big-picture") >= 0 ? "steam-big-picture" : "steam-direct"
const steamCopy = steamMode === "steam-big-picture" ? "Steam Big Picture" : "Steam direct"
selectedLaunchPreviewText = "preview://nova-deck/launch?host="
+ previewComponent(hostId)
+ "&game="
+ previewComponent(gameTitle)
+ "&state=copy-preview-only"
+ "&mode="
+ steamMode
+ "&stream="
+ previewComponent(streamMode)
+ "&state=noop-preview"
selectedLaunchPublicCopy = "Preview " + gameTitle + " on " + hostName + " via " + steamCopy + "; no launch will run."
selectedStreamLifecycleCopy = "Preview stream for " + gameTitle + " on " + hostName + " remains noop_preview/not_started."
launchPreviewCopyAction = {
"id": novaLaunchPreviewCopyAction.id,
"label": novaLaunchPreviewCopyAction.label,
Expand Down Expand Up @@ -570,6 +590,22 @@ ApplicationWindow {
wrapMode: Text.WordWrap
}

Label {
Layout.preferredWidth: detailTextWidth
text: selectedLaunchPublicCopy
color: "#C9F0D4"
font.pixelSize: 11
wrapMode: Text.WordWrap
}

Label {
Layout.preferredWidth: detailTextWidth
text: selectedStreamLifecycleCopy
color: "#A8B0D8"
font.pixelSize: 10
wrapMode: Text.WordWrap
}

Label {
Layout.preferredWidth: detailTextWidth
text: selectedLaunchPreviewText
Expand Down
111 changes: 103 additions & 8 deletions clients/deck/src/deck_layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,17 +383,114 @@ DeckLaunchIntentBoundary previewOnlyLaunchIntentBoundary() {
};
}

DeckLaunchMode launchModeFor(const PolarisGameFixture& game) {
const std::string mode = game.steamLaunch.recommendedMode.empty() ? "direct" : game.steamLaunch.recommendedMode;
if (mode == "direct") {
return DeckLaunchMode::SteamDirect;
}
if (mode == "big-picture") {
return DeckLaunchMode::SteamBigPicture;
}
return DeckLaunchMode::UnsupportedPreview;
}

std::string launchModeIdFor(const DeckLaunchMode mode) {
if (mode == DeckLaunchMode::SteamDirect) {
return "steam-direct";
}
if (mode == DeckLaunchMode::SteamBigPicture) {
return "steam-big-picture";
}
return "unsupported-preview";
}

std::string launchModeCopyFor(const DeckLaunchMode mode) {
if (mode == DeckLaunchMode::SteamDirect) {
return "Steam direct";
}
if (mode == DeckLaunchMode::SteamBigPicture) {
return "Steam Big Picture";
}
return "unsupported preview";
}

DeckStreamProfilePreview streamProfileFor(const PolarisGameFixture& game) {
const std::string id = game.launchMode.recommendedMode.empty() ? "preview" : game.launchMode.recommendedMode;
return DeckStreamProfilePreview{
.id = id,
.displayName = id == "headless" ? std::string{"Headless preview"}
: id == "virtual_display" ? std::string{"Virtual display preview"}
: std::string{"Preview profile"},
.virtualDisplayRecommended = id == "virtual_display",
.headlessRecommended = id == "headless",
};
}

DeckLaunchIntent resolveLaunchIntent(const DeckHostDetail& detail, const PolarisGameFixture& game) {
const auto mode = launchModeFor(game);
const auto streamProfile = streamProfileFor(game);
const std::string hostId(detail.id);
const std::string hostName(detail.displayName);
const std::string gameTitle = game.name.empty() ? std::string{"Untitled game"} : game.name;
const auto boundary = previewOnlyLaunchIntentBoundary();
const std::string uri = "preview://nova-deck/launch?host=" + encodePreviewComponent(hostId)
+ "&game=" + encodePreviewComponent(gameTitle)
+ "&mode=" + launchModeIdFor(mode)
+ "&stream=" + encodePreviewComponent(streamProfile.id)
+ "&state=noop-preview";
return DeckLaunchIntent{
.targetHostId = std::string(detail.id),
.targetHostName = std::string(detail.displayName),
.targetHostId = hostId,
.targetHostName = hostName,
.sampleGameId = game.id,
.gameTitle = game.name,
.streamLaunchMode = game.launchMode.recommendedMode.empty() ? std::string{"preview"} : game.launchMode.recommendedMode,
.gameTitle = gameTitle,
.streamLaunchMode = streamProfile.id,
.steamLaunchMode = game.steamLaunch.recommendedMode.empty() ? std::string{"direct"} : game.steamLaunch.recommendedMode,
.boundary = previewOnlyLaunchIntentBoundary(),
.boundary = boundary,
.executable = false,
.safetyLabel = std::string(kPreviewStateLabel),
.host = DeckHostIdentity{
.id = hostId,
.displayName = hostName,
.addressClass = hostId == "host-detail-empty" ? DeckHostAddressClass::UnknownUnavailable : DeckHostAddressClass::DemoOnly,
.addressLabel = "redacted preview host",
},
.game = DeckGameIdentity{
.identityKind = game.steamAppid.empty() ? DeckGameIdentityKind::LibraryFixture : DeckGameIdentityKind::SteamApp,
.libraryId = game.id,
.title = gameTitle,
.appId = game.appId,
.steamAppId = game.steamAppid,
},
.launchMode = mode,
.streamProfile = streamProfile,
.preflight = DeckPreflightFailureState{
.state = mode == DeckLaunchMode::UnsupportedPreview ? DeckPreflightState::UnsupportedLaunchMode : DeckPreflightState::ReadyPreview,
.reason = "Preflight-only preview; no backend, launch, or stream session starts.",
},
.privacy = DeckPreviewPrivacyPolicy{
.redactionPolicy = DeckPreviewRedactionPolicy::PublicSafe,
.publicSafeCopyOnly = true,
.localPrivateArtRedacted = true,
},
.safety = DeckPreviewSafetyBooleans{},
.publicPreviewCopy = "Preview " + gameTitle + " on " + hostName + " via " + launchModeCopyFor(mode) + "; no launch will run.",
.inertPreviewUri = uri,
};
}

DeckStreamIntent resolveStreamIntent(const DeckLaunchIntent& intent) {
return DeckStreamIntent{
.provider = DeckStreamProvider::PreviewOnly,
.action = DeckStreamAction::NoopPreview,
.session = DeckStreamSessionPreview{
.state = DeckStreamSessionState::NotStarted,
.reason = "not_started: preview-only boundary never opens a stream session",
},
.lifecycle = DeckStreamLifecycle::PreflightOnly,
.recovery = DeckStreamRecovery::UserReviewRequired,
.privacy = intent.privacy,
.safety = intent.safety,
.publicCopy = "Preview stream for " + intent.gameTitle + " on " + intent.targetHostName + " remains noop_preview/not_started.",
};
}

Expand Down Expand Up @@ -440,9 +537,7 @@ bool canExecuteLaunchIntent(const DeckLaunchIntent& intent) {

DeckLaunchPreview fakeLaunchCommandPreviewFor(const DeckLaunchIntent& intent) {
return DeckLaunchPreview{
.text = "preview://nova-deck/launch?host=" + encodePreviewComponent(intent.targetHostId)
+ "&game=" + encodePreviewComponent(intent.gameTitle)
+ "&state=copy-preview-only",
.text = intent.inertPreviewUri,
.stateLabel = std::string(kPreviewStateLabel),
.boundaryId = intent.boundary.id,
.boundaryLabel = intent.boundary.label,
Expand Down
119 changes: 119 additions & 0 deletions clients/deck/src/deck_layout.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,78 @@ struct DeckLaunchIntentBoundary {
bool allowsHostMutation = false;
};

enum class DeckHostAddressClass {
DemoOnly,
SnapshotOnly,
UnknownUnavailable,
};

enum class DeckGameIdentityKind {
SteamApp,
LibraryFixture,
Unknown,
};

enum class DeckLaunchMode {
SteamDirect,
SteamBigPicture,
UnsupportedPreview,
};

enum class DeckPreflightState {
ReadyPreview,
HostUnavailable,
HostBusy,
PairingNeeded,
UnsupportedLaunchMode,
};

enum class DeckPreviewRedactionPolicy {
PublicSafe,
LocalPrivateRedacted,
};

struct DeckHostIdentity {
std::string id;
std::string displayName;
DeckHostAddressClass addressClass = DeckHostAddressClass::DemoOnly;
std::string addressLabel;
};

struct DeckGameIdentity {
DeckGameIdentityKind identityKind = DeckGameIdentityKind::Unknown;
std::string libraryId;
std::string title;
int appId = 0;
std::string steamAppId;
};

struct DeckStreamProfilePreview {
std::string id;
std::string displayName;
bool virtualDisplayRecommended = false;
bool headlessRecommended = false;
};

struct DeckPreflightFailureState {
DeckPreflightState state = DeckPreflightState::ReadyPreview;
std::string reason;
};

struct DeckPreviewPrivacyPolicy {
DeckPreviewRedactionPolicy redactionPolicy = DeckPreviewRedactionPolicy::PublicSafe;
bool publicSafeCopyOnly = true;
bool localPrivateArtRedacted = true;
};

struct DeckPreviewSafetyBooleans {
bool allowsNetwork = false;
bool allowsProcessExecution = false;
bool allowsMoonlight = false;
bool allowsHostMutation = false;
bool executable = false;
};

struct DeckLaunchIntent {
std::string targetHostId;
std::string targetHostName;
Expand All @@ -84,6 +156,51 @@ struct DeckLaunchIntent {
DeckLaunchIntentBoundary boundary;
bool executable = false;
std::string safetyLabel;
DeckHostIdentity host;
DeckGameIdentity game;
DeckLaunchMode launchMode = DeckLaunchMode::UnsupportedPreview;
DeckStreamProfilePreview streamProfile;
DeckPreflightFailureState preflight;
DeckPreviewPrivacyPolicy privacy;
DeckPreviewSafetyBooleans safety;
std::string publicPreviewCopy;
std::string inertPreviewUri;
};

enum class DeckStreamProvider {
PreviewOnly,
};

enum class DeckStreamAction {
NoopPreview,
};

enum class DeckStreamSessionState {
NotStarted,
};

enum class DeckStreamLifecycle {
PreflightOnly,
};

enum class DeckStreamRecovery {
UserReviewRequired,
};

struct DeckStreamSessionPreview {
DeckStreamSessionState state = DeckStreamSessionState::NotStarted;
std::string reason;
};

struct DeckStreamIntent {
DeckStreamProvider provider = DeckStreamProvider::PreviewOnly;
DeckStreamAction action = DeckStreamAction::NoopPreview;
DeckStreamSessionPreview session;
DeckStreamLifecycle lifecycle = DeckStreamLifecycle::PreflightOnly;
DeckStreamRecovery recovery = DeckStreamRecovery::UserReviewRequired;
DeckPreviewPrivacyPolicy privacy;
DeckPreviewSafetyBooleans safety;
std::string publicCopy;
};

struct DeckLaunchPreview {
Expand Down Expand Up @@ -184,6 +301,8 @@ DeckLaunchIntentBoundary previewOnlyLaunchIntentBoundary();

DeckLaunchIntent resolveLaunchIntent(const DeckHostDetail& detail, const PolarisGameFixture& game);

DeckStreamIntent resolveStreamIntent(const DeckLaunchIntent& intent);

DeckLaunchPreviewBinding resolveLaunchPreviewBinding(
const std::vector<DeckHostListItem>& hosts,
const PolarisGameLibraryFixture& library,
Expand Down
21 changes: 21 additions & 0 deletions clients/deck/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ QVariantMap toLaunchIntentBoundaryModel(const nova::deck::DeckLaunchIntentBounda
return model;
}

QVariantMap toLaunchIntentPreviewModel(
const nova::deck::DeckLaunchIntent& intent,
const nova::deck::DeckStreamIntent& streamIntent) {
QVariantMap model;
model.insert("hostId", toQString(intent.host.id));
model.insert("hostDisplayName", toQString(intent.host.displayName));
model.insert("gameId", toQString(intent.game.libraryId));
model.insert("gameTitle", toQString(intent.game.title));
model.insert("streamProfileId", toQString(intent.streamProfile.id));
model.insert("streamProfileLabel", toQString(intent.streamProfile.displayName));
model.insert("preflightReason", toQString(intent.preflight.reason));
model.insert("publicCopy", toQString(intent.publicPreviewCopy));
model.insert("inertPreviewUri", toQString(intent.inertPreviewUri));
model.insert("streamLifecycleCopy", toQString(streamIntent.publicCopy));
model.insert("noopPreview", true);
model.insert("notStarted", true);
return model;
}

QVariantMap toPreviewCopyActionModel(const nova::deck::DeckLaunchPreviewCopyAction& copyAction) {
QVariantMap model;
model.insert("id", toQString(copyAction.id));
Expand Down Expand Up @@ -244,6 +263,7 @@ int main(int argc, char *argv[]) {
initialGameId);
const auto& selectedHostDetail = selectedBinding.hostDetail;
const auto& launchIntent = selectedBinding.intent;
const auto streamIntent = nova::deck::resolveStreamIntent(launchIntent);
const auto& launchCta = selectedBinding.launchCta;
const auto& launchPreviewCopyAction = selectedBinding.copyAction;

Expand All @@ -264,6 +284,7 @@ int main(int argc, char *argv[]) {
engine.rootContext()->setContextProperty("novaSelectedLaunchPreviewText", toQString(selectedBinding.preview.text));
engine.rootContext()->setContextProperty("novaHostLaunchCta", toLaunchCtaModel(launchCta));
engine.rootContext()->setContextProperty("novaLaunchIntentBoundary", toLaunchIntentBoundaryModel(launchIntent.boundary));
engine.rootContext()->setContextProperty("novaLaunchIntentPreview", toLaunchIntentPreviewModel(launchIntent, streamIntent));
engine.rootContext()->setContextProperty("novaLaunchPreviewCopyAction", toPreviewCopyActionModel(launchPreviewCopyAction));
engine.rootContext()->setContextProperty("novaLocalClipboard", &localClipboard);
engine.rootContext()->setContextProperty("novaGamepad", &gamepadBridge);
Expand Down
Loading
Loading