feat: initial commit
This commit is contained in:
@@ -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
@@ -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/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "assets"]
|
||||||
|
path = assets
|
||||||
|
url = https://github.com/stoatchat/assets
|
||||||
|
update = none
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"plugins": [
|
||||||
|
"@trivago/prettier-plugin-sort-imports"
|
||||||
|
],
|
||||||
|
"importOrder": [
|
||||||
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"^electron",
|
||||||
|
"^\\.\\.",
|
||||||
|
"^[./]"
|
||||||
|
],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true
|
||||||
|
}
|
||||||
Vendored
+22
@@ -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
@@ -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
@@ -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;
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
|
||||||
+12
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6773
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- electron
|
||||||
|
- electron-winstaller
|
||||||
|
- esbuild
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
declare type DesktopConfig = {
|
||||||
|
firstLaunch: boolean;
|
||||||
|
customFrame: boolean;
|
||||||
|
minimiseToTray: boolean;
|
||||||
|
spellchecker: boolean;
|
||||||
|
hardwareAcceleration: boolean;
|
||||||
|
discordRpc: boolean;
|
||||||
|
windowState: {
|
||||||
|
isMaximised: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import "./world/config";
|
||||||
|
import "./world/window";
|
||||||
@@ -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',
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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"),
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
Reference in New Issue
Block a user