Skip to content
Open
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
66 changes: 66 additions & 0 deletions assets/electron/template/app/src/handlers/steam/saveScreenshot.js
Original file line number Diff line number Diff line change
@@ -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<import('@pipelab/steamworks.js').Client, "init" | "runCallbacks">} client
* @param {Object} json
* @returns {Promise<number>} 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<import('@pipelab/steamworks.js').Client, "init" | "runCallbacks">} client
*/
export default async (json, ws, client) => {
await handleSteamRequest(client, json, ws, saveScreenshotHandler)
}
107 changes: 102 additions & 5 deletions assets/electron/template/app/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
*/
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
}
Expand Down Expand Up @@ -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()
})
}
})
Loading