diff --git a/assets/electron/template/app/src/handlers/steam/saveScreenshot.js b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js new file mode 100644 index 0000000..193a9d7 --- /dev/null +++ b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js @@ -0,0 +1,66 @@ +import { handleSteamRequest } from './utils.js' +import { writeFileSync, unlinkSync, mkdirSync, rmdirSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { randomUUID } from 'node:crypto' + +/** + * Save screenshot from base64 data URL to Steam library + * @param {Omit} client + * @param {Object} json + * @returns {Promise} The screenshot handle + */ +const saveScreenshotHandler = async (client, json) => { + const { body } = json + const { dataUrl, width, height } = body + + // Validate input + if (!dataUrl || typeof dataUrl !== 'string') { + throw new Error('dataUrl is required and must be a string') + } + + if (!dataUrl.startsWith('data:image/')) { + throw new Error('dataUrl must be a base64 data URL (data:image/...)') + } + + // Extract base64 data + const matches = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/) + if (!matches) { + throw new Error('Invalid base64 data URL format') + } + + const [, format, base64Data] = matches + const buffer = Buffer.from(base64Data, 'base64') + + // Create temp file + const tempDir = join(tmpdir(), 'pipelab-screenshots') + mkdirSync(tempDir, { recursive: true }) + const tempPath = join(tempDir, `screenshot-${randomUUID()}.${format}`) + + // Write to temp file + writeFileSync(tempPath, buffer) + + // Add to Steam library + const handle = client.screenshots.addScreenshotToLibrary(tempPath, null, width, height) + + // Delete temp file after a delay to allow Steam to process it + setTimeout(() => { + try { + unlinkSync(tempPath) + rmdirSync(tempDir) + } catch { + // Ignore cleanup errors + } + }, 5000) + + return handle +} + +/** + * @param {Object} json + * @param {import('ws').WebSocket} ws + * @param {Omit} client + */ +export default async (json, ws, client) => { + await handleSteamRequest(client, json, ws, saveScreenshotHandler) +} diff --git a/assets/electron/template/app/src/index.js b/assets/electron/template/app/src/index.js index fa164ae..e27f970 100644 --- a/assets/electron/template/app/src/index.js +++ b/assets/electron/template/app/src/index.js @@ -72,6 +72,7 @@ import steamInstallInfo from './handlers/steam/installInfo.js' import steamDownloadInfo from './handlers/steam/downloadInfo.js' import steamDownload from './handlers/steam/download.js' import steamDeleteItem from './handlers/steam/deleteItem.js' +import steamSaveScreenshot from './handlers/steam/saveScreenshot.js' // discord set activity import discordSetActivity from './handlers/discord/set-activity.js' @@ -286,6 +287,21 @@ app.setPath('userData', sessionDataPath) */ const clients = new Set() +/** + * @type {import('http').Server | null} + */ +let httpServer = null + +/** + * @type {import('ws').WebSocketServer | null} + */ +let wss = null + +/** + * @type {boolean} + */ +let isQuitting = false + /** * @param {string} message */ @@ -307,7 +323,8 @@ const dir = app.isPackaged ? join(metaDirname, './app') : './src/app' const createAppServer = (mainWindow, serveStatic = true) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - const server = createServer() + httpServer = createServer() + const server = httpServer if (serveStatic) { server.on('request', (req, res) => { @@ -347,7 +364,7 @@ const createAppServer = (mainWindow, serveStatic = true) => { } try { - const wss = new WebSocketServer({ server }) + wss = new WebSocketServer({ server }) wss.on('connection', function connection(ws) { clients.add(ws) @@ -512,6 +529,9 @@ const createAppServer = (mainWindow, serveStatic = true) => { case '/steam/workshop/delete-item': await steamDeleteItem(json, ws, client) break + case '/steam/screenshots/save': + await steamSaveScreenshot(json, ws, client) + break case '/discord/set-activity': await discordSetActivity(json, ws, mainWindow, rpc) break @@ -657,9 +677,53 @@ const createWindow = async () => { return mainWindow } +/** + * Cleanup all resources before quitting + */ +const cleanup = () => { + return new Promise((resolve) => { + console.log('Cleaning up resources...') + + // Close all WebSocket clients + for (const client of clients) { + try { + client.close() + } catch (e) { + console.error('Error closing WebSocket client:', e) + } + } + clients.clear() + + // Close WebSocket server + if (wss) { + try { + wss.close() + } catch (e) { + console.error('Error closing WebSocket server:', e) + } + wss = null + } + + // Close HTTP server + if (httpServer) { + httpServer.close(() => { + console.log('HTTP server closed') + httpServer = null + resolve() + }) + // Force resolve after timeout in case server doesn't close cleanly + setTimeout(resolve, 500) + } else { + resolve() + } + }) +} + const registerHandlers = async () => { - ipcMain.on('exit', (event, code) => { + ipcMain.on('exit', async (event, code) => { console.log('exit', code) + isQuitting = true + await cleanup() app.exit(code) }) } @@ -708,6 +772,39 @@ app.whenReady().then(async () => { }) }) -app.on('window-all-closed', async () => { - app.quit() +app.on('before-quit', (event) => { + if (!isQuitting) { + event.preventDefault() + isQuitting = true + cleanup().then(() => { + app.quit() + }) + } +}) + +app.on('will-quit', () => { + // Final cleanup - synchronous operations only + // Close any remaining WebSocket clients + for (const client of clients) { + try { + client.terminate() // Force close + } catch (e) { + // Ignore errors during final cleanup + } + } + clients.clear() +}) + +app.on('window-all-closed', () => { + // On macOS, apps typically stay open until explicitly quit + // But for games, we usually want to quit when the window is closed + if (process.platform !== 'darwin' || isQuitting) { + app.quit() + } else { + // On macOS, trigger the quit process which will run cleanup + isQuitting = true + cleanup().then(() => { + app.quit() + }) + } })