Skip to content

Commit f319ad7

Browse files
authored
Merge pull request #53 from xAbdoAT/main
Add Discord Rich Presence and improve settings UI
2 parents bfca7d1 + b67f9d0 commit f319ad7

16 files changed

Lines changed: 429 additions & 65 deletions

File tree

main/application.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Authentication from "./authentication";
77
import MsalAuthentication from "./MsalAuthentication";
88
import Ipc from "./ipc";
99
import { defaultSettings } from '../renderer/context/userContext.defaults'
10+
import DiscordRpcHelper from './helpers/discordrpc'
1011

1112
import xboxWebApi from "xbox-webapi";
1213
import xCloudApi from "./helpers/xcloudapi";
@@ -38,6 +39,8 @@ export default class Application {
3839
public _authentication: Authentication;
3940
public _msalAuthentication: MsalAuthentication;
4041

42+
public _discordRpc: DiscordRpcHelper;
43+
4144
public streamingTokens: any
4245

4346
public webToken: any
@@ -74,6 +77,11 @@ export default class Application {
7477
this._ipc = new Ipc(this);
7578
this._authentication = new Authentication(this);
7679
this._msalAuthentication = new MsalAuthentication(this);
80+
this._discordRpc = new DiscordRpcHelper();
81+
82+
if (settings.discord_rpc !== false) {
83+
this._discordRpc.enable()
84+
}
7785

7886
this._ipc.startUp();
7987

@@ -202,7 +210,10 @@ export default class Application {
202210
? this._mainWindow.show()
203211
: this.openMainWindow();
204212
});
205-
ElectronApp.on("before-quit", () => (this._isQuitting = true));
213+
ElectronApp.on("before-quit", () => {
214+
this._isQuitting = true;
215+
this._discordRpc && this._discordRpc.destroy();
216+
});
206217
}
207218

