Skip to content

Commit

Permalink
Refactor analytics and deep-linking code
Browse files Browse the repository at this point in the history
  • Loading branch information
JustJoostNL committed Feb 24, 2024
1 parent f054751 commit e581ca2
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 50 deletions.
2 changes: 0 additions & 2 deletions src/main/analytics/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export async function handleRegisterUser(): Promise<{
}

analyticsPostInterval = setInterval(async () => {
console.log("Posting user active");
await handlePostUserActive();
}, 15000); // 15 seconds

Expand All @@ -54,7 +53,6 @@ export async function handleUserActiveExit(): Promise<any | null> {

if (userActiveId && analyticsPostInterval) {
clearInterval(analyticsPostInterval);
console.log("Cleared analyticsPostInterval");
const res = await fetch(postUrl, {
method: "POST",
headers: {
Expand Down
102 changes: 102 additions & 0 deletions src/main/deeplinking/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { dialog } from "electron";
import { openInMainWindow } from "../utils/openInMainWindow";
import { getConfig, patchConfig } from "../ipc/config";
import {
ActionType,
Event,
eventTypeReadableMap,
} from "../../shared/config/config_types";

export async function handleOpenWhitelisted(
url: URL,
_groups: RegExpMatchArray | null,
) {
await openInMainWindow(`${url.pathname}${url.search}`);
}

export async function handlePatchConfig(
url: URL,
_groups: RegExpMatchArray | null,
) {
try {
const config = JSON.parse(url.searchParams.get("config") ?? "{}");

if (!config) return;

const { response } = await dialog.showMessageBox({
type: "question",
defaultId: 0,
buttons: ["Apply config changes", "Cancel"],
title: "Do you want to apply config changes?",
message: "Do you want to apply config changes?",
detail: `Config patch:\n\n${JSON.stringify(config, null, 2)}`,
});

if (response === 1) return;

await patchConfig(config);
} catch (e) {
dialog.showErrorBox("Error", "Invalid config");
}
}

export async function handleAddEvent(
url: URL,
_groups: RegExpMatchArray | null,
) {
const getReadableEvent = (event: Event) => {
const name = event.name;
const triggers = event.triggers
.map((trigger) => eventTypeReadableMap[trigger])
.join(", ");

const actions = event.actions
.map((action, index) => {
let actionString = `${index + 1}. ${action.type}`;
switch (action.type) {
case ActionType.On:
actionString += `: ${action.color ? `RGB = ${action.color.r}, ${action.color.g}, ${action.color.b}` : "N/A"}, Brightness = ${action.brightness ?? "N/A"}`;
break;
case ActionType.Off:
break;
case ActionType.Delay:
actionString += `: ${action.delay ?? "N/A"}ms`;
break;
case ActionType.GoBackToCurrentStatus:
break;
default:
break;
}
return actionString;
})
.join("\n");

return `Event:\n\nName: ${name}\nTriggers: ${triggers}\nActions:\n${actions}`;
};

try {
const event = JSON.parse(url.searchParams.get("event") ?? "{}");

if (!event) return;

const { response } = await dialog.showMessageBox({
type: "question",
defaultId: 0,
buttons: ["Apply config changes", "Cancel"],
title: "Add shared event?",
message: "Do you want to add this shared event to your config?",
detail: getReadableEvent(event),
});

if (response === 1) return;

const config = await getConfig();
const events = config.events ?? [];
const nextId = Math.max(...events.map((e) => e.id)) + 1;
await patchConfig({
events: [...events, { ...event, id: nextId }],
});
} catch (e) {
dialog.showErrorBox("Error", "Invalid config");
}
}
107 changes: 107 additions & 0 deletions src/main/deeplinking/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import path from "path";
import { app, ipcMain } from "electron";
import { createMainWindow, mainWindow } from "..";
import {
handleAddEvent,
handleOpenWhitelisted,
handlePatchConfig,
} from "./app";

interface IRoute {
host: string;
match: RegExp;
handler: (uri: URL, groups: RegExpMatchArray | null) => void;
}

const protocols = ["f1mvlightsintegration", "f1mvli", "f1mvlights"];
const uriRegex = new RegExp(`^(${protocols.join("|")})://(.*)$`);

const router: IRoute[] = [
{
host: "app",
match: /^\/settings/i,
handler: handleOpenWhitelisted,
},
{
host: "app",
match: /^\/config\/patch/i,
handler: handlePatchConfig,
},
{
host: "app",
match: /^\/event-editor/i,
handler: handleOpenWhitelisted,
},
{
host: "app",
match: /^\/config\/add-event/i,
handler: handleAddEvent,
},
];

function handleOpenURL(event: Electron.Event | undefined, url: string) {
event?.preventDefault();

const uri = new URL(url);
const { pathname, host } = uri;

const route = router.find((r) => r.host === host && r.match.test(pathname));
if (!route) return;

route.handler(uri, pathname.match(route.match));
}

function handleSecondInstance(event: Electron.Event, argv: string[]) {
if (process.platform === "win32" || process.platform === "linux") {
const uri = argv.find((arg) => uriRegex.test(arg));
if (uri) handleOpenURL(event, uri);
}

if (!mainWindow) createMainWindow();
if (mainWindow?.isMinimized()) mainWindow.restore();

mainWindow?.focus();
}

export function handleDeepLink(argv: string[]) {
const uri = argv.find((arg) => uriRegex.test(arg));
if (uri) handleOpenURL(undefined, uri);
}

export function registerDeepLink() {
try {
if (process.env.NODE_ENV !== "development") {
if (process.defaultApp) {
if (process.argv.length >= 2) {
protocols.forEach((protocol) => {
app.setAsDefaultProtocolClient(protocol, process.execPath, [
path.resolve(process.argv[1]),
]);
});
}
} else {
protocols.forEach((protocol) => {
app.setAsDefaultProtocolClient(protocol);
});
}
}

app.on("open-url", handleOpenURL);
app.on("second-instance", handleSecondInstance);

ipcMain.handle("f1mvli:deep-link:open-url", handleOpenURL);
} catch (e: any) {
console.error("Failed to register F1MV Lights Integration protocol:", e);
}

return () => {
app.off("open-url", handleOpenURL);
app.off("second-instance", handleSecondInstance);

ipcMain.removeHandler("f1mvli:deep-link:open-url");

protocols.forEach((protocol) => {
app.removeAsDefaultProtocolClient(protocol);
});
};
}
72 changes: 25 additions & 47 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { registerEventManagerIPCHandlers } from "./ipc/eventManager";
import { registerIntegrationsIPCHandlers } from "./ipc/integrations";
import { initializeIntegrations } from "./initIntegrations";
import { handleRegisterUser, handleUserActiveExit } from "./analytics/api";
import { registerDeepLink } from "./deeplinking";

Sentry.init({
dsn: "https://e64c3ec745124566b849043192e58711@o4504289317879808.ingest.sentry.io/4504289338392576",
Expand All @@ -31,39 +32,12 @@ Sentry.init({
tracesSampleRate: 1.0,
});

// handle deep-link protocol
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient("f1mvli", process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient("f1mvli");
}

// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();

// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());

app.on("open-url", async (event) => {
event.preventDefault();
if (win) {
win.show();
win.focus();
}
});

app.on("second-instance", async () => {
if (win) {
if (win.isMinimized()) win.restore();
win.show();
win.focus();
}
});

if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
Expand All @@ -78,13 +52,13 @@ process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
// Remove electron security warnings
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

let win: BrowserWindow | null = null;
export let mainWindow: BrowserWindow | null = null;
export let availablePort: number | null = null;
export const preload = path.join(__dirname, "../preload/index.js");
export const url = process.env.VITE_DEV_SERVER_URL;
export const devServerUrl = process.env.VITE_DEV_SERVER_URL;

async function createWindow() {
win = new BrowserWindow({
export async function createMainWindow() {
mainWindow = new BrowserWindow({
title: "F1MV Lights Integration",
icon: join(process.env.PUBLIC, "favicon.ico"),
width: 1100,
Expand All @@ -104,11 +78,11 @@ async function createWindow() {

if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
await win.loadURL(url);
win.webContents.openDevTools({ mode: "detach" });
win.setMenuBarVisibility(false);
await mainWindow.loadURL(devServerUrl);
mainWindow.webContents.openDevTools({ mode: "detach" });
mainWindow.setMenuBarVisibility(false);
} else {
win.setMenuBarVisibility(false);
mainWindow.setMenuBarVisibility(false);
availablePort = await portfinder.getPortPromise({
port: 30303,
host: "localhost",
Expand All @@ -119,22 +93,25 @@ async function createWindow() {
});
});
server.listen(availablePort, () => {
win?.loadURL(`http://localhost:${availablePort}/index.html`);
mainWindow?.loadURL(`http://localhost:${availablePort}/index.html`);
});
}

win.webContents.setWindowOpenHandler(({ url }) => {
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:" || "http:")) shell.openExternal(url);
return { action: "deny" };
});
win.webContents.on("will-navigate", (event: Electron.Event, url: string) => {
if (url.startsWith("https:" || "http:") && !url.includes("localhost")) {
event.preventDefault();
shell.openExternal(url);
}
});
mainWindow.webContents.on(
"will-navigate",
(event: Electron.Event, url: string) => {
if (url.startsWith("https:" || "http:") && !url.includes("localhost")) {
event.preventDefault();
shell.openExternal(url);
}
},
);

win.webContents.setZoomLevel(0);
mainWindow.webContents.setZoomLevel(0);
}

let _configIPCCleanup: () => void;
Expand All @@ -148,7 +125,7 @@ let _integrationsIPCCleanup: () => void;
app.whenReady().then(onReady);

function onReady() {
createWindow();
createMainWindow();

_configIPCCleanup = registerConfigIPCHandlers();
_updaterIPCCleanup = registerUpdaterIPCHandlers();
Expand All @@ -172,6 +149,7 @@ function onReady() {
}
log.transports.file.level = globalConfig.debugMode ? "debug" : "info";
log.info("App starting...");
registerDeepLink();
startLiveTimingDataPolling();
fetchAuthoritativeConfig();
setInterval(
Expand All @@ -187,7 +165,7 @@ function onReady() {

app.on("window-all-closed", async () => {
await handleUserActiveExit();
win = null;
mainWindow = null;

//todo: add cleanup for integrations

Expand All @@ -199,6 +177,6 @@ app.on("activate", () => {
if (allWindows.length) {
allWindows[0].focus();
} else {
createWindow();
createMainWindow();
}
});
9 changes: 9 additions & 0 deletions src/main/utils/openInMainWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { mainWindow, availablePort } from "..";

export async function openInMainWindow(pathname: string) {
const baseUrl =
process.env.VITE_DEV_SERVER_URL || `http://localhost:${availablePort}`;
const url = new URL(baseUrl);
url.hash = `#${pathname}${url.search}`;
await mainWindow?.loadURL(url.toString());
}
5 changes: 4 additions & 1 deletion src/renderer/pages/EventEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function EventEditorPage() {
(id: GridRowId) => {
const event = config.events.find((event) => event.id === id);
if (!event) return;
window.navigator.clipboard.writeText(JSON.stringify(event));
const url =
"https://api.jstt.me/api/v2/f1mvli/go/app/config/add-event?event=" +
JSON.stringify(event);
window.navigator.clipboard.writeText(url);
enqueueSnackbar("Event url copied to clipboard", { variant: "success" });
},
[config],
Expand Down

0 comments on commit e581ca2

Please sign in to comment.