electron#systemPreferences TypeScript Examples

The following examples show how to use electron#systemPreferences. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.ts    From bluebubbles-server with Apache License 2.0 6 votes vote down vote up
async checkPermissions(): Promise<Array<NodeJS.Dict<any>>> {
        const output = [
            {
                name: "Accessibility",
                pass: systemPreferences.isTrustedAccessibilityClient(false),
                solution: "Open System Preferences > Security > Privacy > Accessibility, then add BlueBubbles"
            },
            {
                name: "Full Disk Access",
                pass: this.hasDiskAccess,
                solution:
                    "Open System Preferences > Security > Privacy > Full Disk Access, " +
                    "then add BlueBubbles. Lastly, restart BlueBubbles."
            }
        ];

        return output;
    }
Example #2
Source File: main.ts    From animation-editor with MIT License 6 votes vote down vote up
// When the draggable area is double clicked, trigger a window maximize/minimize
ipcMain.on("double-click-drag-area", () => {
	const win = BrowserWindow.getFocusedWindow()!;
	switch (systemPreferences.getUserDefault("AppleActionOnDoubleClick", "string")) {
		case "Minimize":
			win.minimize();
			break;
		case "Maximize":
			win.isMaximized() ? win.unmaximize() : win.maximize();
			break;
	}
});
Example #3
Source File: overlay-window.ts    From awakened-poe-trade with MIT License 5 votes vote down vote up
export async function createOverlayWindow () {
  if (process.platform === 'win32' && !systemPreferences.isAeroGlassEnabled()) {
    dialog.showErrorBox(
      'Windows 7 - Aero',
      // ----------------------
      'You must enable Windows Aero in "Appearance and Personalization".\n' +
      'It is required to create a transparent overlay window.'
    )
  }

  overlayOnEvent('OVERLAY->MAIN::ready', _resolveOverlayReady)
  overlayOnEvent('OVERLAY->MAIN::devicePixelRatio-change', (_, dpr) => handleDprChange(dpr))
  overlayOnEvent('OVERLAY->MAIN::close-overlay', assertPoEActive)
  PoeWindow.on('active-change', handlePoeWindowActiveChange)
  PoeWindow.onAttach(handleOverlayAttached)

  overlayWindow = new BrowserWindow({
    icon: path.join(__dirname, process.env.STATIC!, 'icon.png'),
    ...OW.WINDOW_OPTS,
    width: 800,
    height: 600,
    webPreferences: {
      webSecurity: false,
      allowRunningInsecureContent: false,
      webviewTag: true,
      spellcheck: false,
      defaultFontSize: config.get('fontSize'),
      preload: path.join(__dirname, 'preload.js')
    }
  })

  overlayWindow.setMenu(Menu.buildFromTemplate([
    { role: 'editMenu' },
    { role: 'reload' },
    { role: 'toggleDevTools' }
  ]))
  overlayWindow.webContents.on('before-input-event', handleExtraCommands)

  modifyResponseHeaders(overlayWindow.webContents)

  overlayWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  if (process.env.VITE_DEV_SERVER_URL) {
    overlayWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
    overlayWindow.webContents.openDevTools({ mode: 'detach', activate: false })
  } else {
    overlayWindow.loadURL('app://./index.html')
  }

  const electronReadyToShow = new Promise<void>(resolve =>
    overlayWindow!.once('ready-to-show', resolve))
  await electronReadyToShow
  await overlayReady
  PoeWindow.attach(overlayWindow)
}
Example #4
Source File: index.ts    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
private async preChecks(): Promise<void> {
        this.log("Running pre-start checks...");

        // Set the dock icon according to the config
        this.setDockIcon();

        try {
            // Restart via terminal if configured
            const restartViaTerminal = Server().repo.getConfig("start_via_terminal") as boolean;
            const parentProc = await findProcess("pid", process.ppid);
            const parentName = isNotEmpty(parentProc) ? parentProc[0].name : null;

            // Restart if enabled and the parent process is the app being launched
            if (restartViaTerminal && (!parentProc[0].name || parentName === "launchd")) {
                this.isRestarting = true;
                this.log("Restarting via terminal after post-check (configured)");
                await this.restartViaTerminal();
            }
        } catch (ex: any) {
            this.log(`Failed to restart via terminal!\n${ex}`);
        }

        // Get the current region
        this.region = await FileSystem.getRegion();

        // Log some server metadata
        this.log(`Server Metadata -> Server Version: v${app.getVersion()}`, "debug");
        this.log(`Server Metadata -> macOS Version: v${osVersion}`, "debug");
        this.log(`Server Metadata -> Local Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`, "debug");
        this.log(`Server Metadata -> Time Synchronization: ${await this.getTimeSync()}`, "debug");
        this.log(`Server Metadata -> Detected Region: ${this.region}`, "debug");

        if (!this.region) {
            this.log("No region detected, defaulting to US...", "debug");
            this.region = "US";
        }

        // If the user is on el capitan, we need to force cloudflare
        const proxyService = this.repo.getConfig("proxy_service") as string;
        if (!isMinSierra && proxyService === "Ngrok") {
            this.log("El Capitan detected. Forcing Cloudflare Proxy");
            await this.repo.setConfig("proxy_service", "Cloudflare");
        }

        // If the user is using tcp, force back to http
        const ngrokProtocol = this.repo.getConfig("ngrok_protocol") as string;
        if (ngrokProtocol === "tcp") {
            this.log("TCP protocol detected. Forcing HTTP protocol");
            await this.repo.setConfig("ngrok_protocol", "http");
        }

        this.log("Checking Permissions...");

        // Log if we dont have accessibility access
        if (systemPreferences.isTrustedAccessibilityClient(false) === true) {
            this.hasAccessibilityAccess = true;
            this.log("Accessibility permissions are enabled");
        } else {
            this.log("Accessibility permissions are required for certain actions!", "debug");
        }

        // Log if we dont have accessibility access
        if (this.iMessageRepo?.db) {
            this.hasDiskAccess = true;
            this.log("Full-disk access permissions are enabled");
        } else {
            this.log("Full-disk access permissions are required!", "error");
        }

        // Make sure Messages is running
        await FileSystem.startMessages();
        const msgCheckInterval = setInterval(async () => {
            try {
                // This won't start it if it's already open
                await FileSystem.startMessages();
            } catch (ex: any) {
                Server().log(`Unable to check if Messages.app is running! CLI Error: ${ex?.message ?? String(ex)}`);
                clearInterval(msgCheckInterval);
            }
        }, 150000); // Make sure messages is open every 2.5 minutes

        this.log("Finished pre-start checks...");
    }
