Files
homeassistant-desktop/app.js
mrvnklm df325082fe v1.4.8 - add fullscreen mode
fixes window mispositioning on launch

added #59
2021-11-10 10:16:36 +01:00

661 lines
17 KiB
JavaScript

const {
app,
dialog,
ipcMain,
shell,
globalShortcut,
screen,
net,
Menu,
Tray,
BrowserWindow,
} = require("electron");
const { autoUpdater } = require("electron-updater");
const AutoLaunch = require("auto-launch");
const Positioner = require("electron-traywindow-positioner");
const Store = require("electron-store");
const bonjour = require("bonjour")();
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const registerKeyboardShortcut = () => {
globalShortcut.register("CommandOrControl+Alt+X", () => {
if (window.isVisible()) window.hide();
else showWindow();
});
};
const unregisterKeyboardShortcut = () => {
globalShortcut.unregisterAll();
};
app.allowRendererProcessReuse = true;
// prevent multiple instances
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
app.on("second-instance", () => {
if (window) showWindow();
});
}
// hide dock icon on macOS
if (process.platform === "darwin") app.dock.hide();
const store = new Store();
const autoLauncher = new AutoLaunch({ name: "Home Assistant Desktop" });
const indexFile = `file://${__dirname}/web/index.html`;
const errorFile = `file://${__dirname}/web/error.html`;
let autostartEnabled = false;
let forceQuit = false;
let resizeEvent = false;
const useAutoUpdater = () => {
autoUpdater.on("error", (message) => {
console.error("There was a problem updating the application");
console.error(message);
});
autoUpdater.on("update-downloaded", () => {
forceQuit = true;
autoUpdater.quitAndInstall();
});
setInterval(() => {
if (store.get("autoUpdate")) autoUpdater.checkForUpdates();
}, 1000 * 60 * 60);
if (store.get("autoUpdate")) autoUpdater.checkForUpdates();
};
const checkAutoStart = () => {
autoLauncher
.isEnabled()
.then((isEnabled) => {
autostartEnabled = isEnabled;
})
.catch((err) => {
console.error(err);
});
};
const startAvailabilityCheck = () => {
setInterval(() => {
const request = net.request(`${currentInstance()}/auth/providers`);
request.on("response", (response) => {
showError(response.statusCode !== 200);
});
request.on("error", (error) => {
showError(true);
if (store.get("automaticSwitching")) checkForAvailableInstance();
});
request.end();
}, 3000);
};
const changePosition = () => {
const trayBounds = tray.getBounds();
const windowBounds = window.getBounds();
const displayWorkArea = screen.getDisplayNearestPoint({
x: trayBounds.x,
y: trayBounds.y,
}).workArea;
const taskBarPosition = Positioner.getTaskbarPosition(trayBounds);
if (taskBarPosition == "top" || taskBarPosition == "bottom") {
const alignment = {
x: "center",
y: taskBarPosition == "top" ? "up" : "down",
};
if (
trayBounds.x + (trayBounds.width + windowBounds.width) / 2 <
displayWorkArea.width
)
Positioner.position(window, trayBounds, alignment);
else {
const { y } = Positioner.calculate(
window.getBounds(),
trayBounds,
alignment
);
window.setPosition(
displayWorkArea.width - windowBounds.width + displayWorkArea.x,
y + (taskBarPosition == "bottom" && displayWorkArea.y),
false
);
}
} else {
const alignment = { x: taskBarPosition, y: "center" };
if (
trayBounds.y + (trayBounds.height + windowBounds.height) / 2 <
displayWorkArea.height
) {
const { x, y } = Positioner.calculate(
window.getBounds(),
trayBounds,
alignment
);
window.setPosition(
x + (taskBarPosition == "right" && displayWorkArea.x),
y
);
} else {
const { x } = Positioner.calculate(
window.getBounds(),
trayBounds,
alignment
);
window.setPosition(
x,
displayWorkArea.y + displayWorkArea.height - windowBounds.height,
false
);
}
}
};
const checkForAvailableInstance = () => {
const instances = store.get("allInstances");
if (instances?.length > 1) {
bonjour.find({ type: "home-assistant" }, (instance) => {
if (instances.indexOf(instance.txt.internal_url) !== -1)
return currentInstance(instance.txt.internal_url);
if (instances.indexOf(instance.txt.external_url) !== -1)
return currentInstance(instance.txt.external_url);
});
let found;
for (let instance of instances.filter((e) => e.url !== currentInstance())) {
const request = net.request(`${instance}/auth/providers`);
request.on("response", (response) => {
if (response.statusCode === 200) {
found = instance;
}
});
request.on("error", (error) => {});
request.end();
if (found) {
currentInstance(found);
break;
}
}
}
};
const getMenu = () => {
let instancesMenu = [
{
label: "Open in Browser",
enabled: currentInstance(),
click: () => {
shell.openExternal(currentInstance());
},
},
{
type: "separator",
},
];
const allInstances = store.get("allInstances");
if (allInstances) {
allInstances.forEach((e) => {
instancesMenu.push({
label: e,
type: "checkbox",
checked: currentInstance() === e,
click: () => {
currentInstance(e);
window.loadURL(e);
window.show();
},
});
});
instancesMenu.push(
{
type: "separator",
},
{
label: "Add another Instance...",
click: () => {
store.delete("currentInstance");
window.loadURL(indexFile);
window.show();
},
},
{
label: "Automatic Switching",
type: "checkbox",
enabled:
store.has("allInstances") && store.get("allInstances").length > 1,
checked: store.get("automaticSwitching"),
click: () => {
store.set("automaticSwitching", !store.get("automaticSwitching"));
},
}
);
} else {
instancesMenu.push({ label: "Not Connected...", enabled: false });
}
return Menu.buildFromTemplate([
{
label: "Show/Hide Window",
visible: process.platform === "linux",
click: () => {
if (window.isVisible()) window.hide();
else showWindow();
},
},
{
visible: process.platform === "linux",
type: "separator",
},
...instancesMenu,
{
type: "separator",
},
{
label: "Hover to Show",
visible: process.platform !== "linux" && !store.get("detachedMode"),
enabled: !store.get("detachedMode"),
type: "checkbox",
checked: !store.get("disableHover"),
click: () => {
store.set("disableHover", !store.get("disableHover"));
},
},
{
label: "Stay on Top",
type: "checkbox",
checked: store.get("stayOnTop"),
click: () => {
store.set("stayOnTop", !store.get("stayOnTop"));
window.setAlwaysOnTop(store.get("stayOnTop"));
if (window.isAlwaysOnTop()) showWindow();
},
},
{
label: "Start at Login",
type: "checkbox",
checked: autostartEnabled,
click: () => {
if (autostartEnabled) autoLauncher.disable();
else autoLauncher.enable();
checkAutoStart();
},
},
{
label: `Enable Shortcut`,
type: "checkbox",
accelerator: "CommandOrControl+Alt+X",
checked: store.get("shortcutEnabled"),
click: () => {
store.set("shortcutEnabled", !store.get("shortcutEnabled"));
if (store.get("shortcutEnabled")) registerKeyboardShortcut();
else unregisterKeyboardShortcut();
},
},
{
type: "separator",
},
{
label: "Use detached Window",
type: "checkbox",
checked: store.get("detachedMode"),
click: () => {
store.set("detachedMode", !store.get("detachedMode"));
window.hide();
createMainWindow(store.get("detachedMode"));
},
},
{
label: "Use Fullscreen",
type: "checkbox",
checked: window.isFullScreen(),
accelerator: "CommandOrControl+Alt+Return",
click: () => {
toggleFullScreen();
},
},
{
type: "separator",
},
{
label: `v${app.getVersion()}`,
enabled: false,
},
{
label: "Automatic Updates",
type: "checkbox",
checked: store.get("autoUpdate"),
click: () => {
store.set("autoUpdate", !store.get("autoUpdate"));
},
},
{
label: "Open on github.com",
click: () => {
shell.openExternal("https://github.com/mrvnklm/homeassistant-desktop");
},
},
{
type: "separator",
},
{
label: "Reload Window",
click: () => {
window.reload();
window.show();
window.focus();
},
},
{
label: "Reset Application...",
click: () => {
dialog
.showMessageBox({
message: "Are you sure you want to reset Home Assistant Desktop?",
buttons: ["Reset Everything!", "Reset Windows", "Cancel"],
})
.then((res) => {
if (res.response === 0) {
store.clear();
window.webContents.session.clearCache();
window.webContents.session.clearStorageData();
app.relaunch();
app.exit();
}
if (res.response === 1) {
store.delete("windowSizeDetached");
store.delete("windowSize");
store.delete("windowPosition");
store.delete("fullScreen");
store.delete("detachedMode");
app.relaunch();
app.exit();
}
});
},
},
{
type: "separator",
},
{
label: "Quit",
click: () => {
forceQuit = true;
app.quit();
},
},
]);
};
const createMainWindow = (show = false) => {
window = new BrowserWindow({
width: 420,
height: 420,
show: false,
skipTaskbar: true,
autoHideMenuBar: true,
frame: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
// window.webContents.openDevTools();
window.loadURL(indexFile);
// open extenal links in default browser
window.webContents.on("new-window", function (e, url) {
e.preventDefault();
require("electron").shell.openExternal(url);
});
// hide scrollbar
window.webContents.on("did-finish-load", function () {
window.webContents.insertCSS(
"::-webkit-scrollbar { display: none; } body { -webkit-user-select: none; }"
);
if (store.get("detachedMode") && process.platform === "darwin") {
window.webContents.insertCSS("body { -webkit-app-region: drag; }");
}
// let code = `document.addEventListener("mousemove", () => { ipcRenderer.send("mousemove"); });`;
// window.webContents.executeJavaScript(code);
});
if (store.get("detachedMode")) {
if (store.has("windowPosition"))
window.setSize(...store.get("windowSizeDetached"));
else store.set("windowPosition", window.getPosition());
if (store.has("windowSizeDetached"))
window.setPosition(...store.get("windowPosition"));
else store.set("windowSizeDetached", window.getSize());
} else {
if (store.has("windowSize")) window.setSize(...store.get("windowSize"));
else store.set("windowSize", window.getSize());
}
window.on("resize", (e) => {
// ignore resize event when using fullscreen mode
if (window.isFullScreen()) return e;
if (!store.get("disableHover") || resizeEvent) {
store.set("disableHover", true);
resizeEvent = e;
setTimeout(() => {
if (resizeEvent === e) {
store.set("disableHover", false);
resizeEvent = false;
}
}, 600);
}
if (store.get("detachedMode")) {
store.set("windowSizeDetached", window.getSize());
} else {
if (process.platform !== "linux") changePosition();
store.set("windowSize", window.getSize());
}
});
window.on("move", () => {
if (store.get("detachedMode")) {
store.set("windowPosition", window.getPosition());
}
});
window.on("close", (e) => {
if (!forceQuit) {
window.hide();
e.preventDefault();
}
});
window.on("blur", () => {
if (!store.get("detachedMode") && !window.isAlwaysOnTop()) window.hide();
});
window.setAlwaysOnTop(!!store.get("stayOnTop"));
if (window.isAlwaysOnTop() || show) showWindow();
toggleFullScreen(!!store.get("fullScreen"));
};
const showWindow = () => {
if (!store.get("detachedMode")) changePosition();
if (!window.isVisible()) {
window.setVisibleOnAllWorkspaces(true); // put the window on all screens
window.show();
window.focus();
window.setVisibleOnAllWorkspaces(false); // disable all screen behavior
}
};
const createTray = () => {
tray = new Tray(
["win32", "linux"].includes(process.platform)
? `${__dirname}/assets/IconWin.png`
: `${__dirname}/assets/IconTemplate.png`
);
tray.on("click", () => {
if (window.isVisible()) window.hide();
else showWindow();
});
tray.on("right-click", () => {
if (!store.get("detachedMode")) window.hide();
tray.popUpContextMenu(getMenu());
});
let timer = undefined;
tray.on("mouse-move", (e) => {
if (
store.get("detachedMode") ||
window.isAlwaysOnTop() ||
store.get("disableHover")
) {
return;
}
if (!window.isVisible()) {
showWindow();
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
let mousePos = screen.getCursorScreenPoint();
let trayBounds = tray.getBounds();
if (
!(
mousePos.x >= trayBounds.x &&
mousePos.x <= trayBounds.x + trayBounds.width
) ||
!(
mousePos.y >= trayBounds.y &&
mousePos.y <= trayBounds.y + trayBounds.height
)
) {
setWindowFocusTimer();
}
}, 100);
});
if (process.platform === "linux") tray.setContextMenu(getMenu());
};
const setWindowFocusTimer = () => {
let timer = setTimeout(() => {
let mousePos = screen.getCursorScreenPoint();
let windowPosition = window.getPosition();
let windowSize = window.getSize();
if (
!resizeEvent &&
(!(
mousePos.x >= windowPosition[0] &&
mousePos.x <= windowPosition[0] + windowSize[0]
) ||
!(
mousePos.y >= windowPosition[1] &&
mousePos.y <= windowPosition[1] + windowSize[1]
))
) {
window.hide();
} else {
setWindowFocusTimer();
}
}, 110);
};
app.on("ready", async () => {
checkAutoStart();
useAutoUpdater();
createTray();
// workaround for initial window misplacement due to traybounds being incorrect
while (tray.getBounds().x === 0 || process.uptime() >= 1) {
await delay(15);
}
createMainWindow(!store.has("currentInstance"));
startAvailabilityCheck();
// register shortcut
if (store.get("shortcutEnabled")) registerKeyboardShortcut();
globalShortcut.register("CommandOrControl+Alt+Return", () => {
toggleFullScreen();
});
// disable hover for first start
if (!store.has("currentInstance")) store.set("disableHover", true);
// enable auto update by default
if (!store.has("autoUpdate")) store.set("autoUpdate", true);
});
app.on("will-quit", () => {
unregisterKeyboardShortcut();
});
const toggleFullScreen = (mode = !window.isFullScreen()) => {
store.set("fullScreen", mode);
window.setFullScreen(mode);
if (mode) window.setAlwaysOnTop(true);
else window.setAlwaysOnTop(store.get("stayOnTop"));
};
const currentInstance = (url = null) => {
if (url) {
store.set("currentInstance", store.get("allInstances").indexOf(url));
}
if (store.has("currentInstance")) {
return store.get("allInstances")[store.get("currentInstance")];
}
return false;
};
const addInstance = (url) => {
if (!store.has("allInstances")) store.set("allInstances", []);
let instances = store.get("allInstances");
if (instances.find((e) => e === url)) {
currentInstance(url);
return;
}
// active hover by default after adding first instance
if (!instances.length) store.set("disableHover", false);
instances.push(url);
store.set("allInstances", instances);
currentInstance(url);
};
const showError = (isError) => {
if (!isError && window.webContents.getURL().includes("error.html"))
window.loadURL(indexFile);
if (
isError &&
currentInstance() &&
!window.webContents.getURL().includes("error.html")
)
window.loadURL(errorFile);
};
ipcMain.on("get-instances", (event) => {
event.reply("get-instances", store.get("allInstances") || []);
});
ipcMain.on("ha-instance", (event, url) => {
if (url) addInstance(url);
if (currentInstance()) event.reply("ha-instance", currentInstance());
});