From 5ace13f43c2a7518054730cce11f4b1045a823f9 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Sun, 3 May 2020 17:43:14 +0200 Subject: [PATCH 1/9] Renamed file --- src/main.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main.ts diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e69de29 From f3e70e714057a86d8fc594aa63bfb5eb710a24fc Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Mon, 1 Jun 2020 17:27:12 +0200 Subject: [PATCH 2/9] Moved files in separate directories --- src/main.ts | 0 src/shell/scripts/appclose.ts | 12 ------------ 2 files changed, 12 deletions(-) delete mode 100644 src/main.ts diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/shell/scripts/appclose.ts b/src/shell/scripts/appclose.ts index 5442138..e69de29 100644 --- a/src/shell/scripts/appclose.ts +++ b/src/shell/scripts/appclose.ts @@ -1,12 +0,0 @@ -require(["playbackManager"], function (playbackManager) { - window["AppCloseHelper"] = { - onClosing: function (): void { - // Prevent backwards navigation from stopping video - history.back = (): void => { - return; - }; - - playbackManager.onAppClose(); - }, - }; -}); From 595d809498ac20207bbb9a09ab51cb148e4ec191 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Mon, 1 Jun 2020 19:57:17 +0200 Subject: [PATCH 3/9] Fixed build and file paths --- src/common/tsconfig.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 6501944..e69de29 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "experimentalDecorators": true, - "moduleResolution": "Node", - "module": "commonjs", - "target": "es2018", - "sourceMap": true, - "rootDir": ".", - "outDir": "../../build/common", - "importHelpers": true, - "lib": ["es2018"], - "types": [], - "composite": true - }, - "include": ["./**/*.ts"], - "exclude": ["node_modules"] -} From d4ff6b44876c5706013c001a78437b19871a90bd Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Thu, 4 Jun 2020 17:01:29 +0200 Subject: [PATCH 4/9] Initialized native shell structure --- .yarnrc | 1 + package.json | 1 + src/shell/amd.ts | 2 -- src/shell/globals.d.ts | 10 ++++++++++ src/shell/preload.ts | 3 +++ yarn.lock | 5 +++++ 6 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .yarnrc delete mode 100644 src/shell/amd.ts create mode 100644 src/shell/preload.ts diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..dda5c38 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +"@cromefire_:registry" "https://gitlab.com/api/v4/packages/npm" diff --git a/package.json b/package.json index 57f5e66..7feb974 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/jellyfin/jellyfin-desktop#readme", "dependencies": { + "@cromefire_/nativeshell-api-definition": "^1.0.0-nightly28", "detect-rpi": "^1.4.0", "electron-settings": "^3.2.0", "is-windows": "^1.0.2", diff --git a/src/shell/amd.ts b/src/shell/amd.ts deleted file mode 100644 index 4482ad9..0000000 --- a/src/shell/amd.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function define(moduleDefinitions: string[], module: (...modules: any) => any): void; -declare function require(moduleDefinitions: string[], module: (...modules: any) => any): void; diff --git a/src/shell/globals.d.ts b/src/shell/globals.d.ts index e69de29..d78468c 100644 --- a/src/shell/globals.d.ts +++ b/src/shell/globals.d.ts @@ -0,0 +1,10 @@ +import { INativeShell } from "@cromefire_/nativeshell-api-definition"; + +declare function define(moduleDefinitions: string[], module: (...modules: any) => any): void; +declare function require(moduleDefinitions: string[], module: (...modules: any) => any): void; + +declare global { + interface Window { + NativeShell: INativeShell; + } +} diff --git a/src/shell/preload.ts b/src/shell/preload.ts new file mode 100644 index 0000000..7da0aef --- /dev/null +++ b/src/shell/preload.ts @@ -0,0 +1,3 @@ +window.NativeShell = { + +} diff --git a/yarn.lock b/yarn.lock index cd9a066..30c7a8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,11 @@ resolved "https://registry.yarnpkg.com/@chbrown/bind/-/bind-1.0.0.tgz#5bcb9fd875643426ec86ab3544718915fa337d74" integrity sha512-fOSHX/DJE0i1xijEqH5SI1v0iLwubtdrWjuKAHIRQAc656JrZbecpaJnKXMNjrY+xMnB1lDn0u05pBfJlpyyZA== +"@cromefire_/nativeshell-api-definition@^1.0.0-nightly28": + version "1.0.0-nightly28" + resolved "https://gitlab.com/api/v4/projects/18148878/packages/npm/@cromefire_/nativeshell-api-definition/-/@cromefire_/nativeshell-api-definition-1.0.0-nightly28.tgz#7364cad0178343a62c0b34f47b021c4ee5b9facd" + integrity sha1-c2TK0BeDQ6YsCzT0ewIcTuW5+s0= + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" From d270db25da4fe87375ddfdd9766c33b69b714407 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Tue, 21 Jul 2020 20:51:37 +0200 Subject: [PATCH 5/9] Added myself to the contributors --- CONTRIBUTERS.md => CONTRIBUTORS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename CONTRIBUTERS.md => CONTRIBUTORS.md (80%) diff --git a/CONTRIBUTERS.md b/CONTRIBUTORS.md similarity index 80% rename from CONTRIBUTERS.md rename to CONTRIBUTORS.md index 539459a..9510386 100644 --- a/CONTRIBUTERS.md +++ b/CONTRIBUTORS.md @@ -1,4 +1,5 @@ # Jellyfin Contributors +- [Cromefire_](https://github.com/cromefire) - [winters-brown](https://github.com/winters-brown) - [gravypod](https://github.com/gravypod) - [lachlan](https://github.com/lachlan-00) @@ -8,4 +9,4 @@ - [randomevents](https://github.com/randomevents) - [hatharry](https://github.com/hatharry) - [KeyserSoze1](https://github.com/KeyserSoze1) -- [heksesang](https://github.com/heksesang) \ No newline at end of file +- [heksesang](https://github.com/heksesang) From 3efdabeef664b4d4b52d1a325642fb4589c9b2d2 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Wed, 19 Aug 2020 23:07:36 +0200 Subject: [PATCH 6/9] Rebased the branch --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7feb974..49def46 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "homepage": "https://github.com/jellyfin/jellyfin-desktop#readme", "dependencies": { - "@cromefire_/nativeshell-api-definition": "^1.0.0-nightly28", + "@cromefire_/nativeshell-api-definition": "^1.0.0-nightly5", "detect-rpi": "^1.4.0", "electron-settings": "^3.2.0", "is-windows": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 30c7a8b..9d3eebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,10 +33,10 @@ resolved "https://registry.yarnpkg.com/@chbrown/bind/-/bind-1.0.0.tgz#5bcb9fd875643426ec86ab3544718915fa337d74" integrity sha512-fOSHX/DJE0i1xijEqH5SI1v0iLwubtdrWjuKAHIRQAc656JrZbecpaJnKXMNjrY+xMnB1lDn0u05pBfJlpyyZA== -"@cromefire_/nativeshell-api-definition@^1.0.0-nightly28": - version "1.0.0-nightly28" - resolved "https://gitlab.com/api/v4/projects/18148878/packages/npm/@cromefire_/nativeshell-api-definition/-/@cromefire_/nativeshell-api-definition-1.0.0-nightly28.tgz#7364cad0178343a62c0b34f47b021c4ee5b9facd" - integrity sha1-c2TK0BeDQ6YsCzT0ewIcTuW5+s0= +"@cromefire_/nativeshell-api-definition@^1.0.0-nightly5": + version "1.0.0-nightly5" + resolved "https://gitlab.com/api/v4/projects/19188465/packages/npm/@cromefire_/nativeshell-api-definition/-/@cromefire_/nativeshell-api-definition-1.0.0-nightly5.tgz#da09006f53cdbf471d8bce27ba64ff8fcecdf572" + integrity sha1-2gkAb1PNv0cdi84numT/j87N9XI= "@develar/schema-utils@~2.6.5": version "2.6.5" From 8081cfa7b5aeb85425cac22e90198b274bda8348 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Wed, 19 Aug 2020 23:53:50 +0200 Subject: [PATCH 7/9] Fixed build --- .editorconfig | 2 +- {src/shell => res}/plugins/mpvplayer.css | 0 src/common/tsconfig.json | 18 ++++++ src/main/cec/index.ts | 2 +- src/main/main.ts | 72 +++--------------------- src/main/tsconfig.json | 3 +- src/main/wakeonlan.ts | 50 ---------------- src/shell/apphost.ts | 18 +++--- src/shell/filesystem.ts | 2 +- src/shell/globals.d.ts | 6 +- src/shell/preload.ts | 52 ++++++++++++++++- src/shell/serverdiscovery.ts | 2 +- src/shell/shell.ts | 26 +++++---- src/shell/tsconfig.json | 7 ++- src/shell/wakeonlan.ts | 20 ------- 15 files changed, 112 insertions(+), 168 deletions(-) rename {src/shell => res}/plugins/mpvplayer.css (100%) delete mode 100644 src/main/wakeonlan.ts delete mode 100644 src/shell/wakeonlan.ts diff --git a/.editorconfig b/.editorconfig index ff160b9..a74e294 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,6 @@ insert_final_newline = true max_line_length = 120 tab_width = 4 -[{package.json, *.yml, *.yaml}] +[{*.yml, *.yaml, *.json}] indent_size = 2 tab_width = 2 diff --git a/src/shell/plugins/mpvplayer.css b/res/plugins/mpvplayer.css similarity index 100% rename from src/shell/plugins/mpvplayer.css rename to res/plugins/mpvplayer.css diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index e69de29..148f07d 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "moduleResolution": "Node", + "module": "commonjs", + "target": "es2018", + "sourceMap": true, + "rootDir": ".", + "outDir": "../../build/common", + "importHelpers": true, + "lib": ["es2018"], + "types": [], + "composite": true, + "incremental": true + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/src/main/cec/index.ts b/src/main/cec/index.ts index b3c062b..ec44c55 100644 --- a/src/main/cec/index.ts +++ b/src/main/cec/index.ts @@ -143,7 +143,7 @@ export class CEC { }); this.process.on("close", function (code) { - console.warn(`cec-client exited with code ${code}`); + console.info(`cec-client exited with code ${code}`); logStream = createWriteStream(logFile, { flags: "a" }); logStream.write(`child process exited with code ${code}`); logStream.end(); diff --git a/src/main/main.ts b/src/main/main.ts index 6205992..9e36858 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -24,7 +24,6 @@ import * as isRpi from "detect-rpi"; import { CEC } from "./cec"; import { PlaybackHandler } from "./playbackhandler"; import { findServers } from "./serverdiscovery"; -import { wake } from "./wakeonlan"; const readdir = promisify(readdirCb); @@ -36,12 +35,10 @@ const isWindows = platform === "win32"; // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -let mainWindow = null; -let playerWindow = null; +let mainWindow: BrowserWindow | null = null; let hasAppLoaded = false; const enableDevTools = true; -const enableDevToolsOnStartup = false; let initialShowEventsComplete = false; let previousBounds; let cec; @@ -72,7 +69,6 @@ ipcMain.on("asynchronous-message", (event, arg) => { function onWindowMoved(): void { mainWindow.webContents.executeJavaScript('window.dispatchEvent(new CustomEvent("move", {}));'); const winPosition = mainWindow.getPosition(); - playerWindow.setPosition(winPosition[0], winPosition[1]); } let currentWindowState: WindowState = "Normal"; @@ -80,7 +76,6 @@ let currentWindowState: WindowState = "Normal"; function onWindowResize(): void { if (!useTrueFullScreen || currentWindowState === "Normal") { const bounds = mainWindow.getBounds(); - playerWindow.setBounds(bounds); } } @@ -160,7 +155,6 @@ function onWindowStateChanged(state: WindowState): void { } function onMinimize(): void { - playerWindow.minimize(); onWindowStateChanged("Minimized"); } @@ -172,8 +166,6 @@ function onRestore(): void { } else { onWindowStateChanged("Normal"); } - - playerWindow.restore(); } function onMaximize(): void { @@ -184,9 +176,6 @@ function onEnterFullscreen(): void { onWindowStateChanged("Fullscreen"); if (initialShowEventsComplete) { - if (useTrueFullScreen) { - playerWindow.setFullScreen(true); - } mainWindow.movable = false; } } @@ -195,7 +184,6 @@ function onLeaveFullscreen(): void { onWindowStateChanged("Normal"); if (initialShowEventsComplete) { - playerWindow.setFullScreen(false); mainWindow.movable = true; } } @@ -245,6 +233,7 @@ function setMainWindowResizable(resizable): void { let isTransparencyRequired = false; let windowStateOnLoad; + function registerAppHost(): void { const customProtocol = "electronapphost"; @@ -318,16 +307,11 @@ function registerAppHost(): void { function onLoaded(): void { //var globalShortcut = electron.globalShortcut; - //globalShortcut.register('mediastop', function () { // sendCommand('stop'); //}); - //globalShortcut.register('mediaplaypause', function () { //}); - - // language=JavaScript - sendJavascript(`window.PlayerWindowId="${getWindowId(playerWindow)}";`); } const processes = {}; @@ -414,31 +398,6 @@ function registerServerdiscovery(): void { }); } -function registerWakeOnLan(): void { - const customProtocol = "electronwakeonlan"; - - protocol.registerStringProtocol(customProtocol, function (request, callback) { - // Add 3 to account for :// - const url = request.url.substr(customProtocol.length + 3).split("?")[0]; - let mac: string; - let port: number; - - switch (url) { - case "wakeserver": - mac = request.url.split("=")[1].split("&")[0]; - port = parseInt(request.url.split("=")[2]); - - wake(mac, port) - .then((res) => callback(String(res))) - .catch((error) => callback(error)); - break; - default: - callback(""); - break; - } - }); -} - function alert(text): void { dialog.showMessageBox(mainWindow, { message: text.toString(), @@ -465,6 +424,7 @@ function getAppUrl(): string { } let startInfoJson; + async function loadStartInfo(): Promise { const topDirectory = normalize(`${__dirname}/../shell`); const pluginDirectory = normalize(`${topDirectory}/plugins`); @@ -706,7 +666,7 @@ function closeWindow(win): void { function onWindowClose(): void { if (hasAppLoaded) { - const data = mainWindow.getBounds(); + const data: any = mainWindow.getBounds(); data.state = currentWindowState; const windowStatePath = getWindowStateDataPath(); require("fs").writeFileSync(windowStatePath, JSON.stringify(data)); @@ -717,7 +677,6 @@ function onWindowClose(): void { // Unregister all shortcuts. globalShortcut.unregisterAll(); - closeWindow(playerWindow); if (cec) { cec.kill(); @@ -792,10 +751,7 @@ function getWindowId(win): string { return longVal.toString(); } -function initPlaybackHandler(mpvPath): void { - const pbHandler = new PlaybackHandler(getWindowId(playerWindow), mpvPath, mainWindow); - pbHandler.registerMediaPlayerProtocol(protocol); -} +function initPlaybackHandler(mpvPath): void {} setCommandLineSwitches(); @@ -861,10 +817,7 @@ app.on("ready", function () { windowOptions.y = previousWindowInfo.y; } - playerWindow = new BrowserWindow(windowOptions); - windowOptions.backgroundColor = "#00000000"; - windowOptions.parent = playerWindow; windowOptions.transparent = true; windowOptions.resizable = true; windowOptions.skipTaskbar = false; @@ -873,10 +826,6 @@ app.on("ready", function () { loadStartInfo().then(function () { mainWindow = new BrowserWindow(windowOptions); - if (enableDevToolsOnStartup) { - mainWindow.openDevTools(); - } - mainWindow.webContents.on("dom-ready", setStartInfo); const url = getAppUrl(); @@ -887,7 +836,6 @@ app.on("ready", function () { registerAppHost(); registerFileSystem(); registerServerdiscovery(); - registerWakeOnLan(); if (url) { mainWindow.loadURL(url); @@ -896,7 +844,8 @@ app.on("ready", function () { mainWindow.loadURL(localPath); } - mainWindow.setMenu(null); + mainWindow.autoHideMenuBar = true; + mainWindow.setMenuBarVisibility(false); mainWindow.on("move", onWindowMoved); mainWindow.on("app-command", onAppCommand); mainWindow.on("close", onWindowClose); @@ -908,15 +857,8 @@ app.on("ready", function () { mainWindow.on("unmaximize", onUnMaximize); mainWindow.on("resize", onWindowResize); - playerWindow.on("restore", onPlayerWindowRestore); - playerWindow.on("enter-full-screen", onPlayerWindowRestore); - playerWindow.on("maximize", onPlayerWindowRestore); - playerWindow.on("focus", onPlayerWindowRestore); - - playerWindow.on("show", onWindowShow); mainWindow.on("show", onWindowShow); - playerWindow.show(); mainWindow.show(); initCec(); diff --git a/src/main/tsconfig.json b/src/main/tsconfig.json index d77b9a2..6e424d9 100644 --- a/src/main/tsconfig.json +++ b/src/main/tsconfig.json @@ -10,7 +10,8 @@ "importHelpers": true, "lib": ["es2018"], "types": ["node"], - "skipLibCheck": true + "skipLibCheck": true, + "incremental": true }, "include": ["./**/*.ts"], "exclude": ["node_modules"], diff --git a/src/main/wakeonlan.ts b/src/main/wakeonlan.ts deleted file mode 100644 index fbde8d4..0000000 --- a/src/main/wakeonlan.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as net from "net"; -import * as dgram from "dgram"; - -export function createMagicPacket(mac: string): Buffer { - const MAC_LENGTH = 6; - const MAC_REPEAT = 16; - const PACKET_HEADER = 6; - const parts = mac.match(/[0-9a-fA-F]{2}/g); - if (!parts || parts.length != MAC_LENGTH) throw new Error(`malformed MAC address '${mac}'`); - let buffer = new Buffer(PACKET_HEADER); - const bufMac = new Buffer( - parts.map(function (p) { - return parseInt(p, 16); - }) - ); - buffer.fill(0xff); - for (let i = 0; i < MAC_REPEAT; i++) { - buffer = Buffer.concat([buffer, bufMac]); - } - return buffer; -} - -export function wake(mac: string, port = 9, address = "255.255.255.255"): Promise { - // create magic packet - let magicPacket: Buffer; - try { - magicPacket = createMagicPacket(mac); - } catch (err) { - return Promise.reject(err); - } - return new Promise((resolve, reject) => { - const socket = dgram - .createSocket(net.isIPv6(address) ? "udp6" : "udp4") - .on("error", (err) => { - socket.close(); - reject(err); - }) - .once("listening", () => { - socket.setBroadcast(true); - }); - socket.send(magicPacket, 0, magicPacket.length, port, address, function (err, res) { - if (err) { - reject(err); - } else { - resolve(res == magicPacket.length); - } - socket.close(); - }); - }); -} diff --git a/src/shell/apphost.ts b/src/shell/apphost.ts index dc956ea..4d234e2 100644 --- a/src/shell/apphost.ts +++ b/src/shell/apphost.ts @@ -25,7 +25,7 @@ define([], function () { return sendCommand(`windowstate-${state}`); } - function sendCommand(name): Promise { + function sendCommand(name: string): Promise { return fetch(`electronapphost://${name}`) .then((response) => { if (!response.ok) { @@ -38,11 +38,11 @@ define([], function () { function supportsVoiceInput(): boolean { return ( - window.SpeechRecognition || - window["webkitSpeechRecognition"] || - window["mozSpeechRecognition"] || - window["oSpeechRecognition"] || - !!window["msSpeechRecognition"] + (window as any).SpeechRecognition || + (window as any)["webkitSpeechRecognition"] || + (window as any)["mozSpeechRecognition"] || + (window as any)["oSpeechRecognition"] || + !!(window as any)["msSpeechRecognition"] ); } @@ -70,7 +70,7 @@ define([], function () { features.push("voiceinput"); } - if (self["ServiceWorkerSyncRegistered"]) { + if (window["ServiceWorkerSyncRegistered"]) { features.push("sync"); } @@ -174,12 +174,12 @@ define([], function () { } }, - setUserScalable: function (scalable): void { + setUserScalable: function (scalable: boolean): void { const att = scalable ? "viewport-fit=cover, width=device-width, initial-scale=1, minimum-scale=1, user-scalable=yes" : "viewport-fit=cover, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"; - document.querySelector("meta[name=viewport]").setAttribute("content", att); + document.querySelector("meta[name=viewport]")?.setAttribute("content", att); }, deviceIconUrl: function (): string | null { diff --git a/src/shell/filesystem.ts b/src/shell/filesystem.ts index ebb4018..2503959 100644 --- a/src/shell/filesystem.ts +++ b/src/shell/filesystem.ts @@ -1,5 +1,5 @@ define([], function () { - function exits(endpoint, path): Promise { + function exits(endpoint: string, path: string): Promise { return fetch(`electronfs://${endpoint}?path=${path}`, { method: "POST" }) .then((response) => { if (!response.ok) { diff --git a/src/shell/globals.d.ts b/src/shell/globals.d.ts index d78468c..71e1e26 100644 --- a/src/shell/globals.d.ts +++ b/src/shell/globals.d.ts @@ -1,9 +1,9 @@ import { INativeShell } from "@cromefire_/nativeshell-api-definition"; -declare function define(moduleDefinitions: string[], module: (...modules: any) => any): void; -declare function require(moduleDefinitions: string[], module: (...modules: any) => any): void; - declare global { + function define(moduleDefinitions: string[], module: (...modules: any) => any): void; + function require(moduleDefinitions: string[], module: (...modules: any) => any): void; + interface Window { NativeShell: INativeShell; } diff --git a/src/shell/preload.ts b/src/shell/preload.ts index 7da0aef..b95da72 100644 --- a/src/shell/preload.ts +++ b/src/shell/preload.ts @@ -1,3 +1,51 @@ -window.NativeShell = { +import { Command, IAppHost, IAppInfo, IDownloadInfo, IMediaInfo, Layout } from "@cromefire_/nativeshell-api-definition"; + +const appHost: IAppHost = { + appName(): string { + return ""; + }, + appVersion(): string { + return ""; + }, + deviceID(): string { + return ""; + }, + deviceName(): string { + return ""; + }, + exit(): void {}, + getDefaultLayout(): Layout { + return Layout.DESKTOP; + }, + async getDeviceProfile(profileBuilder: unknown): Promise { + return Promise.resolve({}); + }, + getSyncProfile(profileBuilder: unknown, appSettings: unknown): Promise { + return Promise.resolve(undefined); + }, + init(): IAppInfo { + // TODO: Add proper values + return { + appName: "Jellyfin Desktop", + appVersion: "1.0.0-alpha0", + deviceId: "not so random", + deviceName: "Desktop test", + }; + }, + supports(command: Command): boolean { + return false; + }, +}; -} +window.NativeShell = { + AppHost: appHost, + disableFullscreen(): void {}, + downloadFile(downloadInfo: IDownloadInfo): void {}, + enableFullscreen(): void {}, + getPlugins(): string[] { + return []; + }, + hideMediaSession(): void {}, + openUrl(url: string, target?: string): void {}, + updateMediaSession(mediaInfo: IMediaInfo): void {}, +}; diff --git a/src/shell/serverdiscovery.ts b/src/shell/serverdiscovery.ts index 791ac01..b5dd48a 100644 --- a/src/shell/serverdiscovery.ts +++ b/src/shell/serverdiscovery.ts @@ -2,7 +2,7 @@ define([], function () { return { - findServers: async function (timeoutMs): Promise { + findServers: async function (timeoutMs: number): Promise { const response = await fetch(`electronserverdiscovery://findservers?timeout=${timeoutMs}`, { method: "POST", }); diff --git a/src/shell/shell.ts b/src/shell/shell.ts index 1009e34..6914519 100644 --- a/src/shell/shell.ts +++ b/src/shell/shell.ts @@ -1,21 +1,21 @@ define(["events", "apphost"], function (events, apphost) { - function sendCommand(name: string): Promise { + function sendCommand(name: string): Promise { return fetch(`electronapphost://${name}`).then((response) => { if (!response.ok) { console.error("Error sending command: ", response); throw response; } - return response; + return response.json(); }); } - const shell = {}; - let closingWindowState; + const shell: Record = {}; + let closingWindowState: string | null; - function getProcessClosePromise(pid): Promise { + function getProcessClosePromise(pid: number): Promise { // returns a promise that resolves or rejects when a process closes return new Promise(function (resolve, reject) { - events.on(shell, "closed", function (e, processId, error) { + events.on(shell, "closed", function (e: any, processId: number, error: any) { if (processId === pid) { if (closingWindowState) { apphost.setWindowState(closingWindowState); @@ -32,12 +32,14 @@ }); } - window["onChildProcessClosed"] = function (processId, error): void { + (window as any)["onChildProcessClosed"] = function (processId: number, error: any): void { events.trigger(shell, "closed", [processId, error]); }; - function paramsToString(params): string { - const values = []; + type QueryParams = Record; + + function paramsToString(params: QueryParams): string { + const values: string[] = []; for (const key in params) { const value = params[key]; @@ -49,19 +51,19 @@ return values.join("&"); } - shell["openUrl"] = function (url): Promise { + shell["openUrl"] = function (url: string): Promise { return sendCommand(`openurl?url=${url}`); }; shell["canExec"] = true; - shell["close"] = function (processId): Promise { + shell["close"] = function (processId: number): Promise { const url = `shellclose?id=${processId}`; return sendCommand(url); }; - shell["exec"] = function (options): Promise { + shell["exec"] = function (options: QueryParams): Promise { const url = `shellstart?${paramsToString(options)}`; return sendCommand(url).then(function (response) { diff --git a/src/shell/tsconfig.json b/src/shell/tsconfig.json index 1151c24..04b228a 100644 --- a/src/shell/tsconfig.json +++ b/src/shell/tsconfig.json @@ -9,10 +9,13 @@ "outDir": "../../build/shell", "importHelpers": true, "lib": ["es2018", "dom"], - "types": [] + "types": [], + "strict": true, + "noImplicitAny": false, + "incremental": true }, "include": ["./**/*.ts"], - "exclude": ["node_modules"], + "exclude": ["plugins"], "references": [ { "path": "../common" diff --git a/src/shell/wakeonlan.ts b/src/shell/wakeonlan.ts deleted file mode 100644 index 28aea45..0000000 --- a/src/shell/wakeonlan.ts +++ /dev/null @@ -1,20 +0,0 @@ -define([], function () { - function send(info): Promise { - return fetch(`electronwakeonlan://wakeserver?macaddress=${info.MacAddress}&port=${info.Port}`, { - method: "POST", - }).then((response) => { - if (!response.ok) { - throw response; - } - }); - } - - function isSupported(): boolean { - return true; - } - - return { - send, - isSupported, - }; -}); From 00959684489e64adfe4fd33f33b818c298aa11e0 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Thu, 20 Aug 2020 00:28:35 +0200 Subject: [PATCH 8/9] Removed some security warnings --- src/main/main.ts | 60 ++++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 9e36858..ad1ffb4 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -17,13 +17,12 @@ import * as powerOff from "power-off"; import { parse, ParsedUrlQuery } from "querystring"; import { access, readdir as readdirCb } from "fs"; import { promisify } from "util"; -import { endianness, hostname } from "os"; -import * as Long from "long"; +import { hostname } from "os"; import * as isRpi from "detect-rpi"; import { CEC } from "./cec"; -import { PlaybackHandler } from "./playbackhandler"; import { findServers } from "./serverdiscovery"; +import { URL } from "url"; const readdir = promisify(readdirCb); @@ -43,7 +42,6 @@ let initialShowEventsComplete = false; let previousBounds; let cec; -const useTrueFullScreen = isLinux; const shellDir = normalize(`${__dirname}/../shell`); const iconsDir = normalize(`${__dirname}/../../icons`); const resDir = normalize(`${__dirname}/../../res`); @@ -68,15 +66,11 @@ ipcMain.on("asynchronous-message", (event, arg) => { function onWindowMoved(): void { mainWindow.webContents.executeJavaScript('window.dispatchEvent(new CustomEvent("move", {}));'); - const winPosition = mainWindow.getPosition(); } let currentWindowState: WindowState = "Normal"; function onWindowResize(): void { - if (!useTrueFullScreen || currentWindowState === "Normal") { - const bounds = mainWindow.getBounds(); - } } let restoreWindowState: WindowState | null; @@ -361,7 +355,7 @@ function registerFileSystem(): void { case "directoryexists": path = request.url.split("=")[1]; - access(join(__dirname, "..", "shell", path), (err) => { + access(join(shellDir, path), (err) => { if (err) { console.error(`fs access result for path: ${err}`); @@ -734,24 +728,7 @@ function initCec(): void { } } -function getWindowId(win): string { - const handle = win.getNativeWindowHandle(); - - if (endianness() == "LE") { - if (handle.length == 4) { - handle.swap32(); - } else if (handle.length == 8) { - handle.swap64(); - } else { - console.warn("Unknown Native Window Handle Format."); - } - } - const longVal = Long.fromString(handle.toString("hex"), true, 16); - - return longVal.toString(); -} - -function initPlaybackHandler(mpvPath): void {} +function initPlaybackHandler(): void {} setCommandLineSwitches(); @@ -770,15 +747,10 @@ app.on("quit", function () { closeWindow(mainWindow); }); -function onPlayerWindowRestore(): void { - mainWindow.focus(); -} - // This method will be called when Electron has finished // initialization and is ready to create browser windows. app.on("ready", function () { const windowStatePath = getWindowStateDataPath(); - const enableNodeIntegration = !getAppUrl(); let previousWindowInfo; try { previousWindowInfo = JSON.parse(require("fs").readFileSync(windowStatePath, "utf8")); @@ -791,18 +763,18 @@ app.on("ready", function () { title: "Jellyfin Desktop", minWidth: 1280, minHeight: 720, - //alwaysOnTop: true, backgroundColor: "#00000000", center: true, show: false, resizable: true, webPreferences: { - webSecurity: false, + webSecurity: true, webgl: false, - nodeIntegration: enableNodeIntegration, + nodeIntegration: false, + enableRemoteModule: false, plugins: false, - allowRunningInsecureContent: true, + allowRunningInsecureContent: false, experimentalFeatures: false, devTools: enableDevTools, }, @@ -840,7 +812,7 @@ app.on("ready", function () { if (url) { mainWindow.loadURL(url); } else { - const localPath = path.join(`file://${__dirname}/../../res/firstrun/Jellyfin.html`); + const localPath = path.join(`file://${resDir}/firstrun/Jellyfin.html`); mainWindow.loadURL(localPath); } @@ -863,10 +835,22 @@ app.on("ready", function () { initCec(); - initPlaybackHandler(commandLineOptions.mpvPath); + initPlaybackHandler(); if (isRpi()) { mainWindow.setFullScreen(true); mainWindow.setAlwaysOnTop(true); } }); }); + +app.on("web-contents-created", (event, contents) => { + contents.on("will-navigate", (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + + const base = getAppBaseUrl(); + if (!base || parsedUrl.origin !== base) { + event.preventDefault(); + shell.openExternal(navigationUrl); + } + }); +}); From 40c4d3d9bd1a5e484a36f4ce9efa7f30dd966e7f Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Thu, 20 Aug 2020 15:49:50 +0200 Subject: [PATCH 9/9] Added basic NativeShell implementation --- package.json | 7 ++-- src/common/ipc/api.ts | 5 +++ src/main/api.ts | 35 +++++++++++++++++++ src/main/main.ts | 42 +++++++++++------------ src/shell/globals.d.ts | 1 - src/shell/preload.ts | 56 +++++++++++++++++++++++-------- src/shell/scripts/appclose.ts | 0 src/shell/scripts/videohandler.ts | 25 -------------- src/shell/serverdiscovery.ts | 17 ---------- yarn.lock | 23 ++++++++----- 10 files changed, 119 insertions(+), 92 deletions(-) create mode 100644 src/common/ipc/api.ts create mode 100644 src/main/api.ts delete mode 100644 src/shell/scripts/appclose.ts delete mode 100644 src/shell/scripts/videohandler.ts delete mode 100644 src/shell/serverdiscovery.ts diff --git a/package.json b/package.json index 49def46..1a0c96a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellyfin-desktop", - "version": "1.0.0", + "version": "1.0.0-alpha0", "description": "Jellyfin Desktop made with Electron", "main": "build/main/main.js", "scripts": { @@ -41,9 +41,10 @@ "homepage": "https://github.com/jellyfin/jellyfin-desktop#readme", "dependencies": { "@cromefire_/nativeshell-api-definition": "^1.0.0-nightly5", + "comlink": "^4.3.0", + "comlink-electron-adapter": "^0.0.1", "detect-rpi": "^1.4.0", "electron-settings": "^3.2.0", - "is-windows": "^1.0.2", "long": "^4.0.0", "node-mpv": "^1.5.0", "power-off": "^1.1.2", @@ -57,7 +58,7 @@ "@types/node": "^12.12.37", "@typescript-eslint/eslint-plugin": "^2.29.0", "@typescript-eslint/parser": "^2.29.0", - "electron": "^8.2.4", + "electron": "^8.5.0", "electron-builder": "^22.5.1", "eslint": "^6.8.0", "eslint-config-prettier": "^6.11.0", diff --git a/src/common/ipc/api.ts b/src/common/ipc/api.ts new file mode 100644 index 0000000..837de4e --- /dev/null +++ b/src/common/ipc/api.ts @@ -0,0 +1,5 @@ +export interface MainApi { + appVersion(): string; + deviceId(): Promise; + deviceName(): Promise; +} diff --git a/src/main/api.ts b/src/main/api.ts new file mode 100644 index 0000000..f2706b4 --- /dev/null +++ b/src/main/api.ts @@ -0,0 +1,35 @@ +import { MainApi } from "../common/ipc/api"; +import { app } from "electron"; +import { hostname } from "os"; +import { join } from "path"; +import { promises as fs } from "fs"; + +const genRanHex = (size) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(""); + +async function exists(path: string) { + try { + await fs.access(path); + } catch (e) { + return false; + } + return true; +} + +export class MainApiService implements MainApi { + appVersion(): string { + return app.getVersion(); + } + async deviceId(): Promise { + const deviceIdPath = join(app.getPath("userData"), "device.id"); + + if (await exists(deviceIdPath)) { + return fs.readFile(deviceIdPath, { encoding: "utf-8" }); + } + const id = genRanHex(16); + await fs.writeFile(deviceIdPath, id, { encoding: "utf-8" }); + return id; + } + deviceName(): Promise { + return Promise.resolve(hostname()); + } +} diff --git a/src/main/main.ts b/src/main/main.ts index ad1ffb4..3be1372 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -15,16 +15,17 @@ import { WindowState } from "../common/types"; import * as sleepMode from "sleep-mode"; import * as powerOff from "power-off"; import { parse, ParsedUrlQuery } from "querystring"; -import { access, readdir as readdirCb } from "fs"; -import { promisify } from "util"; +import { access} from "fs"; import { hostname } from "os"; import * as isRpi from "detect-rpi"; import { CEC } from "./cec"; import { findServers } from "./serverdiscovery"; import { URL } from "url"; - -const readdir = promisify(readdirCb); +import { MainApiService } from "./api"; +import { expose } from "comlink"; +import { mainProcObjectEndpoint } from "comlink-electron-adapter"; +import { MainApi } from "../common/ipc/api"; app.allowRendererProcessReuse = true; // Disable warning by opting into Electron v9 default @@ -70,8 +71,7 @@ function onWindowMoved(): void { let currentWindowState: WindowState = "Normal"; -function onWindowResize(): void { -} +function onWindowResize(): void {} let restoreWindowState: WindowState | null; @@ -399,10 +399,6 @@ function alert(text): void { }); } -function replaceAll(str: string, find: string, replace: string): string { - return str.split(find).join(replace); -} - function getAppBaseUrl(): any | null { return settings.get("server.url", null); } @@ -420,12 +416,6 @@ function getAppUrl(): string { let startInfoJson; async function loadStartInfo(): Promise { - const topDirectory = normalize(`${__dirname}/../shell`); - const pluginDirectory = normalize(`${topDirectory}/plugins`); - const scriptsDirectory = normalize(`${topDirectory}/scripts`); - - const pluginFiles = await readdir(pluginDirectory); - const scriptFiles = await readdir(scriptsDirectory); const startInfo = { paths: { apphost: `${customFileProtocol}://apphost`, @@ -440,10 +430,6 @@ async function loadStartInfo(): Promise { deviceName: hostname(), deviceId: hostname(), supportsTransparentWindow: supportsTransparentWindow(), - plugins: pluginFiles - .filter((f) => f.endsWith(".js")) - .map((f) => `file://${replaceAll(path.normalize(`${pluginDirectory}/${f}`), "\\", "/")}`), - scripts: scriptFiles.map((f) => `file://${replaceAll(path.normalize(`${scriptsDirectory}/${f}`), "\\", "/")}`), }; startInfoJson = JSON.stringify(startInfo); @@ -743,6 +729,12 @@ function onWindowShow(): void { } } +function initializeApi() { + const api: MainApi = new MainApiService(); + + expose(api, mainProcObjectEndpoint(ipcMain)); +} + app.on("quit", function () { closeWindow(mainWindow); }); @@ -750,6 +742,8 @@ app.on("quit", function () { // This method will be called when Electron has finished // initialization and is ready to create browser windows. app.on("ready", function () { + const url = getAppUrl(); + initializeApi(); const windowStatePath = getWindowStateDataPath(); let previousWindowInfo; try { @@ -769,9 +763,10 @@ app.on("ready", function () { resizable: true, webPreferences: { + preload: join(shellDir, "preload.js"), webSecurity: true, webgl: false, - nodeIntegration: false, + nodeIntegration: !url, enableRemoteModule: false, plugins: false, allowRunningInsecureContent: false, @@ -800,7 +795,6 @@ app.on("ready", function () { mainWindow.webContents.on("dom-ready", setStartInfo); - const url = getAppUrl(); windowStateOnLoad = previousWindowInfo.state; addPathIntercepts(); @@ -831,7 +825,9 @@ app.on("ready", function () { mainWindow.on("show", onWindowShow); - mainWindow.show(); + mainWindow.on("ready-to-show", () => { + mainWindow.show(); + }); initCec(); diff --git a/src/shell/globals.d.ts b/src/shell/globals.d.ts index 71e1e26..3891788 100644 --- a/src/shell/globals.d.ts +++ b/src/shell/globals.d.ts @@ -2,7 +2,6 @@ import { INativeShell } from "@cromefire_/nativeshell-api-definition"; declare global { function define(moduleDefinitions: string[], module: (...modules: any) => any): void; - function require(moduleDefinitions: string[], module: (...modules: any) => any): void; interface Window { NativeShell: INativeShell; diff --git a/src/shell/preload.ts b/src/shell/preload.ts index b95da72..8f38213 100644 --- a/src/shell/preload.ts +++ b/src/shell/preload.ts @@ -1,35 +1,61 @@ -import { Command, IAppHost, IAppInfo, IDownloadInfo, IMediaInfo, Layout } from "@cromefire_/nativeshell-api-definition"; +import { + Command, + IAppHost, + IAppInfo, + IDownloadInfo, + IMediaInfo, + Layout, + INativeShell, +} from "@cromefire_/nativeshell-api-definition"; +import { Remote, wrap } from "comlink"; +import { MainApi } from "../common/ipc/api"; +import { mainProcObjectEndpoint } from "comlink-electron-adapter"; +import { ipcRenderer } from "electron"; -const appHost: IAppHost = { +const mainApi: Remote = wrap(mainProcObjectEndpoint(ipcRenderer)); + +const appName = "Jellyfin Desktop"; +// TODO: Remove once jf-web#1826 is stable +let appVersion = ""; +let deviceId = ""; +let deviceName = ""; + +const appHost: any = { appName(): string { - return ""; + return appName; }, appVersion(): string { - return ""; + return appVersion; }, - deviceID(): string { - return ""; + deviceId(): string { + return deviceId; }, deviceName(): string { - return ""; + return deviceName; }, exit(): void {}, getDefaultLayout(): Layout { return Layout.DESKTOP; }, async getDeviceProfile(profileBuilder: unknown): Promise { - return Promise.resolve({}); + return {}; }, getSyncProfile(profileBuilder: unknown, appSettings: unknown): Promise { return Promise.resolve(undefined); }, - init(): IAppInfo { + async init(): Promise { + const appVersion$ = mainApi.appVersion(); + const deviceId$ = mainApi.deviceId(); + const deviceName$ = mainApi.deviceName(); + appVersion = await appVersion$; + deviceId = await deviceId$; + deviceName = await deviceName$; // TODO: Add proper values return { - appName: "Jellyfin Desktop", - appVersion: "1.0.0-alpha0", - deviceId: "not so random", - deviceName: "Desktop test", + appName, + appVersion, + deviceId, + deviceName, }; }, supports(command: Command): boolean { @@ -37,7 +63,7 @@ const appHost: IAppHost = { }, }; -window.NativeShell = { +const nativeShell: INativeShell = { AppHost: appHost, disableFullscreen(): void {}, downloadFile(downloadInfo: IDownloadInfo): void {}, @@ -49,3 +75,5 @@ window.NativeShell = { openUrl(url: string, target?: string): void {}, updateMediaSession(mediaInfo: IMediaInfo): void {}, }; + +window.NativeShell = nativeShell; diff --git a/src/shell/scripts/appclose.ts b/src/shell/scripts/appclose.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/shell/scripts/videohandler.ts b/src/shell/scripts/videohandler.ts deleted file mode 100644 index 0dd165c..0000000 --- a/src/shell/scripts/videohandler.ts +++ /dev/null @@ -1,25 +0,0 @@ -require(["playbackManager", "events"], function (playbackManager, events) { - function sendCommand(name: string, method = "GET"): void { - fetch(`electronapphost://${name}`, { - method, - }).catch(console.error); - } - - let videoOn: boolean | undefined; - - events.on(playbackManager, "playbackstart", () => { - if (playbackManager.isPlayingVideo()) { - videoOn = true; - sendCommand("video-on", "POST"); - } - }); - - events.on(playbackManager, "playbackstop", () => { - if (videoOn) { - videoOn = false; - sendCommand("video-off", "POST"); - } - }); - - sendCommand("loaded"); -}); diff --git a/src/shell/serverdiscovery.ts b/src/shell/serverdiscovery.ts deleted file mode 100644 index b5dd48a..0000000 --- a/src/shell/serverdiscovery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { JsonObject } from "../common/types"; - -define([], function () { - return { - findServers: async function (timeoutMs: number): Promise { - const response = await fetch(`electronserverdiscovery://findservers?timeout=${timeoutMs}`, { - method: "POST", - }); - if (!response.ok) { - throw response; - } - // Expected server properties - // Name, Id, Address, EndpointAddress (optional) - return response.json(); - }, - }; -}); diff --git a/yarn.lock b/yarn.lock index 9d3eebe..b823fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -534,6 +534,16 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +comlink-electron-adapter@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/comlink-electron-adapter/-/comlink-electron-adapter-0.0.1.tgz#eb5107e7f39b32f7c8487c90310af75a4f73b5c2" + integrity sha512-viYKRvfW/V5c5m0CiQHIzEIMLc3CNt0A+fchKxsWl6NluEi84C5tQNDARl2gPibReoy8leJevpqyJVzTlAi3rA== + +comlink@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.3.0.tgz#80b3366baccd87897dab3638ebfcfae28b2f87c7" + integrity sha512-mu4KKKNuW8TvkfpW/H88HBPeILubBS6T94BdD1VWBXNXfiyqVtwUCVNO1GeNOBTsIswzsMjWlycYr+77F5b84g== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -764,10 +774,10 @@ electron-settings@^3.2.0: clone "^2.1.1" jsonfile "^4.0.0" -electron@^8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-8.2.4.tgz#c4e51ca8e84b5a5beaaabdae1024bd52ba487ba4" - integrity sha512-Lle0InIgSAHZxD5KDY0wZ1A2Zlc6GHwMhAxoHMzn05mndyP1YBkCYHc0TDDofzUTrsLFofduPjlknO5Oj9fTPA== +electron@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-8.5.0.tgz#a202738672214775fda27450b00ee516a66bffc4" + integrity sha512-ERqSTRlaQ4PqME5a1z9DWQbwQy2LbgSN1Jnau1MJCRRvHgq1FJlqbbb/ij/RGWuQuaxy4Djb2KnTs5rsemR5mQ== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -1497,11 +1507,6 @@ is-windows@^1.0.0: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.0.tgz#c61d61020c3ebe99261b781bd3d1622395f547f8" integrity sha1-xh1hAgw+vpkmG3gb09FiI5X1R/g= -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"