Example #5
Source File: index.ts    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
/**
     * Starts configuration related inter-process-communication handlers.
     */
    static startIpcListeners() {
        ipcMain.handle("set-config", async (_, args) => {
            // Make sure that the server address being sent is using https (if enabled)
            if (args.server_address) {
                args.server_address = fixServerUrl(args.server_address);
            }

            // Make sure the Ngrok key is properly formatted
            if (args.ngrok_key) {
                if (args.ngrok_key.startsWith("./ngrok")) {
                    args.ngrok_key = args.ngrok_key.replace("./ngrok", "").trim();
                }
                if (args.ngrok_key.startsWith("authtoken")) {
                    args.ngrok_key = args.ngrok_key.replace("authtoken", "").trim();
                }
                args.ngrok_key = args.ngrok_key.trim();
            }

            // If we are changing the proxy service to a non-dyn dns service, we need to make sure "use https" is off
            if (args.proxy_service && args.proxy_service !== "dynamic-dns") {
                const httpsStatus = (args.use_custom_certificate ??
                    Server().repo.getConfig("use_custom_certificate")) as boolean;
                if (httpsStatus) {
                    Server().repo.setConfig("use_custom_certificate", false);
                }
            }

            for (const item of Object.keys(args)) {
                if (Server().repo.hasConfig(item) && Server().repo.getConfig(item) !== args[item]) {
                    Server().repo.setConfig(item, args[item]);
                }
            }

            return Server().repo?.config;
        });

        ipcMain.handle("get-config", async (_, __) => {
            if (!Server().repo.db) return {};
            return Server().repo?.config;
        });

        ipcMain.handle("get-alerts", async (_, __) => {
            const alerts = await AlertService.find();
            return alerts;
        });

        ipcMain.handle("mark-alerts-as-read", async (_, args) => {
            for (const i of args) {
                await AlertService.markAsRead(i);
                Server().notificationCount -= 1;
            }

            if (Server().notificationCount < 0) Server().notificationCount = 0;
            app.setBadgeCount(Server().notificationCount);
        });

        ipcMain.handle("set-fcm-server", async (_, args) => {
            FileSystem.saveFCMServer(args);
        });

        ipcMain.handle("set-fcm-client", async (_, args) => {
            FileSystem.saveFCMClient(args);
            await Server().fcm.start();
        });

        ipcMain.handle("get-devices", async (event, args) => {
            return await Server().repo.devices().find();
        });

        ipcMain.handle("get-private-api-requirements", async (_, __) => {
            return await Server().checkPrivateApiRequirements();
        });

        ipcMain.handle("reinstall-helper-bundle", async (_, __) => {
            return await BlueBubblesHelperService.installBundle(true);
        });

        ipcMain.handle("get-fcm-server", (event, args) => {
            return FileSystem.getFCMServer();
        });

        ipcMain.handle("get-fcm-client", (event, args) => {
            return FileSystem.getFCMClient();
        });

        ipcMain.handle("get-webhooks", async (event, args) => {
            const res = await Server().repo.getWebhooks();
            return res.map(e => ({ id: e.id, url: e.url, events: e.events, created: e.created }));
        });

        ipcMain.handle("create-webhook", async (event, payload) => {
            const res = await Server().repo.addWebhook(payload.url, payload.events);
            const output = { id: res.id, url: res.url, events: res.events, created: res.created };
            return output;
        });

        ipcMain.handle("delete-webhook", async (event, args) => {
            return await Server().repo.deleteWebhook({ url: args.url, id: args.id });
        });

        ipcMain.handle("update-webhook", async (event, args) => {
            return await Server().repo.updateWebhook({ id: args.id, url: args?.url, events: args?.events });
        });

        ipcMain.handle("contact-permission-status", async (event, _) => {
            return await getContactPermissionStatus();
        });

        ipcMain.handle("request-contact-permission", async (event, _) => {
            return await requestContactPermission();
        });

        ipcMain.handle("get-contacts", async (event, _) => {
            return await ContactInterface.getAllContacts();
        });

        ipcMain.handle("delete-contacts", async (event, _) => {
            return await ContactInterface.deleteAllContacts();
        });

        ipcMain.handle("add-contact", async (event, args) => {
            return await ContactInterface.createContact({
                firstName: args.firstName,
                lastName: args.lastName,
                displayName: args.displayname,
                emails: args.emails ?? [],
                phoneNumbers: args.phoneNumbers ?? []
            });
        });

        ipcMain.handle("update-contact", async (event, args) => {
            return await ContactInterface.createContact({
                id: args.contactId ?? args.id,
                firstName: args.firstName,
                lastName: args.lastName,
                displayName: args.displayName,
                emails: args.emails ?? [],
                phoneNumbers: args.phoneNumbers ?? [],
                updateEntry: true
            });
        });

        ipcMain.handle("remove-contact", async (event, id) => {
            return await ContactInterface.deleteContact({ contactId: id });
        });

        ipcMain.handle("remove-address", async (event, id) => {
            return await ContactInterface.deleteContactAddress({ contactAddressId: id });
        });

        ipcMain.handle("add-address", async (event, args) => {
            return await ContactInterface.addAddressToContactById(args.contactId, args.address, args.type);
        });

        ipcMain.handle("import-vcf", async (event, path) => {
            return await ContactInterface.importFromVcf(path);
        });

        ipcMain.handle("get-contact-name", async (event, address) => {
            const res = await ContactInterface.queryContacts([address]);
            return res && res.length > 0 ? res[0] : null;
        });

        ipcMain.handle("toggle-tutorial", async (_, toggle) => {
            await Server().repo.setConfig("tutorial_is_done", toggle);

            if (toggle) {
                await Server().hotRestart();
            }
        });

        ipcMain.handle("get-message-count", async (event, args) => {
            if (!Server().iMessageRepo?.db) return 0;
            const count = await Server().iMessageRepo.getMessageCount(args?.after, args?.before, args?.isFromMe);
            return count;
        });

        ipcMain.handle("get-chat-image-count", async (event, args) => {
            if (!Server().iMessageRepo?.db) return 0;
            const count = await Server().iMessageRepo.getMediaCountsByChat({ mediaType: "image" });
            return count;
        });

        ipcMain.handle("get-chat-video-count", async (event, args) => {
            if (!Server().iMessageRepo?.db) return 0;
            const count = await Server().iMessageRepo.getMediaCountsByChat({ mediaType: "video" });
            return count;
        });

        ipcMain.handle("get-group-message-counts", async (event, args) => {
            if (!Server().iMessageRepo?.db) return 0;
            const count = await Server().iMessageRepo.getChatMessageCounts("group");
            return count;
        });

        ipcMain.handle("get-individual-message-counts", async (event, args) => {
            if (!Server().iMessageRepo?.db) return 0;
            const count = await Server().iMessageRepo.getChatMessageCounts("individual");
            return count;
        });

        ipcMain.handle("refresh-api-contacts", async (_, __) => {
            ContactInterface.refreshApiContacts();
        });

        ipcMain.handle("check-permissions", async (_, __) => {
            return await Server().checkPermissions();
        });

        ipcMain.handle("prompt_accessibility", async (_, __) => {
            return {
                abPerms: systemPreferences.isTrustedAccessibilityClient(true) ? "authorized" : "denied"
            };
        });

        ipcMain.handle("prompt_disk_access", async (_, __) => {
            return {
                fdPerms: "authorized"
            };
        });

        ipcMain.handle("get-caffeinate-status", (_, __) => {
            return {
                isCaffeinated: Server().caffeinate.isCaffeinated,
                autoCaffeinate: Server().repo.getConfig("auto_caffeinate")
            };
        });

        ipcMain.handle("purge-event-cache", (_, __) => {
            if (Server().eventCache.size() === 0) {
                Server().log("No events to purge from event cache!");
            } else {
                Server().log(`Purging ${Server().eventCache.size()} items from the event cache!`);
                Server().eventCache.purge();
            }
        });

        ipcMain.handle("purge-devices", (_, __) => {
            Server().repo.devices().clear();
        });

        ipcMain.handle("restart-via-terminal", (_, __) => {
            Server().restartViaTerminal();
        });

        ipcMain.handle("hot-restart", async (_, __) => {
            await Server().hotRestart();
        });

        ipcMain.handle("full-restart", async (_, __) => {
            await Server().relaunch();
        });

        ipcMain.handle("reset-app", async (_, __) => {
            await Server().stopAll();
            FileSystem.removeDirectory(FileSystem.baseDir);
            await Server().relaunch();
        });

        ipcMain.handle("show-dialog", (_, opts: Electron.MessageBoxOptions) => {
            return dialog.showMessageBox(Server().window, opts);
        });

        ipcMain.handle("open-log-location", (_, __) => {
            FileSystem.executeAppleScript(openLogs());
        });

        ipcMain.handle("open-app-location", (_, __) => {
            FileSystem.executeAppleScript(openAppData());
        });

        ipcMain.handle("clear-alerts", async (_, __) => {
            Server().notificationCount = 0;
            app.setBadgeCount(0);
            await Server().repo.alerts().clear();
        });
    }
