Tray improvements.
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "esdp",
|
"name": "esdp",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1-alpha.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "esdp",
|
"name": "esdp",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1-alpha.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^4.1.6",
|
"@apollo/client": "^4.1.6",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.8.3",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"source-map-support": "^0.5.21"
|
"source-map-support": "^0.5.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/pngjs": "^6.0.5",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
@@ -5269,6 +5271,16 @@
|
|||||||
"xmlbuilder": ">=11.0.1"
|
"xmlbuilder": ">=11.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pngjs": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
|
||||||
|
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.18",
|
"version": "6.9.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||||
@@ -12221,6 +12233,15 @@
|
|||||||
"node": ">=10.4.0"
|
"node": ">=10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.8.3",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"source-map-support": "^0.5.21"
|
"source-map-support": "^0.5.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
|
"@types/pngjs": "^6.0.5",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"antd": "^6.3.1",
|
"antd": "^6.3.1",
|
||||||
|
|||||||
@@ -151,19 +151,9 @@ async function ImportJob(filepath: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
setAppProgressbar(-1);
|
setAppProgressbar(-1);
|
||||||
|
|
||||||
const items = ["One", "Two", "Three"];
|
|
||||||
const n = new Notification({
|
|
||||||
title: "Choose an Action!",
|
|
||||||
actions: [
|
|
||||||
{ type: "button", text: "Action 1" },
|
|
||||||
{ type: "button", text: "Action 2" },
|
|
||||||
{ type: "selection", text: "Apply", items },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadNotification = new Notification({
|
const uploadNotification = new Notification({
|
||||||
title: "Job Scrubbed",
|
title: "Job Scrubbed",
|
||||||
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`,
|
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}.`,
|
||||||
actions: [
|
actions: [
|
||||||
{ text: "View in App", type: "button" as const },
|
{ text: "View in App", type: "button" as const },
|
||||||
...(scrubPdfURL ? [{ text: "View PDF", type: "button" as const }] : []),
|
...(scrubPdfURL ? [{ text: "View PDF", type: "button" as const }] : []),
|
||||||
|
|||||||
@@ -28,7 +28,16 @@ import store from "./store/store";
|
|||||||
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
||||||
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
||||||
import { getMainWindow } from "./util/toRenderer";
|
import { getMainWindow } from "./util/toRenderer";
|
||||||
import { GetAllEnvFiles, GetLatestEnvFile } from "./watcher/watcher";
|
import {
|
||||||
|
setTrayBaseImage,
|
||||||
|
setTrayInstance,
|
||||||
|
setWatcherTrayStatus,
|
||||||
|
} from "./util/trayStatus";
|
||||||
|
import {
|
||||||
|
GetAllEnvFiles,
|
||||||
|
GetLatestEnvFile,
|
||||||
|
StartWatcher,
|
||||||
|
} from "./watcher/watcher";
|
||||||
|
|
||||||
const appIconToUse = imexAppIcon;
|
const appIconToUse = imexAppIcon;
|
||||||
|
|
||||||
@@ -497,8 +506,15 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create Tray
|
//Create Tray
|
||||||
const trayicon = nativeImage.createFromPath(appIconToUse);
|
const trayBase = nativeImage
|
||||||
const tray = new Tray(trayicon.resize({ width: 16 }));
|
.createFromPath(appIconToUse)
|
||||||
|
.resize({ width: 16, height: 16 });
|
||||||
|
const tray = new Tray(trayBase);
|
||||||
|
|
||||||
|
setTrayInstance(tray);
|
||||||
|
setTrayBaseImage(trayBase);
|
||||||
|
setWatcherTrayStatus(false);
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: "Show App",
|
label: "Show App",
|
||||||
@@ -568,6 +584,7 @@ app.whenReady().then(async () => {
|
|||||||
isKeepAliveLaunch = true;
|
isKeepAliveLaunch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartWatcher();
|
||||||
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||||
openMainWindow();
|
openMainWindow();
|
||||||
|
|
||||||
|
|||||||
88
src/main/util/trayStatus.ts
Normal file
88
src/main/util/trayStatus.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { nativeImage, type NativeImage, type Tray } from "electron";
|
||||||
|
|
||||||
|
let trayInstance: Tray | undefined;
|
||||||
|
let trayBaseImage: NativeImage | undefined;
|
||||||
|
let watcherStarted = false;
|
||||||
|
|
||||||
|
export function setTrayInstance(tray: Tray): void {
|
||||||
|
trayInstance = tray;
|
||||||
|
applyTrayImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTrayBaseImage(image: NativeImage): void {
|
||||||
|
trayBaseImage = image;
|
||||||
|
applyTrayImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWatcherTrayStatus(started: boolean): void {
|
||||||
|
watcherStarted = started;
|
||||||
|
applyTrayImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTrayImage(): void {
|
||||||
|
if (!trayInstance || !trayBaseImage) return;
|
||||||
|
trayInstance.setImage(buildStatusTrayImage(trayBaseImage, watcherStarted));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStatusTrayImage(
|
||||||
|
base: NativeImage,
|
||||||
|
started: boolean,
|
||||||
|
): NativeImage {
|
||||||
|
try {
|
||||||
|
const pngjs = (() => {
|
||||||
|
try {
|
||||||
|
return require("pngjs") as {
|
||||||
|
PNG?: { sync?: { read?: (b: Buffer) => unknown; write?: (p: unknown) => Buffer } };
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!pngjs?.PNG?.sync?.read || !pngjs.PNG.sync.write) return base;
|
||||||
|
|
||||||
|
const basePng = base.toPNG();
|
||||||
|
const png = pngjs.PNG.sync.read(basePng) as {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
data: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
const width = png.width;
|
||||||
|
const height = png.height;
|
||||||
|
const radius = Math.max(2, Math.round(Math.min(width, height) * 0.2));
|
||||||
|
const centerX = width - radius - 1;
|
||||||
|
const centerY = height - radius - 1;
|
||||||
|
|
||||||
|
const fill = started
|
||||||
|
? { r: 0x52, g: 0xc4, b: 0x1a, a: 0xff }
|
||||||
|
: { r: 0xff, g: 0x4d, b: 0x4f, a: 0xff };
|
||||||
|
const stroke = { r: 0xff, g: 0xff, b: 0xff, a: 0xff };
|
||||||
|
|
||||||
|
for (let y = centerY - radius - 1; y <= centerY + radius + 1; y++) {
|
||||||
|
for (let x = centerX - radius - 1; x <= centerX + radius + 1; x++) {
|
||||||
|
if (x < 0 || y < 0 || x >= width || y >= height) continue;
|
||||||
|
const dx = x - centerX;
|
||||||
|
const dy = y - centerY;
|
||||||
|
const d2 = dx * dx + dy * dy;
|
||||||
|
const r2 = radius * radius;
|
||||||
|
const inner2 = (radius - 1) * (radius - 1);
|
||||||
|
|
||||||
|
let color: typeof fill | null = null;
|
||||||
|
if (d2 <= r2 && d2 >= inner2) color = stroke;
|
||||||
|
else if (d2 < inner2) color = fill;
|
||||||
|
if (!color) continue;
|
||||||
|
|
||||||
|
const idx = (png.width * y + x) << 2;
|
||||||
|
png.data[idx] = color.r;
|
||||||
|
png.data[idx + 1] = color.g;
|
||||||
|
png.data[idx + 2] = color.b;
|
||||||
|
png.data[idx + 3] = color.a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nativeImage.createFromBuffer(pngjs.PNG.sync.write(png));
|
||||||
|
} catch {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import chokidar, { FSWatcher } from "chokidar";
|
import chokidar, { FSWatcher } from "chokidar";
|
||||||
import { BrowserWindow, Notification } from "electron";
|
import { Notification } from "electron";
|
||||||
import log from "electron-log/main";
|
import log from "electron-log/main";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
@@ -8,6 +8,7 @@ import ipcTypes from "../../util/ipcTypes.json";
|
|||||||
import ImportJob from "../decoder/decoder";
|
import ImportJob from "../decoder/decoder";
|
||||||
import store from "../store/store";
|
import store from "../store/store";
|
||||||
import getMainWindow from "../../util/getMainWindow";
|
import getMainWindow from "../../util/getMainWindow";
|
||||||
|
import { setWatcherTrayStatus } from "../util/trayStatus";
|
||||||
let watcher: FSWatcher | null;
|
let watcher: FSWatcher | null;
|
||||||
|
|
||||||
async function StartWatcher(): Promise<boolean> {
|
async function StartWatcher(): Promise<boolean> {
|
||||||
@@ -78,6 +79,7 @@ async function StartWatcher(): Promise<boolean> {
|
|||||||
// })
|
// })
|
||||||
.on("error", function (error) {
|
.on("error", function (error) {
|
||||||
log.error("Error in Watcher", errorTypeCheck(error));
|
log.error("Error in Watcher", errorTypeCheck(error));
|
||||||
|
setWatcherTrayStatus(false);
|
||||||
// mainWindow.webContents.send(
|
// mainWindow.webContents.send(
|
||||||
// ipcTypes.toRenderer.watcher.error,
|
// ipcTypes.toRenderer.watcher.error,
|
||||||
// errorTypeCheck(error)
|
// errorTypeCheck(error)
|
||||||
@@ -114,6 +116,7 @@ function onWatcherReady(): void {
|
|||||||
body: "Newly exported estimates will be automatically uploaded.",
|
body: "Newly exported estimates will be automatically uploaded.",
|
||||||
}).show();
|
}).show();
|
||||||
log.info("Confirmed watched paths:", watcher.getWatched());
|
log.info("Confirmed watched paths:", watcher.getWatched());
|
||||||
|
setWatcherTrayStatus(true);
|
||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +127,7 @@ async function StopWatcher(): Promise<boolean> {
|
|||||||
if (watcher) {
|
if (watcher) {
|
||||||
await watcher.close();
|
await watcher.close();
|
||||||
log.info("Watcher stopped.");
|
log.info("Watcher stopped.");
|
||||||
|
setWatcherTrayStatus(false);
|
||||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
||||||
|
|
||||||
new Notification({
|
new Notification({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Badge,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -25,7 +26,10 @@ import {
|
|||||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { selectWatcherStatus } from "@renderer/redux/app.slice";
|
||||||
|
import { useAppSelector } from "@renderer/redux/reduxHooks";
|
||||||
import ipcTypes from "../../../../util/ipcTypes.json";
|
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
@@ -80,6 +84,8 @@ const Home: FC = () => {
|
|||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const ipcRenderer = window.electron.ipcRenderer;
|
const ipcRenderer = window.electron.ipcRenderer;
|
||||||
|
|
||||||
|
const isWatcherStarted = useAppSelector(selectWatcherStatus);
|
||||||
|
|
||||||
const [history, setHistory] = useState<ScrubHistoryItem[]>([]);
|
const [history, setHistory] = useState<ScrubHistoryItem[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [totalJobs, setTotalJobs] = useState<number>(0);
|
const [totalJobs, setTotalJobs] = useState<number>(0);
|
||||||
@@ -231,20 +237,20 @@ const Home: FC = () => {
|
|||||||
<Text strong>{record.results?.length ?? 0}</Text>
|
<Text strong>{record.results?.length ?? 0}</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: "Status",
|
// title: "Status",
|
||||||
key: "status",
|
// key: "status",
|
||||||
width: 120,
|
// width: 120,
|
||||||
render: () => (
|
// render: () => (
|
||||||
<Tag
|
// <Tag
|
||||||
color="success"
|
// color="success"
|
||||||
icon={<CheckCircleOutlined />}
|
// icon={<CheckCircleOutlined />}
|
||||||
style={{ margin: 0 }}
|
// style={{ margin: 0 }}
|
||||||
>
|
// >
|
||||||
Done
|
// Done
|
||||||
</Tag>
|
// </Tag>
|
||||||
),
|
// ),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: "Actions",
|
title: "Actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
@@ -309,14 +315,24 @@ const Home: FC = () => {
|
|||||||
<Title level={2} style={{ margin: 0 }}>
|
<Title level={2} style={{ margin: 0 }}>
|
||||||
{t("dashboard.labels.dashboard")}
|
{t("dashboard.labels.dashboard")}
|
||||||
</Title>
|
</Title>
|
||||||
<Button
|
<Space align="center" size="middle">
|
||||||
type="default"
|
<Badge
|
||||||
icon={<SettingOutlined />}
|
status={isWatcherStarted ? "success" : "error"}
|
||||||
onClick={() => navigate("/settings")}
|
text={
|
||||||
size="large"
|
isWatcherStarted
|
||||||
>
|
? t("settings.labels.started")
|
||||||
Settings
|
: t("settings.labels.stopped")
|
||||||
</Button>
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
onClick={() => navigate("/settings")}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -374,7 +390,7 @@ const Home: FC = () => {
|
|||||||
}
|
}
|
||||||
value={
|
value={
|
||||||
lastProcessed
|
lastProcessed
|
||||||
? new Date(lastProcessed).toLocaleTimeString()
|
? dayjs(lastProcessed).format("MM/DD/YYYY @ hh:mm a")
|
||||||
: "—"
|
: "—"
|
||||||
}
|
}
|
||||||
prefix={<ClockCircleOutlined style={{ color: "#fff" }} />}
|
prefix={<ClockCircleOutlined style={{ color: "#fff" }} />}
|
||||||
|
|||||||
@@ -1,4 +1,45 @@
|
|||||||
import { BrowserWindow, shell } from "electron";
|
import { BrowserView, BrowserWindow, shell } from "electron";
|
||||||
|
|
||||||
|
const loadingHtml = `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Loading external PDF…</title>
|
||||||
|
<style>
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: #ffffff;
|
||||||
|
color: rgba(0,0,0,0.88);
|
||||||
|
}
|
||||||
|
.wrap { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid rgba(0,0,0,0.15);
|
||||||
|
border-top-color: rgba(0,0,0,0.55);
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.text { font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="spinner" aria-hidden="true"></div>
|
||||||
|
<div class="text">Loading external PDF…</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
function toDataUrl(html: string): string {
|
||||||
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function newWindow(url: string): Promise<void> {
|
async function newWindow(url: string): Promise<void> {
|
||||||
// BrowserWindow is a main-process API. If this gets called from preload/renderer
|
// BrowserWindow is a main-process API. If this gets called from preload/renderer
|
||||||
@@ -9,13 +50,67 @@ async function newWindow(url: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfWindow = new BrowserWindow({
|
const pdfWindow = new BrowserWindow({
|
||||||
|
title: "Loading…",
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const view = new BrowserView({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
plugins: true, // Enable PDF viewing
|
plugins: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await pdfWindow.loadURL(url);
|
const attachView = (): void => {
|
||||||
pdfWindow.focus();
|
const [width, height] = pdfWindow.getContentSize();
|
||||||
|
pdfWindow.setBrowserView(view);
|
||||||
|
view.setBounds({ x: 0, y: 0, width, height });
|
||||||
|
view.setAutoResize({ width: true, height: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
attachView();
|
||||||
|
|
||||||
|
await view.webContents.loadURL(toDataUrl(loadingHtml));
|
||||||
|
pdfWindow.setProgressBar(2);
|
||||||
|
|
||||||
|
const showError = async (message: string): Promise<void> => {
|
||||||
|
pdfWindow.setProgressBar(-1);
|
||||||
|
await view.webContents.loadURL(
|
||||||
|
toDataUrl(`<!doctype html><meta charset="utf-8" />
|
||||||
|
<title>Failed to load</title>
|
||||||
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; padding: 16px;">
|
||||||
|
<h3 style="margin: 0 0 8px 0;">Failed to load</h3>
|
||||||
|
<div style="color: rgba(0,0,0,0.65);">${message.replaceAll("<", "<").replaceAll(">", ">")}</div>
|
||||||
|
</body>`),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTitle = (maybeTitle?: string): void => {
|
||||||
|
const title = (maybeTitle ?? view.webContents.getTitle() ?? "").trim();
|
||||||
|
pdfWindow.setTitle(title || url);
|
||||||
|
};
|
||||||
|
|
||||||
|
view.webContents.on("page-title-updated", (_event, title) => {
|
||||||
|
updateTitle(title);
|
||||||
|
});
|
||||||
|
|
||||||
|
view.webContents.on("did-start-loading", () => {
|
||||||
|
pdfWindow.setTitle("Loading…");
|
||||||
|
pdfWindow.setProgressBar(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
view.webContents.on("did-stop-loading", () => {
|
||||||
|
pdfWindow.setProgressBar(-1);
|
||||||
|
updateTitle();
|
||||||
|
pdfWindow.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
view.webContents.on("did-fail-load", async (_event, _code, description) => {
|
||||||
|
await showError(description || "Unknown error");
|
||||||
|
});
|
||||||
|
|
||||||
|
view.webContents.loadURL(url).catch(async (err) => {
|
||||||
|
await showError(err instanceof Error ? err.message : String(err));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default newWindow;
|
export default newWindow;
|
||||||
|
|||||||
Reference in New Issue
Block a user