Skip to content

Commit 6b3c130

Browse files
committed
Update 2.0.0 - See changelog.txt for details
1 parent 3a265e3 commit 6b3c130

23 files changed

+269
-23
lines changed

README.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ StreamLight is a fork of [Moonlight](https://github.com/moonlight-stream/moonlig
1414

1515
StreamLight is currently available for **Windows only**.
1616

17-
## ✨ What's New in Version 1.2.0 - "The Host Metrics Update" (20/03/2026)
17+
## ✨ What's New in Version 2.0.0 - "The Library Update" (21/03/2026)
1818

1919
### 🚀 New Features
20-
* **Host metrics in overlay**: the performance overlay now includes a **"Host Metrics (StreamTweak)"** section with real-time data from the host PC:
21-
* **GPU** usage % — cross-vendor (NVIDIA, AMD, Intel Arc) via PDH PerformanceCounters
22-
* **GPU Enc** (encoder) usage %
23-
* **GPU Temp** (°C) — NVIDIA only via NVML; shows N/A on non-NVIDIA systems
24-
* **VRAM** used / total (MB) — total shown only on NVIDIA; AMD/Intel show used only
25-
* **CPU** usage %
26-
* **Net TX** (Mbps) — host outbound network throughput
27-
* **Graceful degradation**: the host metrics section is entirely hidden when StreamTweak is not reachable; individual metrics that are unavailable show N/A rather than a placeholder value
20+
* **New FoggyBytes icon** — a new app icon visually unifies StreamLight and StreamTweak across the FoggyBytes suite
21+
* **Store badges on game covers**: each game synced by StreamTweak's Game Library displays a small badge in the bottom-right corner of its cover art, showing the store it belongs to — Steam, Epic Games, GOG, Ubisoft Connect, Xbox, or Battle.net; fetched live from the host via the new APPSTORES command on the TCP bridge
22+
* **Battle.net badge**: a dedicated Battle.net badge (white icon + label on semi-transparent dark background) joins the existing badge set
2823

29-
> StreamTweak 4.4.0 or later is required on the host PC for host metrics to appear in the overlay.
24+
### 🎨 UI Redesign
25+
* **Unified visual identity**: the full StreamLight UI has been revised to match StreamTweak's design language — color palette, spacing, and component styling now align across both apps for a seamless paired-app experience
26+
27+
### 🔧 Improvements
28+
* **App list sort order**: Desktop always appears first, Steam Big Picture second, then all other apps in alphabetical order
29+
30+
> StreamTweak 5.0.0 or later is required on the host PC for store badges to appear.
3031
3132
## 🛠️ Differences from Upstream Moonlight
3233

@@ -39,16 +40,26 @@ Right-clicking a paired host PC now exposes two additional actions:
3940
* Includes a **10-second countdown** before the connection starts.
4041
* **Safety Fallback**: If no connection is made within 30 seconds, the host reverts to its original speed automatically.
4142

43+
### 🎮 Game Library — Store Badges
44+
Each game synced from StreamTweak's Game Library displays a store badge (icon + label) in the bottom-right corner of its cover art:
45+
* Badges are fetched from the host via the **APPSTORES** TCP command on connection
46+
* Supported stores: **Steam**, **Epic Games**, **GOG**, **Ubisoft Connect**, **Xbox**, **Battle.net**
47+
* Requires StreamTweak 5.0.0 or later on the host PC
48+
4249
### 🎨 Visual & Identity
50+
* **New FoggyBytes icon**: app icon updated to match StreamTweak's new unified FoggyBytes identity.
4351
* **Branding**: Window title changed to `StreamLight (a Moonlight fork)`.
44-
* **Theme**: Color palette aligned with *StreamTweak* for a seamless user experience.
52+
* **Theme**: Color palette and UI fully aligned with *StreamTweak* for a seamless paired-app experience.
4553
* **Cleanup**:
4654
* Removed Discord links (to avoid redirecting users to the upstream Moonlight support channels).
4755
* Disabled the **Auto-update checker** to prevent accidental overwrites by upstream releases.
4856

57+
### 🔧 Improvements
58+
* **App list sort order**: Desktop always first, Steam Big Picture second, then all other apps alphabetically.
59+
4960
## 🖥️ Requirements
5061

51-
- [StreamTweak](https://github.com/FoggyBytes/StreamTweak) must be installed and running on the **host PC** (4.4.0+ required for host metrics in the overlay)
62+
- [StreamTweak](https://github.com/FoggyBytes/StreamTweak) must be installed and running on the **host PC** (5.0.0+ required for store badges; 4.4.0+ for host metrics in the overlay)
5263
- Windows 10 or later on the **client PC**
5364
- A Sunshine or Apollo-compatible host
5465

StreamLight.iss

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
; =====================================================
2-
; StreamLight v1.2.0 - Installer
2+
; StreamLight v2.0.0 - Installer
33
; A Moonlight fork with StreamTweak integration
44
; =====================================================
55
#define AppName "StreamLight"
6-
#define AppVersion "1.2.0"
6+
#define AppVersion "2.0.0"
77
#define AppPublisher "FoggyBytes"
88
#define AppURL "https://github.com/FoggyBytes/StreamLight"
99
#define AppExeName "StreamLight.exe"
10-
#define SourceDir "build\build-x64-release\app\release"
10+
#define SourceDir "build\deploy-x64-release"
1111

1212
[Setup]
1313
AppId={{B7A2C3D4-E5F6-7890-ABCD-EF1234567890}
@@ -48,7 +48,6 @@ WelcomeLabel2=
4848
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
4949

5050
[Files]
51-
Source: "libs\windows\lib\x64\*.dll"; DestDir: "{app}"; Flags: ignoreversion
5251
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
5352
Source: "installer\resources\streamlight.bmp"; Flags: dontcopy
5453
Source: "changelog.txt"; DestDir: "{app}"; Flags: ignoreversion

app/StreamTweakBridge.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,31 @@ void StreamTweakBridge::requestStats(const QString& hostAddress)
9797

9898
socket->connectToHost(hostAddress, BridgePort);
9999
}
100+
101+
void StreamTweakBridge::requestAppStores(const QString& hostAddress)
102+
{
103+
QTcpSocket* socket = new QTcpSocket();
104+
105+
QObject::connect(socket, &QTcpSocket::disconnected,
106+
socket, &QObject::deleteLater);
107+
108+
QObject::connect(socket, &QAbstractSocket::errorOccurred,
109+
[this, socket](QAbstractSocket::SocketError) {
110+
emit appStoresReceived(QString());
111+
socket->deleteLater();
112+
});
113+
114+
QObject::connect(socket, &QTcpSocket::connected, [socket]() {
115+
QTextStream stream(socket);
116+
stream << "APPSTORES\n";
117+
stream.flush();
118+
});
119+
120+
QObject::connect(socket, &QTcpSocket::readyRead, [this, socket]() {
121+
QString response = QString::fromUtf8(socket->readAll()).trimmed();
122+
emit appStoresReceived(response);
123+
socket->disconnectFromHost();
124+
});
125+
126+
socket->connectToHost(hostAddress, BridgePort);
127+
}

app/StreamTweakBridge.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,21 @@ class StreamTweakBridge : public QObject
5050
*/
5151
void requestStats(const QString& hostAddress);
5252

53+
/**
54+
* Asynchronously requests the store map for all managed apps from StreamTweak.
55+
* Emits appStoresReceived(QString) with a JSON object mapping app names to
56+
* store names, e.g. {"Cyberpunk 2077":"Steam","Fortnite":"Epic Games"}.
57+
* Emits appStoresReceived("") on connection error or if StreamTweak is
58+
* unreachable.
59+
*/
60+
void requestAppStores(const QString& hostAddress);
61+
5362
static constexpr quint16 BridgePort = 47998;
5463

5564
signals:
5665
void statusReceived(const QString& status);
5766
void statsReceived(const QString& statsJson);
67+
void appStoresReceived(const QString& storesJson);
5868

5969
private:
6070
void sendCommand(const QString& hostAddress, const QString& command);

app/backend/nvcomputer.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,16 @@ bool NvComputer::isEqualSerialized(const NvComputer &that) const
122122

123123
void NvComputer::sortAppList()
124124
{
125-
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
126-
return app1.name.toLower() < app2.name.toLower();
125+
auto appOrder = [](const NvApp& app) -> int {
126+
if (app.name.compare("Desktop", Qt::CaseInsensitive) == 0) return 0;
127+
if (app.name.compare("Steam Big Picture", Qt::CaseInsensitive) == 0) return 1;
128+
return 2;
129+
};
130+
131+
std::stable_sort(appList.begin(), appList.end(), [&appOrder](const NvApp& a, const NvApp& b) {
132+
int oa = appOrder(a), ob = appOrder(b);
133+
if (oa != ob) return oa < ob;
134+
return a.name.toLower() < b.name.toLower();
127135
});
128136
}
129137

app/gui/AppView.qml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ CenteredGridView {
1313
property bool showHiddenGames
1414
property bool showGames
1515

16+
// StreamTweak store badge support
17+
property var hostComputerModel: null
18+
property var storeMap: ({})
19+
1620
id: appGrid
1721
focus: true
1822
activeFocusOnTab: true
@@ -33,6 +37,22 @@ CenteredGridView {
3337
currentIndex = -1
3438
}
3539

40+
function storeIconSource(store) {
41+
if (store === "Steam") return "qrc:/res/store_steam.svg"
42+
if (store === "Epic Games") return "qrc:/res/store_epic.svg"
43+
if (store === "GOG") return "qrc:/res/store_gog.svg"
44+
if (store === "Ubisoft Connect") return "qrc:/res/store_ubisoft.svg"
45+
if (store === "Xbox") return "qrc:/res/store_xbox.svg"
46+
if (store === "Battle.net") return "qrc:/res/store_battlenet.svg"
47+
return ""
48+
}
49+
50+
function handleAppStoresReceived(idx, stores) {
51+
if (idx === computerIndex) {
52+
storeMap = stores
53+
}
54+
}
55+
3656
StackView.onActivated: {
3757
appModel.computerLost.connect(computerLost)
3858
activated = true
@@ -42,6 +62,13 @@ CenteredGridView {
4262
currentIndex = 0
4363
}
4464

65+
// Request store badges from StreamTweak
66+
if (hostComputerModel) {
67+
storeMap = hostComputerModel.getCachedAppStores(computerIndex)
68+
hostComputerModel.appStoresReceived.connect(handleAppStoresReceived)
69+
hostComputerModel.requestAppStores(computerIndex)
70+
}
71+
4572
if (!showGames && !showHiddenGames) {
4673
// Check if there's a direct launch app
4774
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
@@ -59,6 +86,10 @@ CenteredGridView {
5986
StackView.onDeactivating: {
6087
appModel.computerLost.disconnect(computerLost)
6188
activated = false
89+
90+
if (hostComputerModel) {
91+
hostComputerModel.appStoresReceived.disconnect(handleAppStoresReceived)
92+
}
6293
}
6394

6495
function createModel()
@@ -116,6 +147,44 @@ CenteredGridView {
116147
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
117148
}
118149

150+
// Store badge — bottom-right of cover art
151+
Rectangle {
152+
id: storeBadge
153+
154+
property string store: appGrid.storeMap[model.name] || ""
155+
156+
visible: store !== ""
157+
anchors.right: appIcon.right
158+
anchors.bottom: appIcon.bottom
159+
anchors.rightMargin: 5
160+
anchors.bottomMargin: 5
161+
color: "#CC000000"
162+
radius: 3
163+
width: storeBadgeRow.implicitWidth + 8
164+
height: storeBadgeRow.implicitHeight + 5
165+
166+
Row {
167+
id: storeBadgeRow
168+
anchors.centerIn: parent
169+
spacing: 4
170+
171+
Image {
172+
source: appGrid.storeIconSource(storeBadge.store)
173+
width: 12
174+
height: 12
175+
anchors.verticalCenter: parent.verticalCenter
176+
fillMode: Image.PreserveAspectFit
177+
}
178+
179+
Label {
180+
text: storeBadge.store
181+
font.pointSize: 7
182+
color: "white"
183+
anchors.verticalCenter: parent.verticalCenter
184+
}
185+
}
186+
}
187+
119188
Loader {
120189
active: model.running
121190
asynchronous: true

app/gui/PcView.qml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,32 @@ CenteredGridView {
107107
model: computerModel
108108

109109
delegate: NavigableItemDelegate {
110+
id: pcDelegate
110111
width: 300; height: 320;
111112
grid: pcGrid
112113

113114
property alias pcContextMenu : pcContextMenuLoader.item
114115
property string streamTweakStatus: ""
115116

117+
background: Rectangle {
118+
color: pcDelegate.hovered ? "#2E2E2E" : "#252525"
119+
radius: 10
120+
border.color: model.online && model.paired ? "#BE5438" : "#383838"
121+
border.width: 1
122+
}
123+
124+
// Online / offline status dot — consistent with StreamTweak's #4CAF50
125+
Rectangle {
126+
width: 10; height: 10
127+
radius: 5
128+
color: model.online ? "#4CAF50" : "#808080"
129+
visible: !model.statusUnknown
130+
anchors.right: parent.right
131+
anchors.bottom: parent.bottom
132+
anchors.rightMargin: 10
133+
anchors.bottomMargin: 10
134+
}
135+
116136
function formatStreamTweakStatus(raw) {
117137
if (raw === "" || raw === null) return qsTr("N/A")
118138
var mbps = parseInt(raw)
@@ -196,7 +216,7 @@ CenteredGridView {
196216
text: qsTr("View All Apps")
197217
onTriggered: {
198218
var component = Qt.createComponent("AppView.qml")
199-
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
219+
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true, "hostComputerModel": computerModel})
200220
stackView.push(appView)
201221
}
202222
visible: model.online && model.paired
@@ -266,7 +286,7 @@ CenteredGridView {
266286
else if (model.paired) {
267287
// go to game view
268288
var component = Qt.createComponent("AppView.qml")
269-
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
289+
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "hostComputerModel": computerModel})
270290
stackView.push(appView)
271291
}
272292
else {

app/gui/computermodel.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "computermodel.h"
22

3+
#include <QJsonDocument>
4+
#include <QJsonObject>
35
#include <QThreadPool>
46

57
ComputerModel::ComputerModel(QObject* object)
@@ -283,4 +285,47 @@ void ComputerModel::requestStreamTweakStatus(int computerIndex)
283285
m_streamTweakBridge.requestStatus(address);
284286
}
285287

288+
void ComputerModel::requestAppStores(int computerIndex)
289+
{
290+
if (computerIndex < 0 || computerIndex >= m_Computers.count())
291+
return;
292+
293+
NvComputer* computer = m_Computers[computerIndex];
294+
QReadLocker lock(&computer->lock);
295+
296+
QString address = computer->activeAddress.address();
297+
if (address.isEmpty()) {
298+
emit appStoresReceived(computerIndex, QVariantMap());
299+
return;
300+
}
301+
302+
QMetaObject::Connection* conn = new QMetaObject::Connection();
303+
*conn = connect(&m_streamTweakBridge, &StreamTweakBridge::appStoresReceived,
304+
[this, computerIndex, conn](const QString& json) {
305+
disconnect(*conn);
306+
delete conn;
307+
308+
QVariantMap stores;
309+
if (!json.isEmpty()) {
310+
QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
311+
if (doc.isObject()) {
312+
QJsonObject obj = doc.object();
313+
for (auto it = obj.begin(); it != obj.end(); ++it) {
314+
stores[it.key()] = it.value().toString();
315+
}
316+
}
317+
}
318+
319+
m_appStoresCache[computerIndex] = stores;
320+
emit appStoresReceived(computerIndex, stores);
321+
});
322+
323+
m_streamTweakBridge.requestAppStores(address);
324+
}
325+
326+
QVariantMap ComputerModel::getCachedAppStores(int computerIndex) const
327+
{
328+
return m_appStoresCache.value(computerIndex, QVariantMap());
329+
}
330+
286331
#include "computermodel.moc"

0 commit comments

Comments
 (0)