parent
ebfe647cb1
commit
e5bb4e693f
@ -0,0 +1,58 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { createWriteStream } from "fs"; |
||||
import { Readable } from "stream"; |
||||
import { pipeline } from "stream/promises"; |
||||
import { setTimeout } from "timers/promises"; |
||||
|
||||
interface FetchieOptions { |
||||
retryOnNetworkError?: boolean; |
||||
} |
||||
|
||||
export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { |
||||
const res = await fetchie(url, options, fetchieOpts); |
||||
await pipeline( |
||||
// @ts-expect-error odd type error
|
||||
Readable.fromWeb(res.body!), |
||||
createWriteStream(file, { |
||||
autoClose: true |
||||
}) |
||||
); |
||||
} |
||||
|
||||
const ONE_MINUTE_MS = 1000 * 60; |
||||
|
||||
export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { |
||||
let res: Response | undefined; |
||||
|
||||
try { |
||||
res = await fetch(url, options); |
||||
} catch (err) { |
||||
if (retryOnNetworkError) { |
||||
console.error("Failed to fetch", url + ".", "Gonna retry with backoff."); |
||||
|
||||
for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { |
||||
await setTimeout(delayMs); |
||||
try { |
||||
res = await fetch(url, options); |
||||
break; |
||||
} catch {} |
||||
} |
||||
} |
||||
|
||||
if (!res) throw new Error(`Failed to fetch ${url}\n${err}`); |
||||
} |
||||
|
||||
if (res.ok) return res; |
||||
|
||||
let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`; |
||||
|
||||
const reason = await res.text().catch(() => ""); |
||||
if (reason) msg += `\n${reason}`; |
||||
|
||||
throw new Error(msg); |
||||
} |
@ -0,0 +1,30 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; |
||||
import { DISCORD_HOSTNAMES } from "main/constants"; |
||||
import { IpcEvents } from "shared/IpcEvents"; |
||||
|
||||
export function validateSender(frame: WebFrameMain) { |
||||
const { hostname, protocol } = new URL(frame.url); |
||||
if (protocol === "file:") return; |
||||
|
||||
if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname); |
||||
} |
||||
|
||||
export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { |
||||
ipcMain.on(event, (e, ...args) => { |
||||
validateSender(e.senderFrame); |
||||
e.returnValue = cb(e, ...args); |
||||
}); |
||||
} |
||||
|
||||
export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { |
||||
ipcMain.handle(event, (e, ...args) => { |
||||
validateSender(e.senderFrame); |
||||
return cb(e, ...args); |
||||
}); |
||||
} |
@ -0,0 +1,71 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { BrowserWindow, shell } from "electron"; |
||||
import { DISCORD_HOSTNAMES } from "main/constants"; |
||||
|
||||
import { Settings } from "../settings"; |
||||
import { createOrFocusPopup, setupPopout } from "./popout"; |
||||
import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS"; |
||||
|
||||
export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } { |
||||
if (protocol == null) { |
||||
try { |
||||
protocol = new URL(url).protocol; |
||||
} catch { |
||||
return { action: "deny" }; |
||||
} |
||||
} |
||||
|
||||
switch (protocol) { |
||||
case "http:": |
||||
case "https:": |
||||
if (Settings.store.openLinksWithElectron) { |
||||
return { action: "allow" }; |
||||
} |
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "mailto:": |
||||
case "spotify:": |
||||
if (isDeckGameMode) { |
||||
steamOpenURL(url); |
||||
} else { |
||||
shell.openExternal(url); |
||||
} |
||||
break; |
||||
case "steam:": |
||||
if (isDeckGameMode) { |
||||
execSteamURL(url); |
||||
} else { |
||||
shell.openExternal(url); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
return { action: "deny" }; |
||||
} |
||||
|
||||
export function makeLinksOpenExternally(win: BrowserWindow) { |
||||
win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { |
||||
try { |
||||
var { protocol, hostname, pathname } = new URL(url); |
||||
} catch { |
||||
return { action: "deny" }; |
||||
} |
||||
|
||||
if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { |
||||
return createOrFocusPopup(frameName, features); |
||||
} |
||||
|
||||
if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname))) |
||||
return { action: "allow" }; |
||||
|
||||
return handleExternalUrl(url, protocol); |
||||
}); |
||||
|
||||
win.webContents.on("did-create-window", (win, { frameName }) => { |
||||
if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName); |
||||
}); |
||||
} |
@ -0,0 +1,116 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; |
||||
import { Settings } from "main/settings"; |
||||
|
||||
import { handleExternalUrl } from "./makeLinksOpenExternally"; |
||||
|
||||
const ALLOWED_FEATURES = new Set([ |
||||
"width", |
||||
"height", |
||||
"left", |
||||
"top", |
||||
"resizable", |
||||
"movable", |
||||
"alwaysOnTop", |
||||
"frame", |
||||
"transparent", |
||||
"hasShadow", |
||||
"closable", |
||||
"skipTaskbar", |
||||
"backgroundColor", |
||||
"menubar", |
||||
"toolbar", |
||||
"location", |
||||
"directories", |
||||
"titleBarStyle" |
||||
]); |
||||
|
||||
const MIN_POPOUT_WIDTH = 320; |
||||
const MIN_POPOUT_HEIGHT = 180; |
||||
const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = { |
||||
title: "Discord Popout", |
||||
backgroundColor: "#2f3136", |
||||
minWidth: MIN_POPOUT_WIDTH, |
||||
minHeight: MIN_POPOUT_HEIGHT, |
||||
frame: Settings.store.customTitleBar !== true, |
||||
titleBarStyle: process.platform === "darwin" ? "hidden" : undefined, |
||||
trafficLightPosition: |
||||
process.platform === "darwin" |
||||
? { |
||||
x: 10, |
||||
y: 3 |
||||
} |
||||
: undefined, |
||||
webPreferences: { |
||||
nodeIntegration: false, |
||||
contextIsolation: true |
||||
}, |
||||
autoHideMenuBar: Settings.store.enableMenu |
||||
}; |
||||
|
||||
export const PopoutWindows = new Map<string, BrowserWindow>(); |
||||
|
||||
function focusWindow(window: BrowserWindow) { |
||||
window.setAlwaysOnTop(true); |
||||
window.focus(); |
||||
window.setAlwaysOnTop(false); |
||||
} |
||||
|
||||
function parseFeatureValue(feature: string) { |
||||
if (feature === "yes") return true; |
||||
if (feature === "no") return false; |
||||
|
||||
const n = Number(feature); |
||||
if (!isNaN(n)) return n; |
||||
|
||||
return feature; |
||||
} |
||||
|
||||
function parseWindowFeatures(features: string) { |
||||
const keyValuesParsed = features.split(","); |
||||
|
||||
return keyValuesParsed.reduce((features, feature) => { |
||||
const [key, value] = feature.split("="); |
||||
if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value); |
||||
|
||||
return features; |
||||
}, {}); |
||||
} |
||||
|
||||
export function createOrFocusPopup(key: string, features: string) { |
||||
const existingWindow = PopoutWindows.get(key); |
||||
if (existingWindow) { |
||||
focusWindow(existingWindow); |
||||
return <const>{ action: "deny" }; |
||||
} |
||||
|
||||
return <const>{ |
||||
action: "allow", |
||||
overrideBrowserWindowOptions: { |
||||
...DEFAULT_POPOUT_OPTIONS, |
||||
...parseWindowFeatures(features) |
||||
} |
||||
}; |
||||
} |
||||
|
||||
export function setupPopout(win: BrowserWindow, key: string) { |
||||
win.setMenuBarVisibility(false); |
||||
|
||||
PopoutWindows.set(key, win); |
||||
|
||||
/* win.webContents.on("will-navigate", (evt, url) => { |
||||
// maybe prevent if not origin match
|
||||
})*/ |
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url)); |
||||
|
||||
win.once("closed", () => { |
||||
win.removeAllListeners(); |
||||
PopoutWindows.delete(key); |
||||
}); |
||||
} |
@ -0,0 +1,97 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { BrowserWindow, dialog } from "electron"; |
||||
import { writeFile } from "fs/promises"; |
||||
import { join } from "path"; |
||||
|
||||
import { MessageBoxChoice } from "../constants"; |
||||
import { State } from "../settings"; |
||||
|
||||
// Bump this to re-show the prompt
|
||||
const layoutVersion = 2; |
||||
// Get this from "show details" on the profile after exporting as a shared personal layout or using share with community
|
||||
const layoutId = "3080264545"; // Vesktop Layout v2
|
||||
const numberRegex = /^[0-9]*$/; |
||||
|
||||
let steamPipeQueue = Promise.resolve(); |
||||
|
||||
export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; |
||||
|
||||
export function applyDeckKeyboardFix() { |
||||
if (!isDeckGameMode) return; |
||||
// Prevent constant virtual keyboard spam that eventually crashes Steam.
|
||||
process.env.GTK_IM_MODULE = "None"; |
||||
} |
||||
|
||||
// For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead.
|
||||
function getAppId(): string | null { |
||||
// /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1
|
||||
const path = process.env.STEAM_COMPAT_MEDIA_PATH; |
||||
if (!path) return null; |
||||
const pathElems = path?.split("/"); |
||||
const appId = pathElems[pathElems.length - 2]; |
||||
if (appId.match(numberRegex)) { |
||||
console.log(`Got Steam App ID ${appId}`); |
||||
return appId; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
export function execSteamURL(url: string) { |
||||
// This doesn't allow arbitrary execution despite the weird syntax.
|
||||
steamPipeQueue = steamPipeQueue.then(() => |
||||
writeFile( |
||||
join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"), |
||||
// replace ' to prevent argument injection
|
||||
`'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`, |
||||
"utf-8" |
||||
) |
||||
); |
||||
} |
||||
|
||||
export function steamOpenURL(url: string) { |
||||
execSteamURL(`steam://openurl/${url}`); |
||||
} |
||||
|
||||
export async function showGamePage() { |
||||
const appId = getAppId(); |
||||
if (!appId) return; |
||||
await execSteamURL(`steam://nav/games/details/${appId}`); |
||||
} |
||||
|
||||
async function showLayout(appId: string) { |
||||
execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); |
||||
} |
||||
|
||||
export async function askToApplySteamLayout(win: BrowserWindow) { |
||||
const appId = getAppId(); |
||||
if (!appId) return; |
||||
if (State.store.steamOSLayoutVersion === layoutVersion) return; |
||||
const update = Boolean(State.store.steamOSLayoutVersion); |
||||
|
||||
// Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed.
|
||||
const { response } = await dialog.showMessageBox(win, { |
||||
message: `${update ? "Update" : "Apply"} Vesktop Steam Input Layout?`, |
||||
detail: `Would you like to ${update ? "Update" : "Apply"} Vesktop's recommended Steam Deck controller settings?
|
||||
${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${ |
||||
update ? " Doing so will undo any customizations you have made." : "" |
||||
} |
||||
${update ? "Click" : "Tap"} no to keep your current layout.`,
|
||||
buttons: ["Yes", "No"], |
||||
cancelId: MessageBoxChoice.Cancel, |
||||
defaultId: MessageBoxChoice.Default, |
||||
type: "question" |
||||
}); |
||||
|
||||
if (State.store.steamOSLayoutVersion !== layoutVersion) { |
||||
State.store.steamOSLayoutVersion = layoutVersion; |
||||
} |
||||
|
||||
if (response === MessageBoxChoice.Cancel) return; |
||||
|
||||
await showLayout(appId); |
||||
} |
Loading…
Reference in new issue