Example #6
Source File: ipc-events.ts    From WowUp with GNU General Public License v3.0 4 votes vote down vote up
export function initializeIpcHandlers(window: BrowserWindow): void {
  log.info("process.versions", process.versions);

  ipcMain.on("webview-error", (evt, err, msg) => {
    log.error("webview-error", err, msg);
  });

  // Just forward the token event out to the window
  // this is not a handler, just a passive listener
  ipcMain.on("wago-token-received", (evt, token) => {
    window?.webContents?.send("wago-token-received", token);
  });

  // Remove the pending URLs once read so they are only able to be gotten once
  handle(IPC_GET_PENDING_OPEN_URLS, (): string[] => {
    const urls = PENDING_OPEN_URLS;
    PENDING_OPEN_URLS = [];
    return urls;
  });

  handle(
    IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT,
    (
      _evt,
      key: string,
      type: "string" | "boolean" | "integer" | "float" | "double" | "url" | "array" | "dictionary"
    ) => {
      return systemPreferences.getUserDefault(key, type);
    }
  );

  handle("clipboard-read-text", () => {
    return clipboard.readText();
  });

  handle(IPC_SHOW_DIRECTORY, async (evt, filePath: string): Promise<string> => {
    return await shell.openPath(filePath);
  });

  handle(IPC_GET_ASSET_FILE_PATH, (evt, fileName: string) => {
    return path.join(__dirname, "..", "assets", fileName);
  });

  handle(IPC_CREATE_DIRECTORY_CHANNEL, async (evt, directoryPath: string): Promise<boolean> => {
    log.info(`[CreateDirectory] '${directoryPath}'`);
    await fsp.mkdir(directoryPath, { recursive: true });
    return true;
  });

  handle(IPC_GET_ZOOM_FACTOR, () => {
    return window?.webContents?.getZoomFactor();
  });

  handle(IPC_UPDATE_APP_BADGE, (evt, count: number) => {
    return app.setBadgeCount(count);
  });

  handle(IPC_SET_ZOOM_LIMITS, (evt, minimumLevel: number, maximumLevel: number) => {
    return window.webContents?.setVisualZoomLevelLimits(minimumLevel, maximumLevel);
  });

  handle("show-item-in-folder", (evt, path: string) => {
    shell.showItemInFolder(path);
  });

  handle(IPC_SET_ZOOM_FACTOR, (evt, zoomFactor: number) => {
    if (window?.webContents) {
      window.webContents.zoomFactor = zoomFactor;
    }
  });

  handle(IPC_ADDONS_SAVE_ALL, (evt, addons: Addon[]) => {
    if (!Array.isArray(addons)) {
      return;
    }

    for (const addon of addons) {
      addonStore.set(addon.id, addon);
    }
  });

  handle(IPC_GET_APP_VERSION, () => {
    return app.getVersion();
  });

  handle(IPC_GET_LOCALE, () => {
    return `${app.getLocale()}`;
  });

  handle(IPC_GET_LAUNCH_ARGS, () => {
    return process.argv;
  });

  handle(IPC_GET_LOGIN_ITEM_SETTINGS, () => {
    return app.getLoginItemSettings();
  });

  handle(IPC_SET_LOGIN_ITEM_SETTINGS, (evt, settings: Settings) => {
    return app.setLoginItemSettings(settings);
  });

  handle(IPC_READDIR, async (evt, dirPath: string): Promise<string[]> => {
    return await fsp.readdir(dirPath);
  });

  handle(IPC_IS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
    return app.isDefaultProtocolClient(protocol);
  });

  handle(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
    return app.setAsDefaultProtocolClient(protocol);
  });

  handle(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
    return app.removeAsDefaultProtocolClient(protocol);
  });

  handle(IPC_LIST_DIRECTORIES_CHANNEL, async (evt, filePath: string, scanSymlinks: boolean) => {
    const files = await fsp.readdir(filePath, { withFileTypes: true });
    let symlinkNames: string[] = [];
    if (scanSymlinks === true) {
      log.info("Scanning symlinks");
      const symlinkDirs = await getSymlinkDirs(filePath, files);
      symlinkNames = _.map(symlinkDirs, (symLink) => symLink.original.name);
    }

    const directories = files.filter((file) => file.isDirectory()).map((file) => file.name);
    return [...directories, ...symlinkNames];
  });

  handle(IPC_STAT_FILES_CHANNEL, async (evt, filePaths: string[]) => {
    const results: { [path: string]: FsStats } = {};

    const taskResults = await firstValueFrom(
      from(filePaths).pipe(
        mergeMap((filePath) => from(statFile(filePath)), 3),
        toArray()
      )
    );

    taskResults.forEach((r) => (results[r.path] = r.fsStats));

    return results;
  });

  handle(IPC_LIST_ENTRIES, async (evt, sourcePath: string, filter: string) => {
    const globFilter = globrex(filter);
    const results = await fsp.readdir(sourcePath, { withFileTypes: true });
    const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
    return _.map(matches, (match) => {
      const dirEnt: FsDirent = {
        isBlockDevice: match.isBlockDevice(),
        isCharacterDevice: match.isCharacterDevice(),
        isDirectory: match.isDirectory(),
        isFIFO: match.isFIFO(),
        isFile: match.isFile(),
        isSocket: match.isSocket(),
        isSymbolicLink: match.isSymbolicLink(),
        name: match.name,
      };
      return dirEnt;
    });
  });

  handle(IPC_LIST_FILES_CHANNEL, async (evt, sourcePath: string, filter: string) => {
    const pathExists = await exists(sourcePath);
    if (!pathExists) {
      return [];
    }

    const globFilter = globrex(filter);
    const results = await fsp.readdir(sourcePath, { withFileTypes: true });
    const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
    return _.map(matches, (match) => match.name);
  });

  handle(IPC_PATH_EXISTS_CHANNEL, async (evt, filePath: string) => {
    if (!filePath) {
      return false;
    }

    try {
      await fsp.access(filePath);
    } catch (e) {
      if (e.code !== "ENOENT") {
        log.error(e);
      }
      return false;
    }

    return true;
  });

  handle(IPC_WOWUP_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise<WowUpScanResult[]> => {
    const taskResults = await firstValueFrom(
      from(filePaths).pipe(
        mergeMap((folder) => from(new WowUpFolderScanner(folder).scanFolder()), 3),
        toArray()
      )
    );

    return taskResults;
  });

  handle(IPC_UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => {
    await new Promise((resolve, reject) => {
      yauzl.open(arg.zipFilePath, { lazyEntries: true }, (err, zipfile) => {
        handleZipFile(err, zipfile, arg.outputFolder).then(resolve).catch(reject);
      });
    });

    await chmodDir(arg.outputFolder, DEFAULT_FILE_MODE);

    return arg.outputFolder;
  });

  handle("zip-file", async (evt, srcPath: string, destPath: string) => {
    log.info(`[ZipFile]: '${srcPath} -> ${destPath}`);
    return await zipFile(srcPath, destPath);
  });

  handle("zip-read-file", async (evt, zipPath: string, filePath: string) => {
    log.info(`[ZipReadFile]: '${zipPath} : ${filePath}`);
    return await readFileInZip(zipPath, filePath);
  });

  handle("zip-list-files", (evt, zipPath: string, filter: string) => {
    log.info(`[ZipListEntries]: '${zipPath}`);
    return listZipFiles(zipPath, filter);
  });

  handle("rename-file", async (evt, srcPath: string, destPath: string) => {
    log.info(`[RenameFile]: '${srcPath} -> ${destPath}`);
    return await fsp.rename(srcPath, destPath);
  });

  handle("base64-encode", (evt, content: string) => {
    const buff = Buffer.from(content);
    return buff.toString("base64");
  });

  handle("base64-decode", (evt, content: string) => {
    const buff = Buffer.from(content, "base64");
    return buff.toString("utf-8");
  });

  handle(IPC_COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest): Promise<boolean> => {
    log.info(`[FileCopy] '${arg.sourceFilePath}' -> '${arg.destinationFilePath}'`);
    const stat = await fsp.lstat(arg.sourceFilePath);
    if (stat.isDirectory()) {
      await copyDir(arg.sourceFilePath, arg.destinationFilePath);
      await chmodDir(arg.destinationFilePath, DEFAULT_FILE_MODE);
    } else {
      await fsp.copyFile(arg.sourceFilePath, arg.destinationFilePath);
      await fsp.chmod(arg.destinationFilePath, DEFAULT_FILE_MODE);
    }
    return true;
  });

  handle(IPC_DELETE_DIRECTORY_CHANNEL, async (evt, filePath: string) => {
    log.info(`[FileRemove] ${filePath}`);
    return await remove(filePath);
  });

  handle(IPC_READ_FILE_CHANNEL, async (evt, filePath: string) => {
    return await fsp.readFile(filePath, { encoding: "utf-8" });
  });

  handle(IPC_READ_FILE_BUFFER_CHANNEL, async (evt, filePath: string) => {
    return await fsp.readFile(filePath);
  });

  handle("decode-product-db", async (evt, filePath: string) => {
    const productDbData = await fsp.readFile(filePath);
    const productDb = ProductDb.decode(productDbData);
    setImmediate(() => {
      console.log("productDb", JSON.stringify(productDb));
    });

    return productDb;
  });

  handle(IPC_WRITE_FILE_CHANNEL, async (evt, filePath: string, contents: string) => {
    return await fsp.writeFile(filePath, contents, { encoding: "utf-8", mode: DEFAULT_FILE_MODE });
  });

  handle(IPC_CREATE_TRAY_MENU_CHANNEL, (evt, config: SystemTrayConfig) => {
    return createTray(window, config);
  });

  handle(IPC_CREATE_APP_MENU_CHANNEL, (evt, config: MenuConfig) => {
    return createAppMenu(window, config);
  });

  handle(IPC_GET_LATEST_DIR_UPDATE_TIME, (evt, dirPath: string) => {
    return getLastModifiedFileDate(dirPath);
  });

  handle(IPC_LIST_DIR_RECURSIVE, (evt, dirPath: string): Promise<string[]> => {
    return readDirRecursive(dirPath);
  });

  handle(IPC_GET_DIRECTORY_TREE, (evt, args: GetDirectoryTreeRequest): Promise<TreeNode> => {
    log.debug(IPC_GET_DIRECTORY_TREE, args);
    return getDirTree(args.dirPath, args.opts);
  });

  handle(IPC_GET_HOME_DIR, (): string => {
    return os.homedir();
  });

  handle(IPC_MINIMIZE_WINDOW, () => {
    if (window?.minimizable) {
      window.minimize();
    }
  });

  handle(IPC_MAXIMIZE_WINDOW, () => {
    if (window?.maximizable) {
      if (window.isMaximized()) {
        window.unmaximize();
      } else {
        window.maximize();
      }
    }
  });

  handle(IPC_CLOSE_WINDOW, () => {
    window?.close();
  });

  handle(IPC_FOCUS_WINDOW, () => {
    restoreWindow(window);
    window?.focus();
  });

  handle(IPC_RESTART_APP, () => {
    log.info(`[RestartApp]`);
    app.relaunch();
    app.quit();
  });

  handle(IPC_QUIT_APP, () => {
    log.info(`[QuitApp]`);
    app.quit();
  });

  handle(IPC_LIST_DISKS_WIN32, async () => {
    const diskInfos = await nodeDiskInfo.getDiskInfo();
    // Cant pass complex objects over the wire, make them simple
    return diskInfos.map((di) => {
      return {
        mounted: di.mounted,
        filesystem: di.filesystem,
      };
    });
  });

  handle(IPC_WINDOW_LEAVE_FULLSCREEN, () => {
    window?.setFullScreen(false);
  });

  handle(IPC_SHOW_OPEN_DIALOG, async (evt, options: OpenDialogOptions) => {
    return await dialog.showOpenDialog(options);
  });

  handle(IPC_PUSH_INIT, () => {
    return push.startPushService();
  });

  handle(IPC_PUSH_REGISTER, async (evt, appId: string) => {
    return await push.registerForPush(appId);
  });

  handle(IPC_PUSH_UNREGISTER, async () => {
    return await push.unregisterPush();
  });

  handle(IPC_PUSH_SUBSCRIBE, async (evt, channel: string) => {
    return await push.subscribeToChannel(channel);
  });

  handle("get-focus", () => {
    return window.isFocused();
  });

  ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => {
    handleDownloadFile(arg).catch((e) => log.error(e.toString()));
  });

  // In order to allow concurrent downloads, we have to get creative with this session handler
  window.webContents.session.on("will-download", (evt, item, wc) => {
    for (const key of _dlMap.keys()) {
      log.info(`will-download: ${key}`);
      if (!item.getURLChain().includes(key)) {
        continue;
      }

      try {
        const action = _dlMap.get(key);
        action.call(null, evt, item, wc);
      } catch (e) {
        log.error(e);
      } finally {
        _dlMap.delete(key);
      }
    }
  });

  async function statFile(filePath: string) {
    const stats = await fsp.stat(filePath);
    const fsStats: FsStats = {
      atime: stats.atime,
      atimeMs: stats.atimeMs,
      birthtime: stats.birthtime,
      birthtimeMs: stats.birthtimeMs,
      blksize: stats.blksize,
      blocks: stats.blocks,
      ctime: stats.ctime,
      ctimeMs: stats.ctimeMs,
      dev: stats.dev,
      gid: stats.gid,
      ino: stats.ino,
      isBlockDevice: stats.isBlockDevice(),
      isCharacterDevice: stats.isCharacterDevice(),
      isDirectory: stats.isDirectory(),
      isFIFO: stats.isFIFO(),
      isFile: stats.isFile(),
      isSocket: stats.isSocket(),
      isSymbolicLink: stats.isSymbolicLink(),
      mode: stats.mode,
      mtime: stats.mtime,
      mtimeMs: stats.mtimeMs,
      nlink: stats.nlink,
      rdev: stats.rdev,
      size: stats.size,
      uid: stats.uid,
    };
    return { path: filePath, fsStats };
  }

  async function handleDownloadFile(arg: DownloadRequest) {
    const status: DownloadStatus = {
      type: DownloadStatusType.Pending,
      savePath: "",
    };

    try {
      await fsp.mkdir(arg.outputFolder, { recursive: true });

      const downloadUrl = new URL(arg.url);
      if (typeof arg.auth?.queryParams === "object") {
        for (const [key, value] of Object.entries(arg.auth.queryParams)) {
          downloadUrl.searchParams.set(key, value);
        }
      }

      const savePath = path.join(arg.outputFolder, `${nanoid()}-${arg.fileName}`);
      log.info(`[DownloadFile] '${downloadUrl.toString()}' -> '${savePath}'`);

      const url = downloadUrl.toString();
      const writer = fs.createWriteStream(savePath);

      try {
        await new Promise((resolve, reject) => {
          let size = 0;
          let percentMod = -1;

          const req = net.request({
            url,
            redirect: "manual",
          });

          if (typeof arg.auth?.headers === "object") {
            for (const [key, value] of Object.entries(arg.auth.headers)) {
              log.info(`Setting header: ${key}=${value}`);
              req.setHeader(key, value);
            }
          }

          req.on("redirect", (status, method, redirectUrl) => {
            log.info(`[download] caught redirect`, status, redirectUrl);
            req.followRedirect();
          });

          req.on("response", (response) => {
            const fileLength = parseInt((response.headers["content-length"] as string) ?? "0", 10);

            response.on("data", (data) => {
              writer.write(data, () => {
                size += data.length;
                const percent = fileLength <= 0 ? 0 : Math.floor((size / fileLength) * 100);
                if (percent % 5 === 0 && percentMod !== percent) {
                  percentMod = percent;
                  log.debug(`Write: [${percent}] ${size}`);
                }
              });
            });

            response.on("end", () => {
              if (response.statusCode < 200 || response.statusCode >= 300) {
                return reject(new Error(`Invalid response (${response.statusCode}): ${url}`));
              }

              return resolve(undefined);
            });
            response.on("error", (err) => {
              return reject(err);
            });
          });
          req.end();
        });
      } finally {
        // always close stream
        writer.end();
      }

      status.type = DownloadStatusType.Complete;
      status.savePath = savePath;

      window.webContents.send(arg.responseKey, status);
    } catch (err) {
      log.error(err);
      status.type = DownloadStatusType.Error;
      status.error = err;
      window.webContents.send(arg.responseKey, status);
    }
  }
}