'init'
This commit is contained in:
BIN
lib/.DS_Store
vendored
Normal file
BIN
lib/.DS_Store
vendored
Normal file
Binary file not shown.
227
lib/main/app.ts
Normal file
227
lib/main/app.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { BrowserWindow, shell, app, ipcMain, nativeTheme, screen, powerMonitor } from 'electron'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import { WORKING_DIR } from './constants';
|
||||
import { boot } from './boot/bootloader';
|
||||
|
||||
export async function startApplication() {
|
||||
let preloaderWindow = createPreloaderWindow();
|
||||
await fs.promises.mkdir(WORKING_DIR, { recursive: true });
|
||||
createAppWindow(preloaderWindow);
|
||||
}
|
||||
|
||||
export function createPreloaderWindow() {
|
||||
let preloaderWindow = new BrowserWindow({
|
||||
width: 150,
|
||||
height: 150,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
center: true,
|
||||
resizable: false,
|
||||
alwaysOnTop: true
|
||||
});
|
||||
|
||||
preloaderWindow.loadFile(join(__dirname, '../../resources/preload.html'));
|
||||
|
||||
return preloaderWindow;
|
||||
}
|
||||
|
||||
export function createAppWindow(preloaderWindow?: BrowserWindow): void {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 670,
|
||||
minWidth: 385,
|
||||
minHeight: 555,
|
||||
show: false,
|
||||
title: 'Rosetta Messager',
|
||||
icon: join(__dirname, '../../resources/R.png'),
|
||||
frame: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#000',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/preload.js'),
|
||||
sandbox: false,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true
|
||||
}
|
||||
});
|
||||
|
||||
powerMonitor.on('lock-screen', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.reload();
|
||||
}
|
||||
});
|
||||
|
||||
foundationIpcRegistration(mainWindow);
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
if (preloaderWindow && !preloaderWindow.isDestroyed()) {
|
||||
preloaderWindow.close();
|
||||
}
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
});
|
||||
|
||||
boot(mainWindow);
|
||||
}
|
||||
|
||||
export function foundationIpcRegistration(mainWindow: BrowserWindow) {
|
||||
ipcMain.removeAllListeners('window-resize');
|
||||
ipcMain.removeAllListeners('window-resizeble');
|
||||
ipcMain.removeAllListeners('window-theme');
|
||||
ipcMain.removeAllListeners("write-file");
|
||||
ipcMain.removeAllListeners("read-file");
|
||||
ipcMain.removeAllListeners("mkdir");
|
||||
ipcMain.removeHandler("get-core-version");
|
||||
ipcMain.removeHandler("get-arch");
|
||||
ipcMain.removeAllListeners("get-user-dir");
|
||||
ipcMain.removeHandler("get-downloads-path")
|
||||
ipcMain.removeHandler("get-app-path");
|
||||
ipcMain.removeHandler('open-dev-tools');
|
||||
ipcMain.removeHandler('window-state');
|
||||
ipcMain.removeHandler('window-toggle');
|
||||
ipcMain.removeHandler('window-close');
|
||||
ipcMain.removeHandler('window-minimize');
|
||||
ipcMain.removeHandler('showItemInFolder');
|
||||
ipcMain.removeHandler('openExternal');
|
||||
|
||||
ipcMain.handle('showItemInFolder', (_, fullPath: string) => {
|
||||
shell.showItemInFolder(fullPath);
|
||||
});
|
||||
|
||||
ipcMain.handle('openExternal', (_, url: string) => {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
ipcMain.handle('open-dev-tools', () => {
|
||||
if (mainWindow.webContents.isDevToolsOpened()) {
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
});
|
||||
|
||||
ipcMain.on('window-resize', (_, { width, height }) => {
|
||||
const { x: currentX, y: currentY, width: currentWidth, height: currentHeight } = mainWindow.getBounds();
|
||||
|
||||
const newX = currentX + (currentWidth - width) / 2;
|
||||
const newY = currentY + (currentHeight - height) / 2;
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
const clampedX = Math.max(0, Math.min(newX, screenWidth - width));
|
||||
//const clampedY = Math.max(0, Math.min(newY, screenHeight - height));
|
||||
|
||||
mainWindow.setBounds({
|
||||
x: Math.round(clampedX),
|
||||
//y: Math.round(clampedY),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('window-resizeble', (_, isResizeble) => {
|
||||
mainWindow.setResizable(isResizeble);
|
||||
mainWindow.webContents.send('window-state-changed');
|
||||
});
|
||||
|
||||
ipcMain.on('window-theme', (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
})
|
||||
|
||||
ipcMain.on("write-file", (_, filePath, content) => {
|
||||
fs.writeFile(filePath, content, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
ipcMain.handle('window-state', () => {
|
||||
return {
|
||||
isMinimized: mainWindow.isMinimized(),
|
||||
isMaximized: mainWindow.isMaximized(),
|
||||
isFullScreen: mainWindow.isFullScreen(),
|
||||
isVisible: mainWindow.isVisible(),
|
||||
isFocused: mainWindow.isFocused(),
|
||||
isResizable: mainWindow.isResizable(),
|
||||
isClosable: mainWindow.isClosable(),
|
||||
isDestroyed: mainWindow.isDestroyed(),
|
||||
bounds: mainWindow.getBounds()
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('window-toggle', () => {
|
||||
if (mainWindow.isMaximized() || mainWindow.isFullScreen()) {
|
||||
mainWindow.setFullScreen(false);
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.setFullScreen(true);
|
||||
}
|
||||
setTimeout(() => {
|
||||
/**
|
||||
* Разобраться с этой хуйней
|
||||
*/
|
||||
mainWindow.webContents.send('window-state-changed');
|
||||
}, 700);
|
||||
});
|
||||
|
||||
ipcMain.handle('window-minimize', () => {
|
||||
mainWindow.minimize();
|
||||
mainWindow.webContents.send('window-state-changed');
|
||||
});
|
||||
|
||||
ipcMain.handle('window-close', () => {
|
||||
mainWindow.hide();
|
||||
mainWindow.webContents.send('window-state-changed');
|
||||
});
|
||||
|
||||
ipcMain.on("read-file", (_, filePath) => {
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send("read-file-reply", data);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("mkdir", (_, dirPath) => {
|
||||
fs.mkdir(dirPath, { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send("mkdir-reply");
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Change to get-core-version
|
||||
*/
|
||||
ipcMain.handle("get-core-version", () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-arch", () => {
|
||||
return process.arch;
|
||||
})
|
||||
|
||||
ipcMain.on("get-user-dir", () => {
|
||||
const userDir = app.getPath("userData");
|
||||
mainWindow.webContents.send("get-user-dir-reply", userDir);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-app-path", () => {
|
||||
return app.getAppPath();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-downloads-path", () => {
|
||||
return app.getPath("downloads");
|
||||
});
|
||||
}
|
||||
67
lib/main/boot/bootloader.ts
Normal file
67
lib/main/boot/bootloader.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import fs from 'fs/promises'
|
||||
import { WORKING_DIR } from "../constants";
|
||||
import path from "path";
|
||||
import { Logger } from "../logger";
|
||||
|
||||
const logger = Logger('bootloader');
|
||||
|
||||
|
||||
ipcMain.handleOnce('report-boot-process-failed', async () => {
|
||||
/**
|
||||
* Если процесс загрузки не завершился успешно, то preload показывает
|
||||
* экран ошибки, а нам нужно откатиться назад к загрузке dev.html
|
||||
* и удалить скомпилированные файлы, чтобы при следующем запуске
|
||||
* приложение попыталось загрузиться в режиме разработки.
|
||||
*/
|
||||
let filePath = path.join(WORKING_DIR, 'b');
|
||||
await fs.rmdir(filePath, { recursive: true });
|
||||
logger.log("Boot process failed, removed compiled files");
|
||||
logger.log(`Removed compiled files at ${filePath}`);
|
||||
logger.log(`Restarting application in safe mode`);
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Boot функция, эта функция запускает приложение
|
||||
* @param window окно
|
||||
*/
|
||||
export async function boot(window : BrowserWindow) {
|
||||
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
|
||||
await bootDevelopment(window);
|
||||
// await bootProduction(window);
|
||||
window.webContents.openDevTools({ mode: 'detach' });
|
||||
//console.info(window.webContents);
|
||||
return;
|
||||
}
|
||||
//window.webContents.openDevTools({ mode: 'detach' });
|
||||
await bootProduction(window);
|
||||
}
|
||||
|
||||
async function bootProduction(window : BrowserWindow) {
|
||||
logger.log("Booting in production mode");
|
||||
let html = path.join(WORKING_DIR, 'b', 'j.html');
|
||||
if(await existsFile(html)){
|
||||
logger.log(`Loading compiled file`);
|
||||
window.loadFile(html);
|
||||
} else {
|
||||
logger.log(`Loading conatiner file`);
|
||||
window.loadFile(path.join(__dirname, '../renderer/dev.html'));
|
||||
}
|
||||
}
|
||||
|
||||
async function bootDevelopment(window : BrowserWindow) {
|
||||
logger.log("Booting in development mode");
|
||||
window.loadURL(process.env['ELECTRON_RENDERER_URL'] + "/dev.html");
|
||||
window.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
|
||||
async function existsFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
130
lib/main/boot/compiler.ts
Normal file
130
lib/main/boot/compiler.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import path from "path";
|
||||
import { WORKING_DIR } from "../constants";
|
||||
import fs from 'fs/promises'
|
||||
import JSZip from "jszip";
|
||||
import crypto from "crypto";
|
||||
import { Logger } from "../logger";
|
||||
|
||||
const logger = Logger('compiler');
|
||||
|
||||
export interface BundleFile {
|
||||
file: string;
|
||||
type: string;
|
||||
main: boolean;
|
||||
}
|
||||
|
||||
const binaryFileTypes = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mp3', 'wav', 'ogg', 'pdf', 'zip', 'rar', '7z'];
|
||||
|
||||
|
||||
export async function compileBundleFile(zip : JSZip) {
|
||||
let timestart = Date.now();
|
||||
logger.log("Starting compilation of bundle file");
|
||||
let files : BundleFile[] = await getAllFilesInBundleForCompile(zip);
|
||||
if(files.length == 0){
|
||||
logger.log("No files to compile in bundle");
|
||||
throw new Error("No files to compile in bundle");
|
||||
}
|
||||
let compileOutputDir = path.join(WORKING_DIR, 'b');
|
||||
await fs.mkdir(compileOutputDir, { recursive: true });
|
||||
|
||||
let mainFiles = files.filter(f => f.main);
|
||||
let otherFiles = files.filter(f => !f.main);
|
||||
|
||||
logger.log(`Compiling ${files.length} files, ${mainFiles.length} main files and ${otherFiles.length} other files`);
|
||||
|
||||
for (let file of files) {
|
||||
let zipFile = zip.file(file.file);
|
||||
if (zipFile) {
|
||||
let content = await zipFile.async('nodebuffer');
|
||||
let outputPath = path.join(compileOutputDir, await hashFileName(path.basename(file.file)));
|
||||
await fs.writeFile(outputPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
await compileHtmlFile(mainFiles);
|
||||
await transitionFilenamesInFiles([...mainFiles, ...otherFiles]);
|
||||
logger.log("Compilation done successfully in " + (Date.now() - timestart) + "ms");
|
||||
}
|
||||
|
||||
async function transitionFilenamesInFiles(otherFiles: BundleFile[]) {
|
||||
logger.log(`Transitioning filenames in ${otherFiles.length} files`);
|
||||
for(let i = 0; i < otherFiles.length; i++){
|
||||
let file = otherFiles[i];
|
||||
let filePath = path.join(WORKING_DIR, 'b', await hashFileName(path.basename(file.file)));
|
||||
let buffer = await fs.readFile(filePath, {
|
||||
encoding: file.type && binaryFileTypes.includes(file.type) ? 'binary' : 'utf-8'
|
||||
});
|
||||
let content = buffer.toString();
|
||||
logger.log(`Processing file ${path.basename(file.file)}`);
|
||||
|
||||
for(let j = 0; j < otherFiles.length; j++){
|
||||
let targetFile = otherFiles[j];
|
||||
let originalName = path.basename(targetFile.file);
|
||||
let hashedName = await hashFileName(originalName);
|
||||
let entries = content.split(originalName);
|
||||
if(entries.length <= 0){
|
||||
continue;
|
||||
}
|
||||
logger.log(`In file ${path.basename(file.file)} replacing ${originalName} with ${hashedName} (${entries.length - 1} occurrences)`);
|
||||
content = entries.join(hashedName);
|
||||
}
|
||||
logger.log(`Flush transitioned file ${path.basename(file.file)}`);
|
||||
await fs.writeFile(filePath, content, {
|
||||
encoding: file.type && binaryFileTypes.includes(file.type) ? 'binary' : 'utf-8'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function compileHtmlFile(mainFiles: BundleFile[]) {
|
||||
let html = await constructHtmlTemplateToCompile();
|
||||
html = html.replace(`<script type="module" src="/renderer.tsx"></script>`, '');
|
||||
let scripts = mainFiles.filter(f => f.type === 'js').map(f => f.file);
|
||||
let styles = mainFiles.filter(f => f.type === 'css').map(f => f.file);
|
||||
|
||||
let scriptTags : string = "";
|
||||
for (let script of scripts) {
|
||||
let hashedName = await hashFileName(path.basename(script));
|
||||
scriptTags += `<script type="module" src="${hashedName}"></script>\n`;
|
||||
}
|
||||
|
||||
let styleTags : string = "";
|
||||
for (let style of styles) {
|
||||
let hashedName = await hashFileName(path.basename(style));
|
||||
styleTags += `<link rel="stylesheet" href="${hashedName}">\n`;
|
||||
}
|
||||
|
||||
|
||||
html = html.replace('<!--RR-->', `${scriptTags}\n${styleTags}`);
|
||||
html = html.replace('\n', '');
|
||||
|
||||
|
||||
let outputHtmlPath = path.join(WORKING_DIR, 'b', 'j.html');
|
||||
await fs.writeFile(outputHtmlPath, html);
|
||||
}
|
||||
|
||||
export async function getAllFilesInBundleForCompile(zip : JSZip) : Promise<BundleFile[]> {
|
||||
const mainFilePrefixes = ['index-', 'main-'];
|
||||
return Object.keys(zip.files)
|
||||
.filter(filePath => filePath.indexOf('__MACOSX') == -1)
|
||||
.map(filePath => {
|
||||
const ext = path.extname(filePath).slice(1);
|
||||
const isMain = mainFilePrefixes.some(prefix => path.basename(filePath).startsWith(prefix));
|
||||
return {
|
||||
file: filePath,
|
||||
type: ext,
|
||||
main: isMain
|
||||
} as BundleFile;
|
||||
});
|
||||
}
|
||||
|
||||
async function constructHtmlTemplateToCompile() {
|
||||
let buffer = await fs.readFile(
|
||||
path.join(__dirname, '../../resources/prod.html')
|
||||
);
|
||||
let html = buffer.toString();
|
||||
return html;
|
||||
}
|
||||
|
||||
async function hashFileName(name : string) : Promise<string> {
|
||||
return crypto.createHash('md5').update(name).digest('hex') + path.extname(name);
|
||||
}
|
||||
10
lib/main/boot/updater.ts
Normal file
10
lib/main/boot/updater.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import fs from 'fs/promises'
|
||||
import JSZip from "jszip";
|
||||
import { compileBundleFile } from './compiler';
|
||||
|
||||
export async function installServiceUpdate(pathToUpdate : string) {
|
||||
let data = await fs.readFile(pathToUpdate);
|
||||
let zip = await JSZip.loadAsync(data);
|
||||
await compileBundleFile(zip);
|
||||
await fs.unlink(pathToUpdate);
|
||||
}
|
||||
8
lib/main/constants.ts
Normal file
8
lib/main/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
export const WORKING_DIR =
|
||||
path.join(app.getPath('home'), 'rosed');
|
||||
|
||||
export const LOGFILE_PATH =
|
||||
path.join(WORKING_DIR, 'rosetta.log');
|
||||
51
lib/main/database.ts
Normal file
51
lib/main/database.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import path from 'path';
|
||||
import { WORKING_DIR } from './constants';
|
||||
import { promises as fs } from 'fs';
|
||||
import sqlite3 from 'sqlite3'
|
||||
|
||||
let db : any = null;
|
||||
|
||||
export async function initializeDatabase(){
|
||||
await fs.mkdir(WORKING_DIR, { recursive: true });
|
||||
const dbPath = path.join(WORKING_DIR, 'r_d');
|
||||
const dbLink = new sqlite3.Database(dbPath);
|
||||
db = dbLink;
|
||||
}
|
||||
|
||||
export function runQuery(query: string, params: any[] = []) : Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(query, params, function (err) {
|
||||
if (err) {
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getQuery(query: string, params: any[] = []): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(query, params, (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function allQuery(query: string, params: any[] = []): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initializeDatabase();
|
||||
27
lib/main/index.d.ts
vendored
Normal file
27
lib/main/index.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types="electron-vite/node" />
|
||||
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.web' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
14
lib/main/ipcs/ipcDatabase.ts
Normal file
14
lib/main/ipcs/ipcDatabase.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { runQuery, getQuery, allQuery } from '../database';
|
||||
|
||||
ipcMain.handle('db:run', async (_, query: string, params: any[]) => {
|
||||
return await runQuery(query, params);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get', async (_, query: string, params: any[]) => {
|
||||
return await getQuery(query, params);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:all', async (_, query: string, params: any[]) => {
|
||||
return await allQuery(query, params);
|
||||
});
|
||||
36
lib/main/ipcs/ipcDevice.ts
Normal file
36
lib/main/ipcs/ipcDevice.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ipcMain } from "electron";
|
||||
import os from "os";
|
||||
import {machineId} from 'node-machine-id';
|
||||
|
||||
/**
|
||||
* Consturct device name.
|
||||
* Ex: Macbook Pro M3
|
||||
*/
|
||||
ipcMain.handle('device:name', () => {
|
||||
const type = os.type(); // 'Darwin', 'Windows_NT', 'Linux'
|
||||
|
||||
let deviceName = "";
|
||||
|
||||
if (type === "Darwin") {
|
||||
deviceName += "Mac";
|
||||
} else if (type === "Windows_NT") {
|
||||
deviceName += "Windows";
|
||||
} else if (type === "Linux") {
|
||||
deviceName += "Linux";
|
||||
} else {
|
||||
deviceName += type + " ";
|
||||
}
|
||||
|
||||
const cpus = os.cpus();
|
||||
if (cpus && cpus.length > 0) {
|
||||
const cpuModel = cpus[0].model;
|
||||
deviceName += cpuModel.replace("Apple", "").replace("Processor", "");
|
||||
}
|
||||
|
||||
return deviceName.trim();
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('device:id', async () => {
|
||||
return await machineId();
|
||||
});
|
||||
22
lib/main/ipcs/ipcFilestorage.ts
Normal file
22
lib/main/ipcs/ipcFilestorage.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { WORKING_DIR } from "../constants";
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
ipcMain.handle('fileStorage:writeFile', async (_, file: string, data: string | Buffer, inWorkingDir : boolean = true) => {
|
||||
const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file);
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
await fs.writeFile(fullPath, data);
|
||||
console.info("File written to " + fullPath);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('fileStorage:readFile', async (_, file: string, inWorkingDir : boolean = true) => {
|
||||
try{
|
||||
const fullPath = path.join(inWorkingDir ? WORKING_DIR : '', file);
|
||||
const data = await fs.readFile(fullPath);
|
||||
return data;
|
||||
}catch(e){
|
||||
return null;
|
||||
}
|
||||
});
|
||||
10
lib/main/ipcs/ipcLogger.ts
Normal file
10
lib/main/ipcs/ipcLogger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { promises as fs } from 'fs';
|
||||
import { LOGFILE_PATH } from '../constants';
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('logger:log', async (_, logString) => {
|
||||
console.log(logString);
|
||||
await fs.appendFile(LOGFILE_PATH, logString + '\n');
|
||||
});
|
||||
16
lib/main/ipcs/ipcNotification.ts
Normal file
16
lib/main/ipcs/ipcNotification.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ipcMain, Notification } from "electron";
|
||||
import { restoreApplicationAfterClickOnTrayOrDock } from "../main";
|
||||
|
||||
ipcMain.handle('notification:show', async (_, title: string, body: string) => {
|
||||
let id = Math.random().toString(36).substring(2, 15);
|
||||
let note = new Notification({
|
||||
title: title,
|
||||
body: body
|
||||
});
|
||||
note.on('click', () => {
|
||||
restoreApplicationAfterClickOnTrayOrDock();
|
||||
ipcMain.emit('notification:clicked', id);
|
||||
});
|
||||
note.show();
|
||||
return id;
|
||||
});
|
||||
14
lib/main/ipcs/ipcUpdate.ts
Normal file
14
lib/main/ipcs/ipcUpdate.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import { installServiceUpdate } from "../boot/updater";
|
||||
import path from "path";
|
||||
import { WORKING_DIR } from "../constants";
|
||||
|
||||
|
||||
ipcMain.handle('update:installServiceUpdate', async (_, bundleName: string) => {
|
||||
await installServiceUpdate(path.join(WORKING_DIR, bundleName));
|
||||
});
|
||||
|
||||
ipcMain.handle('update:restartApp', async () => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
16
lib/main/logger.ts
Normal file
16
lib/main/logger.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { LOGFILE_PATH } from './constants';
|
||||
|
||||
export function Logger(component: string) {
|
||||
|
||||
const log = async (message: string) => {
|
||||
const date = new Date().toISOString();
|
||||
const logMessage = `[main_proc] [${date}] [${component}] ${message}`;
|
||||
console.log(logMessage);
|
||||
await fs.appendFile(LOGFILE_PATH, logMessage + '\n');
|
||||
}
|
||||
|
||||
return {
|
||||
log
|
||||
};
|
||||
}
|
||||
102
lib/main/main.ts
Normal file
102
lib/main/main.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { app, BrowserWindow, Menu, nativeImage } from 'electron'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { createAppWindow, startApplication } from './app'
|
||||
import './ipcs/ipcDatabase'
|
||||
import './ipcs/ipcLogger'
|
||||
import './ipcs/ipcFilestorage'
|
||||
import './ipcs/ipcUpdate'
|
||||
import './ipcs/ipcNotification'
|
||||
import './ipcs/ipcDevice'
|
||||
import { Tray } from 'electron/main'
|
||||
import { join } from 'path'
|
||||
import { Logger } from './logger'
|
||||
|
||||
let lockInstance = app.requestSingleInstanceLock();
|
||||
let tray : Tray | null = null;
|
||||
const size = process.platform === 'darwin' ? 18 : 22;
|
||||
const logger = Logger('main');
|
||||
|
||||
|
||||
const icon = nativeImage.createFromPath(
|
||||
join(__dirname, '../../resources/R.png')
|
||||
).resize({ width: size, height: size });
|
||||
|
||||
if(!lockInstance){
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logger.log(`main thread error, reason: ${reason}`);
|
||||
});
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
app.on('second-instance', () => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length) {
|
||||
const mainWindow = allWindows[0];
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
if (mainWindow.isVisible() === false) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
export const restoreApplicationAfterClickOnTrayOrDock = () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length > 0) {
|
||||
const mainWindow = allWindows[0];
|
||||
if (mainWindow.isMinimized()){
|
||||
mainWindow.restore();
|
||||
return;
|
||||
}
|
||||
if(mainWindow.isVisible() === false){
|
||||
mainWindow.show();
|
||||
}
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
createAppWindow();
|
||||
}
|
||||
}
|
||||
|
||||
//Menu.setApplicationMenu(null);
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
electronApp.setAppUserModelId('Rosetta');
|
||||
tray = new Tray(icon);
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Open App', click: () => restoreApplicationAfterClickOnTrayOrDock() },
|
||||
{ label: 'Quit', click: () => app.quit() }
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.setToolTip('Rosetta');
|
||||
tray.on('click', () => {
|
||||
restoreApplicationAfterClickOnTrayOrDock();
|
||||
});
|
||||
startApplication();
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
restoreApplicationAfterClickOnTrayOrDock();
|
||||
});
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform == 'darwin') {
|
||||
app.hide();
|
||||
}
|
||||
})
|
||||
// In this file, you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
18
lib/preload/api.ts
Normal file
18
lib/preload/api.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const api = {
|
||||
send: (channel: string, ...args: any[]) => {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
receive: (channel: string, func: (...args: any[]) => void) => {
|
||||
ipcRenderer.on(channel, (_, ...args) => func(...args));
|
||||
},
|
||||
invoke: (channel: string, ...args: any[]) => {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
removeAllListeners: (channel: string) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
17
lib/preload/index.d.ts
vendored
Normal file
17
lib/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type api from './api'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: typeof api,
|
||||
version: string,
|
||||
platform: string,
|
||||
appPath: string,
|
||||
arch: string,
|
||||
shell: Electron.Shell;
|
||||
downloadsPath: string;
|
||||
deviceName: string;
|
||||
deviceId: string;
|
||||
}
|
||||
}
|
||||
137
lib/preload/preload.ts
Normal file
137
lib/preload/preload.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import api from './api'
|
||||
|
||||
const applicationLoader = `
|
||||
<div style="
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
">
|
||||
<div style="
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #333333;
|
||||
border-top-color: #0071e3;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const applicationError = `
|
||||
<div style="
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 100vh;
|
||||
color: #ffffff;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
">
|
||||
<div style="
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
">
|
||||
<div style="
|
||||
font-size: 72px;
|
||||
margin-bottom: 20px;
|
||||
">
|
||||
<svg width="64" height="64" zoomAndPan="magnify" viewBox="0 0 384 383.999986" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="ab9f3e3410"><path d="M 134 109 L 369.46875 109 L 369.46875 381.34375 L 134 381.34375 Z M 134 109 " clip-rule="nonzero"/></clipPath><clipPath id="39bead0a6b"><path d="M 14.71875 2.59375 L 249 2.59375 L 249 222 L 14.71875 222 Z M 14.71875 2.59375 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#ab9f3e3410)"><path fill="#ffffff" d="M 254.15625 284.453125 C 288.414062 275.191406 316.179688 260.617188 337.414062 240.769531 C 358.632812 220.917969 369.257812 195.238281 369.257812 163.691406 L 369.257812 109.996094 L 249.550781 110.222656 L 249.550781 168.148438 C 249.550781 184.847656 241.75 198.195312 226.148438 208.21875 C 210.550781 218.226562 188.175781 223.234375 159.007812 223.234375 L 134.070312 223.234375 L 134.070312 300.996094 L 206.652344 381.429688 L 344.765625 381.429688 L 254.15625 284.453125 " fill-opacity="1" fill-rule="nonzero"/></g><g clip-path="url(#39bead0a6b)"><path fill="#ffffff" d="M 248.417969 109.257812 L 248.417969 2.605469 L 14.769531 2.605469 L 14.769531 221.519531 L 132.9375 221.519531 L 132.9375 109.257812 L 248.417969 109.257812 " fill-opacity="1" fill-rule="nonzero"/></g></svg>
|
||||
</div>
|
||||
<h1 style="
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -0.5px;
|
||||
">Application Error</h1>
|
||||
<p style="
|
||||
font-size: 17px;
|
||||
color: #a1a1a6;
|
||||
margin: 0 0 32px 0;
|
||||
max-width: 500px;
|
||||
line-height: 1.5;
|
||||
">The application failed to load properly. Please wait for application repairing or reinstall application.</p>
|
||||
${applicationLoader}
|
||||
</div>
|
||||
<div style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
">
|
||||
<p style="
|
||||
font-size: 13px;
|
||||
color: #5a5a5f;
|
||||
">rosetta - powering freedom. visit about rosetta-im.com. error: boot_process_failed</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
const exposeContext = async () => {
|
||||
let version = await ipcRenderer.invoke("get-core-version");
|
||||
let appPath = await ipcRenderer.invoke("get-app-path");
|
||||
let arch = await ipcRenderer.invoke("get-arch");
|
||||
let deviceName = await ipcRenderer.invoke("device:name");
|
||||
let deviceId = await ipcRenderer.invoke("device:id");
|
||||
|
||||
setTimeout(() => {
|
||||
if(document.body.innerHTML.length < 100){
|
||||
document.body.innerHTML = applicationError;
|
||||
ipcRenderer.invoke("report-boot-process-failed");
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
let downloadsPath = await ipcRenderer.invoke("get-downloads-path");
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld('version', version);
|
||||
contextBridge.exposeInMainWorld('platform', process.platform);
|
||||
contextBridge.exposeInMainWorld('appPath', appPath);
|
||||
contextBridge.exposeInMainWorld('arch', arch);
|
||||
contextBridge.exposeInMainWorld('deviceName', deviceName);
|
||||
contextBridge.exposeInMainWorld('deviceId', deviceId);
|
||||
contextBridge.exposeInMainWorld('shell', {
|
||||
openExternal: (url: string) => {
|
||||
ipcRenderer.invoke('openExternal', url);
|
||||
},
|
||||
showItemInFolder: (fullPath: string) => {
|
||||
ipcRenderer.invoke('showItemInFolder', fullPath);
|
||||
}
|
||||
});
|
||||
contextBridge.exposeInMainWorld('downloadsPath', downloadsPath)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
window.electron = electronAPI
|
||||
window.api = api
|
||||
window.version = version;
|
||||
window.platform = process.platform;
|
||||
window.appPath = appPath;
|
||||
window.arch = arch;
|
||||
window.shell = shell;
|
||||
window.downloadsPath = downloadsPath;
|
||||
window.deviceName = deviceName;
|
||||
window.deviceId = deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
exposeContext();
|
||||
35
lib/window/ipcEvents.ts
Normal file
35
lib/window/ipcEvents.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { type BrowserWindow, ipcMain } from 'electron'
|
||||
import os from 'os'
|
||||
|
||||
const handleIPC = (channel: string, handler: (...args: any[]) => void) => {
|
||||
ipcMain.handle(channel, handler)
|
||||
}
|
||||
|
||||
export const registerWindowIPC = (mainWindow: BrowserWindow) => {
|
||||
// Hide the menu bar
|
||||
mainWindow.setMenuBarVisibility(false)
|
||||
|
||||
// Register window IPC
|
||||
handleIPC('init-window', () => {
|
||||
const { width, height } = mainWindow.getBounds()
|
||||
const minimizable = mainWindow.isMinimizable()
|
||||
const maximizable = mainWindow.isMaximizable()
|
||||
const platform = os.platform()
|
||||
|
||||
return { width, height, minimizable, maximizable, platform }
|
||||
})
|
||||
|
||||
handleIPC('is-window-minimizable', () => mainWindow.isMinimizable())
|
||||
handleIPC('is-window-maximizable', () => mainWindow.isMaximizable())
|
||||
handleIPC('window-minimize', () => mainWindow.minimize())
|
||||
handleIPC('window-maximize', () => mainWindow.maximize())
|
||||
handleIPC('window-close', () => mainWindow.close())
|
||||
handleIPC('window-maximize-toggle', () => {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize()
|
||||
} else {
|
||||
mainWindow.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user