208219
_webApi: xboxWebApi;
@@ -270,6 +281,7 @@ export default class Application {
270281
// Run workers
271282
this._xboxWorker = new xboxWorker(this);
272283
this._ipc.onUserLoaded();
284+
this._discordRpc.setIdle();
273285
})
274286
.catch((error) => {
275287
this.log(

main/helpers/discordrpc.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import DiscordRpc from 'discord-rpc'
2+
3+
const CLIENT_ID = '1215618460231401482'
4+
5+
const IDLE_STATE = {
6+
details: 'In Library',
7+
state: 'xCloud & Console Streaming',
8+
largeImageKey: 'xstreaming_logo',
9+
largeImageText: 'XStreaming Desktop',
10+
instance: false,
11+
}
12+
13+
export default class DiscordRpcHelper {
14+
private _client: DiscordRpc.Client | null = null
15+
private _connected = false
16+
private _appStartTime: Date
17+
private _playStartTime: Date | null = null
18+
private _reconnectTimer: NodeJS.Timeout | null = null
19+
private _currentGameName = ''
20+
private _currentGameImage = ''
21+
private _isPlaying = false
22+
23+
private _enabled = false
24+
25+
private _targetEnabled = false
26+
private _debounceTimer: NodeJS.Timeout | null = null
27+
28+
private _opQueue: Promise<void> = Promise.resolve()
29+
30+
constructor() {
31+
this._appStartTime = new Date()
32+
try { DiscordRpc.register(CLIENT_ID) } catch {}
33+
}
34+
35+
private _connect() {
36+
if (!this._enabled) return
37+
38+
try {
39+
const client = new DiscordRpc.Client({ transport: 'ipc' })
40+
this._client = client
41+
42+
client.on('ready', () => {
43+
if (this._client !== client) return
44+
this._connected = true
45+
this._cancelReconnect()
46+
if (this._isPlaying) {
47+
this._applyPlaying(this._currentGameName, this._currentGameImage)
48+
} else {
49+
this._applyIdle()
50+
}
51+
})
52+
53+
client.on('disconnected' as any, () => {
54+
if (this._client !== client) return
55+
this._connected = false
56+
if (this._enabled) this._scheduleReconnect()
57+
})
58+
59+
client
60+
.login({ clientId: CLIENT_ID })
61+
.catch(() => {
62+
if (this._client !== client) return
63+
this._connected = false
64+
if (this._enabled) this._scheduleReconnect()
65+
})
66+
} catch {
67+
if (this._enabled) this._scheduleReconnect()
68+
}
69+
}
70+
71+
private _scheduleReconnect() {
72+
if (this._reconnectTimer) return
73+
this._reconnectTimer = setTimeout(() => {
74+
this._reconnectTimer = null
75+
if (this._enabled) this._connect()
76+
}, 15_000)
77+
}
78+
79+
private _cancelReconnect() {
80+
if (this._reconnectTimer) {
81+
clearTimeout(this._reconnectTimer)
82+
this._reconnectTimer = null
83+
}
84+
}
85+
86+
private _destroyClient(): Promise<void> {
87+
this._cancelReconnect()
88+
const clientToDestroy = this._client
89+
this._client = null
90+
this._connected = false
91+
92+
if (!clientToDestroy) return Promise.resolve()
93+
94+
return clientToDestroy
95+
.clearActivity()
96+
.catch(() => { })
97+
.finally(() => clientToDestroy.destroy().catch(() => { }))
98+
}
99+
100+
private _applyIdle() {
101+
if (!this._connected || !this._client) return
102+
this._client
103+
.setActivity({
104+
...IDLE_STATE,
105+
startTimestamp: this._appStartTime,
106+
})
107+
.catch(() => { })
108+
}
109+
110+
private _applyPlaying(gameName: string, gameImageUrl: string) {
111+
if (!this._connected || !this._client) return
112+
113+
const largeImage =
114+
gameImageUrl && gameImageUrl.startsWith('http')
115+
? gameImageUrl
116+
: 'xstreaming_logo'
117+
118+
this._client
119+
.setActivity({
120+
details: gameName || 'Playing a game',
121+
state: 'Streaming via xCloud',
122+
largeImageKey: largeImage,
123+
largeImageText: gameName || 'Game',
124+
smallImageKey: 'xstreaming_logo',
125+
smallImageText: 'XStreaming Desktop',
126+
startTimestamp: this._playStartTime!,
127+
instance: false,
128+
})
129+
.catch(() => { })
130+
}
131+
132+
enable() {
133+
this._targetEnabled = true
134+
this._scheduleUpdate()
135+
}
136+
137+
disable() {
138+
this._targetEnabled = false
139+
this._scheduleUpdate()
140+
}
141+
142+
private _scheduleUpdate() {
143+
if (this._debounceTimer) clearTimeout(this._debounceTimer)
144+
145+
this._debounceTimer = setTimeout(() => {
146+
this._debounceTimer = null
147+
148+
if (this._targetEnabled && !this._enabled) {
149+
this._enabled = true
150+
this._opQueue = this._opQueue.then(() => {
151+
if (this._enabled) this._connect()
152+
})
153+
} else if (!this._targetEnabled && this._enabled) {
154+
this._enabled = false
155+
this._opQueue = this._opQueue.then(() => this._destroyClient())
156+
}
157+
}, 500)
158+
}
159+
160+
setIdle() {
161+
if (!this._targetEnabled) return
162+
this._isPlaying = false
163+
this._playStartTime = null
164+
this._applyIdle()
165+
}
166+
167+
setPlaying(gameName: string, gameImageUrl: string) {
168+
if (!this._targetEnabled) return
169+
this._isPlaying = true
170+
this._playStartTime = new Date()
171+
this._currentGameName = gameName
172+
this._currentGameImage = gameImageUrl
173+
this._applyPlaying(gameName, gameImageUrl)
174+
}
175+
176+
clearPlaying() {
177+
if (!this._targetEnabled) return
178+
this._isPlaying = false
179+
this._playStartTime = null
180+
this._applyIdle()
181+
}
182+
183+
destroy() {
184+
this._targetEnabled = false
185+
this._enabled = false
186+
if (this._debounceTimer) clearTimeout(this._debounceTimer)
187+
this._debounceTimer = null
188+
this._opQueue = this._opQueue.then(() => this._destroyClient())
189+
}
190+
}

main/ipc/settings.ts

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,48 @@
1-
import IpcBase from './base'
2-
import { defaultSettings } from '../../renderer/context/userContext.defaults'
3-
4-
export default class IpcSettings extends IpcBase {
5-
6-
setSettings(args:(typeof defaultSettings)){
7-
return new Promise((resolve) => {
8-
const newSettings = {...defaultSettings, ...args}
9-
this._application._store.set('settings', newSettings)
10-
resolve(newSettings)
11-
})
12-
}
13-
14-
getSettings(){
15-
return new Promise<typeof defaultSettings>((resolve) => {
16-
const settings = this._application._store.get('settings', defaultSettings) as object
17-
resolve({...defaultSettings, ...settings})
18-
})
19-
}
20-
21-
resetSettings() {
22-
return new Promise((resolve) => {
23-
const settings = {...defaultSettings}
24-
this._application._store.delete('settings')
25-
26-
this._application._store.set('settings', settings)
27-
resolve(settings)
28-
})
29-
}
30-
}
1+
import IpcBase from './base'
2+
import { defaultSettings } from '../../renderer/context/userContext.defaults'
3+
4+
export default class IpcSettings extends IpcBase {
5+
6+
setSettings(args:(typeof defaultSettings)){
7+
return new Promise((resolve) => {
8+
const oldSettings = this._application._store.get('settings', defaultSettings) as any
9+
const newSettings = {...defaultSettings, ...args}
10+
this._application._store.set('settings', newSettings)
11+
12+
const rpcEnabled = (newSettings as any).discord_rpc
13+
const rpcWasEnabled = (oldSettings as any).discord_rpc
14+
15+
if (rpcEnabled && !rpcWasEnabled) {
16+
this._application._discordRpc.enable()
17+
} else if (!rpcEnabled && rpcWasEnabled) {
18+
this._application._discordRpc.disable()
19+
}
20+
21+
resolve(newSettings)
22+
})
23+
}
24+
25+
getSettings(){
26+
return new Promise<typeof defaultSettings>((resolve) => {
27+
const settings = this._application._store.get('settings', defaultSettings) as object
28+
resolve({...defaultSettings, ...settings})
29+
})
30+
}
31+
32+
resetSettings() {
33+
return new Promise((resolve) => {
34+
const settings = {...defaultSettings}
35+
this._application._store.delete('settings')
36+
37+
this._application._store.set('settings', settings)
38+
39+
if (settings.discord_rpc !== false) {
40+
this._application._discordRpc.enable()
41+
} else {
42+
this._application._discordRpc.disable()
43+
}
44+
45+
resolve(settings)
46+
})
47+
}
48+
}

0 commit comments

Comments
 (0)