Upload files to 'src/main/utils'
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