Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
run: yarn install --frozen-lockfile

- name: Build
env:
VITE_DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
run: yarn run build

- name: Package Application
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,15 @@
"@electron-toolkit/utils": "^4.0.0",
"@iconify/iconify": "^3.1.1",
"@ryelite/core": "0.1.1",
"discord-rpc": "^4.0.1",
"electron-log": "^5.4.2",
"electron-updater": "^6.6.7",
"idb": "^8.0.3",
"interactjs": "^1.10.27"
},
"devDependencies": {
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/discord-rpc": "^4.0.10",
"@types/node": "^22.17.1",
"electron": "^37.2.6",
"electron-builder": "^26.1.0",
Expand Down
11 changes: 11 additions & 0 deletions src/main/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This file provides type definitions for Vite environment variables
// specifically for the Electron main process, which is compiled
// separately from the renderer process via tsconfig.node.json.

interface ImportMetaEnv {
readonly VITE_DISCORD_CLIENT_ID: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
3 changes: 3 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createConsoleWindow } from './windows/console';
import { createClientWindow } from './windows/client';
import log from 'electron-log';
import registerScreenshotIPC from './modules/screenshotManagement/index';
import discordModule from './modules/discord/index';

log.initialize({ spyRendererConsole: true });
log.transports.console.level = 'info';
Expand All @@ -44,6 +45,8 @@ app.whenReady().then(async () => {
});

registerScreenshotIPC();
discordModule.initialize();

ipcMain.once('delay-update', async () => {
await createClientWindow();
updateWindow.close();
Expand Down
104 changes: 104 additions & 0 deletions src/main/modules/discord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import DiscordRPC from 'discord-rpc';
import { settingsService } from '../settingsManagement/index';
import log from 'electron-log';
import { ipcMain } from 'electron';

const clientId = import.meta.env.VITE_DISCORD_CLIENT_ID || '';
const rpc = new DiscordRPC.Client({ transport: 'ipc' });

class DiscordModule {
private isConnected: boolean = false;
private isEnabled: boolean = false;
private isLoggedIn: boolean = false;
private activityStartTimestamp: Date = new Date();

constructor() {
// Handle when RPC successfully connects
rpc.on('ready', () => {
this.isConnected = true;
log.info('Discord RPC connected successfully.');
this.setActivity();
});

// Listen for setting changes
settingsService.on('setting-changed', (section, key, value) => {
if (section === 'Integrations' && key === 'Enable Discord Rich Presence') {
this.updateEnabledState(value as boolean);
}
});

settingsService.on('settings-applied', () => {
const enabled = settingsService.getByName('Enable Discord Rich Presence');
this.updateEnabledState(enabled as boolean);
});

// Listen for login state changes from the renderer
ipcMain.on('discord:update-state', (_event, isLoggedIn: boolean) => {
if (this.isLoggedIn !== isLoggedIn) {
this.isLoggedIn = isLoggedIn;
log.info(`[Discord RPC] State updated. Logged in: ${isLoggedIn}`);
this.setActivity();
}
});
}

private updateEnabledState(enabled: boolean) {
if (this.isEnabled === enabled) return;
this.isEnabled = enabled;
if (this.isEnabled) {
this.start();
} else {
this.stop();
}
}

public async initialize() {
// Wait for settings to load if they haven't already
await settingsService.load();
const enabled = settingsService.getByName('Enable Discord Rich Presence');
this.isEnabled = enabled === true; // Default to false if not set

if (this.isEnabled) {
this.start();
}
}

public start() {
if (!this.isEnabled) return;

rpc.login({ clientId }).catch((err) => {
// It's normal for login to fail if Discord isn't running
log.warn('Discord RPC failed to login (Discord might not be running).', err.message);
});
}

public stop() {
if (this.isConnected) {
rpc.clearActivity().catch(console.error);
rpc.destroy().catch(console.error);
this.isConnected = false;
}
log.info('Discord RPC stopped.');
}

public setActivity() {
if (!this.isConnected || !this.isEnabled) return;

const detailText = this.isLoggedIn ? 'Exploring the world' : 'XP wasting';
const stateText = this.isLoggedIn ? 'In Game' : 'At Login Screen';

rpc.setActivity({
details: detailText,
state: stateText,
startTimestamp: this.activityStartTimestamp,
largeImageKey: 'logo',
largeImageText: 'HighSpell',
instance: false,
}).catch(err => {
log.error('Failed to set Discord activity', err);
});
}
}

export const discordModule = new DiscordModule();
export default discordModule;
14 changes: 9 additions & 5 deletions src/main/modules/settingsManagement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import { createSettingsModal } from "../../windows/settings/index";
import { BrowserWindow, ipcMain, dialog, app } from "electron";
import fs from 'fs';
import path from 'path';
import { EventEmitter } from 'events';

// Centralized Settings Service
class SettingsService {
class SettingsService extends EventEmitter {
private static instance: SettingsService;
private settingsPath: string;

private constructor() {
super();
this.settingsPath = path.join(app.getPath('userData'), 'settings.json');
}

Expand Down Expand Up @@ -83,11 +85,12 @@ class SettingsService {
settingsSchema.loadFromJSON(schemaJSONString);
await fs.promises.mkdir(path.dirname(this.settingsPath), { recursive: true });
await fs.promises.writeFile(this.settingsPath, schemaJSONString, 'utf-8');
this.emit('settings-applied');
}

getAll(): any {
// Return current settings as plain object
this.ensureDynamicDefaults();
this.ensureDynamicDefaults();
const out: Record<string, any> = {};
Object.entries(settingsSchema.settings).forEach(([sectionKey, section]: any) => {
out[sectionKey] = {};
Expand All @@ -112,6 +115,7 @@ class SettingsService {
if (field) {
field.value = value;
await this.saveCurrent();
this.emit('setting-changed', section, key, value);
}
}

Expand All @@ -133,7 +137,7 @@ export const settingsService = SettingsService.getInstance();


// IPC Handlers for settings API
let settingsWindowRef : BrowserWindow | null = null;
let settingsWindowRef: BrowserWindow | null = null;
ipcMain.on('settings:open', async (event) => {
console.warn("Here");
const parent = BrowserWindow.fromWebContents(event.sender) || BrowserWindow.getFocusedWindow();
Expand Down Expand Up @@ -173,8 +177,8 @@ ipcMain.handle('settings:load', async () => {

ipcMain.handle('settings:apply', async (_event, newSettings) => {
try {
// Expect newSettings as a full schema JSON string
await settingsService.saveFromSchemaJSON(String(newSettings));
// Expect newSettings as a full schema JSON string
await settingsService.saveFromSchemaJSON(String(newSettings));
return true;
} catch (e) {
console.error('Failed to save settings:', e);
Expand Down
7 changes: 7 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ const screenshotAPI = {
capture: async () => ipcRenderer.invoke('screenshot:capture') as Promise<{ ok: boolean; path?: string; error?: string }>,
};

const discordAPI = {
updateState: (isLoggedIn: boolean) => ipcRenderer.send('discord:update-state', isLoggedIn),
};

if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI);
contextBridge.exposeInMainWorld('settings', settingsAPI);
contextBridge.exposeInMainWorld('screenshot', screenshotAPI);
contextBridge.exposeInMainWorld('discord', discordAPI);
} catch (error) {
console.error(error);
}
Expand All @@ -45,4 +50,6 @@ if (process.contextIsolated) {
window.settings = settingsAPI;
// @ts-ignore
window.screenshot = screenshotAPI;
// @ts-ignore
window.discord = discordAPI;
}
11 changes: 11 additions & 0 deletions src/preload/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ export class settingsSchema extends SettingsSchema {
}
} as DirectoryField
]
},
Integrations: {
heading: "Integrations",
fields: [
{
label: "Enable Discord Rich Presence",
type: SettingTypes.BOOLEAN,
description: "Show your game status on Discord.",
default: false
} as Field
]
}
};
}
2 changes: 2 additions & 0 deletions src/renderer/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import '@static/css/item-tooltip.css';

import './helpers/titlebarHelpers.js';
import { setupWorldSelectorObserver } from './helpers/worldSelectHelper';
import { setupAuthObserver } from './helpers/authHelper';

// Load settings via centralized API (values are available via window.settings)
await window.settings.getAll();
Expand Down Expand Up @@ -229,6 +230,7 @@ toElements.forEach(element => {

// Inject World Selector into Login Screen
setupWorldSelectorObserver();
setupAuthObserver();

// Page Setup Completed, Add Game Client Script
const clientScript = document.createElement('script');
Expand Down
46 changes: 46 additions & 0 deletions src/renderer/client/helpers/authHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) 2025 HighLite

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

export function setupAuthObserver() {
let lastLoggedInState: boolean | null = null;

const checkState = () => {
const gameContainer = document.querySelector('#game-container');
const loginContainer = document.querySelector('#login-screen-container');

// Use presence and visibility to determine login status
const isLoggedIn = !!gameContainer && (!loginContainer || (loginContainer as HTMLElement).style.display === 'none');

if (lastLoggedInState !== isLoggedIn) {
lastLoggedInState = isLoggedIn;
console.log(`[AuthHelper] Auth state changed. isLoggedIn: ${isLoggedIn}`);

// @ts-ignore - The preload api might not be perfectly typed for TS here
if (window.discord && typeof window.discord.updateState === 'function') {
// @ts-ignore
window.discord.updateState(isLoggedIn);
}
}
};

// Initial check
checkState();

const observer = new MutationObserver(() => {
checkState();
});

// We observe body for childList (add/remove) and attributes (style display none)
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});

console.log('[AuthHelper] Setup Auth Observer Complete.');
}
8 changes: 8 additions & 0 deletions src/types/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_DISCORD_CLIENT_ID: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
Loading