feat: initial commit

This commit is contained in:
izzy
2025-09-30 10:31:37 -05:00
commit 7ab6795a88
31 changed files with 7913 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/electron",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser"
}
+92
View File
@@ -0,0 +1,92 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
+4
View File
@@ -0,0 +1,4 @@
[submodule "assets"]
path = assets
url = https://github.com/stoatchat/assets
update = none
+15
View File
@@ -0,0 +1,15 @@
{
"tabWidth": 2,
"useTabs": false,
"plugins": [
"@trivago/prettier-plugin-sort-imports"
],
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^electron",
"^\\.\\.",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
+22
View File
@@ -0,0 +1,22 @@
{
"editor.formatOnSave": true,
"files.exclude": {
"**/.vite": true,
"**/node_modules": true,
"**/pnpm-lock.yaml": true,
"**/tsconfig.json": false
},
"editor.detectIndentation": false,
"editor.insertSpaces": true,
"editor.tabSize": 2,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
}
Submodule
+1
Submodule assets added at 3a0d29a0e7
+25
View File
@@ -0,0 +1,25 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell rec {
buildInputs = [
# Tools
pkgs.git
pkgs.gh
# Node
pkgs.nodejs
pkgs.nodejs.pkgs.pnpm
# build target: deb
pkgs.dpkg
pkgs.fakeroot
# build target: flatpak
pkgs.flatpak
pkgs.flatpak-builder
pkgs.elfutils
# flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
];
}
+148
View File
@@ -0,0 +1,148 @@
import { MakerAppX } from "@electron-forge/maker-appx";
import { MakerDeb } from "@electron-forge/maker-deb";
import { MakerFlatpak } from "@electron-forge/maker-flatpak";
import { MakerFlatpakOptionsConfig } from "@electron-forge/maker-flatpak/dist/Config";
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { PublisherGithub } from "@electron-forge/publisher-github";
import type { ForgeConfig } from "@electron-forge/shared-types";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { globSync } from "node:fs";
const STRINGS = {
name: "Stoat",
execName: "stoat-desktop",
description: "Open source user-first chat platform.",
};
const ASSET_DIR = "assets/desktop";
const config: ForgeConfig = {
packagerConfig: {
asar: true,
name: STRINGS.name,
executableName: STRINGS.execName,
icon: `${ASSET_DIR}/icon`,
extraResource: [
// include all the asset files
...globSync(ASSET_DIR + "/**/*"),
],
},
rebuildConfig: {},
makers: [
new MakerAppX({}),
new MakerSquirrel({
iconUrl: `${ASSET_DIR}/icon.ico`,
}),
new MakerFlatpak({
options: {
id: "chat.stoat.stoat-desktop",
description: STRINGS.description,
productName: STRINGS.name,
productDescription: STRINGS.description,
runtimeVersion: "21.08",
icon: `${ASSET_DIR}/icon.png`,
categories: ["Network"],
modules: [
// use the latest zypak -- Electron sandboxing for Flatpak
{
name: "zypak",
sources: [
{
type: "git",
url: "https://github.com/refi64/zypak",
tag: "v2025.09",
},
],
},
],
finishArgs: [
// default arguments found by running
// DEBUG=electron-installer-flatpak* pnpm make
"--socket=x11",
"--share=ipc",
"--device=dri",
"--socket=pulseaudio",
"--filesystem=home",
"--env=TMPDIR=/var/tmp",
"--share=network",
"--talk-name=org.freedesktop.Notifications",
// add Unity talk name for badges
"--talk-name=com.canonical.Unity",
],
// files: [
// // is this necessary?
// // https://stackoverflow.com/q/79745700
// ...[16, 32, 64, 128, 256, 512].map(
// (size) =>
// [
// `assets/desktop/hicolor/${size}x${size}.png`,
// `/app/share/icons/hicolor/${size}x${size}/apps/chat.stoat.stoat-desktop.png`,
// ] as [string, string],
// ),
// [
// `assets/desktop/icon.svg`,
// `/app/share/icons/hicolor/scalable/apps/chat.stoat.stoat-desktop.svg`,
// ] as [string, string],
// ],
files: [],
} as MakerFlatpakOptionsConfig,
/* as Omit<
MakerFlatpakOptionsConfig,
"files"
> */
}),
new MakerDeb({
options: {
icon: `${ASSET_DIR}/icon.png`,
},
}),
],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: "src/main.ts",
config: "vite.main.config.ts",
target: "main",
},
{
entry: "src/preload.ts",
config: "vite.preload.config.ts",
target: "preload",
},
],
renderer: [
{
name: "main_window",
config: "vite.renderer.config.ts",
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
publishers: [
new PublisherGithub({
repository: {
owner: "stoatchat",
name: "for-desktop",
},
}),
],
};
export default config;
+1
View File
@@ -0,0 +1 @@
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>
<body>
<h1>💖 Hello World!</h1>
<p>Welcome to your Electron application.</p>
<script type="module" src="/src/renderer.ts"></script>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
{
"name": "stoat-desktop",
"productName": "stoat-desktop",
"version": "1.0.0",
"description": "My Electron application description",
"main": ".vite/build/main.js",
"repo": "stoatchat/desktop",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint --ext .ts,.tsx .",
"install:flatpak": "flatpak --user install out/make/flatpak/x86_64/chat.stoat.stoat-desktop_stable_x86_64.flatpak",
"run:flatpak": "flatpak run --socket=session-bus chat.stoat.stoat-desktop"
},
"keywords": [],
"author": {
"name": "izzy",
"email": "me@insrt.uk"
},
"license": "MIT",
"devDependencies": {
"@electron-forge/cli": "^7.9.0",
"@electron-forge/maker-deb": "^7.9.0",
"@electron-forge/maker-flatpak": "^7.9.0",
"@electron-forge/maker-squirrel": "^7.9.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.9.0",
"@electron-forge/plugin-fuses": "^7.9.0",
"@electron-forge/plugin-vite": "^7.9.0",
"@electron-forge/publisher-github": "^7.9.0",
"@electron/fuses": "^1.8.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/auto-launch": "^5.0.5",
"@types/discord-rpc": "^4.0.9",
"@types/electron-squirrel-startup": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"electron": "38.1.2",
"eslint": "^8.57.1",
"eslint-plugin-import": "^2.32.0",
"json-schema-typed": "^8.0.1",
"prettier": "^3.6.2",
"typescript": "~4.5.4",
"vite": "^5.4.20"
},
"dependencies": {
"@electron-forge/maker-appx": "^7.9.0",
"@homebridge/dbus-native": "^0.7.2",
"auto-launch": "^5.0.6",
"discord-rpc": "^4.0.1",
"electron-squirrel-startup": "^1.0.1",
"electron-store": "^10.1.0",
"update-electron-app": "^3.1.1"
}
}
+6773
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- electron
- electron-winstaller
- esbuild
+11
View File
@@ -0,0 +1,11 @@
declare type DesktopConfig = {
firstLaunch: boolean;
customFrame: boolean;
minimiseToTray: boolean;
spellchecker: boolean;
hardwareAcceleration: boolean;
discordRpc: boolean;
windowState: {
isMaximised: boolean;
};
};
+8
View File
@@ -0,0 +1,8 @@
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
margin: auto;
max-width: 38rem;
padding: 2rem;
}
+102
View File
@@ -0,0 +1,102 @@
import { updateElectronApp } from "update-electron-app";
import { BrowserWindow, app, shell } from "electron";
import started from "electron-squirrel-startup";
import { autoLaunch } from "./native/autoLaunch";
import { config } from "./native/config";
import { initDiscordRpc } from "./native/discordRpc";
import { initTray } from "./native/tray";
import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
// Squirrel-specific logic
// create/remove shortcuts on Windows when installing / uninstalling
// we just need to close out of the app immediately
if (started) {
app.quit();
}
// disable hw-accel if so requested
if (!config.hardwareAcceleration) {
app.disableHardwareAcceleration();
}
// ensure only one copy of the application can run
const acquiredLock = app.requestSingleInstanceLock();
if (acquiredLock) {
// start auto update logic
// todo: updateElectronApp();
// create and configure the app when electron is ready
app.on("ready", () => {
// enable auto start on Windows and MacOS
if (config.firstLaunch) {
if (process.platform === "win32" || process.platform === "darwin") {
autoLaunch.enable();
}
}
// create window and application contexts
createMainWindow();
initTray();
initDiscordRpc();
// Windows specific fix for notifications
if (process.platform === "win32") {
app.setAppUserModelId("chat.stoat.notifications");
}
});
// focus the window if we try to launch again
app.on("second-instance", () => {
mainWindow.show();
mainWindow.restore();
mainWindow.focus();
});
// macOS specific behaviour to keep app active in dock:
// (irrespective of the minimise-to-tray option)
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
} else {
mainWindow.show();
mainWindow.focus();
}
});
// ensure URLs launch in external context
app.on("web-contents-created", (_, contents) => {
// prevent navigation out of build URL origin
contents.on("will-navigate", (event, navigationUrl) => {
if (new URL(navigationUrl).origin !== BUILD_URL.origin) {
event.preventDefault();
}
});
// handle links externally
contents.setWindowOpenHandler(({ url }) => {
if (
url.startsWith("http:") ||
url.startsWith("https:") ||
url.startsWith("mailto:")
) {
setImmediate(() => {
shell.openExternal(url);
});
}
return { action: "deny" };
});
});
} else {
app.quit();
}
+19
View File
@@ -0,0 +1,19 @@
import AutoLaunch from "auto-launch";
import { ipcMain } from "electron";
import { mainWindow } from "./window";
export const autoLaunch = new AutoLaunch({
name: "Revolt",
});
ipcMain.on("isAutostart?", () =>
autoLaunch
.isEnabled()
.then((enabled) => mainWindow.webContents.send("isAutostart", enabled)),
);
ipcMain.on("setAutostart", (state) =>
state ? autoLaunch.enable() : autoLaunch.disable(),
);
+66
View File
@@ -0,0 +1,66 @@
import dbus from "@homebridge/dbus-native";
import { resolve } from "node:path";
import { NativeImage, app, nativeImage } from "electron";
import { mainWindow } from "./window";
// internal state
const nativeIcons: Record<number, NativeImage> = {};
let sessionBus: dbus.MessageBus | null;
export async function setBadgeCount(count: number) {
switch (process.platform) {
case "win32":
case "linux":
if (count === 0) {
mainWindow.setOverlayIcon(null, "No Notifications");
break;
}
if (!nativeIcons[count])
nativeIcons[count] = nativeImage.createFromPath(
resolve(process.resourcesPath, `${Math.min(count, 10)}.ico`),
);
mainWindow.setOverlayIcon(
nativeIcons[count],
count === -1 ? `Unread Messages` : `${count} Notifications`,
);
break;
// @ts-expect-error this is `linux` block
case "_": // todo: try to get this to work
// send D-Bus message
// @ts-expect-error undocumented API
if (!sessionBus) sessionBus = dbus.sessionBus();
// @ts-expect-error undocumented API
sessionBus.connection.message({
// @ts-expect-error undocumented API
type: dbus.messageType.signal,
serial: 1,
path: "/",
interface: "com.canonical.Unity.LauncherEntry",
member: "Update",
signature: "sa{sv}",
body: [
process.env.container === "1"
? "application://chat.stoat.stoat-desktop.desktop" // flatpak handling
: "application://stoat-desktop.desktop",
[
["count", ["x", Math.min(count, 0)]],
["count-visible", ["b", count !== 0]],
],
],
});
break;
case "darwin":
app.dock.setBadge(
count === -1 ? "•" : count === 0 ? "" : count.toString(),
);
break;
}
}
+168
View File
@@ -0,0 +1,168 @@
import { type JSONSchema } from "json-schema-typed";
import { ipcMain } from "electron";
import Store from "electron-store";
import { destroyDiscordRpc, initDiscordRpc } from "./discordRpc";
import { mainWindow } from "./window";
const schema = {
firstLaunch: {
type: "boolean",
} as JSONSchema.Boolean,
customFrame: {
type: "boolean",
} as JSONSchema.Boolean,
minimiseToTray: {
type: "boolean",
} as JSONSchema.Boolean,
spellchecker: {
type: "boolean",
} as JSONSchema.Boolean,
hardwareAcceleration: {
type: "boolean",
} as JSONSchema.Boolean,
discordRpc: {
type: "boolean",
} as JSONSchema.Boolean,
windowState: {
type: "object",
properties: {
// x: {
// type: 'number'
// } as JSONSchema.Number,
// y: {
// type: 'number'
// } as JSONSchema.Number,
// width: {
// type: 'number'
// } as JSONSchema.Number,
// height: {
// type: 'number'
// } as JSONSchema.Number,
isMaximised: {
type: "boolean",
} as JSONSchema.Boolean,
},
} as JSONSchema.Object,
};
const store = new Store({
schema,
defaults: {
firstLaunch: true,
customFrame: true,
minimiseToTray: true,
spellchecker: true,
hardwareAcceleration: true,
discordRpc: true,
windowState: {
isMaximised: false,
},
} as DesktopConfig,
});
/**
* Shim for `electron-store` because typings are broken
*/
class Config {
get firstLaunch() {
return (store as never as { get(k: string): boolean }).get("firstLaunch");
}
set firstLaunch(value: boolean) {
(store as never as { set(k: string, value: boolean): void }).set(
"firstLaunch",
value,
);
}
get customFrame() {
return (store as never as { get(k: string): boolean }).get("customFrame");
}
set customFrame(value: boolean) {
(store as never as { set(k: string, value: boolean): void }).set(
"customFrame",
value,
);
}
get minimiseToTray() {
return (store as never as { get(k: string): boolean }).get(
"minimiseToTray",
);
}
set minimiseToTray(value: boolean) {
(store as never as { set(k: string, value: boolean): void }).set(
"minimiseToTray",
value,
);
}
get spellchecker() {
return (store as never as { get(k: string): boolean }).get("spellchecker");
}
set spellchecker(value: boolean) {
mainWindow.webContents.session.setSpellCheckerEnabled(value);
(store as never as { set(k: string, value: boolean): void }).set(
"spellchecker",
value,
);
}
get hardwareAcceleration() {
return (store as never as { get(k: string): boolean }).get(
"hardwareAcceleration",
);
}
set hardwareAcceleration(value: boolean) {
(store as never as { set(k: string, value: boolean): void }).set(
"hardwareAcceleration",
value,
);
}
get discordRpc() {
return (store as never as { get(k: string): boolean }).get("discordRpc");
}
set discordRpc(value: boolean) {
if (value) {
initDiscordRpc();
} else {
destroyDiscordRpc();
}
(store as never as { set(k: string, value: boolean): void }).set(
"discordRpc",
value,
);
}
get windowState() {
return (
store as never as { get(k: string): DesktopConfig["windowState"] }
).get("windowState");
}
set windowState(value: DesktopConfig["windowState"]) {
(
store as never as {
set(k: string, value: DesktopConfig["windowState"]): void;
}
).set("windowState", value);
}
}
export const config = new Config();
ipcMain.on("config", (newConfig) =>
Object.entries(newConfig).forEach(
([key, value]) => (config[key as keyof DesktopConfig] = value),
),
);
+42
View File
@@ -0,0 +1,42 @@
import { Client } from "discord-rpc";
import { config } from "./config";
// internal state
let rpc: Client;
export async function initDiscordRpc() {
if (!config.discordRpc) return;
try {
rpc = new Client({ transport: "ipc" });
rpc.on("ready", () =>
rpc.setActivity({
state: "stoat.chat",
details: "Chatting with others",
largeImageKey: "qr",
// largeImageText: "Communication is critical use Revolt.",
largeImageText: "",
buttons: [
{
label: "Join Stoat",
url: "https://stoat.chat/",
},
],
}),
);
rpc.on("disconnected", reconnect);
rpc.login({ clientId: "872068124005007420" });
} catch (err) {
reconnect();
}
}
const reconnect = () => setTimeout(() => initDiscordRpc(), 1e4);
export async function destroyDiscordRpc() {
rpc?.destroy();
}
+60
View File
@@ -0,0 +1,60 @@
import { resolve } from "node:path";
import { Menu, Tray, nativeImage } from "electron";
import { version } from "../../package.json";
import { mainWindow, quitApp } from "./window";
// internal tray state
let tray: Tray = null;
// load the tray icon
const trayIcon = nativeImage.createFromPath(
resolve(process.resourcesPath, "icon.png"),
);
// trayIcon.setTemplateImage(true);
export function initTray() {
tray = new Tray(trayIcon);
updateTrayMenu();
tray.setToolTip("Stoat for Desktop");
tray.setImage(trayIcon);
}
export function updateTrayMenu() {
tray.setContextMenu(
Menu.buildFromTemplate([
{ label: "Stoat for Desktop", type: "normal", enabled: false },
{
label: "Version",
type: "submenu",
submenu: Menu.buildFromTemplate([
{
label: version,
type: "normal",
enabled: false,
},
]),
},
{ type: "separator" },
{
label: mainWindow.isVisible() ? "Hide App" : "Show App",
type: "normal",
click() {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
},
},
{
label: "Quit App",
type: "normal",
click: quitApp,
},
]),
);
}
+174
View File
@@ -0,0 +1,174 @@
import { join, resolve } from "node:path";
import {
BrowserWindow,
Menu,
MenuItem,
app,
ipcMain,
nativeImage,
} from "electron";
import { setBadgeCount } from "./badges";
import { config } from "./config";
import { updateTrayMenu } from "./tray";
// global reference to main window
export let mainWindow: BrowserWindow;
// currently in-use build
export const BUILD_URL = new URL(
app.commandLine.hasSwitch("force-server")
? app.commandLine.getSwitchValue("force-server")
: (MAIN_WINDOW_VITE_DEV_SERVER_URL ?? "https://beta.revolt.chat"),
);
// internal window state
let shouldQuit = false;
// load the window icon
const windowIcon = nativeImage.createFromPath(
resolve(process.resourcesPath, "icon.png"),
);
console.info(resolve(process.resourcesPath, "icon.png"));
// windowIcon.setTemplateImage(true);
/**
* Create the main application window
*/
export function createMainWindow() {
// create the window
mainWindow = new BrowserWindow({
minWidth: 300,
minHeight: 300,
width: 800,
height: 600,
backgroundColor: "#191919",
frame: !config.customFrame,
icon: windowIcon,
webPreferences: {
// relative to `.vite/build`
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
spellcheck: true,
},
});
// maximise the window if it was maximised before
if (config.windowState.isMaximised) {
mainWindow.maximize();
}
// load the entrypoint
mainWindow.loadURL(BUILD_URL.toString());
// minimise window to tray
mainWindow.on("close", (event) => {
if (!shouldQuit && config.minimiseToTray) {
event.preventDefault();
mainWindow.hide();
}
});
// update tray menu when window is shown/hidden
mainWindow.on("show", updateTrayMenu);
mainWindow.on("hide", updateTrayMenu);
// keep track of window state
function generateState() {
config.windowState = {
isMaximised: mainWindow.isMaximized(),
};
}
mainWindow.on("maximize", generateState);
mainWindow.on("unmaximize", generateState);
// rebind zoom controls to be more sensible
mainWindow.webContents.on("before-input-event", (event, input) => {
if (input.control && input.key === "=") {
// zoom in (+)
event.preventDefault();
mainWindow.webContents.setZoomLevel(
mainWindow.webContents.getZoomLevel() + 1,
);
} else if (input.control && input.key === "-") {
// zoom out (-)
event.preventDefault();
mainWindow.webContents.setZoomLevel(
mainWindow.webContents.getZoomLevel() - 1,
);
}
});
// configure spellchecker context menu
mainWindow.webContents.on("context-menu", (_, params) => {
const menu = new Menu();
// add all suggestions
for (const suggestion of params.dictionarySuggestions) {
menu.append(
new MenuItem({
label: suggestion,
click: () => mainWindow.webContents.replaceMisspelling(suggestion),
}),
);
}
// allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: "Add to dictionary",
click: () =>
mainWindow.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord,
),
}),
);
}
// add an option to toggle spellchecker
menu.append(
new MenuItem({
label: "Toggle spellcheck",
click() {
config.spellchecker = !config.spellchecker;
},
}),
);
// show menu if we've generated enough entries
if (menu.items.length > 0) {
menu.popup();
}
});
// push world events to the window
ipcMain.on("minimise", () => mainWindow.minimize());
ipcMain.on("maximise", () =>
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize(),
);
ipcMain.on("close", () => mainWindow.close());
// mainWindow.webContents.openDevTools();
let i = 0;
setInterval(() => setBadgeCount((++i % 30) + 1), 1000);
}
/**
* Quit the entire app
*/
export function quitApp() {
shouldQuit = true;
mainWindow.close();
}
// Ensure global app quit works properly
app.on("before-quit", () => {
shouldQuit = true;
});
+2
View File
@@ -0,0 +1,2 @@
import "./world/config";
import "./world/window";
+32
View File
@@ -0,0 +1,32 @@
/**
* This file will automatically be loaded by vite and run in the "renderer" context.
* To learn more about the differences between the "main" and the "renderer" context in
* Electron, visit:
*
* https://electronjs.org/docs/tutorial/process-model
*
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
* in a renderer process, please be aware of potential security implications. You can read
* more about security risks here:
*
* https://electronjs.org/docs/tutorial/security
*
* To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration`
* flag:
*
* ```
* // Create the browser window.
* mainWindow = new BrowserWindow({
* width: 800,
* height: 600,
* webPreferences: {
* nodeIntegration: true
* }
* });
* ```
*/
import "./index.css";
console.log(
'👋 This message is being logged by "renderer.ts", included via Vite',
);
+17
View File
@@ -0,0 +1,17 @@
import { contextBridge, ipcRenderer } from "electron";
let config: DesktopConfig;
ipcRenderer.on("config", (_, data) => (config = data));
contextBridge.exposeInMainWorld("desktopConfig", {
get: () => config,
set: (config: DesktopConfig) => ipcRenderer.send("config", config),
getAutostart() {
ipcRenderer.send("isAutostart?");
return new Promise((resolve) => ipcRenderer.once("isAutostart", resolve));
},
setAutostart(value: boolean) {
ipcRenderer.send("setAutostart", value);
},
});
+16
View File
@@ -0,0 +1,16 @@
import { contextBridge, ipcRenderer } from "electron";
import { version } from "../../package.json";
contextBridge.exposeInMainWorld("native", {
versions: {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
desktop: () => version,
},
minimise: () => ipcRenderer.send("minimise"),
maximise: () => ipcRenderer.send("maximise"),
close: () => ipcRenderer.send("close"),
});
View File
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"noImplicitAny": true,
"sourceMap": true,
"baseUrl": ".",
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true
}
}
+4
View File
@@ -0,0 +1,4 @@
import { defineConfig } from "vite";
// https://vitejs.dev/config
export default defineConfig({});
+4
View File
@@ -0,0 +1,4 @@
import { defineConfig } from "vite";
// https://vitejs.dev/config
export default defineConfig({});
+4
View File
@@ -0,0 +1,4 @@
import { defineConfig } from "vite";
// https://vitejs.dev/config
export default defineConfig({});