diff --git a/.editorconfig b/.editorconfig index 8cf633eb5..3fa547342 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true diff --git a/.nx/cache/18.3.4-nx.win32-x64-msvc.node b/.nx/cache/18.3.4-nx.win32-x64-msvc.node new file mode 100644 index 000000000..4981b201d Binary files /dev/null and b/.nx/cache/18.3.4-nx.win32-x64-msvc.node differ diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 58862f7ce..000000000 --- a/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2020 - Maxime GRIS - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 7ffb15173..5e2a854d9 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,95 @@ [![Angular Logo](https://www.vectorlogo.zone/logos/angular/angular-icon.svg)](https://angular.io/) [![Electron Logo](https://www.vectorlogo.zone/logos/electronjs/electronjs-icon.svg)](https://electronjs.org/) -![Maintained][maintained-badge] -[![Make a pull request][prs-badge]][prs] -[![License][license-badge]](LICENSE.md) -[![Linux Build][linux-build-badge]][linux-build] -[![MacOS Build][macos-build-badge]][macos-build] -[![Windows Build][windows-build-badge]][windows-build] - -[![Watch on GitHub][github-watch-badge]][github-watch] -[![Star on GitHub][github-star-badge]][github-star] -[![Tweet][twitter-badge]][twitter] - -# Introduction - -Bootstrap and package your project with Angular 17 and Electron 30 (Typescript + SASS + Hot Reload) for creating Desktop applications. - -Currently runs with: - -- Angular v17.3.6 -- Electron v30.0.1 - -With this sample, you can: - -- Run your app in a local development environment with Electron & Hot reload -- Run your app in a production environment -- Execute your tests with Jest and Playwright (E2E) -- Package your app into an executable file for Linux, Windows & Mac - -/!\ Hot reload only pertains to the renderer process. The main electron process is not able to be hot reloaded, only restarted. - -/!\ Angular CLI & Electron Builder needs Node 18.10 or later to work correctly. - -## Getting Started - -*Clone this repository locally:* - -``` bash -git clone https://github.com/maximegris/angular-electron.git +# **Angular with Electron** +### ***Coding Electron with Angular-like Structure: Class-Based, Decorators, and Dependency Injection*** +## Handling Tray Events with Decorators + +Using decorators, you can efficiently manage Tray events. Here’s how you can listen to Tray events using the `@TrayListener` decorator: +### Example: Listening for `click` and `double-click` Events + +```typescript +@TrayListener(TrayEventEnum.CLICK) +onTrayClick(event: TrayEventType[TrayEventEnum.CLICK]) { + if (this.window?.isVisible()) { + this.window?.hide(); + } else { + this.window?.show(); + } + console.log('Tray clicked'); +} + +@TrayListener(TrayEventEnum.DOUBLE_CLICK) +onTrayDoubleClick(event: TrayEventType[TrayEventEnum.DOUBLE_CLICK]) { + console.log('Tray double-clicked'); +} ``` +## Handling BrowserWindow Events with Decorators -*Install dependencies with npm (used by Electron renderer process):* +Similarly, you can use decorators to handle BrowserWindow events effectively. For instance, to manage the `close` event, use the `@WindowListener` decorator: +### Example: Listening for the `close` Event -``` bash -npm install +```typescript +@WindowListener(WindowEventEnum.CLOSED) +onWindowClose(event: WindowEventType[WindowEventEnum.CLOSED]) { + this.window = null; + console.log('Window closed'); +} ``` +## Handling IPC Events with Decorators -There is an issue with `yarn` and `node_modules` when the application is built by the packager. Please use `npm` as dependencies manager. - -If you want to generate Angular components with Angular-cli , you **MUST** install `@angular/cli` in npm global context. -Please follow [Angular-cli documentation](https://github.com/angular/angular-cli) if you had installed a previous version of `angular-cli`. +You can also use decorators to manage IPC events effectively. For example, to handle the `UPDATE_TRAY_TEXT` event from the main process, you can use the `@IpcListener` decorator: -``` bash -npm install -g @angular/cli -``` +### Listening for the `UPDATE_TRAY_TEXT` IPC Event -*Install NodeJS dependencies with npm (used by Electron main process):* +```typescript +@IpcListener(IPCChannelEnum.UPDATE_TRAY_TEXT) +onUpdateText(event: IpcMainEvent, timeLeft: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]): void { + console.log(timeLeft); +} -``` bash -cd app/ -npm install ``` +## Dependency Injection -Why two package.json ? This project follow [Electron Builder two package.json structure](https://www.electron.build/tutorials/two-package-structure) in order to optimize final bundle and be still able to use Angular `ng add` feature. - -## To build for development - -- **in a terminal window** -> npm start - -Voila! You can use your Angular + Electron app in a local development environment with hot reload! - -The application code is managed by `app/main.ts`. In this sample, the app runs with a simple Angular App (http://localhost:4200), and an Electron window. \ -The Angular component contains an example of Electron and NodeJS native lib import. \ -You can disable "Developer Tools" by commenting `win.webContents.openDevTools();` in `app/main.ts`. - -## Project structure - -| Folder | Description | -|--------|--------------------------------------------------| -| app | Electron main process folder (NodeJS) | -| src | Electron renderer process folder (Web / Angular) | - -## How to import 3rd party libraries +### Define a Service -This sample project runs in both modes (web and electron). To make this work, **you have to import your dependencies the right way**. \ +Use the `@injectable` decorator to mark your classes for dependency injection. -There are two kind of 3rd party libraries : -- NodeJS's one - Uses NodeJS core module (crypto, fs, util...) - - I suggest you add this kind of 3rd party library in `dependencies` of both `app/package.json` and `package.json (root folder)` in order to make it work in both Electron's Main process (app folder) and Electron's Renderer process (src folder). +```typescript +import { injectable } from 'inversify'; -Please check `providers/electron.service.ts` to watch how conditional import of libraries has to be done when using NodeJS / 3rd party libraries in renderer context (i.e. Angular). - -- Web's one (like bootstrap, material, tailwind...) - - It have to be added in `dependencies` of `package.json (root folder)` - -## Add a dependency with ng-add - -You may encounter some difficulties with `ng-add` because this project doesn't use the defaults `@angular-builders`. \ -For example you can find [here](HOW_TO.md) how to install Angular-Material with `ng-add`. - -## Browser mode - -Maybe you only want to execute the application in the browser with hot reload? Just run `npm run ng:serve:web`. - -## Included Commands - -| Command | Description | -|--------------------------|-------------------------------------------------------------------------------------------------------| -| `npm run ng:serve` | Execute the app in the web browser (DEV mode) | -| `npm run web:build` | Build the app that can be used directly in the web browser. Your built files are in the /dist folder. | -| `npm run electron:local` | Builds your application and start electron locally | -| `npm run electron:build` | Builds your application and creates an app consumable based on your operating system | - -**Your application is optimised. Only /dist folder and NodeJS dependencies are included in the final bundle.** - -## You want to use a specific lib (like rxjs) in electron main thread ? - -YES! You can do it! Just by importing your library in npm dependencies section of `app/package.json` with `npm install --save XXXXX`. \ -It will be loaded by electron during build phase and added to your final bundle. \ -Then use your library by importing it in `app/main.ts` file. Quite simple, isn't it? - -## E2E Testing - -E2E Test scripts can be found in `e2e` folder. - -| Command | Description | -|---------------|---------------------------| -| `npm run e2e` | Execute end to end tests | - -Note: To make it work behind a proxy, you can add this proxy exception in your terminal -`export {no_proxy,NO_PROXY}="127.0.0.1,localhost"` - -## Debug with VsCode - -[VsCode](https://code.visualstudio.com/) debug configuration is available! In order to use it, you need the extension [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome). - -Then set some breakpoints in your application's source code. +@injectable() +export class FileService { + // Implementation +} +``` +### Inject Dependencies into Classes -Finally from VsCode press **Ctrl+Shift+D** and select **Application Debug** and press **F5**. +Inject dependencies into your classes using the `@inject` decorator. -Please note that Hot reload is only available in Renderer process. +```typescript +@injectable() +export class MainWindow extends MainWindowBaseClass implements OnAppReady { + private readonly TRAY_ICON_PATH = '/src/assets/icons/favicon.256x256.png'; -## Want to use Angular Material ? Ngx-Bootstrap ? + constructor(@inject(FileService) protected readonly fileService: FileService) { + super(); + } -Please refer to [HOW_TO file](./HOW_TO.md) + // Other methods and properties +} +``` +### Set Up the DI Container -## Branch & Packages version +Configure the DI container with the services you need. -- Angular 4 & Electron 1 : Branch [angular4](https://github.com/maximegris/angular-electron/tree/angular4) -- Angular 5 & Electron 1 : Branch [angular5](https://github.com/maximegris/angular-electron/tree/angular5) -- Angular 6 & Electron 3 : Branch [angular6](https://github.com/maximegris/angular-electron/tree/angular6) -- Angular 7 & Electron 3 : Branch [angular7](https://github.com/maximegris/angular-electron/tree/angular7) -- Angular 8 & Electron 7 : Branch [angular8](https://github.com/maximegris/angular-electron/tree/angular8) -- Angular 9 & Electron 7 : Branch [angular9](https://github.com/maximegris/angular-electron/tree/angular9) -- Angular 10 & Electron 9 : Branch [angular10](https://github.com/maximegris/angular-electron/tree/angular10) -- Angular 11 & Electron 12 : Branch [angular11](https://github.com/maximegris/angular-electron/tree/angular11) -- Angular 12 & Electron 16 : Branch [angular12](https://github.com/maximegris/angular-electron/tree/angular12) -- Angular 13 & Electron 18 : Branch [angular13](https://github.com/maximegris/angular-electron/tree/angular13) -- Angular 14 & Electron 21 : Branch [angular14](https://github.com/maximegris/angular-electron/tree/angular14) -- Angular 15 & Electron 24 : Branch [angular15](https://github.com/maximegris/angular-electron/tree/angular15) -- Angular 16 & Electron 25 : Branch [angular16](https://github.com/maximegris/angular-electron/tree/angular16) -- Angular 17 & Electron 30 : (main) -- -[maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen -[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[license]: https://github.com/maximegris/angular-electron/blob/main/LICENSE.md -[prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg -[prs]: http://makeapullrequest.com +```typescript +const container = useProvide([FileService]); +``` +### Resolve the MainWindow Class with Dependencies -[linux-build-badge]: https://github.com/maximegris/angular-electron/workflows/Linux%20Build/badge.svg -[linux-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22Linux+Build%22 -[macos-build-badge]: https://github.com/maximegris/angular-electron/workflows/MacOS%20Build/badge.svg -[macos-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22MacOS+Build%22 -[windows-build-badge]: https://github.com/maximegris/angular-electron/workflows/Windows%20Build/badge.svg -[windows-build]: https://github.com/maximegris/angular-electron/actions?query=workflow%3A%22Windows+Build%22 +Resolve your main class with its dependencies from the container. -[github-watch-badge]: https://img.shields.io/github/watchers/maximegris/angular-electron.svg?style=social -[github-watch]: https://github.com/maximegris/angular-electron/watchers -[github-star-badge]: https://img.shields.io/github/stars/maximegris/angular-electron.svg?style=social -[github-star]: https://github.com/maximegris/angular-electron/stargazers -[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20angular-electron!%20https://github.com/maximegris/angular-electron%20%F0%9F%91%8D -[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/maximegris/angular-electron.svg?style=social +```typescript +const mainWindowWithDependencies = container.resolve(MainWindow); +``` diff --git a/app/main.ts b/app/main.ts index 17c3f2aff..eb36b05b2 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,83 +1,10 @@ -import {app, BrowserWindow, screen} from 'electron'; -import * as path from 'path'; -import * as fs from 'fs'; - -let win: BrowserWindow | null = null; -const args = process.argv.slice(1), - serve = args.some(val => val === '--serve'); - -function createWindow(): BrowserWindow { - - const size = screen.getPrimaryDisplay().workAreaSize; - - // Create the browser window. - win = new BrowserWindow({ - x: 0, - y: 0, - width: size.width, - height: size.height, - webPreferences: { - nodeIntegration: true, - allowRunningInsecureContent: (serve), - contextIsolation: false, - }, - }); - - if (serve) { - const debug = require('electron-debug'); - debug(); - - require('electron-reloader')(module); - win.loadURL('http://localhost:4200'); - } else { - // Path when running electron executable - let pathIndex = './index.html'; - - if (fs.existsSync(path.join(__dirname, '../dist/index.html'))) { - // Path when running electron in local folder - pathIndex = '../dist/index.html'; - } - - const url = new URL(path.join('file:', __dirname, pathIndex)); - win.loadURL(url.href); - } - - // Emitted when the window is closed. - win.on('closed', () => { - // Dereference the window object, usually you would store window - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - win = null; - }); - - return win; -} +import {MainWindow} from "./windows/main.window"; +import {useProvide} from "./utils/hooks/provide.hook"; +import {FileService} from "./utils/services/file.service"; try { - // 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. - // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947 - app.on('ready', () => setTimeout(createWindow, 400)); - - // Quit when all windows are closed. - app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } - }); - - app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - createWindow(); - } - }); - + const mainWindow = useProvide([FileService]).resolve(MainWindow) } catch (e) { - // Catch Error - // throw e; + } + diff --git a/app/utils/base-classes/main-window.base-class.js b/app/utils/base-classes/main-window.base-class.js new file mode 100644 index 000000000..5c13cc3f2 --- /dev/null +++ b/app/utils/base-classes/main-window.base-class.js @@ -0,0 +1,23 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MainWindowBaseClass = void 0; +require("reflect-metadata"); // Required for InversifyJS to work properly +const inversify_1 = require("inversify"); +const window_base_class_1 = require("./window.base-class"); +let MainWindowBaseClass = class MainWindowBaseClass extends window_base_class_1.WindowBaseClass { + constructor() { + super(...arguments); + this.tray = null; + } +}; +exports.MainWindowBaseClass = MainWindowBaseClass; +exports.MainWindowBaseClass = MainWindowBaseClass = __decorate([ + (0, inversify_1.injectable)() +], MainWindowBaseClass); +//# sourceMappingURL=main-window.base-class.js.map \ No newline at end of file diff --git a/app/utils/base-classes/main-window.base-class.ts b/app/utils/base-classes/main-window.base-class.ts new file mode 100644 index 000000000..bf7e69333 --- /dev/null +++ b/app/utils/base-classes/main-window.base-class.ts @@ -0,0 +1,9 @@ +import {Tray} from "electron"; +import "reflect-metadata"; // Required for InversifyJS to work properly +import {injectable} from "inversify"; +import {WindowBaseClass} from "./window.base-class"; + +@injectable() +export abstract class MainWindowBaseClass extends WindowBaseClass { + protected tray: Tray | null = null; +} diff --git a/app/utils/base-classes/window.base-class.js b/app/utils/base-classes/window.base-class.js new file mode 100644 index 000000000..37836ed35 --- /dev/null +++ b/app/utils/base-classes/window.base-class.js @@ -0,0 +1,68 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +var WindowBaseClass_1; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WindowBaseClass = void 0; +const electron_1 = require("electron"); +const fs = require("node:fs"); +const path = require("node:path"); +const inversify_1 = require("inversify"); +require("reflect-metadata"); // Required for InversifyJS to work properly +const rxjs_1 = require("rxjs"); +const window_listener_decorator_1 = require("../decorators/window-listener.decorator"); +const window_listener_enum_1 = require("../enums/window-listener.enum"); +let WindowBaseClass = WindowBaseClass_1 = class WindowBaseClass { + constructor() { + this.window = null; + this.tray = null; + this.app = electron_1.app; + this._window$ = new rxjs_1.ReplaySubject(); + electron_1.app.whenReady().then(() => this.onAppReady()); + } + loadUrl(window, absolutePath) { + this.window = window; + this._window$.next(window); + this._window$.complete(); + if (WindowBaseClass_1.isServeMode) { + const debug = require('electron-debug'); + debug(); + require('electron-reloader')(module); + this.window.loadURL('http://localhost:4200'); + } + else { + let pathIndex = './index.html'; + if (fs.existsSync(path.join(absolutePath, '../dist/index.html'))) { + pathIndex = '../dist/index.html'; + } + const url = new URL(path.join('file:', absolutePath, pathIndex)); + this.window.loadURL(url.href); + } + } + getWindow() { + return this._window$.asObservable(); + } + onWindowClose() { + this.window = null; + } +}; +exports.WindowBaseClass = WindowBaseClass; +WindowBaseClass.isServeMode = process.argv.slice(1).some(val => val === '--serve'); +__decorate([ + (0, window_listener_decorator_1.WindowListener)(window_listener_enum_1.WindowEventEnum.CLOSED), + __metadata("design:type", Function), + __metadata("design:paramtypes", []), + __metadata("design:returntype", void 0) +], WindowBaseClass.prototype, "onWindowClose", null); +exports.WindowBaseClass = WindowBaseClass = WindowBaseClass_1 = __decorate([ + (0, inversify_1.injectable)(), + __metadata("design:paramtypes", []) +], WindowBaseClass); +//# sourceMappingURL=window.base-class.js.map \ No newline at end of file diff --git a/app/utils/base-classes/window.base-class.ts b/app/utils/base-classes/window.base-class.ts new file mode 100644 index 000000000..346feeb8c --- /dev/null +++ b/app/utils/base-classes/window.base-class.ts @@ -0,0 +1,56 @@ +import {app, BrowserWindow, Tray} from "electron"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import {injectable} from "inversify"; +import "reflect-metadata"; // Required for InversifyJS to work properly +import {OnAppReady} from "../interfaces/on-app-ready.interface"; +import {Observable, ReplaySubject} from "rxjs"; +import {WindowListener} from "../decorators/window-listener.decorator"; +import {WindowEventEnum} from "../enums/window-listener.enum"; + +@injectable() +export abstract class WindowBaseClass implements OnAppReady { + protected static isServeMode = process.argv.slice(1).some(val => val === '--serve'); + protected window: BrowserWindow | null = null; + protected tray: Tray | null = null; + protected readonly app = app; + private _window$: ReplaySubject = new ReplaySubject(); + + constructor() { + app.whenReady().then(() => this.onAppReady()); + } + + abstract onAppReady(): void; + + protected loadUrl(window: BrowserWindow, absolutePath: string) { + this.window = window; + this._window$.next(window); + this._window$.complete(); + if (WindowBaseClass.isServeMode) { + const debug = require('electron-debug'); + debug(); + + require('electron-reloader')(module); + this.window.loadURL('http://localhost:4200'); + } else { + let pathIndex = './index.html'; + + if (fs.existsSync(path.join(absolutePath, '../dist/index.html'))) { + pathIndex = '../dist/index.html'; + } + + const url = new URL(path.join('file:', absolutePath, pathIndex)); + this.window.loadURL(url.href); + } + } + + public getWindow(): Observable { + return this._window$.asObservable(); + } + + @WindowListener(WindowEventEnum.CLOSED) + onWindowClose() { + this.window = null; + } + +} diff --git a/app/utils/decorators/ipc-listener.decorator.js b/app/utils/decorators/ipc-listener.decorator.js new file mode 100644 index 000000000..4478bf4c4 --- /dev/null +++ b/app/utils/decorators/ipc-listener.decorator.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IpcListener = void 0; +const electron_1 = require("electron"); +function IpcListener(channel) { + return function (target, propertyKey, descriptor) { + if (!target.__ipcEvents) { + target.__ipcEvents = []; + } + const eventKey = `${channel}_${propertyKey}`; + // Prevent duplicate event binding + if (!target.__ipcEvents.includes(eventKey)) { + target.__ipcEvents.push(eventKey); + const originalOnAppReady = target.onAppReady; + target.onAppReady = function (...args) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + // Listen to the IPC event + electron_1.ipcMain.on(channel, (event, ...eventArgs) => { + if (typeof this[propertyKey] === 'function') { + this[propertyKey](event, ...eventArgs); + } + }); + }; + } + }; +} +exports.IpcListener = IpcListener; +//# sourceMappingURL=ipc-listener.decorator.js.map \ No newline at end of file diff --git a/app/utils/decorators/ipc-listener.decorator.ts b/app/utils/decorators/ipc-listener.decorator.ts new file mode 100644 index 000000000..a06545fcc --- /dev/null +++ b/app/utils/decorators/ipc-listener.decorator.ts @@ -0,0 +1,32 @@ +import {ipcMain, IpcMainEvent} from 'electron'; +import {IPCChannelEnum} from "../../../shared/enums/ipc-channel.enum"; + +export function IpcListener(channel: IPCChannelEnum) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target.__ipcEvents) { + target.__ipcEvents = []; + } + + const eventKey = `${channel}_${propertyKey}`; + + // Prevent duplicate event binding + if (!target.__ipcEvents.includes(eventKey)) { + target.__ipcEvents.push(eventKey); + + const originalOnAppReady = target.onAppReady; + + target.onAppReady = function (...args: any[]) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + + // Listen to the IPC event + ipcMain.on(channel, (event: IpcMainEvent, ...eventArgs: any[]) => { + if (typeof this[propertyKey] === 'function') { + this[propertyKey](event, ...eventArgs); + } + }); + }; + } + }; +} diff --git a/app/utils/decorators/tray-listener.decorator.js b/app/utils/decorators/tray-listener.decorator.js new file mode 100644 index 000000000..6940e1df8 --- /dev/null +++ b/app/utils/decorators/tray-listener.decorator.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TrayListener = void 0; +function TrayListener(eventName) { + return function (target, propertyKey, descriptor) { + if (!target.__trayEvents) { + target.__trayEvents = []; + } + const eventKey = `${eventName}_${propertyKey}`; + // Prevent duplicate event binding + if (!target.__trayEvents.includes(eventKey)) { + target.__trayEvents.push(eventKey); + const originalOnAppReady = target.onAppReady; + target.onAppReady = function (...args) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + const tray = this.tray; + if (tray) { + tray.on(eventName, (...eventArgs) => { + // Ensure the method signature matches the event's arguments + if (typeof this[propertyKey] === 'function') { + const expectedArgs = eventArgs; + this[propertyKey](expectedArgs); + } + }); + } + }; + } + }; +} +exports.TrayListener = TrayListener; +//# sourceMappingURL=tray-listener.decorator.js.map \ No newline at end of file diff --git a/app/utils/decorators/tray-listener.decorator.ts b/app/utils/decorators/tray-listener.decorator.ts new file mode 100644 index 000000000..8a49eae39 --- /dev/null +++ b/app/utils/decorators/tray-listener.decorator.ts @@ -0,0 +1,38 @@ +import { Tray } from 'electron'; +import {TrayEventEnum} from "../enums/tray-listener.enum"; +import {TrayEventType} from "../types/tray-listener.type"; + +export function TrayListener(eventName: T) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target.__trayEvents) { + target.__trayEvents = []; + } + + const eventKey = `${eventName}_${propertyKey}`; + + // Prevent duplicate event binding + if (!target.__trayEvents.includes(eventKey)) { + target.__trayEvents.push(eventKey); + + const originalOnAppReady = target.onAppReady; + + target.onAppReady = function (...args: any[]) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + + const tray = this.tray as Tray; + + if (tray) { + tray.on(eventName as any, (...eventArgs: any[]) => { + // Ensure the method signature matches the event's arguments + if (typeof this[propertyKey] === 'function') { + const expectedArgs = eventArgs as TrayEventType[T]; + this[propertyKey](expectedArgs); + } + }); + } + }; + } + }; +} diff --git a/app/utils/decorators/window-listener.decorator.js b/app/utils/decorators/window-listener.decorator.js new file mode 100644 index 000000000..fb74df8ed --- /dev/null +++ b/app/utils/decorators/window-listener.decorator.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WindowListener = void 0; +function WindowListener(eventName) { + return function (target, propertyKey, descriptor) { + if (!target.__windowEvents) { + target.__windowEvents = []; + } + const eventKey = `${eventName}_${propertyKey}`; + // Prevent duplicate event binding + if (!target.__windowEvents.includes(eventKey)) { + target.__windowEvents.push(eventKey); + const originalOnAppReady = target.onAppReady; + target.onAppReady = function (...args) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + const window = this.window; + if (window) { + window.on(eventName, (...eventArgs) => { + // Ensure the method signature matches the event's arguments + if (typeof this[propertyKey] === 'function') { + const expectedArgs = eventArgs; + this[propertyKey](expectedArgs); + } + }); + } + }; + } + }; +} +exports.WindowListener = WindowListener; +//# sourceMappingURL=window-listener.decorator.js.map \ No newline at end of file diff --git a/app/utils/decorators/window-listener.decorator.ts b/app/utils/decorators/window-listener.decorator.ts new file mode 100644 index 000000000..04074d75f --- /dev/null +++ b/app/utils/decorators/window-listener.decorator.ts @@ -0,0 +1,38 @@ +import {WindowEventEnum} from "../enums/window-listener.enum"; +import {BrowserWindow} from "electron"; +import {WindowEventType} from "../types/window-listener.type"; + +export function WindowListener(eventName: T) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target.__windowEvents) { + target.__windowEvents = []; + } + + const eventKey = `${eventName}_${propertyKey}`; + + // Prevent duplicate event binding + if (!target.__windowEvents.includes(eventKey)) { + target.__windowEvents.push(eventKey); + + const originalOnAppReady = target.onAppReady; + + target.onAppReady = function (...args: any[]) { + if (typeof originalOnAppReady === 'function') { + originalOnAppReady.apply(this, args); + } + + const window = this.window as BrowserWindow; + + if (window) { + window.on(eventName as any, (...eventArgs: any[]) => { + // Ensure the method signature matches the event's arguments + if (typeof this[propertyKey] === 'function') { + const expectedArgs = eventArgs as WindowEventType[T]; + this[propertyKey](expectedArgs); + } + }); + } + }; + } + }; +} diff --git a/app/utils/enums/tray-listener.enum.js b/app/utils/enums/tray-listener.enum.js new file mode 100644 index 000000000..e79104b1b --- /dev/null +++ b/app/utils/enums/tray-listener.enum.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TrayEventEnum = void 0; +var TrayEventEnum; +(function (TrayEventEnum) { + TrayEventEnum["CLICK"] = "click"; + TrayEventEnum["RIGHT_CLICK"] = "right-click"; + TrayEventEnum["DOUBLE_CLICK"] = "double-click"; + TrayEventEnum["MOUSE_ENTER"] = "mouse-enter"; + TrayEventEnum["MOUSE_LEAVE"] = "mouse-leave"; + TrayEventEnum["MOUSE_MOVE"] = "mouse-move"; + TrayEventEnum["BALLOON_SHOW"] = "balloon-show"; + TrayEventEnum["BALLOON_CLICK"] = "balloon-click"; + TrayEventEnum["BALLOON_CLOSED"] = "balloon-closed"; + TrayEventEnum["DROP"] = "drop"; + TrayEventEnum["DROP_FILES"] = "drop-files"; + TrayEventEnum["DROP_TEXT"] = "drop-text"; + TrayEventEnum["DRAG_ENTER"] = "drag-enter"; + TrayEventEnum["DRAG_LEAVE"] = "drag-leave"; + TrayEventEnum["DRAG_END"] = "drag-end"; +})(TrayEventEnum || (exports.TrayEventEnum = TrayEventEnum = {})); +//# sourceMappingURL=tray-listener.enum.js.map \ No newline at end of file diff --git a/app/utils/enums/tray-listener.enum.ts b/app/utils/enums/tray-listener.enum.ts new file mode 100644 index 000000000..58925f9bc --- /dev/null +++ b/app/utils/enums/tray-listener.enum.ts @@ -0,0 +1,17 @@ +export enum TrayEventEnum { + CLICK = 'click', + RIGHT_CLICK = 'right-click', + DOUBLE_CLICK = 'double-click', + MOUSE_ENTER = 'mouse-enter', + MOUSE_LEAVE = 'mouse-leave', + MOUSE_MOVE = 'mouse-move', + BALLOON_SHOW = 'balloon-show', + BALLOON_CLICK = 'balloon-click', + BALLOON_CLOSED = 'balloon-closed', + DROP = 'drop', + DROP_FILES = 'drop-files', + DROP_TEXT = 'drop-text', + DRAG_ENTER = 'drag-enter', + DRAG_LEAVE = 'drag-leave', + DRAG_END = 'drag-end' +} diff --git a/app/utils/enums/window-listener.enum.js b/app/utils/enums/window-listener.enum.js new file mode 100644 index 000000000..60e3f2c12 --- /dev/null +++ b/app/utils/enums/window-listener.enum.js @@ -0,0 +1,40 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WindowEventEnum = void 0; +var WindowEventEnum; +(function (WindowEventEnum) { + WindowEventEnum["PAGE_TITLE_UPDATED"] = "page-title-updated"; + WindowEventEnum["CLOSE"] = "close"; + WindowEventEnum["CLOSED"] = "closed"; + WindowEventEnum["SESSION_END"] = "session-end"; + WindowEventEnum["UNRESPONSIVE"] = "unresponsive"; + WindowEventEnum["RESPONSIVE"] = "responsive"; + WindowEventEnum["BLUR"] = "blur"; + WindowEventEnum["FOCUS"] = "focus"; + WindowEventEnum["SHOW"] = "show"; + WindowEventEnum["HIDE"] = "hide"; + WindowEventEnum["READY_TO_SHOW"] = "ready-to-show"; + WindowEventEnum["MAXIMIZE"] = "maximize"; + WindowEventEnum["UNMAXIMIZE"] = "unmaximize"; + WindowEventEnum["MINIMIZE"] = "minimize"; + WindowEventEnum["RESTORE"] = "restore"; + WindowEventEnum["WILL_RESIZE"] = "will-resize"; + WindowEventEnum["RESIZE"] = "resize"; + WindowEventEnum["WILL_MOVE"] = "will-move"; + WindowEventEnum["MOVE"] = "move"; + WindowEventEnum["MOVED"] = "moved"; + WindowEventEnum["ENTER_FULL_SCREEN"] = "enter-full-screen"; + WindowEventEnum["LEAVE_FULL_SCREEN"] = "leave-full-screen"; + WindowEventEnum["ENTER_HTML_FULL_SCREEN"] = "enter-html-full-screen"; + WindowEventEnum["LEAVE_HTML_FULL_SCREEN"] = "leave-html-full-screen"; + WindowEventEnum["ALWAYS_ON_TOP_CHANGED"] = "always-on-top-changed"; + WindowEventEnum["APP_COMMAND"] = "app-command"; + WindowEventEnum["SCROLL_TOUCH_BEGIN"] = "scroll-touch-begin"; + WindowEventEnum["SCROLL_TOUCH_END"] = "scroll-touch-end"; + WindowEventEnum["SCROLL_TOUCH_EDGE"] = "scroll-touch-edge"; + WindowEventEnum["SWIPE"] = "swipe"; + WindowEventEnum["SHEET_BEGIN"] = "sheet-begin"; + WindowEventEnum["SHEET_END"] = "sheet-end"; + WindowEventEnum["NEW_WINDOW_FOR_TAB"] = "new-window-for-tab"; +})(WindowEventEnum || (exports.WindowEventEnum = WindowEventEnum = {})); +//# sourceMappingURL=window-listener.enum.js.map \ No newline at end of file diff --git a/app/utils/enums/window-listener.enum.ts b/app/utils/enums/window-listener.enum.ts new file mode 100644 index 000000000..8a06cbc87 --- /dev/null +++ b/app/utils/enums/window-listener.enum.ts @@ -0,0 +1,38 @@ + + export enum WindowEventEnum { + PAGE_TITLE_UPDATED = 'page-title-updated', + CLOSE = 'close', + CLOSED = 'closed', + SESSION_END = 'session-end', + UNRESPONSIVE = 'unresponsive', + RESPONSIVE = 'responsive', + BLUR = 'blur', + FOCUS = 'focus', + SHOW = 'show', + HIDE = 'hide', + READY_TO_SHOW = 'ready-to-show', + MAXIMIZE = 'maximize', + UNMAXIMIZE = 'unmaximize', + MINIMIZE = 'minimize', + RESTORE = 'restore', + WILL_RESIZE = 'will-resize', + RESIZE = 'resize', + WILL_MOVE = 'will-move', + MOVE = 'move', + MOVED = 'moved', + ENTER_FULL_SCREEN = 'enter-full-screen', + LEAVE_FULL_SCREEN = 'leave-full-screen', + ENTER_HTML_FULL_SCREEN = 'enter-html-full-screen', + LEAVE_HTML_FULL_SCREEN = 'leave-html-full-screen', + ALWAYS_ON_TOP_CHANGED = 'always-on-top-changed', + APP_COMMAND = 'app-command', + SCROLL_TOUCH_BEGIN = 'scroll-touch-begin', + SCROLL_TOUCH_END = 'scroll-touch-end', + SCROLL_TOUCH_EDGE = 'scroll-touch-edge', + SWIPE = 'swipe', + SHEET_BEGIN = 'sheet-begin', + SHEET_END = 'sheet-end', + NEW_WINDOW_FOR_TAB = 'new-window-for-tab' +} + + diff --git a/app/utils/hooks/provide.hook.js b/app/utils/hooks/provide.hook.js new file mode 100644 index 000000000..c04eec873 --- /dev/null +++ b/app/utils/hooks/provide.hook.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useProvide = void 0; +require("reflect-metadata"); +const inversify_1 = require("inversify"); +function useProvide(providers) { + const container = new inversify_1.Container(); + providers.forEach(item => { + container.bind(item).to(item); + }); + return container; +} +exports.useProvide = useProvide; +//# sourceMappingURL=provide.hook.js.map \ No newline at end of file diff --git a/app/utils/hooks/provide.hook.ts b/app/utils/hooks/provide.hook.ts new file mode 100644 index 000000000..28f7b5cfb --- /dev/null +++ b/app/utils/hooks/provide.hook.ts @@ -0,0 +1,12 @@ +import 'reflect-metadata'; +import {Container} from "inversify"; + +type InjectableClass = new (...args: any[]) => T; + +export function useProvide(providers: InjectableClass[]) { + const container: Container = new Container(); + providers.forEach(item => { + container.bind(item).to(item); + }); + return container; +} diff --git a/app/utils/interfaces/on-app-ready.interface.js b/app/utils/interfaces/on-app-ready.interface.js new file mode 100644 index 000000000..0313cbf69 --- /dev/null +++ b/app/utils/interfaces/on-app-ready.interface.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=on-app-ready.interface.js.map \ No newline at end of file diff --git a/app/utils/interfaces/on-app-ready.interface.ts b/app/utils/interfaces/on-app-ready.interface.ts new file mode 100644 index 000000000..b0a70b7da --- /dev/null +++ b/app/utils/interfaces/on-app-ready.interface.ts @@ -0,0 +1,3 @@ +export interface OnAppReady { + onAppReady(): void; +} diff --git a/app/utils/services/file.service.js b/app/utils/services/file.service.js new file mode 100644 index 000000000..b45663c57 --- /dev/null +++ b/app/utils/services/file.service.js @@ -0,0 +1,21 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileService = void 0; +const path = require("node:path"); +const inversify_1 = require("inversify"); +let FileService = class FileService { + constructor() { + this.rootPath = path.join(__dirname, '../../..'); + } +}; +exports.FileService = FileService; +exports.FileService = FileService = __decorate([ + (0, inversify_1.injectable)() +], FileService); +//# sourceMappingURL=file.service.js.map \ No newline at end of file diff --git a/app/utils/services/file.service.ts b/app/utils/services/file.service.ts new file mode 100644 index 000000000..8630e75eb --- /dev/null +++ b/app/utils/services/file.service.ts @@ -0,0 +1,6 @@ +import * as path from "node:path"; +import {injectable} from "inversify"; +@injectable() +export class FileService { + readonly rootPath = path.join(__dirname, '../../..'); +} diff --git a/app/utils/types/tray-listener.type.js b/app/utils/types/tray-listener.type.js new file mode 100644 index 000000000..cad13d954 --- /dev/null +++ b/app/utils/types/tray-listener.type.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const tray_listener_enum_1 = require("../enums/tray-listener.enum"); +//# sourceMappingURL=tray-listener.type.js.map \ No newline at end of file diff --git a/app/utils/types/tray-listener.type.ts b/app/utils/types/tray-listener.type.ts new file mode 100644 index 000000000..c7c81b0a1 --- /dev/null +++ b/app/utils/types/tray-listener.type.ts @@ -0,0 +1,20 @@ +import {Event as ElectronEvent, Rectangle} from 'electron'; +import {TrayEventEnum} from "../enums/tray-listener.enum"; + +export type TrayEventType = { + [TrayEventEnum.CLICK]: [event: ElectronEvent, bounds: Rectangle]; + [TrayEventEnum.RIGHT_CLICK]: [event: ElectronEvent, bounds: Rectangle]; + [TrayEventEnum.DOUBLE_CLICK]: [event: ElectronEvent, bounds: Rectangle]; + [TrayEventEnum.MOUSE_ENTER]: []; + [TrayEventEnum.MOUSE_LEAVE]: []; + [TrayEventEnum.MOUSE_MOVE]: [event: ElectronEvent, bounds: Rectangle]; + [TrayEventEnum.BALLOON_SHOW]: []; + [TrayEventEnum.BALLOON_CLICK]: []; + [TrayEventEnum.BALLOON_CLOSED]: []; + [TrayEventEnum.DROP]: [event: ElectronEvent]; + [TrayEventEnum.DROP_FILES]: [event: ElectronEvent, files: string[]]; + [TrayEventEnum.DROP_TEXT]: [event: ElectronEvent, text: string]; + [TrayEventEnum.DRAG_ENTER]: [event: ElectronEvent]; + [TrayEventEnum.DRAG_LEAVE]: [event: ElectronEvent]; + [TrayEventEnum.DRAG_END]: [event: ElectronEvent]; +}; diff --git a/app/utils/types/window-listener.type.js b/app/utils/types/window-listener.type.js new file mode 100644 index 000000000..fbe868422 --- /dev/null +++ b/app/utils/types/window-listener.type.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const window_listener_enum_1 = require("../enums/window-listener.enum"); +//# sourceMappingURL=window-listener.type.js.map \ No newline at end of file diff --git a/app/utils/types/window-listener.type.ts b/app/utils/types/window-listener.type.ts new file mode 100644 index 000000000..7dd7237c8 --- /dev/null +++ b/app/utils/types/window-listener.type.ts @@ -0,0 +1,38 @@ +import {Event as ElectronEvent, Rectangle} from "electron"; +import {WindowEventEnum} from "../enums/window-listener.enum"; + +export type WindowEventType = { + [WindowEventEnum.PAGE_TITLE_UPDATED]: [title: string]; + [WindowEventEnum.CLOSE]: []; + [WindowEventEnum.CLOSED]: []; + [WindowEventEnum.SESSION_END]: []; + [WindowEventEnum.UNRESPONSIVE]: []; + [WindowEventEnum.RESPONSIVE]: []; + [WindowEventEnum.BLUR]: []; + [WindowEventEnum.FOCUS]: []; + [WindowEventEnum.SHOW]: []; + [WindowEventEnum.HIDE]: []; + [WindowEventEnum.READY_TO_SHOW]: []; + [WindowEventEnum.MAXIMIZE]: []; + [WindowEventEnum.UNMAXIMIZE]: []; + [WindowEventEnum.MINIMIZE]: []; + [WindowEventEnum.RESTORE]: []; + [WindowEventEnum.WILL_RESIZE]: [event: ElectronEvent, newBounds: Rectangle]; + [WindowEventEnum.RESIZE]: [event: ElectronEvent, newBounds: Rectangle]; + [WindowEventEnum.WILL_MOVE]: [event: ElectronEvent, newBounds: Rectangle]; + [WindowEventEnum.MOVE]: [event: ElectronEvent, newBounds: Rectangle]; + [WindowEventEnum.MOVED]: [event: ElectronEvent]; + [WindowEventEnum.ENTER_FULL_SCREEN]: []; + [WindowEventEnum.LEAVE_FULL_SCREEN]: []; + [WindowEventEnum.ENTER_HTML_FULL_SCREEN]: []; + [WindowEventEnum.LEAVE_HTML_FULL_SCREEN]: []; + [WindowEventEnum.ALWAYS_ON_TOP_CHANGED]: [event: ElectronEvent, isAlwaysOnTop: boolean]; + [WindowEventEnum.APP_COMMAND]: [event: ElectronEvent, command: string]; + [WindowEventEnum.SCROLL_TOUCH_BEGIN]: [event: ElectronEvent]; + [WindowEventEnum.SCROLL_TOUCH_END]: [event: ElectronEvent]; + [WindowEventEnum.SCROLL_TOUCH_EDGE]: [event: ElectronEvent]; + [WindowEventEnum.SWIPE]: [event: ElectronEvent, direction: string]; + [WindowEventEnum.SHEET_BEGIN]: [event: ElectronEvent]; + [WindowEventEnum.SHEET_END]: [event: ElectronEvent]; + [WindowEventEnum.NEW_WINDOW_FOR_TAB]: [event: ElectronEvent, url: string]; +}; diff --git a/app/windows/main.window.js b/app/windows/main.window.js new file mode 100644 index 000000000..a7fba288f --- /dev/null +++ b/app/windows/main.window.js @@ -0,0 +1,145 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +var __param = (this && this.__param) || function (paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MainWindow = void 0; +const electron_1 = require("electron"); +const inversify_1 = require("inversify"); +require("reflect-metadata"); // Import reflect-metadata +const path = require("node:path"); +const file_service_1 = require("../utils/services/file.service"); +const main_window_base_class_1 = require("../utils/base-classes/main-window.base-class"); +const tray_listener_decorator_1 = require("../utils/decorators/tray-listener.decorator"); +const tray_listener_enum_1 = require("../utils/enums/tray-listener.enum"); +const window_listener_decorator_1 = require("../utils/decorators/window-listener.decorator"); +const window_listener_enum_1 = require("../utils/enums/window-listener.enum"); +const ipc_listener_decorator_1 = require("../utils/decorators/ipc-listener.decorator"); +const ipc_channel_enum_1 = require("../../shared/enums/ipc-channel.enum"); +let MainWindow = class MainWindow extends main_window_base_class_1.MainWindowBaseClass { + constructor(fileService) { + super(); + this.fileService = fileService; + this.TRAY_ICON_PATH = '/src/assets/icons/electron.png'; + this.popupWindow = null; + this.mainWindowBounds = { + width: 200, + height: 400, + }; + } + onAppReady() { + this.createMainWindow(); + this.createTray(); + } + createMainWindow() { + const window = new electron_1.BrowserWindow({ + x: 0, + y: 0, + width: this.mainWindowBounds.width, + height: this.mainWindowBounds.height, + resizable: false, + frame: false, + skipTaskbar: true, + show: false, + webPreferences: { + nodeIntegration: true, + allowRunningInsecureContent: true, + contextIsolation: false, + backgroundThrottling: false + }, + }); + this.loadUrl(window, this.fileService.rootPath); + } + createTray() { + this.tray = new electron_1.Tray(path.join(this.fileService.rootPath, this.TRAY_ICON_PATH)); + this.tray.setToolTip('this is my first timer'); + } + onTrayClick([event, bounds]) { + if (this.window) { + const newBounds = { + x: bounds.x - (this.mainWindowBounds.width / 2), + y: bounds.y - this.mainWindowBounds.height - 10, + width: this.mainWindowBounds.width, + height: this.mainWindowBounds.height, + }; + this.window.setBounds(newBounds); + this.window.show(); + } + } + onTrayRightClick(event) { + var _a; + const trayMenuConfig = electron_1.Menu.buildFromTemplate([ + { + label: 'quit', + click: () => { + this.app.quit(); + } + }, + { + label: 'show', + click: () => { + var _a; + (_a = this.window) === null || _a === void 0 ? void 0 : _a.show(); + } + }, + { + label: 'hide', + click: () => { + var _a; + (_a = this.window) === null || _a === void 0 ? void 0 : _a.hide(); + } + } + ]); + (_a = this.tray) === null || _a === void 0 ? void 0 : _a.popUpContextMenu(trayMenuConfig); + } + onWindowBlur() { + var _a; + (_a = this.window) === null || _a === void 0 ? void 0 : _a.hide(); + } + onUpdateText(event, timeLeft) { + var _a; + console.log(timeLeft); + (_a = this.tray) === null || _a === void 0 ? void 0 : _a.setToolTip(timeLeft); + } +}; +exports.MainWindow = MainWindow; +__decorate([ + (0, tray_listener_decorator_1.TrayListener)(tray_listener_enum_1.TrayEventEnum.CLICK), + (0, tray_listener_decorator_1.TrayListener)(tray_listener_enum_1.TrayEventEnum.DOUBLE_CLICK), + __metadata("design:type", Function), + __metadata("design:paramtypes", [Object]), + __metadata("design:returntype", void 0) +], MainWindow.prototype, "onTrayClick", null); +__decorate([ + (0, tray_listener_decorator_1.TrayListener)(tray_listener_enum_1.TrayEventEnum.RIGHT_CLICK), + __metadata("design:type", Function), + __metadata("design:paramtypes", [Object]), + __metadata("design:returntype", void 0) +], MainWindow.prototype, "onTrayRightClick", null); +__decorate([ + (0, window_listener_decorator_1.WindowListener)(window_listener_enum_1.WindowEventEnum.BLUR), + __metadata("design:type", Function), + __metadata("design:paramtypes", []), + __metadata("design:returntype", void 0) +], MainWindow.prototype, "onWindowBlur", null); +__decorate([ + (0, ipc_listener_decorator_1.IpcListener)(ipc_channel_enum_1.IPCChannelEnum.UPDATE_TRAY_TEXT), + __metadata("design:type", Function), + __metadata("design:paramtypes", [Object, Object]), + __metadata("design:returntype", void 0) +], MainWindow.prototype, "onUpdateText", null); +exports.MainWindow = MainWindow = __decorate([ + (0, inversify_1.injectable)(), + __param(0, (0, inversify_1.inject)(file_service_1.FileService)), + __metadata("design:paramtypes", [file_service_1.FileService]) +], MainWindow); +//# sourceMappingURL=main.window.js.map \ No newline at end of file diff --git a/app/windows/main.window.ts b/app/windows/main.window.ts new file mode 100644 index 000000000..87e0d5671 --- /dev/null +++ b/app/windows/main.window.ts @@ -0,0 +1,116 @@ +import {BrowserWindow, IpcMainEvent, Menu, Tray} from 'electron'; +import {inject, injectable} from "inversify"; +import 'reflect-metadata'; // Import reflect-metadata +import * as path from "node:path"; + +import {OnAppReady} from "../utils/interfaces/on-app-ready.interface"; +import {FileService} from "../utils/services/file.service"; +import {MainWindowBaseClass} from "../utils/base-classes/main-window.base-class"; +import {TrayListener} from "../utils/decorators/tray-listener.decorator"; +import {TrayEventEnum} from "../utils/enums/tray-listener.enum"; +import {TrayEventType} from "../utils/types/tray-listener.type"; +import {WindowListener} from "../utils/decorators/window-listener.decorator"; +import {WindowEventEnum} from "../utils/enums/window-listener.enum"; +import {IpcListener} from "../utils/decorators/ipc-listener.decorator"; +import {IPCChannelEnum} from "../../shared/enums/ipc-channel.enum"; +import {IPCChannelType} from "../../shared/types/ipc-channel.type"; + +@injectable() +export class MainWindow extends MainWindowBaseClass implements OnAppReady { + private readonly TRAY_ICON_PATH = '/src/assets/icons/electron.png'; + private popupWindow: BrowserWindow | null = null; + private mainWindowBounds = { + width: 200, + height: 400, + } + + constructor(@inject(FileService) private readonly fileService: FileService) { + super(); + + } + + + onAppReady() { + this.createMainWindow(); + this.createTray(); + } + + private createMainWindow() { + const window = new BrowserWindow({ + x: 0, + y: 0, + width: this.mainWindowBounds.width, + height: this.mainWindowBounds.height, + resizable: false, + frame: false, + skipTaskbar: true, + show: false, + webPreferences: { + nodeIntegration: true, + allowRunningInsecureContent: true, + contextIsolation: false, + backgroundThrottling: false + }, + }); + this.loadUrl(window, this.fileService.rootPath); + } + + private createTray() { + this.tray = new Tray(path.join(this.fileService.rootPath, this.TRAY_ICON_PATH)); + this.tray.setToolTip('this is my first timer') + } + + + @TrayListener(TrayEventEnum.CLICK) + @TrayListener(TrayEventEnum.DOUBLE_CLICK) + onTrayClick([event, bounds]: TrayEventType[TrayEventEnum.CLICK]): void { + if (this.window) { + const newBounds = { + x: bounds.x - (this.mainWindowBounds.width / 2), + y: bounds.y - this.mainWindowBounds.height - 10, + width: this.mainWindowBounds.width, + height: this.mainWindowBounds.height, + }; + this.window.setBounds(newBounds); + this.window.show(); + } + } + + @TrayListener(TrayEventEnum.RIGHT_CLICK) + onTrayRightClick(event: TrayEventType[TrayEventEnum.RIGHT_CLICK]): void { + const trayMenuConfig = Menu.buildFromTemplate([ + { + label: 'quit', + click: () => { + this.app.quit(); + } + }, + { + label: 'show', + click: () => { + this.window?.show(); + } + }, + { + label: 'hide', + click: () => { + this.window?.hide(); + } + } + ]); + this.tray?.popUpContextMenu(trayMenuConfig); + } + + @WindowListener(WindowEventEnum.BLUR) + onWindowBlur() { + this.window?.hide(); + } + + @IpcListener(IPCChannelEnum.UPDATE_TRAY_TEXT) + onUpdateText(event: IpcMainEvent, timeLeft: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]): void { + console.log(timeLeft) + this.tray?.setToolTip(timeLeft); + } +} + + diff --git a/package-lock.json b/package-lock.json index 13b0a5822..dca073a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@angular/platform-browser": "17.3.6", "@angular/platform-browser-dynamic": "17.3.6", "@angular/router": "17.3.6", + "inversify": "6.0.2", + "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "tslib": "2.6.2", "zone.js": "0.14.4" @@ -13724,6 +13726,11 @@ "node": ">= 0.4" } }, + "node_modules/inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -20137,8 +20144,7 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/regenerate": { "version": "1.4.2", diff --git a/package.json b/package.json index 7dcb77127..69ffd5f4c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "postinstall": "electron-builder install-app-deps", "ng": "ng", "start": "npm-run-all -p electron:serve ng:serve", - "ng:serve": "ng serve -c web -o", + "ng:serve": "ng serve -c web", "build": "npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", @@ -54,6 +54,8 @@ "@angular/platform-browser": "17.3.6", "@angular/platform-browser-dynamic": "17.3.6", "@angular/router": "17.3.6", + "inversify": "6.0.2", + "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "tslib": "2.6.2", "zone.js": "0.14.4" diff --git a/shared/enums/ipc-channel.enum.js b/shared/enums/ipc-channel.enum.js new file mode 100644 index 000000000..c89ad4b0a --- /dev/null +++ b/shared/enums/ipc-channel.enum.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IPCChannelEnum = void 0; +var IPCChannelEnum; +(function (IPCChannelEnum) { + IPCChannelEnum["UPDATE_TRAY_TEXT"] = "UPDATE_TRAY_TEXT"; +})(IPCChannelEnum || (exports.IPCChannelEnum = IPCChannelEnum = {})); +//# sourceMappingURL=ipc-channel.enum.js.map \ No newline at end of file diff --git a/shared/enums/ipc-channel.enum.ts b/shared/enums/ipc-channel.enum.ts new file mode 100644 index 000000000..07467a2eb --- /dev/null +++ b/shared/enums/ipc-channel.enum.ts @@ -0,0 +1,3 @@ +export enum IPCChannelEnum { + UPDATE_TRAY_TEXT = 'UPDATE_TRAY_TEXT', +} diff --git a/shared/types/ipc-channel.type.js b/shared/types/ipc-channel.type.js new file mode 100644 index 000000000..93e57cc1a --- /dev/null +++ b/shared/types/ipc-channel.type.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const ipc_channel_enum_1 = require("../enums/ipc-channel.enum"); +//# sourceMappingURL=ipc-channel.type.js.map \ No newline at end of file diff --git a/shared/types/ipc-channel.type.ts b/shared/types/ipc-channel.type.ts new file mode 100644 index 000000000..1464896ce --- /dev/null +++ b/shared/types/ipc-channel.type.ts @@ -0,0 +1,5 @@ +import {IPCChannelEnum} from "../enums/ipc-channel.enum"; + +export type IPCChannelType = { + [IPCChannelEnum.UPDATE_TRAY_TEXT]: string +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 0680b43f9..ee4a58e96 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,3 @@ - + + +{{s}} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a953acb63..45c72df40 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,28 +1,29 @@ -import { Component } from '@angular/core'; -import { ElectronService } from './core/services'; -import { TranslateService } from '@ngx-translate/core'; -import { APP_CONFIG } from '../environments/environment'; +import {Component, OnInit} from '@angular/core'; +import {ElectronService} from './core/services'; +import {interval, map, tap} from "rxjs"; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] }) -export class AppComponent { - constructor( - private electronService: ElectronService, - private translate: TranslateService - ) { - this.translate.setDefaultLang('en'); - console.log('APP_CONFIG', APP_CONFIG); +export class AppComponent implements OnInit { + s = 0; - if (electronService.isElectron) { - console.log(process.env); - console.log('Run in electron'); - console.log('Electron ipcRenderer', this.electronService.ipcRenderer); - console.log('NodeJS childProcess', this.electronService.childProcess); - } else { - console.log('Run in browser'); + constructor( + private electronService: ElectronService, + ) { + } + + ngOnInit() { + interval(1000).pipe( + map((_: number) => { + this.s++; + return this.s.toString(); + }), + tap(time => { + this.electronService.updateTrayText(time); + }) + ).subscribe() } - } } diff --git a/src/app/core/services/electron/electron.service.spec.ts b/src/app/core/services/electron/electron.service.spec.ts deleted file mode 100644 index b82597d07..000000000 --- a/src/app/core/services/electron/electron.service.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {TestBed, waitForAsync} from '@angular/core/testing'; - -import { ElectronService } from './electron.service'; - -describe('ElectronService', () => { - beforeEach(waitForAsync(() => TestBed.configureTestingModule({}))); - - it('should be created', () => { - const service: ElectronService = TestBed.get(ElectronService); - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/core/services/electron/electron.service.ts b/src/app/core/services/electron/electron.service.ts index 3fa67b0e7..b931afeb8 100644 --- a/src/app/core/services/electron/electron.service.ts +++ b/src/app/core/services/electron/electron.service.ts @@ -1,56 +1,64 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; // If you import a module but never use any of the imported values other than as TypeScript types, // the resulting javascript file will look as if you never imported the module at all. -import { ipcRenderer, webFrame } from 'electron'; +import {ipcRenderer, webFrame} from 'electron'; import * as childProcess from 'child_process'; import * as fs from 'fs'; +import {IPCChannelEnum} from "../../../../../shared/enums/ipc-channel.enum"; +import {IPCChannelType} from "../../../../../shared/types/ipc-channel.type"; + +type ElectronWindow = typeof window & { require: (arg: string) => any } @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class ElectronService { - ipcRenderer!: typeof ipcRenderer; - webFrame!: typeof webFrame; - childProcess!: typeof childProcess; - fs!: typeof fs; - - constructor() { - // Conditional imports - if (this.isElectron) { - this.ipcRenderer = (window as any).require('electron').ipcRenderer; - this.webFrame = (window as any).require('electron').webFrame; - - this.fs = (window as any).require('fs'); - - this.childProcess = (window as any).require('child_process'); - this.childProcess.exec('node -v', (error, stdout, stderr) => { - if (error) { - console.error(`error: ${error.message}`); - return; - } - if (stderr) { - console.error(`stderr: ${stderr}`); - return; + ipcRenderer!: typeof ipcRenderer; + webFrame!: typeof webFrame; + childProcess!: typeof childProcess; + fs!: typeof fs; + + constructor() { + // Conditional imports + if (this.isElectron) { + this.ipcRenderer = (window as ElectronWindow).require('electron').ipcRenderer; + this.webFrame = (window as ElectronWindow).require('electron').webFrame; + + this.fs = (window as ElectronWindow).require('fs'); + + this.childProcess = (window as ElectronWindow).require('child_process'); + this.childProcess.exec('node -v', (error, stdout, stderr) => { + if (error) { + console.error(`error: ${error.message}`); + return; + } + if (stderr) { + console.error(`stderr: ${stderr}`); + return; + } + console.log(`stdout:\n${stdout}`); + }); + + // Notes : + // * A NodeJS's dependency imported with 'window.require' MUST BE present in `dependencies` of both `app/package.json` + // and `package.json (root folder)` in order to make it work here in Electron's Renderer process (src folder) + // because it will loaded at runtime by Electron. + // * A NodeJS's dependency imported with TS module import (ex: import { Dropbox } from 'dropbox') CAN only be present + // in `dependencies` of `package.json (root folder)` because it is loaded during build phase and does not need to be + // in the final bundle. Reminder : only if not used in Electron's Main process (app folder) + + // If you want to use a NodeJS 3rd party deps in Renderer process, + // ipcRenderer.invoke can serve many common use cases. + // https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args } - console.log(`stdout:\n${stdout}`); - }); - - // Notes : - // * A NodeJS's dependency imported with 'window.require' MUST BE present in `dependencies` of both `app/package.json` - // and `package.json (root folder)` in order to make it work here in Electron's Renderer process (src folder) - // because it will loaded at runtime by Electron. - // * A NodeJS's dependency imported with TS module import (ex: import { Dropbox } from 'dropbox') CAN only be present - // in `dependencies` of `package.json (root folder)` because it is loaded during build phase and does not need to be - // in the final bundle. Reminder : only if not used in Electron's Main process (app folder) - - // If you want to use a NodeJS 3rd party deps in Renderer process, - // ipcRenderer.invoke can serve many common use cases. - // https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args } - } - get isElectron(): boolean { - return !!(window && window.process && window.process.type); - } + get isElectron(): boolean { + return !!(window && window.process && window.process.type); + } + + updateTrayText(text: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]) { + this.ipcRenderer.send(IPCChannelEnum.UPDATE_TRAY_TEXT, text); + } } diff --git a/src/assets/icons/electron.png b/src/assets/icons/electron.png new file mode 100644 index 000000000..3ac07c7af Binary files /dev/null and b/src/assets/icons/electron.png differ diff --git a/src/assets/icons/electron.svg b/src/assets/icons/electron.svg new file mode 100644 index 000000000..ded875709 --- /dev/null +++ b/src/assets/icons/electron.svg @@ -0,0 +1,2 @@ + +