Tray improvements.
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "esdp",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.1-alpha.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "esdp",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.1-alpha.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.1.6",
|
||||
@@ -19,6 +19,7 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.8.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"source-map-support": "^0.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -32,6 +33,7 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
@@ -5269,6 +5271,16 @@
|
||||
"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": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
@@ -12221,6 +12233,15 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.8.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"source-map-support": "^0.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -49,6 +50,7 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"antd": "^6.3.1",
|
||||
|
||||
@@ -151,19 +151,9 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
});
|
||||
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({
|
||||
title: "Job Scrubbed",
|
||||
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`,
|
||||
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}.`,
|
||||
actions: [
|
||||
{ text: "View in App", 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 ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
||||
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;
|
||||
|
||||
@@ -497,8 +506,15 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
|
||||
//Create Tray
|
||||
const trayicon = nativeImage.createFromPath(appIconToUse);
|
||||
const tray = new Tray(trayicon.resize({ width: 16 }));
|
||||
const trayBase = nativeImage
|
||||
.createFromPath(appIconToUse)
|
||||
.resize({ width: 16, height: 16 });
|
||||
const tray = new Tray(trayBase);
|
||||
|
||||
setTrayInstance(tray);
|
||||
setTrayBaseImage(trayBase);
|
||||
setWatcherTrayStatus(false);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show App",
|
||||
@@ -568,6 +584,7 @@ app.whenReady().then(async () => {
|
||||
isKeepAliveLaunch = true;
|
||||
}
|
||||
|
||||
StartWatcher();
|
||||
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||
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 { BrowserWindow, Notification } from "electron";
|
||||
import { Notification } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -8,6 +8,7 @@ import ipcTypes from "../../util/ipcTypes.json";
|
||||
import ImportJob from "../decoder/decoder";
|
||||
import store from "../store/store";
|
||||
import getMainWindow from "../../util/getMainWindow";
|
||||
import { setWatcherTrayStatus } from "../util/trayStatus";
|
||||
let watcher: FSWatcher | null;
|
||||
|
||||
async function StartWatcher(): Promise<boolean> {
|
||||
@@ -78,6 +79,7 @@ async function StartWatcher(): Promise<boolean> {
|
||||
// })
|
||||
.on("error", function (error) {
|
||||
log.error("Error in Watcher", errorTypeCheck(error));
|
||||
setWatcherTrayStatus(false);
|
||||
// mainWindow.webContents.send(
|
||||
// ipcTypes.toRenderer.watcher.error,
|
||||
// errorTypeCheck(error)
|
||||
@@ -114,6 +116,7 @@ function onWatcherReady(): void {
|
||||
body: "Newly exported estimates will be automatically uploaded.",
|
||||
}).show();
|
||||
log.info("Confirmed watched paths:", watcher.getWatched());
|
||||
setWatcherTrayStatus(true);
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
|
||||
}
|
||||
}
|
||||
@@ -124,6 +127,7 @@ async function StopWatcher(): Promise<boolean> {
|
||||
if (watcher) {
|
||||
await watcher.close();
|
||||
log.info("Watcher stopped.");
|
||||
setWatcherTrayStatus(false);
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
||||
|
||||
new Notification({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Badge,
|
||||
Card,
|
||||
Col,
|
||||
Flex,
|
||||
@@ -25,7 +26,10 @@ import {
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 dayjs from "dayjs";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -80,6 +84,8 @@ const Home: FC = () => {
|
||||
const { token } = theme.useToken();
|
||||
const ipcRenderer = window.electron.ipcRenderer;
|
||||
|
||||
const isWatcherStarted = useAppSelector(selectWatcherStatus);
|
||||
|
||||
const [history, setHistory] = useState<ScrubHistoryItem[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [totalJobs, setTotalJobs] = useState<number>(0);
|
||||
@@ -231,20 +237,20 @@ const Home: FC = () => {
|
||||
<Text strong>{record.results?.length ?? 0}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
key: "status",
|
||||
width: 120,
|
||||
render: () => (
|
||||
<Tag
|
||||
color="success"
|
||||
icon={<CheckCircleOutlined />}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
Done
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: "Status",
|
||||
// key: "status",
|
||||
// width: 120,
|
||||
// render: () => (
|
||||
// <Tag
|
||||
// color="success"
|
||||
// icon={<CheckCircleOutlined />}
|
||||
// style={{ margin: 0 }}
|
||||
// >
|
||||
// Done
|
||||
// </Tag>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
@@ -309,14 +315,24 @@ const Home: FC = () => {
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
{t("dashboard.labels.dashboard")}
|
||||
</Title>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => navigate("/settings")}
|
||||
size="large"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Space align="center" size="middle">
|
||||
<Badge
|
||||
status={isWatcherStarted ? "success" : "error"}
|
||||
text={
|
||||
isWatcherStarted
|
||||
? t("settings.labels.started")
|
||||
: t("settings.labels.stopped")
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => navigate("/settings")}
|
||||
size="large"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -374,7 +390,7 @@ const Home: FC = () => {
|
||||
}
|
||||
value={
|
||||
lastProcessed
|
||||
? new Date(lastProcessed).toLocaleTimeString()
|
||||
? dayjs(lastProcessed).format("MM/DD/YYYY @ hh:mm a")
|
||||
: "—"
|
||||
}
|
||||
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> {
|
||||
// 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({
|
||||
title: "Loading…",
|
||||
show: true,
|
||||
});
|
||||
|
||||
const view = new BrowserView({
|
||||
webPreferences: {
|
||||
plugins: true, // Enable PDF viewing
|
||||
plugins: true,
|
||||
},
|
||||
});
|
||||
|
||||
await pdfWindow.loadURL(url);
|
||||
pdfWindow.focus();
|
||||
const attachView = (): void => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user