├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── composer.json ├── config └── nativephp.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_nativephp_laravel_table.php.stub ├── phpstan.neon ├── resources └── js │ ├── .gitignore │ ├── build │ ├── entitlements.mac.plist │ ├── icon.png │ └── notarize.js │ ├── electron-builder.js │ ├── electron-plugin │ ├── .gitignore │ ├── .stylelintrc │ ├── __mocks__ │ │ └── electron.ts │ ├── babel.config.js │ ├── dist │ │ ├── index.js │ │ ├── preload │ │ │ └── index.mjs │ │ └── server │ │ │ ├── ProcessResult.js │ │ │ ├── api.js │ │ │ ├── api │ │ │ ├── alert.js │ │ │ ├── app.js │ │ │ ├── autoUpdater.js │ │ │ ├── broadcasting.js │ │ │ ├── childProcess.js │ │ │ ├── clipboard.js │ │ │ ├── contextMenu.js │ │ │ ├── debug.js │ │ │ ├── dialog.js │ │ │ ├── dock.js │ │ │ ├── globalShortcut.js │ │ │ ├── helper │ │ │ │ └── index.js │ │ │ ├── menu.js │ │ │ ├── menuBar.js │ │ │ ├── middleware.js │ │ │ ├── notification.js │ │ │ ├── powerMonitor.js │ │ │ ├── process.js │ │ │ ├── progressBar.js │ │ │ ├── screen.js │ │ │ ├── settings.js │ │ │ ├── shell.js │ │ │ ├── system.js │ │ │ └── window.js │ │ │ ├── childProcess.js │ │ │ ├── index.js │ │ │ ├── php.js │ │ │ ├── state.js │ │ │ └── utils.js │ ├── src │ │ ├── index.ts │ │ ├── preload │ │ │ └── index.mts │ │ └── server │ │ │ ├── ProcessResult.ts │ │ │ ├── api.ts │ │ │ ├── api │ │ │ ├── alert.ts │ │ │ ├── app.ts │ │ │ ├── autoUpdater.ts │ │ │ ├── broadcasting.ts │ │ │ ├── childProcess.ts │ │ │ ├── clipboard.ts │ │ │ ├── contextMenu.ts │ │ │ ├── debug.ts │ │ │ ├── dialog.ts │ │ │ ├── dock.ts │ │ │ ├── globalShortcut.ts │ │ │ ├── helper │ │ │ │ └── index.ts │ │ │ ├── menu.ts │ │ │ ├── menuBar.ts │ │ │ ├── middleware.ts │ │ │ ├── notification.ts │ │ │ ├── powerMonitor.ts │ │ │ ├── process.ts │ │ │ ├── progressBar.ts │ │ │ ├── screen.ts │ │ │ ├── settings.ts │ │ │ ├── shell.ts │ │ │ ├── system.ts │ │ │ └── window.ts │ │ │ ├── childProcess.ts │ │ │ ├── index.ts │ │ │ ├── php.ts │ │ │ ├── state.ts │ │ │ └── utils.ts │ ├── tests │ │ ├── api.test.ts │ │ ├── mocking.test.ts │ │ ├── setup.ts │ │ └── utils.test.ts │ ├── tsconfig.json │ └── vitest.config.mts │ ├── electron.vite.config.mjs │ ├── eslint.config.js │ ├── package-lock.json │ ├── package.json │ ├── php.js │ ├── resources │ ├── .gitignore │ ├── IconTemplate.png │ ├── IconTemplate@2x.png │ └── icon.png │ ├── src │ ├── main │ │ └── index.js │ └── preload │ │ └── index.js │ └── yarn.lock └── src ├── Commands ├── BuildCommand.php ├── BundleCommand.php ├── DevelopCommand.php ├── InstallCommand.php ├── PublishCommand.php └── ResetCommand.php ├── ElectronServiceProvider.php ├── Facades └── Updater.php ├── Traits ├── CleansEnvFile.php ├── CopiesBundleToBuildDirectory.php ├── CopiesCertificateAuthority.php ├── CopiesToBuildDirectory.php ├── Developer.php ├── ExecuteCommand.php ├── HandlesZephpyr.php ├── HasPreAndPostProcessing.php ├── Installer.php ├── InstallsAppIcon.php ├── LocatesPhpBinary.php ├── OsAndArch.php ├── PatchesPackagesJson.php └── PrunesVendorDirectory.php └── Updater ├── Contracts └── Updater.php ├── GitHubProvider.php ├── S3Provider.php ├── SpacesProvider.php └── UpdaterManager.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) NativePHP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron "backend" for the NativePHP framework 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/nativephp/electron.svg?style=flat-square)](https://packagist.org/packages/nativephp/electron) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/nativephp/electron/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/nativephp/electron/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/nativephp/electron/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/nativephp/electron/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/nativephp/electron?style=flat-square)](https://packagist.org/packages/nativephp/electron) 7 | 8 | Visit the [official website](https://nativephp.com) to learn more about it. 9 | 10 | ## Testing 11 | 12 | ```bash 13 | composer test 14 | ``` 15 | 16 | ## Changelog 17 | 18 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 19 | 20 | ## Issues 21 | 22 | Please raise any issues on the [NativePHP/laravel](https://github.com/nativephp/laravel/issues/new/choose) repo. 23 | 24 | ## Contributing 25 | 26 | Please see [CONTRIBUTING](https://github.com/NativePHP/laravel/blob/main/CONTRIBUTING.md) for details. 27 | 28 | ## Security Vulnerabilities 29 | 30 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 31 | 32 | ## Credits 33 | 34 | - [Marcel Pociot](https://github.com/mpociot) 35 | - [Simon Hamp](https://github.com/simonhamp) 36 | - [All Contributors](../../contributors) 37 | 38 | ## License 39 | 40 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nativephp/electron", 3 | "description": "Electron wrapper for the NativePHP framework.", 4 | "keywords": [ 5 | "nativephp", 6 | "laravel", 7 | "electron" 8 | ], 9 | "funding": [ 10 | { 11 | "type": "github", 12 | "url": "https://github.com/sponsors/simonhamp" 13 | }, 14 | { 15 | "type": "opencollective", 16 | "url": "https://opencollective.com/nativephp" 17 | } 18 | ], 19 | "homepage": "https://github.com/nativephp/electron", 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "Marcel Pociot", 24 | "email": "marcel@beyondco.de", 25 | "role": "Developer" 26 | }, 27 | { 28 | "name": "Simon Hamp", 29 | "email": "simon.hamp@me.com", 30 | "role": "Developer" 31 | } 32 | ], 33 | "require": { 34 | "php": "^8.3", 35 | "illuminate/contracts": "^10.0|^11.0|^12.0", 36 | "laravel/prompts": "^0.1.1|^0.2|^0.3", 37 | "nativephp/laravel": "^1.0", 38 | "nativephp/php-bin": "^1.0", 39 | "spatie/laravel-package-tools": "^1.16.4", 40 | "symfony/filesystem": "^6.4|^7.2", 41 | "ext-zip": "*" 42 | }, 43 | "require-dev": { 44 | "laravel/pint": "^1.0", 45 | "nunomaduro/collision": "^7.9|^8.1.1", 46 | "larastan/larastan": "^2.0.1|^3.0", 47 | "orchestra/testbench": "^8.18|^9.0|^10.0", 48 | "pestphp/pest": "^2.7|^3.7", 49 | "pestphp/pest-plugin-arch": "^2.0|^3.0", 50 | "pestphp/pest-plugin-laravel": "^2.0|^3.1", 51 | "phpstan/extension-installer": "^1.1", 52 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 53 | "phpstan/phpstan-phpunit": "^1.0|^2.0", 54 | "spatie/laravel-ray": "^1.26" 55 | }, 56 | "autoload": { 57 | "psr-4": { 58 | "Native\\Electron\\": "src/" 59 | } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "Native\\Electron\\Tests\\": "tests/" 64 | } 65 | }, 66 | "scripts": { 67 | "qa": [ 68 | "@composer format", 69 | "@composer analyse", 70 | "@composer test" 71 | ], 72 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 73 | "analyse": "vendor/bin/phpstan analyse", 74 | "test": "vendor/bin/pest", 75 | "test-coverage": "vendor/bin/pest --coverage", 76 | "format": "vendor/bin/pint" 77 | }, 78 | "config": { 79 | "sort-packages": true, 80 | "allow-plugins": { 81 | "pestphp/pest-plugin": true, 82 | "phpstan/extension-installer": true 83 | } 84 | }, 85 | "extra": { 86 | "laravel": { 87 | "providers": [ 88 | "Native\\Electron\\ElectronServiceProvider" 89 | ], 90 | "aliases": { 91 | "Updater": "Native\\Electron\\Facades\\Updater" 92 | } 93 | } 94 | }, 95 | "minimum-stability": "dev", 96 | "prefer-stable": true 97 | } 98 | -------------------------------------------------------------------------------- /config/nativephp.php: -------------------------------------------------------------------------------- 1 | [ 5 | base_path('app/Providers/NativeAppServiceProvider.php'), 6 | ], 7 | ]; 8 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 4 3 | paths: 4 | - src 5 | - config 6 | - database 7 | checkOctaneCompatibility: true 8 | checkModelProperties: true 9 | noEnvCallsOutsideOfConfig: false 10 | -------------------------------------------------------------------------------- /resources/js/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | out 4 | -------------------------------------------------------------------------------- /resources/js/build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/js/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativePHP/electron/82a8a51e0aa80d0962ee5e04b36b3b2c0ba8701a/resources/js/build/icon.png -------------------------------------------------------------------------------- /resources/js/build/notarize.js: -------------------------------------------------------------------------------- 1 | import { notarize } from '@electron/notarize'; 2 | 3 | export default async (context) => { 4 | if (process.platform !== 'darwin') return 5 | 6 | console.log('aftersign hook triggered, start to notarize app.') 7 | 8 | if (!('NATIVEPHP_APPLE_ID' in process.env && 'NATIVEPHP_APPLE_ID_PASS' in process.env && 'NATIVEPHP_APPLE_TEAM_ID' in process.env)) { 9 | console.warn('skipping notarizing, NATIVEPHP_APPLE_ID, NATIVEPHP_APPLE_ID_PASS and NATIVEPHP_APPLE_TEAM_ID env variables must be set.') 10 | return 11 | } 12 | 13 | const appId = process.env.NATIVEPHP_APP_ID; 14 | 15 | const {appOutDir} = context 16 | 17 | const appName = context.packager.appInfo.productFilename 18 | 19 | try { 20 | await notarize({ 21 | appBundleId: appId, 22 | appPath: `${appOutDir}/${appName}.app`, 23 | appleId: process.env.NATIVEPHP_APPLE_ID, 24 | appleIdPassword: process.env.NATIVEPHP_APPLE_ID_PASS, 25 | teamId: process.env.NATIVEPHP_APPLE_TEAM_ID, 26 | tool: 'notarytool', 27 | }) 28 | } catch (error) { 29 | console.error(error) 30 | } 31 | 32 | console.log(`done notarizing ${appId}.`) 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/electron-builder.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { exec } from 'child_process'; 3 | 4 | const appUrl = process.env.APP_URL; 5 | const appId = process.env.NATIVEPHP_APP_ID; 6 | const appName = process.env.NATIVEPHP_APP_NAME; 7 | const isBuilding = process.env.NATIVEPHP_BUILDING; 8 | const appAuthor = process.env.NATIVEPHP_APP_AUTHOR; 9 | const fileName = process.env.NATIVEPHP_APP_FILENAME; 10 | const appVersion = process.env.NATIVEPHP_APP_VERSION; 11 | const appCopyright = process.env.NATIVEPHP_APP_COPYRIGHT; 12 | const deepLinkProtocol = process.env.NATIVEPHP_DEEPLINK_SCHEME; 13 | 14 | // Since we do not copy the php executable here, we only need these for building 15 | const isWindows = process.argv.includes('--win'); 16 | const isLinux = process.argv.includes('--linux'); 17 | const isDarwin = process.argv.includes('--mac'); 18 | 19 | let targetOs; 20 | 21 | if (isWindows) { 22 | targetOs = 'win'; 23 | } 24 | 25 | if (isLinux) { 26 | targetOs = 'linux'; 27 | } 28 | 29 | if (isDarwin) { 30 | targetOs = 'mac'; 31 | } 32 | 33 | let updaterConfig = {}; 34 | 35 | try { 36 | updaterConfig = process.env.NATIVEPHP_UPDATER_CONFIG; 37 | updaterConfig = JSON.parse(updaterConfig); 38 | } catch (e) { 39 | updaterConfig = {}; 40 | } 41 | 42 | if (isBuilding) { 43 | console.log(' • updater config', updaterConfig); 44 | } 45 | 46 | export default { 47 | appId: appId, 48 | productName: appName, 49 | copyright: appCopyright, 50 | directories: { 51 | buildResources: 'build', 52 | output: isBuilding ? join(process.env.APP_PATH, 'dist') : undefined, 53 | }, 54 | files: [ 55 | '!**/.vscode/*', 56 | '!src/*', 57 | '!electron.vite.config.{js,ts,mjs,cjs}', 58 | '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}', 59 | '!{.env,.env.*,.npmrc,pnpm-lock.yaml}', 60 | ], 61 | asarUnpack: [ 62 | 'resources/**', 63 | ], 64 | beforePack: async (context) => { 65 | let arch = { 66 | 1: 'x64', 67 | 3: 'arm64' 68 | }[context.arch]; 69 | 70 | if(arch === undefined) { 71 | console.error('Cannot build PHP for unsupported architecture'); 72 | process.exit(1); 73 | } 74 | 75 | console.log(` • building php binary - exec php.js --${targetOs} --${arch}`); 76 | exec(`node php.js --${targetOs} --${arch}`); 77 | }, 78 | afterSign: 'build/notarize.js', 79 | win: { 80 | executableName: fileName, 81 | }, 82 | nsis: { 83 | artifactName: appName + '-${version}-setup.${ext}', 84 | shortcutName: '${productName}', 85 | uninstallDisplayName: '${productName}', 86 | createDesktopShortcut: 'always', 87 | }, 88 | protocols: { 89 | name: deepLinkProtocol, 90 | schemes: [deepLinkProtocol], 91 | }, 92 | mac: { 93 | entitlementsInherit: 'build/entitlements.mac.plist', 94 | artifactName: appName + '-${version}-${arch}.${ext}', 95 | extendInfo: { 96 | NSCameraUsageDescription: 97 | "Application requests access to the device's camera.", 98 | NSMicrophoneUsageDescription: 99 | "Application requests access to the device's microphone.", 100 | NSDocumentsFolderUsageDescription: 101 | "Application requests access to the user's Documents folder.", 102 | NSDownloadsFolderUsageDescription: 103 | "Application requests access to the user's Downloads folder.", 104 | }, 105 | }, 106 | dmg: { 107 | artifactName: appName + '-${version}-${arch}.${ext}', 108 | }, 109 | linux: { 110 | target: ['AppImage', 'deb'], 111 | maintainer: appUrl, 112 | category: 'Utility', 113 | }, 114 | appImage: { 115 | artifactName: appName + '-${version}.${ext}', 116 | }, 117 | npmRebuild: false, 118 | publish: updaterConfig, 119 | extraMetadata: { 120 | name: fileName, 121 | homepage: appUrl, 122 | version: appVersion, 123 | author: appAuthor, 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # production 2 | build 3 | dist-ssr 4 | *.local 5 | 6 | # logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | lerna-debug.log* 14 | 15 | # editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | coverage 26 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-sass-guidelines" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": ["**/*.scss"], 9 | "customSyntax": "postcss-scss" 10 | } 11 | ], 12 | "rules": { 13 | "function-parentheses-space-inside": null, 14 | "no-descending-specificity": null, 15 | "max-nesting-depth": 2, 16 | "selector-max-id": 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/__mocks__/electron.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const electron = { 4 | app: { 5 | getPath: vi.fn().mockReturnValue('path'), 6 | isPackaged: false, 7 | }, 8 | powerMonitor: { 9 | addListener: vi.fn(), 10 | }, 11 | }; 12 | 13 | // Make sure this is an object with properties 14 | Object.defineProperty(electron, '__esModule', { value: true }); 15 | export default electron; 16 | export const app = electron.app; 17 | export const powerMonitor = electron.powerMonitor; 18 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/babel.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/preload/index.mjs: -------------------------------------------------------------------------------- 1 | import remote from "@electron/remote"; 2 | import { ipcRenderer } from "electron"; 3 | const Native = { 4 | on: (event, callback) => { 5 | ipcRenderer.on('native-event', (_, data) => { 6 | event = event.replace(/^(\\)+/, ''); 7 | data.event = data.event.replace(/^(\\)+/, ''); 8 | if (event === data.event) { 9 | return callback(data.payload, event); 10 | } 11 | }); 12 | }, 13 | contextMenu: (template) => { 14 | let menu = remote.Menu.buildFromTemplate(template); 15 | menu.popup({ window: remote.getCurrentWindow() }); 16 | } 17 | }; 18 | window.Native = Native; 19 | window.remote = remote; 20 | ipcRenderer.on('log', (event, { level, message, context }) => { 21 | if (level === 'error') { 22 | console.error(`[${level}] ${message}`, context); 23 | } 24 | else if (level === 'warn') { 25 | console.warn(`[${level}] ${message}`, context); 26 | } 27 | else { 28 | console.log(`[${level}] ${message}`, context); 29 | } 30 | }); 31 | ipcRenderer.on('native-event', (event, data) => { 32 | data.event = data.event.replace(/^(\\)+/, ''); 33 | if (window.Livewire) { 34 | window.Livewire.dispatch('native:' + data.event, data.payload); 35 | } 36 | if (window.livewire) { 37 | window.livewire.components.components().forEach(component => { 38 | if (Array.isArray(component.listeners)) { 39 | component.listeners.forEach(event => { 40 | if (event.startsWith('native')) { 41 | let event_parts = event.split(/(native:|native-)|:|,/); 42 | if (event_parts[1] == 'native:') { 43 | event_parts.splice(2, 0, 'private', undefined, 'nativephp', undefined); 44 | } 45 | let [s1, signature, channel_type, s2, channel, s3, event_name,] = event_parts; 46 | if (data.event === event_name) { 47 | window.livewire.emit(event, data.payload); 48 | } 49 | } 50 | }); 51 | } 52 | }); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/ProcessResult.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import express from "express"; 11 | import bodyParser from "body-parser"; 12 | import getPort, { portNumbers } from "get-port"; 13 | import middleware from "./api/middleware.js"; 14 | import clipboardRoutes from "./api/clipboard.js"; 15 | import alertRoutes from "./api/alert.js"; 16 | import appRoutes from "./api/app.js"; 17 | import autoUpdaterRoutes from "./api/autoUpdater.js"; 18 | import screenRoutes from "./api/screen.js"; 19 | import dialogRoutes from "./api/dialog.js"; 20 | import debugRoutes from "./api/debug.js"; 21 | import broadcastingRoutes from "./api/broadcasting.js"; 22 | import systemRoutes from "./api/system.js"; 23 | import globalShortcutRoutes from "./api/globalShortcut.js"; 24 | import notificationRoutes from "./api/notification.js"; 25 | import dockRoutes from "./api/dock.js"; 26 | import menuRoutes from "./api/menu.js"; 27 | import menuBarRoutes from "./api/menuBar.js"; 28 | import windowRoutes from "./api/window.js"; 29 | import processRoutes from "./api/process.js"; 30 | import contextMenuRoutes from "./api/contextMenu.js"; 31 | import settingsRoutes from "./api/settings.js"; 32 | import shellRoutes from "./api/shell.js"; 33 | import progressBarRoutes from "./api/progressBar.js"; 34 | import powerMonitorRoutes from "./api/powerMonitor.js"; 35 | import childProcessRoutes from "./api/childProcess.js"; 36 | function startAPIServer(randomSecret) { 37 | return __awaiter(this, void 0, void 0, function* () { 38 | const port = yield getPort({ 39 | port: portNumbers(4000, 5000), 40 | }); 41 | return new Promise((resolve, reject) => { 42 | const httpServer = express(); 43 | httpServer.use(middleware(randomSecret)); 44 | httpServer.use(bodyParser.json()); 45 | httpServer.use("/api/clipboard", clipboardRoutes); 46 | httpServer.use("/api/alert", alertRoutes); 47 | httpServer.use("/api/app", appRoutes); 48 | httpServer.use("/api/auto-updater", autoUpdaterRoutes); 49 | httpServer.use("/api/screen", screenRoutes); 50 | httpServer.use("/api/dialog", dialogRoutes); 51 | httpServer.use("/api/system", systemRoutes); 52 | httpServer.use("/api/global-shortcuts", globalShortcutRoutes); 53 | httpServer.use("/api/notification", notificationRoutes); 54 | httpServer.use("/api/dock", dockRoutes); 55 | httpServer.use("/api/menu", menuRoutes); 56 | httpServer.use("/api/window", windowRoutes); 57 | httpServer.use("/api/process", processRoutes); 58 | httpServer.use("/api/settings", settingsRoutes); 59 | httpServer.use("/api/shell", shellRoutes); 60 | httpServer.use("/api/context", contextMenuRoutes); 61 | httpServer.use("/api/menu-bar", menuBarRoutes); 62 | httpServer.use("/api/progress-bar", progressBarRoutes); 63 | httpServer.use("/api/power-monitor", powerMonitorRoutes); 64 | httpServer.use("/api/child-process", childProcessRoutes); 65 | httpServer.use("/api/broadcast", broadcastingRoutes); 66 | if (process.env.NODE_ENV === "development") { 67 | httpServer.use("/api/debug", debugRoutes); 68 | } 69 | const server = httpServer.listen(port, () => { 70 | resolve({ 71 | server, 72 | port, 73 | }); 74 | }); 75 | }); 76 | }); 77 | } 78 | export default startAPIServer; 79 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/alert.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { dialog } from 'electron'; 3 | const router = express.Router(); 4 | router.post('/message', (req, res) => { 5 | const { message, type, title, detail, buttons, defaultId, cancelId } = req.body; 6 | const result = dialog.showMessageBoxSync({ 7 | message, 8 | type: type !== null && type !== void 0 ? type : undefined, 9 | title: title !== null && title !== void 0 ? title : undefined, 10 | detail: detail !== null && detail !== void 0 ? detail : undefined, 11 | buttons: buttons !== null && buttons !== void 0 ? buttons : undefined, 12 | defaultId: defaultId !== null && defaultId !== void 0 ? defaultId : undefined, 13 | cancelId: cancelId !== null && cancelId !== void 0 ? cancelId : undefined 14 | }); 15 | res.json({ 16 | result 17 | }); 18 | }); 19 | router.post('/error', (req, res) => { 20 | const { title, message } = req.body; 21 | dialog.showErrorBox(title, message); 22 | res.json({ 23 | result: true 24 | }); 25 | }); 26 | export default router; 27 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { app } from 'electron'; 3 | const router = express.Router(); 4 | router.post('/quit', (req, res) => { 5 | app.quit(); 6 | res.sendStatus(200); 7 | }); 8 | router.post('/relaunch', (req, res) => { 9 | app.relaunch(); 10 | app.quit(); 11 | }); 12 | router.post('/show', (req, res) => { 13 | app.show(); 14 | res.sendStatus(200); 15 | }); 16 | router.post('/hide', (req, res) => { 17 | app.hide(); 18 | res.sendStatus(200); 19 | }); 20 | router.get('/is-hidden', (req, res) => { 21 | res.json({ 22 | is_hidden: app.isHidden(), 23 | }); 24 | }); 25 | router.get('/app-path', (req, res) => { 26 | res.json({ 27 | path: app.getAppPath(), 28 | }); 29 | }); 30 | router.get('/path/:name', (req, res) => { 31 | res.json({ 32 | path: app.getPath(req.params.name), 33 | }); 34 | }); 35 | router.get('/version', (req, res) => { 36 | res.json({ 37 | version: app.getVersion(), 38 | }); 39 | }); 40 | router.post('/badge-count', (req, res) => { 41 | app.setBadgeCount(req.body.count); 42 | res.sendStatus(200); 43 | }); 44 | router.get('/badge-count', (req, res) => { 45 | res.json({ 46 | count: app.getBadgeCount(), 47 | }); 48 | }); 49 | router.post('/recent-documents', (req, res) => { 50 | app.addRecentDocument(req.body.path); 51 | res.sendStatus(200); 52 | }); 53 | router.delete('/recent-documents', (req, res) => { 54 | app.clearRecentDocuments(); 55 | res.sendStatus(200); 56 | }); 57 | router.post('/open-at-login', (req, res) => { 58 | app.setLoginItemSettings({ 59 | openAtLogin: req.body.open, 60 | }); 61 | res.sendStatus(200); 62 | }); 63 | router.get('/open-at-login', (req, res) => { 64 | res.json({ 65 | open: app.getLoginItemSettings().openAtLogin, 66 | }); 67 | }); 68 | export default router; 69 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/autoUpdater.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import electronUpdater from 'electron-updater'; 3 | const { autoUpdater } = electronUpdater; 4 | import { notifyLaravel } from "../utils.js"; 5 | const router = express.Router(); 6 | router.post("/check-for-updates", (req, res) => { 7 | autoUpdater.checkForUpdates(); 8 | res.sendStatus(200); 9 | }); 10 | router.post("/download-update", (req, res) => { 11 | autoUpdater.downloadUpdate(); 12 | res.sendStatus(200); 13 | }); 14 | router.post("/quit-and-install", (req, res) => { 15 | autoUpdater.quitAndInstall(); 16 | res.sendStatus(200); 17 | }); 18 | autoUpdater.addListener("checking-for-update", () => { 19 | notifyLaravel("events", { 20 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\CheckingForUpdate`, 21 | }); 22 | }); 23 | autoUpdater.addListener("update-available", (event) => { 24 | notifyLaravel("events", { 25 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateAvailable`, 26 | payload: { 27 | version: event.version, 28 | files: event.files, 29 | releaseDate: event.releaseDate, 30 | releaseName: event.releaseName, 31 | releaseNotes: event.releaseNotes, 32 | stagingPercentage: event.stagingPercentage, 33 | minimumSystemVersion: event.minimumSystemVersion, 34 | }, 35 | }); 36 | }); 37 | autoUpdater.addListener("update-not-available", (event) => { 38 | notifyLaravel("events", { 39 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateNotAvailable`, 40 | payload: { 41 | version: event.version, 42 | files: event.files, 43 | releaseDate: event.releaseDate, 44 | releaseName: event.releaseName, 45 | releaseNotes: event.releaseNotes, 46 | stagingPercentage: event.stagingPercentage, 47 | minimumSystemVersion: event.minimumSystemVersion, 48 | }, 49 | }); 50 | }); 51 | autoUpdater.addListener("error", (error) => { 52 | notifyLaravel("events", { 53 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\Error`, 54 | payload: { 55 | name: error.name, 56 | message: error.message, 57 | stack: error.stack, 58 | }, 59 | }); 60 | }); 61 | autoUpdater.addListener("download-progress", (progressInfo) => { 62 | notifyLaravel("events", { 63 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\DownloadProgress`, 64 | payload: { 65 | total: progressInfo.total, 66 | delta: progressInfo.delta, 67 | transferred: progressInfo.transferred, 68 | percent: progressInfo.percent, 69 | bytesPerSecond: progressInfo.bytesPerSecond, 70 | }, 71 | }); 72 | }); 73 | autoUpdater.addListener("update-downloaded", (event) => { 74 | notifyLaravel("events", { 75 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateDownloaded`, 76 | payload: { 77 | downloadedFile: event.downloadedFile, 78 | version: event.version, 79 | files: event.files, 80 | releaseDate: event.releaseDate, 81 | releaseName: event.releaseName, 82 | releaseNotes: event.releaseNotes, 83 | stagingPercentage: event.stagingPercentage, 84 | minimumSystemVersion: event.minimumSystemVersion, 85 | }, 86 | }); 87 | }); 88 | autoUpdater.addListener("update-cancelled", (event) => { 89 | notifyLaravel("events", { 90 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateCancelled`, 91 | payload: { 92 | version: event.version, 93 | files: event.files, 94 | releaseDate: event.releaseDate, 95 | releaseName: event.releaseName, 96 | releaseNotes: event.releaseNotes, 97 | stagingPercentage: event.stagingPercentage, 98 | minimumSystemVersion: event.minimumSystemVersion, 99 | }, 100 | }); 101 | }); 102 | export default router; 103 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/broadcasting.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { broadcastToWindows } from '../utils.js'; 3 | const router = express.Router(); 4 | router.post('/', (req, res) => { 5 | const { event, payload } = req.body; 6 | broadcastToWindows("native-event", { event, payload }); 7 | res.sendStatus(200); 8 | }); 9 | export default router; 10 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/childProcess.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import express from 'express'; 11 | import { utilityProcess } from 'electron'; 12 | import state from '../state.js'; 13 | import { notifyLaravel } from "../utils.js"; 14 | import { getAppPath, getDefaultEnvironmentVariables, getDefaultPhpIniSettings, runningSecureBuild } from "../php.js"; 15 | import killSync from "kill-sync"; 16 | import { fileURLToPath } from "url"; 17 | import { join } from "path"; 18 | const router = express.Router(); 19 | function startProcess(settings) { 20 | const { alias, cmd, cwd, env, persistent, spawnTimeout = 30000 } = settings; 21 | if (getProcess(alias) !== undefined) { 22 | return state.processes[alias]; 23 | } 24 | try { 25 | const proc = utilityProcess.fork(fileURLToPath(new URL('../../electron-plugin/dist/server/childProcess.js', import.meta.url)), cmd, { 26 | cwd, 27 | stdio: 'pipe', 28 | serviceName: alias, 29 | env: Object.assign(Object.assign({}, process.env), env) 30 | }); 31 | const startTimeout = setTimeout(() => { 32 | if (!state.processes[alias] || !state.processes[alias].pid) { 33 | console.error(`Process [${alias}] failed to start within timeout period`); 34 | try { 35 | proc.kill(); 36 | } 37 | catch (e) { 38 | } 39 | notifyLaravel('events', { 40 | event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', 41 | payload: { 42 | alias, 43 | error: 'Startup timeout exceeded', 44 | } 45 | }); 46 | } 47 | }, spawnTimeout); 48 | proc.stdout.on('data', (data) => { 49 | notifyLaravel('events', { 50 | event: 'Native\\Laravel\\Events\\ChildProcess\\MessageReceived', 51 | payload: { 52 | alias, 53 | data: data.toString(), 54 | } 55 | }); 56 | }); 57 | proc.stderr.on('data', (data) => { 58 | console.error('Process [' + alias + '] ERROR:', data.toString().trim()); 59 | notifyLaravel('events', { 60 | event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', 61 | payload: { 62 | alias, 63 | data: data.toString(), 64 | } 65 | }); 66 | }); 67 | proc.on('spawn', () => { 68 | clearTimeout(startTimeout); 69 | console.log('Process [' + alias + '] spawned!'); 70 | state.processes[alias] = { 71 | pid: proc.pid, 72 | proc, 73 | settings 74 | }; 75 | notifyLaravel('events', { 76 | event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessSpawned', 77 | payload: [alias, proc.pid] 78 | }); 79 | }); 80 | proc.on('exit', (code) => { 81 | clearTimeout(startTimeout); 82 | console.log(`Process [${alias}] exited with code [${code}].`); 83 | notifyLaravel('events', { 84 | event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessExited', 85 | payload: { 86 | alias, 87 | code, 88 | } 89 | }); 90 | const settings = Object.assign({}, getSettings(alias)); 91 | delete state.processes[alias]; 92 | if (settings === null || settings === void 0 ? void 0 : settings.persistent) { 93 | console.log('Process [' + alias + '] watchdog restarting...'); 94 | setTimeout(() => startProcess(settings), 1000); 95 | } 96 | }); 97 | return { 98 | pid: null, 99 | proc, 100 | settings 101 | }; 102 | } 103 | catch (error) { 104 | console.error(`Failed to create process [${alias}]: ${error.message}`); 105 | notifyLaravel('events', { 106 | event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', 107 | payload: { 108 | alias, 109 | error: error.toString(), 110 | } 111 | }); 112 | return { 113 | pid: null, 114 | proc: null, 115 | settings, 116 | error: error.message 117 | }; 118 | } 119 | } 120 | function startPhpProcess(settings) { 121 | const defaultEnv = getDefaultEnvironmentVariables(state.randomSecret, state.electronApiPort); 122 | const customIniSettings = settings.iniSettings || {}; 123 | const iniSettings = Object.assign(Object.assign(Object.assign({}, getDefaultPhpIniSettings()), state.phpIni), customIniSettings); 124 | const iniArgs = Object.keys(iniSettings).map(key => { 125 | return ['-d', `${key}=${iniSettings[key]}`]; 126 | }).flat(); 127 | if (settings.cmd[0] === 'artisan' && runningSecureBuild()) { 128 | settings.cmd.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); 129 | } 130 | settings = Object.assign(Object.assign({}, settings), { cmd: [state.php, ...iniArgs, ...settings.cmd], env: Object.assign(Object.assign({}, settings.env), defaultEnv) }); 131 | return startProcess(settings); 132 | } 133 | function stopProcess(alias) { 134 | const proc = getProcess(alias); 135 | if (proc === undefined) { 136 | return; 137 | } 138 | state.processes[alias].settings.persistent = false; 139 | console.log('Process [' + alias + '] stopping with PID [' + proc.pid + '].'); 140 | killSync(proc.pid, 'SIGTERM', true); 141 | proc.kill(); 142 | } 143 | export function stopAllProcesses() { 144 | for (const alias in state.processes) { 145 | stopProcess(alias); 146 | } 147 | } 148 | function getProcess(alias) { 149 | var _a; 150 | return (_a = state.processes[alias]) === null || _a === void 0 ? void 0 : _a.proc; 151 | } 152 | function getSettings(alias) { 153 | var _a; 154 | return (_a = state.processes[alias]) === null || _a === void 0 ? void 0 : _a.settings; 155 | } 156 | router.post('/start', (req, res) => { 157 | const proc = startProcess(req.body); 158 | res.json(proc); 159 | }); 160 | router.post('/start-php', (req, res) => { 161 | const proc = startPhpProcess(req.body); 162 | res.json(proc); 163 | }); 164 | router.post('/stop', (req, res) => { 165 | const { alias } = req.body; 166 | stopProcess(alias); 167 | res.sendStatus(200); 168 | }); 169 | router.post('/restart', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 170 | const { alias } = req.body; 171 | const settings = Object.assign({}, getSettings(alias)); 172 | stopProcess(alias); 173 | if (settings === undefined) { 174 | res.sendStatus(410); 175 | return; 176 | } 177 | const waitForProcessDeletion = (timeout, retry) => __awaiter(void 0, void 0, void 0, function* () { 178 | const start = Date.now(); 179 | while (state.processes[alias] !== undefined) { 180 | if (Date.now() - start > timeout) { 181 | return; 182 | } 183 | yield new Promise(resolve => setTimeout(resolve, retry)); 184 | } 185 | }); 186 | yield waitForProcessDeletion(5000, 100); 187 | console.log('Process [' + alias + '] restarting...'); 188 | const proc = startProcess(settings); 189 | res.json(proc); 190 | })); 191 | router.get('/get/:alias', (req, res) => { 192 | const { alias } = req.params; 193 | const proc = state.processes[alias]; 194 | if (proc === undefined) { 195 | res.sendStatus(410); 196 | return; 197 | } 198 | res.json(proc); 199 | }); 200 | router.get('/', (req, res) => { 201 | res.json(state.processes); 202 | }); 203 | router.post('/message', (req, res) => { 204 | const { alias, message } = req.body; 205 | const proc = getProcess(alias); 206 | if (proc === undefined) { 207 | res.sendStatus(200); 208 | return; 209 | } 210 | proc.postMessage(message); 211 | res.sendStatus(200); 212 | }); 213 | export default router; 214 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/clipboard.js: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | const router = express.Router(); 3 | import { clipboard, nativeImage } from 'electron'; 4 | const DEFAULT_TYPE = 'clipboard'; 5 | router.get('/text', (req, res) => { 6 | const { type } = req.query; 7 | res.json({ 8 | text: clipboard.readText(type || DEFAULT_TYPE) 9 | }); 10 | }); 11 | router.post('/text', (req, res) => { 12 | const { text } = req.body; 13 | const { type } = req.query; 14 | clipboard.writeText(text, type || DEFAULT_TYPE); 15 | res.json({ 16 | text, 17 | }); 18 | }); 19 | router.get('/html', (req, res) => { 20 | const { type } = req.query; 21 | res.json({ 22 | html: clipboard.readHTML(type || DEFAULT_TYPE) 23 | }); 24 | }); 25 | router.post('/html', (req, res) => { 26 | const { html } = req.body; 27 | const { type } = req.query; 28 | clipboard.writeHTML(html, type || DEFAULT_TYPE); 29 | res.json({ 30 | html, 31 | }); 32 | }); 33 | router.get('/image', (req, res) => { 34 | const { type } = req.query; 35 | const image = clipboard.readImage(type || DEFAULT_TYPE); 36 | res.json({ 37 | image: image.isEmpty() ? null : image.toDataURL() 38 | }); 39 | }); 40 | router.post('/image', (req, res) => { 41 | const { image } = req.body; 42 | const { type } = req.query; 43 | try { 44 | const _nativeImage = nativeImage.createFromDataURL(image); 45 | clipboard.writeImage(_nativeImage, type || DEFAULT_TYPE); 46 | } 47 | catch (e) { 48 | res.status(400).json({ 49 | error: e.message, 50 | }); 51 | return; 52 | } 53 | res.sendStatus(200); 54 | }); 55 | router.delete('/', (req, res) => { 56 | const { type } = req.query; 57 | clipboard.clear(type || DEFAULT_TYPE); 58 | res.sendStatus(200); 59 | }); 60 | export default router; 61 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/contextMenu.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { compileMenu } from "./helper/index.js"; 3 | import contextMenu from "electron-context-menu"; 4 | const router = express.Router(); 5 | let contextMenuDisposable = null; 6 | router.delete('/', (req, res) => { 7 | res.sendStatus(200); 8 | if (contextMenuDisposable) { 9 | contextMenuDisposable(); 10 | contextMenuDisposable = null; 11 | } 12 | }); 13 | router.post('/', (req, res) => { 14 | res.sendStatus(200); 15 | if (contextMenuDisposable) { 16 | contextMenuDisposable(); 17 | contextMenuDisposable = null; 18 | } 19 | contextMenuDisposable = contextMenu({ 20 | showLookUpSelection: false, 21 | showSearchWithGoogle: false, 22 | showInspectElement: false, 23 | prepend: (defaultActions, parameters, browserWindow) => { 24 | return req.body.entries.map(compileMenu); 25 | }, 26 | }); 27 | }); 28 | export default router; 29 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/debug.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { broadcastToWindows } from '../utils.js'; 3 | const router = express.Router(); 4 | router.post('/log', (req, res) => { 5 | const { level, message, context } = req.body; 6 | broadcastToWindows('log', { level, message, context }); 7 | res.sendStatus(200); 8 | }); 9 | export default router; 10 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/dialog.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { dialog } from 'electron'; 3 | import state from '../state.js'; 4 | import { trimOptions } from '../utils.js'; 5 | const router = express.Router(); 6 | router.post('/open', (req, res) => { 7 | const { title, buttonLabel, filters, properties, defaultPath, message, windowReference } = req.body; 8 | let options = { 9 | title, 10 | defaultPath, 11 | buttonLabel, 12 | filters, 13 | message, 14 | properties 15 | }; 16 | options = trimOptions(options); 17 | let result; 18 | let browserWindow = state.findWindow(windowReference); 19 | if (browserWindow) { 20 | result = dialog.showOpenDialogSync(browserWindow, options); 21 | } 22 | else { 23 | result = dialog.showOpenDialogSync(options); 24 | } 25 | res.json({ 26 | result 27 | }); 28 | }); 29 | router.post('/save', (req, res) => { 30 | const { title, buttonLabel, filters, properties, defaultPath, message, windowReference } = req.body; 31 | let options = { 32 | title, 33 | defaultPath, 34 | buttonLabel, 35 | filters, 36 | message, 37 | properties 38 | }; 39 | options = trimOptions(options); 40 | let result; 41 | let browserWindow = state.findWindow(windowReference); 42 | if (browserWindow) { 43 | result = dialog.showSaveDialogSync(browserWindow, options); 44 | } 45 | else { 46 | result = dialog.showSaveDialogSync(options); 47 | } 48 | res.json({ 49 | result 50 | }); 51 | }); 52 | export default router; 53 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/dock.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { app, Menu } from 'electron'; 3 | import { compileMenu } from './helper/index.js'; 4 | import state from '../state.js'; 5 | const router = express.Router(); 6 | router.post('/', (req, res) => { 7 | const menuEntries = req.body.items.map(compileMenu); 8 | const menu = Menu.buildFromTemplate(menuEntries); 9 | app.dock.setMenu(menu); 10 | res.sendStatus(200); 11 | }); 12 | router.post('/show', (req, res) => { 13 | app.dock.show(); 14 | res.sendStatus(200); 15 | }); 16 | router.post('/hide', (req, res) => { 17 | app.dock.hide(); 18 | res.sendStatus(200); 19 | }); 20 | router.post('/icon', (req, res) => { 21 | app.dock.setIcon(req.body.path); 22 | res.sendStatus(200); 23 | }); 24 | router.post('/bounce', (req, res) => { 25 | const { type } = req.body; 26 | state.dockBounce = app.dock.bounce(type); 27 | res.sendStatus(200); 28 | }); 29 | router.post('/cancel-bounce', (req, res) => { 30 | app.dock.cancelBounce(state.dockBounce); 31 | res.sendStatus(200); 32 | }); 33 | router.get('/badge', (req, res) => { 34 | res.json({ 35 | label: app.dock.getBadge(), 36 | }); 37 | }); 38 | router.post('/badge', (req, res) => { 39 | app.dock.setBadge(req.body.label); 40 | res.sendStatus(200); 41 | }); 42 | export default router; 43 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/globalShortcut.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { globalShortcut } from 'electron'; 3 | import { notifyLaravel } from "../utils.js"; 4 | const router = express.Router(); 5 | router.post('/', (req, res) => { 6 | const { key, event } = req.body; 7 | globalShortcut.register(key, () => { 8 | notifyLaravel('events', { 9 | event, 10 | payload: [key] 11 | }); 12 | }); 13 | res.sendStatus(200); 14 | }); 15 | router.delete('/', (req, res) => { 16 | const { key } = req.body; 17 | globalShortcut.unregister(key); 18 | res.sendStatus(200); 19 | }); 20 | router.get('/:key', (req, res) => { 21 | const { key } = req.params; 22 | res.json({ 23 | isRegistered: globalShortcut.isRegistered(key) 24 | }); 25 | }); 26 | export default router; 27 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/helper/index.js: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron'; 2 | import { notifyLaravel, goToUrl } from '../../utils.js'; 3 | import state from '../../state.js'; 4 | function triggerMenuItemEvent(menuItem, combo) { 5 | notifyLaravel('events', { 6 | event: menuItem.event || '\\Native\\Laravel\\Events\\Menu\\MenuItemClicked', 7 | payload: { 8 | item: { 9 | id: menuItem.id, 10 | label: menuItem.label, 11 | checked: menuItem.checked, 12 | }, 13 | combo, 14 | }, 15 | }); 16 | } 17 | export function compileMenu(item) { 18 | var _a, _b; 19 | if (item.submenu) { 20 | if (Array.isArray(item.submenu)) { 21 | item.submenu = (_a = item.submenu) === null || _a === void 0 ? void 0 : _a.map(compileMenu); 22 | } 23 | else { 24 | item.submenu = (_b = item.submenu.submenu) === null || _b === void 0 ? void 0 : _b.map(compileMenu); 25 | } 26 | } 27 | if (item.type === 'link') { 28 | item.type = 'normal'; 29 | item.click = (menuItem, focusedWindow, combo) => { 30 | triggerMenuItemEvent(item, combo); 31 | if (item.openInBrowser) { 32 | shell.openExternal(item.url); 33 | return; 34 | } 35 | if (!focusedWindow) { 36 | return; 37 | } 38 | const id = Object.keys(state.windows) 39 | .find(key => state.windows[key] === focusedWindow); 40 | goToUrl(item.url, id); 41 | }; 42 | return item; 43 | } 44 | if (item.type === 'checkbox' || item.type === 'radio') { 45 | item.click = (menuItem, focusedWindow, combo) => { 46 | item.checked = !item.checked; 47 | triggerMenuItemEvent(item, combo); 48 | }; 49 | return item; 50 | } 51 | if (item.type === 'role') { 52 | let menuItem = { 53 | role: item.role 54 | }; 55 | if (item.label) { 56 | menuItem['label'] = item.label; 57 | } 58 | return menuItem; 59 | } 60 | if (!item.click) { 61 | item.click = (menuItem, focusedWindow, combo) => { 62 | triggerMenuItemEvent(item, combo); 63 | }; 64 | } 65 | return item; 66 | } 67 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/menu.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Menu } from 'electron'; 3 | import { compileMenu } from './helper/index.js'; 4 | const router = express.Router(); 5 | router.post('/', (req, res) => { 6 | Menu.setApplicationMenu(null); 7 | const menuEntries = req.body.items.map(compileMenu); 8 | const menu = Menu.buildFromTemplate(menuEntries); 9 | Menu.setApplicationMenu(menu); 10 | res.sendStatus(200); 11 | }); 12 | export default router; 13 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/menuBar.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { app, Menu, Tray } from "electron"; 3 | import { compileMenu } from "./helper/index.js"; 4 | import state from "../state.js"; 5 | import { menubar } from "menubar"; 6 | import { notifyLaravel } from "../utils.js"; 7 | import { fileURLToPath } from 'url'; 8 | import { enable } from "@electron/remote/main/index.js"; 9 | const router = express.Router(); 10 | router.post("/label", (req, res) => { 11 | var _a; 12 | res.sendStatus(200); 13 | const { label } = req.body; 14 | (_a = state.tray) === null || _a === void 0 ? void 0 : _a.setTitle(label); 15 | }); 16 | router.post("/tooltip", (req, res) => { 17 | var _a; 18 | res.sendStatus(200); 19 | const { tooltip } = req.body; 20 | (_a = state.tray) === null || _a === void 0 ? void 0 : _a.setToolTip(tooltip); 21 | }); 22 | router.post("/icon", (req, res) => { 23 | var _a; 24 | res.sendStatus(200); 25 | const { icon } = req.body; 26 | (_a = state.tray) === null || _a === void 0 ? void 0 : _a.setImage(icon); 27 | }); 28 | router.post("/context-menu", (req, res) => { 29 | var _a; 30 | res.sendStatus(200); 31 | const { contextMenu } = req.body; 32 | (_a = state.tray) === null || _a === void 0 ? void 0 : _a.setContextMenu(buildMenu(contextMenu)); 33 | }); 34 | router.post("/show", (req, res) => { 35 | res.sendStatus(200); 36 | state.activeMenuBar.showWindow(); 37 | }); 38 | router.post("/hide", (req, res) => { 39 | res.sendStatus(200); 40 | state.activeMenuBar.hideWindow(); 41 | }); 42 | router.post("/resize", (req, res) => { 43 | res.sendStatus(200); 44 | const { width, height } = req.body; 45 | state.activeMenuBar.window.setSize(width, height); 46 | }); 47 | router.post("/create", (req, res) => { 48 | res.sendStatus(200); 49 | let shouldSendCreatedEvent = true; 50 | if (state.activeMenuBar) { 51 | state.activeMenuBar.tray.destroy(); 52 | shouldSendCreatedEvent = false; 53 | } 54 | const { width, height, url, label, alwaysOnTop, vibrancy, backgroundColor, transparency, icon, showDockIcon, onlyShowContextMenu, windowPosition, showOnAllWorkspaces, contextMenu, tooltip, resizable, event, } = req.body; 55 | if (onlyShowContextMenu) { 56 | const tray = new Tray(icon || state.icon.replace("icon.png", "IconTemplate.png")); 57 | tray.setContextMenu(buildMenu(contextMenu)); 58 | tray.setToolTip(tooltip); 59 | tray.setTitle(label); 60 | eventsForTray(tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent); 61 | state.tray = tray; 62 | if (!showDockIcon) { 63 | app.dock.hide(); 64 | } 65 | } 66 | else { 67 | state.activeMenuBar = menubar({ 68 | icon: icon || state.icon.replace("icon.png", "IconTemplate.png"), 69 | preloadWindow: true, 70 | tooltip, 71 | index: url, 72 | showDockIcon, 73 | showOnAllWorkspaces: showOnAllWorkspaces !== null && showOnAllWorkspaces !== void 0 ? showOnAllWorkspaces : false, 74 | windowPosition: windowPosition !== null && windowPosition !== void 0 ? windowPosition : "trayCenter", 75 | activateWithApp: false, 76 | browserWindow: { 77 | width, 78 | height, 79 | resizable, 80 | alwaysOnTop, 81 | vibrancy, 82 | backgroundColor, 83 | transparent: transparency, 84 | webPreferences: { 85 | preload: fileURLToPath(new URL('../../electron-plugin/dist/preload/index.mjs', import.meta.url)), 86 | nodeIntegration: true, 87 | sandbox: false, 88 | contextIsolation: false, 89 | } 90 | } 91 | }); 92 | state.activeMenuBar.on("after-create-window", () => { 93 | enable(state.activeMenuBar.window.webContents); 94 | }); 95 | state.activeMenuBar.on("ready", () => { 96 | eventsForTray(state.activeMenuBar.tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent); 97 | state.tray = state.activeMenuBar.tray; 98 | state.tray.setTitle(label); 99 | state.activeMenuBar.on("hide", () => { 100 | notifyLaravel("events", { 101 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarHidden" 102 | }); 103 | }); 104 | state.activeMenuBar.on("show", () => { 105 | notifyLaravel("events", { 106 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarShown" 107 | }); 108 | }); 109 | }); 110 | } 111 | }); 112 | function eventsForTray(tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent) { 113 | if (shouldSendCreatedEvent) { 114 | notifyLaravel("events", { 115 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarCreated" 116 | }); 117 | } 118 | tray.on("drop-files", (event, files) => { 119 | notifyLaravel("events", { 120 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarDroppedFiles", 121 | payload: [ 122 | files 123 | ] 124 | }); 125 | }); 126 | tray.on('click', (combo, bounds, position) => { 127 | notifyLaravel('events', { 128 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarClicked", 129 | payload: { 130 | combo, 131 | bounds, 132 | position, 133 | }, 134 | }); 135 | }); 136 | tray.on("right-click", (combo, bounds) => { 137 | notifyLaravel("events", { 138 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarRightClicked", 139 | payload: { 140 | combo, 141 | bounds, 142 | } 143 | }); 144 | if (!onlyShowContextMenu) { 145 | state.activeMenuBar.hideWindow(); 146 | tray.popUpContextMenu(buildMenu(contextMenu)); 147 | } 148 | }); 149 | tray.on('double-click', (combo, bounds) => { 150 | notifyLaravel('events', { 151 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarDoubleClicked", 152 | payload: { 153 | combo, 154 | bounds, 155 | }, 156 | }); 157 | }); 158 | } 159 | function buildMenu(contextMenu) { 160 | let menu = Menu.buildFromTemplate([{ role: "quit" }]); 161 | if (contextMenu) { 162 | const menuEntries = contextMenu.map(compileMenu); 163 | menu = Menu.buildFromTemplate(menuEntries); 164 | } 165 | return menu; 166 | } 167 | export default router; 168 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/middleware.js: -------------------------------------------------------------------------------- 1 | export default function (secret) { 2 | return function (req, res, next) { 3 | if (req.headers['x-nativephp-secret'] !== secret) { 4 | res.sendStatus(403); 5 | return; 6 | } 7 | next(); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/notification.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Notification } from 'electron'; 3 | import { notifyLaravel } from "../utils.js"; 4 | const router = express.Router(); 5 | router.post('/', (req, res) => { 6 | const { title, body, subtitle, silent, icon, hasReply, timeoutType, replyPlaceholder, sound, urgency, actions, closeButtonText, toastXml, event: customEvent, reference, } = req.body; 7 | const eventName = customEvent !== null && customEvent !== void 0 ? customEvent : '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked'; 8 | const notificationReference = reference !== null && reference !== void 0 ? reference : (Date.now() + '.' + Math.random().toString(36).slice(2, 9)); 9 | const notification = new Notification({ 10 | title, 11 | body, 12 | subtitle, 13 | silent, 14 | icon, 15 | hasReply, 16 | timeoutType, 17 | replyPlaceholder, 18 | sound, 19 | urgency, 20 | actions, 21 | closeButtonText, 22 | toastXml 23 | }); 24 | notification.on("click", (event) => { 25 | notifyLaravel('events', { 26 | event: eventName || '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked', 27 | payload: { 28 | reference: notificationReference, 29 | event: JSON.stringify(event), 30 | }, 31 | }); 32 | }); 33 | notification.on("action", (event, index) => { 34 | notifyLaravel('events', { 35 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationActionClicked', 36 | payload: { 37 | reference: notificationReference, 38 | index, 39 | event: JSON.stringify(event), 40 | }, 41 | }); 42 | }); 43 | notification.on("reply", (event, reply) => { 44 | notifyLaravel('events', { 45 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationReply', 46 | payload: { 47 | reference: notificationReference, 48 | reply, 49 | event: JSON.stringify(event), 50 | }, 51 | }); 52 | }); 53 | notification.on("close", (event) => { 54 | notifyLaravel('events', { 55 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationClosed', 56 | payload: { 57 | reference: notificationReference, 58 | event: JSON.stringify(event), 59 | }, 60 | }); 61 | }); 62 | notification.show(); 63 | res.status(200).json({ 64 | reference: notificationReference, 65 | }); 66 | }); 67 | export default router; 68 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/powerMonitor.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { powerMonitor } from 'electron'; 3 | import { notifyLaravel } from '../utils.js'; 4 | const router = express.Router(); 5 | router.get('/get-system-idle-state', (req, res) => { 6 | let threshold = Number(req.query.threshold) || 60; 7 | res.json({ 8 | result: powerMonitor.getSystemIdleState(threshold), 9 | }); 10 | }); 11 | router.get('/get-system-idle-time', (req, res) => { 12 | res.json({ 13 | result: powerMonitor.getSystemIdleTime(), 14 | }); 15 | }); 16 | router.get('/get-current-thermal-state', (req, res) => { 17 | res.json({ 18 | result: powerMonitor.getCurrentThermalState(), 19 | }); 20 | }); 21 | router.get('/is-on-battery-power', (req, res) => { 22 | res.json({ 23 | result: powerMonitor.isOnBatteryPower(), 24 | }); 25 | }); 26 | powerMonitor.addListener('on-ac', () => { 27 | notifyLaravel("events", { 28 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\PowerStateChanged`, 29 | payload: { 30 | state: 'on-ac' 31 | } 32 | }); 33 | }); 34 | powerMonitor.addListener('on-battery', () => { 35 | notifyLaravel("events", { 36 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\PowerStateChanged`, 37 | payload: { 38 | state: 'on-battery' 39 | } 40 | }); 41 | }); 42 | powerMonitor.addListener('thermal-state-change', (details) => { 43 | notifyLaravel("events", { 44 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ThermalStateChanged`, 45 | payload: { 46 | state: details.state, 47 | }, 48 | }); 49 | }); 50 | powerMonitor.addListener('speed-limit-change', (details) => { 51 | notifyLaravel("events", { 52 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\SpeedLimitChanged`, 53 | payload: { 54 | limit: details.limit, 55 | }, 56 | }); 57 | }); 58 | powerMonitor.addListener('lock-screen', () => { 59 | notifyLaravel("events", { 60 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ScreenLocked`, 61 | }); 62 | }); 63 | powerMonitor.addListener('unlock-screen', () => { 64 | notifyLaravel("events", { 65 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ScreenUnlocked`, 66 | }); 67 | }); 68 | powerMonitor.addListener('shutdown', () => { 69 | notifyLaravel("events", { 70 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\Shutdown`, 71 | }); 72 | }); 73 | powerMonitor.addListener('user-did-become-active', () => { 74 | notifyLaravel("events", { 75 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\UserDidBecomeActive`, 76 | }); 77 | }); 78 | powerMonitor.addListener('user-did-resign-active', () => { 79 | notifyLaravel("events", { 80 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\UserDidResignActive`, 81 | }); 82 | }); 83 | export default router; 84 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/process.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | router.get('/', (req, res) => { 4 | res.json({ 5 | pid: process.pid, 6 | platform: process.platform, 7 | arch: process.arch, 8 | uptime: process.uptime() 9 | }); 10 | }); 11 | export default router; 12 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/progressBar.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import state from "../state.js"; 3 | const router = express.Router(); 4 | router.post('/update', (req, res) => { 5 | const { percent } = req.body; 6 | Object.values(state.windows).forEach((window) => { 7 | window.setProgressBar(percent); 8 | }); 9 | res.sendStatus(200); 10 | }); 11 | export default router; 12 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/screen.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { screen } from 'electron'; 3 | const router = express.Router(); 4 | router.get('/displays', (req, res) => { 5 | res.json({ 6 | displays: screen.getAllDisplays() 7 | }); 8 | }); 9 | router.get('/primary-display', (req, res) => { 10 | res.json({ 11 | primaryDisplay: screen.getPrimaryDisplay() 12 | }); 13 | }); 14 | router.get('/cursor-position', (req, res) => { 15 | res.json(screen.getCursorScreenPoint()); 16 | }); 17 | router.get('/active', (req, res) => { 18 | const cursor = screen.getCursorScreenPoint(); 19 | res.json(screen.getDisplayNearestPoint(cursor)); 20 | }); 21 | export default router; 22 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/settings.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import state from '../state.js'; 3 | const router = express.Router(); 4 | router.get('/:key', (req, res) => { 5 | const key = req.params.key; 6 | const value = state.store.get(key, null); 7 | res.json({ value }); 8 | }); 9 | router.post('/:key', (req, res) => { 10 | const key = req.params.key; 11 | const value = req.body.value; 12 | state.store.set(key, value); 13 | res.sendStatus(200); 14 | }); 15 | router.delete('/:key', (req, res) => { 16 | const key = req.params.key; 17 | state.store.delete(key); 18 | res.sendStatus(200); 19 | }); 20 | router.delete('/', (req, res) => { 21 | state.store.clear(); 22 | res.sendStatus(200); 23 | }); 24 | export default router; 25 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/shell.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import express from "express"; 11 | import { shell } from "electron"; 12 | const router = express.Router(); 13 | router.post("/show-item-in-folder", (req, res) => { 14 | const { path } = req.body; 15 | shell.showItemInFolder(path); 16 | res.sendStatus(200); 17 | }); 18 | router.post("/open-item", (req, res) => __awaiter(void 0, void 0, void 0, function* () { 19 | const { path } = req.body; 20 | let result = yield shell.openPath(path); 21 | res.json({ 22 | result 23 | }); 24 | })); 25 | router.post("/open-external", (req, res) => __awaiter(void 0, void 0, void 0, function* () { 26 | const { url } = req.body; 27 | try { 28 | yield shell.openExternal(url); 29 | res.sendStatus(200); 30 | } 31 | catch (e) { 32 | res.status(500).json({ 33 | error: e 34 | }); 35 | } 36 | })); 37 | router.delete("/trash-item", (req, res) => __awaiter(void 0, void 0, void 0, function* () { 38 | const { path } = req.body; 39 | try { 40 | yield shell.trashItem(path); 41 | res.sendStatus(200); 42 | } 43 | catch (e) { 44 | res.status(400).json(); 45 | } 46 | })); 47 | export default router; 48 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/api/system.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import express from 'express'; 11 | import { BrowserWindow, systemPreferences, safeStorage, nativeTheme } from 'electron'; 12 | const router = express.Router(); 13 | router.get('/can-prompt-touch-id', (req, res) => { 14 | res.json({ 15 | result: systemPreferences.canPromptTouchID(), 16 | }); 17 | }); 18 | router.post('/prompt-touch-id', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 19 | try { 20 | yield systemPreferences.promptTouchID(req.body.reason); 21 | res.sendStatus(200); 22 | } 23 | catch (e) { 24 | res.status(400).json({ 25 | error: e.message, 26 | }); 27 | } 28 | })); 29 | router.get('/can-encrypt', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 30 | res.json({ 31 | result: yield safeStorage.isEncryptionAvailable(), 32 | }); 33 | })); 34 | router.post('/encrypt', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 35 | try { 36 | res.json({ 37 | result: yield safeStorage.encryptString(req.body.string).toString('base64'), 38 | }); 39 | } 40 | catch (e) { 41 | res.status(400).json({ 42 | error: e.message, 43 | }); 44 | } 45 | })); 46 | router.post('/decrypt', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 47 | try { 48 | res.json({ 49 | result: yield safeStorage.decryptString(Buffer.from(req.body.string, 'base64')), 50 | }); 51 | } 52 | catch (e) { 53 | res.status(400).json({ 54 | error: e.message, 55 | }); 56 | } 57 | })); 58 | router.get('/printers', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 59 | const printers = yield BrowserWindow.getAllWindows()[0].webContents.getPrintersAsync(); 60 | res.json({ 61 | printers, 62 | }); 63 | })); 64 | router.post('/print', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 65 | const { printer, html } = req.body; 66 | let printWindow = new BrowserWindow({ 67 | show: false, 68 | }); 69 | printWindow.webContents.on('did-finish-load', () => { 70 | printWindow.webContents.print({ 71 | silent: true, 72 | deviceName: printer, 73 | }, (success, errorType) => { 74 | if (success) { 75 | console.log('Print job completed successfully.'); 76 | res.sendStatus(200); 77 | } 78 | else { 79 | console.error('Print job failed:', errorType); 80 | res.sendStatus(500); 81 | } 82 | if (printWindow) { 83 | printWindow.close(); 84 | printWindow = null; 85 | } 86 | }); 87 | }); 88 | yield printWindow.loadURL(`data:text/html;charset=UTF-8,${html}`); 89 | })); 90 | router.post('/print-to-pdf', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 91 | const { html } = req.body; 92 | let printWindow = new BrowserWindow({ 93 | show: false, 94 | }); 95 | printWindow.webContents.on('did-finish-load', () => { 96 | printWindow.webContents.printToPDF({}).then(data => { 97 | printWindow.close(); 98 | res.json({ 99 | result: data.toString('base64'), 100 | }); 101 | }).catch(e => { 102 | printWindow.close(); 103 | res.status(400).json({ 104 | error: e.message, 105 | }); 106 | }); 107 | }); 108 | yield printWindow.loadURL(`data:text/html;charset=UTF-8,${html}`); 109 | })); 110 | router.get('/theme', (req, res) => { 111 | res.json({ 112 | result: nativeTheme.themeSource, 113 | }); 114 | }); 115 | router.post('/theme', (req, res) => { 116 | const { theme } = req.body; 117 | nativeTheme.themeSource = theme; 118 | res.json({ 119 | result: theme, 120 | }); 121 | }); 122 | export default router; 123 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/childProcess.js: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | const proc = spawn(process.argv[2], process.argv.slice(3)); 3 | process.parentPort.on('message', (message) => { 4 | proc.stdin.write(message.data); 5 | }); 6 | proc.stdout.on('data', (data) => { 7 | console.log(data.toString()); 8 | }); 9 | proc.stderr.on('data', (data) => { 10 | console.error(data.toString()); 11 | }); 12 | proc.on('close', (code) => { 13 | process.exit(code); 14 | }); 15 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/index.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import startAPIServer from "./api.js"; 11 | import { retrieveNativePHPConfig, retrievePhpIniSettings, serveApp, startScheduler, } from "./php.js"; 12 | import { appendCookie } from "./utils.js"; 13 | import state from "./state.js"; 14 | export function startPhpApp() { 15 | return __awaiter(this, void 0, void 0, function* () { 16 | const result = yield serveApp(state.randomSecret, state.electronApiPort, state.phpIni); 17 | state.phpPort = result.port; 18 | yield appendCookie(); 19 | return result.process; 20 | }); 21 | } 22 | export function runScheduler() { 23 | startScheduler(state.randomSecret, state.electronApiPort, state.phpIni); 24 | } 25 | export function startAPI() { 26 | return startAPIServer(state.randomSecret); 27 | } 28 | export { retrieveNativePHPConfig, retrievePhpIniSettings }; 29 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/state.js: -------------------------------------------------------------------------------- 1 | import Store from "electron-store"; 2 | import { notifyLaravel } from "./utils.js"; 3 | const settingsStore = new Store(); 4 | settingsStore.onDidAnyChange((newValue, oldValue) => { 5 | const changedKey = Object.keys(newValue).find((key) => newValue[key] !== oldValue[key]); 6 | if (changedKey) { 7 | notifyLaravel("events", { 8 | event: "Native\\Laravel\\Events\\Settings\\SettingChanged", 9 | payload: { 10 | key: changedKey, 11 | value: newValue[changedKey] || null, 12 | }, 13 | }); 14 | } 15 | }); 16 | function generateRandomString(length) { 17 | let result = ""; 18 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 19 | const charactersLength = characters.length; 20 | for (let i = 0; i < length; i += 1) { 21 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 22 | } 23 | return result; 24 | } 25 | export default { 26 | electronApiPort: null, 27 | activeMenuBar: null, 28 | tray: null, 29 | php: null, 30 | phpPort: null, 31 | phpIni: null, 32 | caCert: null, 33 | icon: null, 34 | store: settingsStore, 35 | randomSecret: generateRandomString(32), 36 | processes: {}, 37 | windows: {}, 38 | findWindow(id) { 39 | return this.windows[id] || null; 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/dist/server/utils.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import { session } from 'electron'; 11 | import state from './state.js'; 12 | import axios from 'axios'; 13 | export function appendCookie() { 14 | return __awaiter(this, void 0, void 0, function* () { 15 | const cookie = { 16 | url: `http://localhost:${state.phpPort}`, 17 | name: "_php_native", 18 | value: state.randomSecret, 19 | }; 20 | yield session.defaultSession.cookies.set(cookie); 21 | }); 22 | } 23 | export function notifyLaravel(endpoint_1) { 24 | return __awaiter(this, arguments, void 0, function* (endpoint, payload = {}) { 25 | if (endpoint === 'events') { 26 | broadcastToWindows('native-event', payload); 27 | } 28 | try { 29 | yield axios.post(`http://127.0.0.1:${state.phpPort}/_native/api/${endpoint}`, payload, { 30 | headers: { 31 | "X-NativePHP-Secret": state.randomSecret, 32 | }, 33 | }); 34 | } 35 | catch (e) { 36 | } 37 | }); 38 | } 39 | export function broadcastToWindows(event, payload) { 40 | var _a; 41 | Object.values(state.windows).forEach(window => { 42 | window.webContents.send(event, payload); 43 | }); 44 | if ((_a = state.activeMenuBar) === null || _a === void 0 ? void 0 : _a.window) { 45 | state.activeMenuBar.window.webContents.send(event, payload); 46 | } 47 | } 48 | export function trimOptions(options) { 49 | Object.keys(options).forEach(key => options[key] == null && delete options[key]); 50 | return options; 51 | } 52 | export function appendWindowIdToUrl(url, id) { 53 | return url + (url.indexOf('?') === -1 ? '?' : '&') + '_windowId=' + id; 54 | } 55 | export function goToUrl(url, windowId) { 56 | var _a; 57 | (_a = state.windows[windowId]) === null || _a === void 0 ? void 0 : _a.loadURL(appendWindowIdToUrl(url, windowId)); 58 | } 59 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import CrossProcessExports from "electron"; 2 | import { app, session, powerMonitor } from "electron"; 3 | import { initialize } from "@electron/remote/main/index.js"; 4 | import state from "./server/state.js"; 5 | import { electronApp, optimizer } from "@electron-toolkit/utils"; 6 | import { 7 | retrieveNativePHPConfig, 8 | retrievePhpIniSettings, 9 | runScheduler, 10 | startAPI, 11 | startPhpApp, 12 | } from "./server/index.js"; 13 | import { notifyLaravel } from "./server/utils.js"; 14 | import { resolve } from "path"; 15 | import { stopAllProcesses } from "./server/api/childProcess.js"; 16 | import ps from "ps-node"; 17 | import killSync from "kill-sync"; 18 | 19 | // Workaround for CommonJS module 20 | import electronUpdater from 'electron-updater'; 21 | const { autoUpdater } = electronUpdater; 22 | 23 | class NativePHP { 24 | processes = []; 25 | schedulerInterval = undefined; 26 | mainWindow = null; 27 | 28 | public bootstrap( 29 | app: CrossProcessExports.App, 30 | icon: string, 31 | phpBinary: string, 32 | cert: string 33 | ) { 34 | 35 | initialize(); 36 | 37 | state.icon = icon; 38 | state.php = phpBinary; 39 | state.caCert = cert; 40 | 41 | this.bootstrapApp(app); 42 | this.addEventListeners(app); 43 | } 44 | 45 | private addEventListeners(app: Electron.CrossProcessExports.App) { 46 | app.on("open-url", (event, url) => { 47 | notifyLaravel("events", { 48 | event: "\\Native\\Laravel\\Events\\App\\OpenedFromURL", 49 | payload: [url], 50 | }); 51 | }); 52 | 53 | app.on("open-file", (event, path) => { 54 | notifyLaravel("events", { 55 | event: "\\Native\\Laravel\\Events\\App\\OpenFile", 56 | payload: [path], 57 | }); 58 | }); 59 | 60 | app.on("window-all-closed", () => { 61 | if (process.platform !== "darwin") { 62 | app.quit(); 63 | } 64 | }); 65 | 66 | app.on("before-quit", () => { 67 | if (this.schedulerInterval) { 68 | clearInterval(this.schedulerInterval); 69 | } 70 | 71 | // close all child processes from the app 72 | stopAllProcesses(); 73 | 74 | this.killChildProcesses(); 75 | }); 76 | 77 | // Default open or close DevTools by F12 in development 78 | // and ignore CommandOrControl + R in production. 79 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 80 | app.on("browser-window-created", (_, window) => { 81 | optimizer.watchWindowShortcuts(window); 82 | }); 83 | 84 | app.on("activate", function (event, hasVisibleWindows) { 85 | // On macOS it's common to re-create a window in the app when the 86 | // dock icon is clicked and there are no other windows open. 87 | if (!hasVisibleWindows) { 88 | notifyLaravel("booted"); 89 | } 90 | 91 | event.preventDefault(); 92 | }); 93 | } 94 | 95 | private async bootstrapApp(app: Electron.CrossProcessExports.App) { 96 | await app.whenReady(); 97 | 98 | const config = await this.loadConfig(); 99 | 100 | this.setDockIcon(); 101 | this.setAppUserModelId(config); 102 | this.setDeepLinkHandler(config); 103 | this.startAutoUpdater(config); 104 | 105 | await this.startElectronApi(); 106 | 107 | state.phpIni = await this.loadPhpIni(); 108 | 109 | await this.startPhpApp(); 110 | this.startScheduler(); 111 | 112 | powerMonitor.on("suspend", () => { 113 | this.stopScheduler(); 114 | }); 115 | 116 | powerMonitor.on("resume", () => { 117 | this.stopScheduler(); 118 | this.startScheduler(); 119 | }); 120 | 121 | const filter = { 122 | urls: [`http://127.0.0.1:${state.phpPort}/*`] 123 | }; 124 | 125 | session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { 126 | details.requestHeaders['X-NativePHP-Secret'] = state.randomSecret; 127 | 128 | callback({ requestHeaders: details.requestHeaders }); 129 | }); 130 | 131 | await notifyLaravel("booted"); 132 | } 133 | 134 | private async loadConfig() { 135 | let config = {}; 136 | 137 | try { 138 | const result = await retrieveNativePHPConfig(); 139 | 140 | config = JSON.parse(result.stdout); 141 | } catch (error) { 142 | console.error(error); 143 | } 144 | 145 | return config; 146 | } 147 | 148 | private setDockIcon() { 149 | // Only run this on macOS 150 | if ( 151 | process.platform === "darwin" && 152 | process.env.NODE_ENV === "development" 153 | ) { 154 | app.dock.setIcon(state.icon); 155 | } 156 | } 157 | 158 | private setAppUserModelId(config) { 159 | electronApp.setAppUserModelId(config?.app_id); 160 | } 161 | 162 | private setDeepLinkHandler(config) { 163 | const deepLinkProtocol = config?.deeplink_scheme; 164 | 165 | if (deepLinkProtocol) { 166 | if (process.defaultApp) { 167 | if (process.argv.length >= 2) { 168 | app.setAsDefaultProtocolClient(deepLinkProtocol, process.execPath, [ 169 | resolve(process.argv[1]), 170 | ]); 171 | } 172 | } else { 173 | app.setAsDefaultProtocolClient(deepLinkProtocol); 174 | } 175 | 176 | /** 177 | * Handle protocol url for windows and linux 178 | * This code will be different in Windows and Linux compared to MacOS. 179 | * This is due to both platforms emitting the second-instance event rather 180 | * than the open-url event and Windows requiring additional code in order to 181 | * open the contents of the protocol link within the same Electron instance. 182 | */ 183 | if (process.platform !== "darwin") { 184 | const gotTheLock = app.requestSingleInstanceLock(); 185 | if (!gotTheLock) { 186 | app.quit(); 187 | return; 188 | } else { 189 | app.on( 190 | "second-instance", 191 | (event, commandLine, workingDirectory) => { 192 | // Someone tried to run a second instance, we should focus our window. 193 | if (this.mainWindow) { 194 | if (this.mainWindow.isMinimized()) 195 | this.mainWindow.restore(); 196 | this.mainWindow.focus(); 197 | } 198 | 199 | // the commandLine is array of strings in which last element is deep link url 200 | notifyLaravel("events", { 201 | event: "\\Native\\Laravel\\Events\\App\\OpenedFromURL", 202 | payload: { 203 | url: commandLine[commandLine.length - 1], 204 | workingDirectory: workingDirectory, 205 | }, 206 | }); 207 | }, 208 | ); 209 | } 210 | } 211 | } 212 | } 213 | 214 | private startAutoUpdater(config) { 215 | if (config?.updater?.enabled === true) { 216 | autoUpdater.checkForUpdatesAndNotify(); 217 | } 218 | } 219 | 220 | private async startElectronApi() { 221 | // Start an Express server so that the Electron app can be controlled from PHP via API 222 | const electronApi = await startAPI(); 223 | 224 | state.electronApiPort = electronApi.port; 225 | 226 | console.log("Electron API server started on port", electronApi.port); 227 | } 228 | 229 | private async loadPhpIni() { 230 | let config = {}; 231 | 232 | try { 233 | const result = await retrievePhpIniSettings(); 234 | 235 | config = JSON.parse(result.stdout); 236 | } catch (error) { 237 | console.error(error); 238 | } 239 | 240 | return config; 241 | } 242 | 243 | private async startPhpApp() { 244 | this.processes.push(await startPhpApp()); 245 | } 246 | 247 | 248 | private stopScheduler() { 249 | if (this.schedulerInterval) { 250 | clearInterval(this.schedulerInterval); 251 | this.schedulerInterval = null; 252 | } 253 | } 254 | 255 | private startScheduler() { 256 | const now = new Date(); 257 | const delay = 258 | (60 - now.getSeconds()) * 1000 + (1000 - now.getMilliseconds()); 259 | 260 | setTimeout(() => { 261 | console.log("Running scheduler..."); 262 | 263 | runScheduler(); 264 | 265 | this.schedulerInterval = setInterval(() => { 266 | console.log("Running scheduler..."); 267 | 268 | runScheduler(); 269 | }, 60 * 1000); 270 | }, delay); 271 | } 272 | 273 | private killChildProcesses() { 274 | this.processes 275 | .filter((p) => p !== undefined) 276 | .forEach((process) => { 277 | try { 278 | // @ts-ignore 279 | killSync(process.pid, 'SIGTERM', true); // Kill tree 280 | ps.kill(process.pid); // Sometimes does not kill the subprocess of php server 281 | } catch (err) { 282 | console.error(err); 283 | } 284 | }); 285 | } 286 | } 287 | 288 | export default new NativePHP(); 289 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/preload/index.mts: -------------------------------------------------------------------------------- 1 | import remote from "@electron/remote"; 2 | import {ipcRenderer} from "electron"; 3 | 4 | const Native = { 5 | on: (event, callback) => { 6 | ipcRenderer.on('native-event', (_, data) => { 7 | // Strip leading slashes 8 | event = event.replace(/^(\\)+/, ''); 9 | data.event = data.event.replace(/^(\\)+/, ''); 10 | 11 | if (event === data.event) { 12 | return callback(data.payload, event); 13 | } 14 | }) 15 | }, 16 | contextMenu: (template) => { 17 | let menu = remote.Menu.buildFromTemplate(template); 18 | menu.popup({ window: remote.getCurrentWindow() }); 19 | } 20 | }; 21 | 22 | // @ts-ignore 23 | window.Native = Native; 24 | 25 | // @ts-ignore 26 | window.remote = remote; 27 | 28 | ipcRenderer.on('log', (event, {level, message, context}) => { 29 | if (level === 'error') { 30 | console.error(`[${level}] ${message}`, context) 31 | } else if (level === 'warn') { 32 | console.warn(`[${level}] ${message}`, context) 33 | } else { 34 | console.log(`[${level}] ${message}`, context) 35 | } 36 | }); 37 | 38 | // Add Livewire event listeners 39 | ipcRenderer.on('native-event', (event, data) => { 40 | 41 | // Strip leading slashes 42 | data.event = data.event.replace(/^(\\)+/, ''); 43 | 44 | // add support for livewire 3 45 | // @ts-ignore 46 | if (window.Livewire) { 47 | // @ts-ignore 48 | window.Livewire.dispatch('native:' + data.event, data.payload); 49 | } 50 | 51 | // add support for livewire 2 52 | // @ts-ignore 53 | if (window.livewire) { 54 | // @ts-ignore 55 | window.livewire.components.components().forEach(component => { 56 | if (Array.isArray(component.listeners)) { 57 | component.listeners.forEach(event => { 58 | if (event.startsWith('native')) { 59 | let event_parts = event.split(/(native:|native-)|:|,/) 60 | 61 | if (event_parts[1] == 'native:') { 62 | event_parts.splice(2, 0, 'private', undefined, 'nativephp', undefined) 63 | } 64 | 65 | let [ 66 | s1, 67 | signature, 68 | channel_type, 69 | s2, 70 | channel, 71 | s3, 72 | event_name, 73 | ] = event_parts 74 | 75 | if (data.event === event_name) { 76 | // @ts-ignore 77 | window.livewire.emit(event, data.payload) 78 | } 79 | } 80 | }) 81 | } 82 | }) 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/ProcessResult.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams } from "child_process"; 2 | 3 | export interface ProcessResult { 4 | process: ChildProcessWithoutNullStreams; 5 | port: number; 6 | } 7 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bodyParser from "body-parser"; 3 | import getPort, {portNumbers} from "get-port"; 4 | import middleware from "./api/middleware.js"; 5 | 6 | import clipboardRoutes from "./api/clipboard.js"; 7 | import alertRoutes from "./api/alert.js"; 8 | import appRoutes from "./api/app.js"; 9 | import autoUpdaterRoutes from "./api/autoUpdater.js"; 10 | import screenRoutes from "./api/screen.js"; 11 | import dialogRoutes from "./api/dialog.js"; 12 | import debugRoutes from "./api/debug.js"; 13 | import broadcastingRoutes from "./api/broadcasting.js"; 14 | import systemRoutes from "./api/system.js"; 15 | import globalShortcutRoutes from "./api/globalShortcut.js"; 16 | import notificationRoutes from "./api/notification.js"; 17 | import dockRoutes from "./api/dock.js"; 18 | import menuRoutes from "./api/menu.js"; 19 | import menuBarRoutes from "./api/menuBar.js"; 20 | import windowRoutes from "./api/window.js"; 21 | import processRoutes from "./api/process.js"; 22 | import contextMenuRoutes from "./api/contextMenu.js"; 23 | import settingsRoutes from "./api/settings.js"; 24 | import shellRoutes from "./api/shell.js"; 25 | import progressBarRoutes from "./api/progressBar.js"; 26 | import powerMonitorRoutes from "./api/powerMonitor.js"; 27 | import childProcessRoutes from "./api/childProcess.js"; 28 | import { Server } from "net"; 29 | 30 | export interface APIProcess { 31 | server: Server; 32 | port: number; 33 | } 34 | 35 | async function startAPIServer(randomSecret: string): Promise { 36 | const port = await getPort({ 37 | port: portNumbers(4000, 5000), 38 | }); 39 | 40 | return new Promise((resolve, reject) => { 41 | const httpServer = express(); 42 | httpServer.use(middleware(randomSecret)); 43 | httpServer.use(bodyParser.json()); 44 | httpServer.use("/api/clipboard", clipboardRoutes); 45 | httpServer.use("/api/alert", alertRoutes); 46 | httpServer.use("/api/app", appRoutes); 47 | httpServer.use("/api/auto-updater", autoUpdaterRoutes); 48 | httpServer.use("/api/screen", screenRoutes); 49 | httpServer.use("/api/dialog", dialogRoutes); 50 | httpServer.use("/api/system", systemRoutes); 51 | httpServer.use("/api/global-shortcuts", globalShortcutRoutes); 52 | httpServer.use("/api/notification", notificationRoutes); 53 | httpServer.use("/api/dock", dockRoutes); 54 | httpServer.use("/api/menu", menuRoutes); 55 | httpServer.use("/api/window", windowRoutes); 56 | httpServer.use("/api/process", processRoutes); 57 | httpServer.use("/api/settings", settingsRoutes); 58 | httpServer.use("/api/shell", shellRoutes); 59 | httpServer.use("/api/context", contextMenuRoutes); 60 | httpServer.use("/api/menu-bar", menuBarRoutes); 61 | httpServer.use("/api/progress-bar", progressBarRoutes); 62 | httpServer.use("/api/power-monitor", powerMonitorRoutes); 63 | httpServer.use("/api/child-process", childProcessRoutes); 64 | httpServer.use("/api/broadcast", broadcastingRoutes); 65 | 66 | if (process.env.NODE_ENV === "development") { 67 | httpServer.use("/api/debug", debugRoutes); 68 | } 69 | 70 | const server = httpServer.listen(port, () => { 71 | resolve({ 72 | server, 73 | port, 74 | }); 75 | }); 76 | }); 77 | } 78 | 79 | export default startAPIServer; 80 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/alert.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { dialog } from 'electron' 3 | const router = express.Router(); 4 | 5 | router.post('/message', (req, res) => { 6 | const { message, type, title, detail, buttons, defaultId, cancelId } = req.body; 7 | const result = dialog.showMessageBoxSync({ 8 | message, 9 | type: type ?? undefined, 10 | title: title ?? undefined, 11 | detail: detail ?? undefined, 12 | buttons: buttons ?? undefined, 13 | defaultId: defaultId ?? undefined, 14 | cancelId: cancelId ?? undefined 15 | }); 16 | res.json({ 17 | result 18 | }); 19 | }); 20 | 21 | router.post('/error', (req, res) => { 22 | const { title, message } = req.body; 23 | 24 | dialog.showErrorBox(title, message); 25 | 26 | res.json({ 27 | result: true 28 | }); 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { app } from 'electron' 3 | const router = express.Router(); 4 | 5 | router.post('/quit', (req, res) => { 6 | app.quit() 7 | res.sendStatus(200); 8 | }); 9 | 10 | router.post('/relaunch', (req, res) => { 11 | app.relaunch() 12 | app.quit() 13 | }); 14 | 15 | router.post('/show', (req, res) => { 16 | app.show() 17 | res.sendStatus(200); 18 | }); 19 | 20 | router.post('/hide', (req, res) => { 21 | app.hide() 22 | res.sendStatus(200); 23 | }); 24 | 25 | router.get('/is-hidden', (req, res) => { 26 | res.json({ 27 | is_hidden: app.isHidden(), 28 | }) 29 | }); 30 | 31 | router.get('/app-path', (req, res) => { 32 | res.json({ 33 | path: app.getAppPath(), 34 | }) 35 | }); 36 | 37 | router.get('/path/:name', (req, res) => { 38 | res.json({ 39 | // @ts-ignore 40 | path: app.getPath(req.params.name), 41 | }) 42 | }); 43 | 44 | router.get('/version', (req, res) => { 45 | res.json({ 46 | version: app.getVersion(), 47 | }) 48 | }); 49 | 50 | router.post('/badge-count', (req, res) => { 51 | app.setBadgeCount(req.body.count) 52 | res.sendStatus(200); 53 | }); 54 | 55 | router.get('/badge-count', (req, res) => { 56 | res.json({ 57 | count: app.getBadgeCount(), 58 | }) 59 | }); 60 | 61 | router.post('/recent-documents', (req, res) => { 62 | app.addRecentDocument(req.body.path); 63 | res.sendStatus(200); 64 | }); 65 | 66 | router.delete('/recent-documents', (req, res) => { 67 | app.clearRecentDocuments(); 68 | res.sendStatus(200); 69 | }); 70 | 71 | router.post('/open-at-login', (req, res) => { 72 | app.setLoginItemSettings({ 73 | openAtLogin: req.body.open, 74 | }); 75 | res.sendStatus(200); 76 | }); 77 | 78 | router.get('/open-at-login', (req, res) => { 79 | res.json({ 80 | open: app.getLoginItemSettings().openAtLogin, 81 | }); 82 | }); 83 | 84 | export default router; 85 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import electronUpdater from 'electron-updater'; 3 | const { autoUpdater } = electronUpdater; 4 | import type { 5 | ProgressInfo, 6 | UpdateDownloadedEvent, 7 | UpdateInfo, 8 | } from "electron-updater"; 9 | import { notifyLaravel } from "../utils.js"; 10 | 11 | const router = express.Router(); 12 | 13 | router.post("/check-for-updates", (req, res) => { 14 | autoUpdater.checkForUpdates(); 15 | res.sendStatus(200); 16 | }); 17 | 18 | router.post("/download-update", (req, res) => { 19 | autoUpdater.downloadUpdate(); 20 | res.sendStatus(200); 21 | }); 22 | 23 | router.post("/quit-and-install", (req, res) => { 24 | autoUpdater.quitAndInstall(); 25 | res.sendStatus(200); 26 | }); 27 | 28 | autoUpdater.addListener("checking-for-update", () => { 29 | notifyLaravel("events", { 30 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\CheckingForUpdate`, 31 | }); 32 | }); 33 | 34 | autoUpdater.addListener("update-available", (event: UpdateInfo) => { 35 | notifyLaravel("events", { 36 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateAvailable`, 37 | payload: { 38 | version: event.version, 39 | files: event.files, 40 | releaseDate: event.releaseDate, 41 | releaseName: event.releaseName, 42 | releaseNotes: event.releaseNotes, 43 | stagingPercentage: event.stagingPercentage, 44 | minimumSystemVersion: event.minimumSystemVersion, 45 | }, 46 | }); 47 | }); 48 | 49 | autoUpdater.addListener("update-not-available", (event: UpdateInfo) => { 50 | notifyLaravel("events", { 51 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateNotAvailable`, 52 | payload: { 53 | version: event.version, 54 | files: event.files, 55 | releaseDate: event.releaseDate, 56 | releaseName: event.releaseName, 57 | releaseNotes: event.releaseNotes, 58 | stagingPercentage: event.stagingPercentage, 59 | minimumSystemVersion: event.minimumSystemVersion, 60 | }, 61 | }); 62 | }); 63 | 64 | autoUpdater.addListener("error", (error: Error) => { 65 | notifyLaravel("events", { 66 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\Error`, 67 | payload: { 68 | name: error.name, 69 | message: error.message, 70 | stack: error.stack, 71 | }, 72 | }); 73 | }); 74 | 75 | autoUpdater.addListener("download-progress", (progressInfo: ProgressInfo) => { 76 | notifyLaravel("events", { 77 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\DownloadProgress`, 78 | payload: { 79 | total: progressInfo.total, 80 | delta: progressInfo.delta, 81 | transferred: progressInfo.transferred, 82 | percent: progressInfo.percent, 83 | bytesPerSecond: progressInfo.bytesPerSecond, 84 | }, 85 | }); 86 | }); 87 | 88 | autoUpdater.addListener("update-downloaded", (event: UpdateDownloadedEvent) => { 89 | notifyLaravel("events", { 90 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateDownloaded`, 91 | payload: { 92 | downloadedFile: event.downloadedFile, 93 | version: event.version, 94 | files: event.files, 95 | releaseDate: event.releaseDate, 96 | releaseName: event.releaseName, 97 | releaseNotes: event.releaseNotes, 98 | stagingPercentage: event.stagingPercentage, 99 | minimumSystemVersion: event.minimumSystemVersion, 100 | }, 101 | }); 102 | }); 103 | 104 | autoUpdater.addListener("update-cancelled", (event: UpdateInfo) => { 105 | notifyLaravel("events", { 106 | event: `\\Native\\Laravel\\Events\\AutoUpdater\\UpdateCancelled`, 107 | payload: { 108 | version: event.version, 109 | files: event.files, 110 | releaseDate: event.releaseDate, 111 | releaseName: event.releaseName, 112 | releaseNotes: event.releaseNotes, 113 | stagingPercentage: event.stagingPercentage, 114 | minimumSystemVersion: event.minimumSystemVersion, 115 | }, 116 | }); 117 | }); 118 | 119 | export default router; 120 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/broadcasting.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { broadcastToWindows } from '../utils.js'; 3 | const router = express.Router(); 4 | 5 | router.post('/', (req, res) => { 6 | const {event, payload} = req.body; 7 | 8 | broadcastToWindows("native-event", { event, payload }); 9 | 10 | res.sendStatus(200) 11 | }) 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/clipboard.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | const router = express.Router(); 3 | import { clipboard, nativeImage } from 'electron' 4 | 5 | const DEFAULT_TYPE = 'clipboard' 6 | 7 | router.get('/text', (req, res) => { 8 | const {type} = req.query 9 | 10 | res.json({ 11 | // @ts-ignore 12 | text: clipboard.readText(type || DEFAULT_TYPE) 13 | }) 14 | }); 15 | 16 | router.post('/text', (req, res) => { 17 | const {text} = req.body 18 | const {type} = req.query 19 | 20 | // @ts-ignore 21 | clipboard.writeText(text, type || DEFAULT_TYPE) 22 | 23 | res.json({ 24 | text, 25 | }) 26 | }); 27 | 28 | router.get('/html', (req, res) => { 29 | const {type} = req.query 30 | 31 | res.json({ 32 | // @ts-ignore 33 | html: clipboard.readHTML(type || DEFAULT_TYPE) 34 | }) 35 | }); 36 | 37 | router.post('/html', (req, res) => { 38 | const {html} = req.body 39 | const {type} = req.query 40 | // @ts-ignore 41 | clipboard.writeHTML(html, type || DEFAULT_TYPE) 42 | 43 | res.json({ 44 | html, 45 | }) 46 | }); 47 | 48 | router.get('/image', (req, res) => { 49 | const {type} = req.query 50 | // @ts-ignore 51 | const image = clipboard.readImage(type || DEFAULT_TYPE); 52 | 53 | res.json({ 54 | image: image.isEmpty() ? null : image.toDataURL() 55 | }) 56 | }); 57 | 58 | router.post('/image', (req, res) => { 59 | const {image} = req.body 60 | const {type} = req.query 61 | 62 | try { 63 | const _nativeImage = nativeImage.createFromDataURL(image) 64 | // @ts-ignore 65 | clipboard.writeImage(_nativeImage, type || DEFAULT_TYPE) 66 | } catch (e) { 67 | res.status(400).json({ 68 | error: e.message, 69 | }) 70 | return 71 | } 72 | 73 | res.sendStatus(200); 74 | }); 75 | 76 | router.delete('/', (req, res) => { 77 | const {type} = req.query 78 | 79 | // @ts-ignore 80 | clipboard.clear(type || DEFAULT_TYPE) 81 | 82 | res.sendStatus(200); 83 | }); 84 | 85 | export default router; 86 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/contextMenu.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { compileMenu } from "./helper/index.js"; 3 | import contextMenu from "electron-context-menu"; 4 | 5 | const router = express.Router(); 6 | 7 | let contextMenuDisposable = null 8 | 9 | router.delete('/', (req, res) => { 10 | res.sendStatus(200); 11 | 12 | if (contextMenuDisposable) { 13 | contextMenuDisposable(); 14 | contextMenuDisposable = null; 15 | } 16 | }); 17 | 18 | router.post('/', (req, res) => { 19 | res.sendStatus(200); 20 | 21 | if (contextMenuDisposable) { 22 | contextMenuDisposable(); 23 | contextMenuDisposable = null; 24 | } 25 | 26 | contextMenuDisposable = contextMenu({ 27 | showLookUpSelection: false, 28 | showSearchWithGoogle: false, 29 | showInspectElement: false, 30 | prepend: (defaultActions, parameters, browserWindow) => { 31 | return req.body.entries.map(compileMenu); 32 | }, 33 | }); 34 | }); 35 | 36 | export default router; 37 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/debug.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { broadcastToWindows } from '../utils.js'; 3 | const router = express.Router(); 4 | 5 | router.post('/log', (req, res) => { 6 | const {level, message, context} = req.body 7 | 8 | broadcastToWindows('log', {level, message, context}); 9 | 10 | res.sendStatus(200) 11 | }) 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/dialog.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import {dialog} from 'electron' 3 | import state from '../state.js' 4 | import {trimOptions} from '../utils.js' 5 | const router = express.Router(); 6 | 7 | router.post('/open', (req, res) => { 8 | const {title, buttonLabel, filters, properties, defaultPath, message, windowReference} = req.body 9 | 10 | let options = { 11 | title, 12 | defaultPath, 13 | buttonLabel, 14 | filters, 15 | message, 16 | properties 17 | }; 18 | 19 | options = trimOptions(options); 20 | 21 | let result; 22 | let browserWindow = state.findWindow(windowReference); 23 | 24 | if (browserWindow) { 25 | result = dialog.showOpenDialogSync(browserWindow, options) 26 | } else { 27 | result = dialog.showOpenDialogSync(options) 28 | } 29 | 30 | res.json({ 31 | result 32 | }) 33 | }); 34 | 35 | router.post('/save', (req, res) => { 36 | const {title, buttonLabel, filters, properties, defaultPath, message, windowReference} = req.body 37 | 38 | let options = { 39 | title, 40 | defaultPath, 41 | buttonLabel, 42 | filters, 43 | message, 44 | properties 45 | }; 46 | 47 | options = trimOptions(options); 48 | 49 | let result; 50 | let browserWindow = state.findWindow(windowReference); 51 | 52 | if (browserWindow) { 53 | result = dialog.showSaveDialogSync(browserWindow, options) 54 | } else { 55 | result = dialog.showSaveDialogSync(options) 56 | } 57 | 58 | res.json({ 59 | result 60 | }) 61 | }); 62 | 63 | export default router; 64 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/dock.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { app, Menu } from 'electron'; 3 | import { compileMenu } from './helper/index.js'; 4 | import state from '../state.js'; 5 | 6 | const router = express.Router(); 7 | 8 | router.post('/', (req, res) => { 9 | const menuEntries = req.body.items.map(compileMenu); 10 | 11 | const menu = Menu.buildFromTemplate(menuEntries); 12 | app.dock.setMenu(menu); 13 | 14 | res.sendStatus(200); 15 | }); 16 | 17 | router.post('/show', (req, res) => { 18 | app.dock.show(); 19 | 20 | res.sendStatus(200); 21 | }); 22 | 23 | router.post('/hide', (req, res) => { 24 | app.dock.hide(); 25 | 26 | res.sendStatus(200); 27 | }); 28 | 29 | router.post('/icon', (req, res) => { 30 | app.dock.setIcon(req.body.path); 31 | 32 | res.sendStatus(200); 33 | }); 34 | 35 | router.post('/bounce', (req, res) => { 36 | const { type } = req.body; 37 | 38 | state.dockBounce = app.dock.bounce(type); 39 | 40 | res.sendStatus(200); 41 | }); 42 | 43 | router.post('/cancel-bounce', (req, res) => { 44 | app.dock.cancelBounce(state.dockBounce); 45 | 46 | res.sendStatus(200); 47 | }); 48 | 49 | router.get('/badge', (req, res) => { 50 | res.json({ 51 | label: app.dock.getBadge(), 52 | }); 53 | }); 54 | 55 | router.post('/badge', (req, res) => { 56 | app.dock.setBadge(req.body.label); 57 | 58 | res.sendStatus(200); 59 | }); 60 | 61 | export default router; 62 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/globalShortcut.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import {globalShortcut} from 'electron' 3 | import {notifyLaravel} from "../utils.js"; 4 | const router = express.Router(); 5 | 6 | router.post('/', (req, res) => { 7 | const {key, event} = req.body 8 | 9 | globalShortcut.register(key, () => { 10 | notifyLaravel('events', { 11 | event, 12 | payload: [key] 13 | }) 14 | }) 15 | 16 | res.sendStatus(200) 17 | }) 18 | 19 | router.delete('/', (req, res) => { 20 | const {key} = req.body 21 | 22 | globalShortcut.unregister(key) 23 | 24 | res.sendStatus(200) 25 | }); 26 | 27 | router.get('/:key', (req, res) => { 28 | const {key} = req.params 29 | 30 | res.json({ 31 | isRegistered: globalShortcut.isRegistered(key) 32 | }); 33 | }); 34 | 35 | export default router; 36 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/helper/index.ts: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron'; 2 | import { notifyLaravel, goToUrl } from '../../utils.js'; 3 | import state from '../../state.js'; 4 | 5 | function triggerMenuItemEvent(menuItem, combo) { 6 | notifyLaravel('events', { 7 | event: menuItem.event || '\\Native\\Laravel\\Events\\Menu\\MenuItemClicked', 8 | payload: { 9 | item: { 10 | id: menuItem.id, 11 | label: menuItem.label, 12 | checked: menuItem.checked, 13 | }, 14 | combo, 15 | }, 16 | }); 17 | } 18 | 19 | export function compileMenu (item) { 20 | if (item.submenu) { 21 | if (Array.isArray(item.submenu)) { 22 | item.submenu = item.submenu?.map(compileMenu); 23 | } else { 24 | item.submenu = item.submenu.submenu?.map(compileMenu); 25 | } 26 | } 27 | 28 | if (item.type === 'link') { 29 | item.type = 'normal'; 30 | 31 | item.click = (menuItem, focusedWindow, combo) => { 32 | triggerMenuItemEvent(item, combo); 33 | 34 | if (item.openInBrowser) { 35 | shell.openExternal(item.url); 36 | return; 37 | } 38 | 39 | if (! focusedWindow) { 40 | // TODO: Bring a window to the front? 41 | return; 42 | } 43 | 44 | const id = Object.keys(state.windows) 45 | .find(key => state.windows[key] === focusedWindow); 46 | 47 | goToUrl(item.url, id); 48 | } 49 | 50 | return item; 51 | } 52 | 53 | if (item.type === 'checkbox' || item.type === 'radio') { 54 | item.click = (menuItem, focusedWindow, combo) => { 55 | item.checked = !item.checked; 56 | triggerMenuItemEvent(item, combo); 57 | }; 58 | 59 | return item; 60 | } 61 | 62 | if (item.type === 'role') { 63 | let menuItem = { 64 | role: item.role 65 | }; 66 | 67 | if (item.label) { 68 | menuItem['label'] = item.label; 69 | } 70 | 71 | return menuItem; 72 | } 73 | 74 | // Default click event 75 | if (! item.click) { 76 | item.click = (menuItem, focusedWindow, combo) => { 77 | triggerMenuItemEvent(item, combo); 78 | } 79 | } 80 | 81 | return item; 82 | } 83 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/menu.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Menu } from 'electron'; 3 | import { compileMenu } from './helper/index.js'; 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/', (req, res) => { 8 | Menu.setApplicationMenu(null); 9 | 10 | const menuEntries = req.body.items.map(compileMenu); 11 | 12 | const menu = Menu.buildFromTemplate(menuEntries); 13 | 14 | Menu.setApplicationMenu(menu); 15 | 16 | res.sendStatus(200); 17 | }); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/menuBar.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { app, Menu, Tray } from "electron"; 3 | import { compileMenu } from "./helper/index.js"; 4 | import state from "../state.js"; 5 | import { menubar } from "menubar"; 6 | import { notifyLaravel } from "../utils.js"; 7 | import { fileURLToPath } from 'url' 8 | import { enable } from "@electron/remote/main/index.js"; 9 | 10 | const router = express.Router(); 11 | 12 | router.post("/label", (req, res) => { 13 | res.sendStatus(200); 14 | 15 | const { label } = req.body; 16 | 17 | state.tray?.setTitle(label); 18 | }); 19 | 20 | router.post("/tooltip", (req, res) => { 21 | res.sendStatus(200); 22 | 23 | const { tooltip } = req.body; 24 | 25 | state.tray?.setToolTip(tooltip); 26 | }); 27 | 28 | router.post("/icon", (req, res) => { 29 | res.sendStatus(200); 30 | 31 | const { icon } = req.body; 32 | 33 | state.tray?.setImage(icon); 34 | }); 35 | 36 | router.post("/context-menu", (req, res) => { 37 | res.sendStatus(200); 38 | 39 | const { contextMenu } = req.body; 40 | 41 | state.tray?.setContextMenu(buildMenu(contextMenu)); 42 | }); 43 | 44 | router.post("/show", (req, res) => { 45 | res.sendStatus(200); 46 | 47 | state.activeMenuBar.showWindow(); 48 | }); 49 | 50 | router.post("/hide", (req, res) => { 51 | res.sendStatus(200); 52 | 53 | state.activeMenuBar.hideWindow(); 54 | }); 55 | 56 | router.post("/resize", (req, res) => { 57 | res.sendStatus(200); 58 | 59 | const { width, height } = req.body; 60 | 61 | state.activeMenuBar.window.setSize(width, height); 62 | }); 63 | 64 | router.post("/create", (req, res) => { 65 | res.sendStatus(200); 66 | 67 | let shouldSendCreatedEvent = true; 68 | 69 | if (state.activeMenuBar) { 70 | state.activeMenuBar.tray.destroy(); 71 | shouldSendCreatedEvent = false; 72 | } 73 | 74 | const { 75 | width, 76 | height, 77 | url, 78 | label, 79 | alwaysOnTop, 80 | vibrancy, 81 | backgroundColor, 82 | transparency, 83 | icon, 84 | showDockIcon, 85 | onlyShowContextMenu, 86 | windowPosition, 87 | showOnAllWorkspaces, 88 | contextMenu, 89 | tooltip, 90 | resizable, 91 | event, 92 | } = req.body; 93 | 94 | 95 | if (onlyShowContextMenu) { 96 | // Create a tray icon 97 | const tray = new Tray(icon || state.icon.replace("icon.png", "IconTemplate.png")); 98 | 99 | // Set the context menu 100 | tray.setContextMenu(buildMenu(contextMenu)); 101 | tray.setToolTip(tooltip); 102 | tray.setTitle(label); 103 | 104 | // Set the event listeners + send created event 105 | eventsForTray(tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent); 106 | 107 | // Set the tray to the state 108 | state.tray = tray; 109 | 110 | if (!showDockIcon) { 111 | app.dock.hide(); 112 | } 113 | 114 | } else { 115 | state.activeMenuBar = menubar({ 116 | icon: icon || state.icon.replace("icon.png", "IconTemplate.png"), 117 | preloadWindow: true, 118 | tooltip, 119 | index: url, 120 | showDockIcon, 121 | showOnAllWorkspaces: showOnAllWorkspaces ?? false, 122 | windowPosition: windowPosition ?? "trayCenter", 123 | activateWithApp: false, 124 | browserWindow: { 125 | width, 126 | height, 127 | resizable, 128 | alwaysOnTop, 129 | vibrancy, 130 | backgroundColor, 131 | transparent: transparency, 132 | webPreferences: { 133 | preload: fileURLToPath(new URL('../../electron-plugin/dist/preload/index.mjs', import.meta.url)), 134 | nodeIntegration: true, 135 | sandbox: false, 136 | contextIsolation: false, 137 | } 138 | } 139 | }); 140 | 141 | state.activeMenuBar.on("after-create-window", () => { 142 | enable(state.activeMenuBar.window.webContents); 143 | }); 144 | 145 | state.activeMenuBar.on("ready", () => { 146 | // Set the event listeners 147 | eventsForTray(state.activeMenuBar.tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent); 148 | 149 | // Set the tray to the state 150 | state.tray = state.activeMenuBar.tray; 151 | 152 | // Set the title 153 | state.tray.setTitle(label); 154 | 155 | state.activeMenuBar.on("hide", () => { 156 | notifyLaravel("events", { 157 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarHidden" 158 | }); 159 | }); 160 | 161 | state.activeMenuBar.on("show", () => { 162 | notifyLaravel("events", { 163 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarShown" 164 | }); 165 | }); 166 | 167 | }); 168 | } 169 | 170 | }); 171 | 172 | 173 | 174 | function eventsForTray(tray, onlyShowContextMenu, contextMenu, shouldSendCreatedEvent) { 175 | 176 | if (shouldSendCreatedEvent) { 177 | notifyLaravel("events", { 178 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarCreated" 179 | }); 180 | } 181 | 182 | tray.on("drop-files", (event, files) => { 183 | notifyLaravel("events", { 184 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarDroppedFiles", 185 | payload: [ 186 | files 187 | ] 188 | }); 189 | }); 190 | 191 | tray.on('click', (combo, bounds, position) => { 192 | notifyLaravel('events', { 193 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarClicked", 194 | payload: { 195 | combo, 196 | bounds, 197 | position, 198 | }, 199 | }); 200 | }); 201 | 202 | tray.on("right-click", (combo, bounds) => { 203 | notifyLaravel("events", { 204 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarRightClicked", 205 | payload: { 206 | combo, 207 | bounds, 208 | } 209 | }); 210 | 211 | if (!onlyShowContextMenu) { 212 | state.activeMenuBar.hideWindow(); 213 | tray.popUpContextMenu(buildMenu(contextMenu)); 214 | } 215 | }); 216 | 217 | tray.on('double-click', (combo, bounds) => { 218 | notifyLaravel('events', { 219 | event: "\\Native\\Laravel\\Events\\MenuBar\\MenuBarDoubleClicked", 220 | payload: { 221 | combo, 222 | bounds, 223 | }, 224 | }); 225 | }); 226 | } 227 | 228 | function buildMenu(contextMenu) { 229 | let menu = Menu.buildFromTemplate([{ role: "quit" }]); 230 | 231 | if (contextMenu) { 232 | const menuEntries = contextMenu.map(compileMenu); 233 | menu = Menu.buildFromTemplate(menuEntries); 234 | } 235 | 236 | return menu; 237 | } 238 | 239 | export default router; 240 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/middleware.ts: -------------------------------------------------------------------------------- 1 | export default function (secret) { 2 | return function (req, res, next) { 3 | if (req.headers['x-nativephp-secret'] !== secret) { 4 | res.sendStatus(403); 5 | return; 6 | } 7 | next(); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/notification.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Notification } from 'electron'; 3 | import {notifyLaravel} from "../utils.js"; 4 | const router = express.Router(); 5 | 6 | router.post('/', (req, res) => { 7 | const { 8 | title, 9 | body, 10 | subtitle, 11 | silent, 12 | icon, 13 | hasReply, 14 | timeoutType, 15 | replyPlaceholder, 16 | sound, 17 | urgency, 18 | actions, 19 | closeButtonText, 20 | toastXml, 21 | event: customEvent, 22 | reference, 23 | } = req.body; 24 | 25 | const eventName = customEvent ?? '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked'; 26 | 27 | const notificationReference = reference ?? (Date.now() + '.' + Math.random().toString(36).slice(2, 9)); 28 | 29 | const notification = new Notification({ 30 | title, 31 | body, 32 | subtitle, 33 | silent, 34 | icon, 35 | hasReply, 36 | timeoutType, 37 | replyPlaceholder, 38 | sound, 39 | urgency, 40 | actions, 41 | closeButtonText, 42 | toastXml 43 | }); 44 | 45 | notification.on("click", (event) => { 46 | notifyLaravel('events', { 47 | event: eventName || '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked', 48 | payload: { 49 | reference: notificationReference, 50 | event: JSON.stringify(event), 51 | }, 52 | }); 53 | }); 54 | 55 | notification.on("action", (event, index) => { 56 | notifyLaravel('events', { 57 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationActionClicked', 58 | payload: { 59 | reference: notificationReference, 60 | index, 61 | event: JSON.stringify(event), 62 | }, 63 | }); 64 | }); 65 | 66 | notification.on("reply", (event, reply) => { 67 | notifyLaravel('events', { 68 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationReply', 69 | payload: { 70 | reference: notificationReference, 71 | reply, 72 | event: JSON.stringify(event), 73 | }, 74 | }); 75 | }); 76 | 77 | notification.on("close", (event) => { 78 | notifyLaravel('events', { 79 | event: '\\Native\\Laravel\\Events\\Notifications\\NotificationClosed', 80 | payload: { 81 | reference: notificationReference, 82 | event: JSON.stringify(event), 83 | }, 84 | }); 85 | }); 86 | 87 | notification.show(); 88 | 89 | res.status(200).json({ 90 | reference: notificationReference, 91 | }); 92 | }); 93 | 94 | export default router; 95 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/powerMonitor.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { powerMonitor } from 'electron' 3 | import { notifyLaravel } from '../utils.js'; 4 | const router = express.Router(); 5 | 6 | router.get('/get-system-idle-state', (req, res) => { 7 | let threshold = Number(req.query.threshold) || 60; 8 | 9 | res.json({ 10 | result: powerMonitor.getSystemIdleState(threshold), 11 | }) 12 | }); 13 | 14 | router.get('/get-system-idle-time', (req, res) => { 15 | res.json({ 16 | result: powerMonitor.getSystemIdleTime(), 17 | }) 18 | }); 19 | 20 | router.get('/get-current-thermal-state', (req, res) => { 21 | res.json({ 22 | result: powerMonitor.getCurrentThermalState(), 23 | }) 24 | }); 25 | 26 | router.get('/is-on-battery-power', (req, res) => { 27 | res.json({ 28 | result: powerMonitor.isOnBatteryPower(), 29 | }) 30 | }); 31 | 32 | powerMonitor.addListener('on-ac', () => { 33 | notifyLaravel("events", { 34 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\PowerStateChanged`, 35 | payload: { 36 | state: 'on-ac' 37 | } 38 | }); 39 | }) 40 | 41 | powerMonitor.addListener('on-battery', () => { 42 | notifyLaravel("events", { 43 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\PowerStateChanged`, 44 | payload: { 45 | state: 'on-battery' 46 | } 47 | }); 48 | }) 49 | 50 | // @ts-ignore 51 | powerMonitor.addListener('thermal-state-change', (details) => { 52 | notifyLaravel("events", { 53 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ThermalStateChanged`, 54 | payload: { 55 | state: details.state, 56 | }, 57 | }); 58 | }) 59 | 60 | // @ts-ignore 61 | powerMonitor.addListener('speed-limit-change', (details) => { 62 | notifyLaravel("events", { 63 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\SpeedLimitChanged`, 64 | payload: { 65 | limit: details.limit, 66 | }, 67 | }); 68 | }) 69 | 70 | // @ts-ignore 71 | powerMonitor.addListener('lock-screen', () => { 72 | notifyLaravel("events", { 73 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ScreenLocked`, 74 | }); 75 | }) 76 | 77 | // @ts-ignore 78 | powerMonitor.addListener('unlock-screen', () => { 79 | notifyLaravel("events", { 80 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\ScreenUnlocked`, 81 | }); 82 | }) 83 | 84 | 85 | // @ts-ignore 86 | powerMonitor.addListener('shutdown', () => { 87 | notifyLaravel("events", { 88 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\Shutdown`, 89 | }); 90 | }) 91 | 92 | 93 | // @ts-ignore 94 | powerMonitor.addListener('user-did-become-active', () => { 95 | notifyLaravel("events", { 96 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\UserDidBecomeActive`, 97 | }); 98 | }) 99 | 100 | 101 | // @ts-ignore 102 | powerMonitor.addListener('user-did-resign-active', () => { 103 | notifyLaravel("events", { 104 | event: `\\Native\\Laravel\\Events\\PowerMonitor\\UserDidResignActive`, 105 | }); 106 | }) 107 | 108 | export default router; 109 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/process.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | router.get('/', (req, res) => { 6 | res.json({ 7 | pid: process.pid, 8 | platform: process.platform, 9 | arch: process.arch, 10 | uptime: process.uptime() 11 | }) 12 | }); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/progressBar.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import state from "../state.js"; 3 | const router = express.Router(); 4 | 5 | router.post('/update', (req, res) => { 6 | const {percent} = req.body 7 | 8 | Object.values(state.windows).forEach((window) => { 9 | window.setProgressBar(percent) 10 | }); 11 | 12 | res.sendStatus(200) 13 | }) 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/screen.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { screen } from 'electron' 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/displays', (req, res) => { 7 | res.json({ 8 | displays: screen.getAllDisplays() 9 | }) 10 | }); 11 | 12 | router.get('/primary-display', (req, res) => { 13 | res.json({ 14 | primaryDisplay: screen.getPrimaryDisplay() 15 | }) 16 | }); 17 | 18 | router.get('/cursor-position', (req, res) => { 19 | res.json(screen.getCursorScreenPoint()) 20 | }); 21 | 22 | router.get('/active', (req, res) => { 23 | const cursor = screen.getCursorScreenPoint() 24 | 25 | res.json(screen.getDisplayNearestPoint(cursor)) 26 | }); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/settings.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import state from '../state.js'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/:key', (req, res) => { 7 | const key = req.params.key; 8 | 9 | const value = state.store.get(key, null); 10 | 11 | res.json({value}); 12 | }); 13 | 14 | router.post('/:key', (req, res) => { 15 | const key = req.params.key; 16 | const value = req.body.value; 17 | 18 | state.store.set(key, value); 19 | 20 | res.sendStatus(200) 21 | }); 22 | 23 | router.delete('/:key', (req, res) => { 24 | const key = req.params.key; 25 | 26 | state.store.delete(key); 27 | 28 | res.sendStatus(200) 29 | }); 30 | 31 | router.delete('/', (req, res) => { 32 | state.store.clear(); 33 | 34 | res.sendStatus(200) 35 | }); 36 | 37 | export default router; 38 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/shell.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { shell } from "electron"; 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/show-item-in-folder", (req, res) => { 7 | const { path } = req.body; 8 | 9 | shell.showItemInFolder(path); 10 | 11 | res.sendStatus(200); 12 | }); 13 | 14 | router.post("/open-item", async (req, res) => { 15 | const { path } = req.body; 16 | 17 | let result = await shell.openPath(path); 18 | 19 | res.json({ 20 | result 21 | }) 22 | }); 23 | 24 | router.post("/open-external", async (req, res) => { 25 | const { url } = req.body; 26 | 27 | try { 28 | await shell.openExternal(url); 29 | 30 | res.sendStatus(200); 31 | } catch (e) { 32 | res.status(500).json({ 33 | error: e 34 | }); 35 | } 36 | }); 37 | 38 | router.delete("/trash-item", async (req, res) => { 39 | const { path } = req.body; 40 | 41 | try { 42 | await shell.trashItem(path); 43 | 44 | res.sendStatus(200); 45 | } catch (e) { 46 | res.status(400).json(); 47 | } 48 | }); 49 | 50 | export default router; 51 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/api/system.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import {BrowserWindow, systemPreferences, safeStorage, nativeTheme} from 'electron'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/can-prompt-touch-id', (req, res) => { 7 | res.json({ 8 | result: systemPreferences.canPromptTouchID(), 9 | }); 10 | }); 11 | 12 | router.post('/prompt-touch-id', async (req, res) => { 13 | try { 14 | await systemPreferences.promptTouchID(req.body.reason) 15 | 16 | res.sendStatus(200); 17 | } catch (e) { 18 | res.status(400).json({ 19 | error: e.message, 20 | }); 21 | } 22 | }); 23 | 24 | router.get('/can-encrypt', async (req, res) => { 25 | res.json({ 26 | result: await safeStorage.isEncryptionAvailable(), 27 | }); 28 | }); 29 | 30 | router.post('/encrypt', async (req, res) => { 31 | try { 32 | res.json({ 33 | result: await safeStorage.encryptString(req.body.string).toString('base64'), 34 | }); 35 | } catch (e) { 36 | res.status(400).json({ 37 | error: e.message, 38 | }); 39 | } 40 | }); 41 | 42 | router.post('/decrypt', async (req, res) => { 43 | try { 44 | res.json({ 45 | result: await safeStorage.decryptString(Buffer.from(req.body.string, 'base64')), 46 | }); 47 | } catch (e) { 48 | res.status(400).json({ 49 | error: e.message, 50 | }); 51 | } 52 | }); 53 | 54 | router.get('/printers', async (req, res) => { 55 | const printers = await BrowserWindow.getAllWindows()[0].webContents.getPrintersAsync(); 56 | 57 | res.json({ 58 | printers, 59 | }); 60 | }); 61 | 62 | router.post('/print', async (req, res) => { 63 | const {printer, html} = req.body; 64 | 65 | let printWindow = new BrowserWindow({ 66 | show: false, 67 | }); 68 | 69 | printWindow.webContents.on('did-finish-load', () => { 70 | printWindow.webContents.print({ 71 | silent: true, 72 | deviceName: printer, 73 | }, (success, errorType) => { 74 | if (success) { 75 | console.log('Print job completed successfully.'); 76 | res.sendStatus(200); 77 | } else { 78 | console.error('Print job failed:', errorType); 79 | res.sendStatus(500); 80 | } 81 | if (printWindow) { 82 | printWindow.close(); // Close the window and the process 83 | printWindow = null; // Free memory 84 | } 85 | }); 86 | }); 87 | 88 | await printWindow.loadURL(`data:text/html;charset=UTF-8,${html}`); 89 | }); 90 | 91 | router.post('/print-to-pdf', async (req, res) => { 92 | const {html} = req.body; 93 | 94 | let printWindow = new BrowserWindow({ 95 | show: false, 96 | }); 97 | 98 | printWindow.webContents.on('did-finish-load', () => { 99 | printWindow.webContents.printToPDF({}).then(data => { 100 | printWindow.close(); 101 | res.json({ 102 | result: data.toString('base64'), 103 | }); 104 | }).catch(e => { 105 | printWindow.close(); 106 | 107 | res.status(400).json({ 108 | error: e.message, 109 | }); 110 | }); 111 | }); 112 | 113 | await printWindow.loadURL(`data:text/html;charset=UTF-8,${html}`); 114 | }); 115 | 116 | router.get('/theme', (req, res) => { 117 | res.json({ 118 | result: nativeTheme.themeSource, 119 | }); 120 | }); 121 | 122 | router.post('/theme', (req, res) => { 123 | const { theme } = req.body; 124 | 125 | nativeTheme.themeSource = theme; 126 | 127 | res.json({ 128 | result: theme, 129 | }); 130 | }); 131 | 132 | export default router; 133 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/childProcess.ts: -------------------------------------------------------------------------------- 1 | import {spawn} from "child_process"; 2 | 3 | const proc = spawn( 4 | process.argv[2], 5 | process.argv.slice(3) 6 | ); 7 | 8 | process.parentPort.on('message', (message) => { 9 | proc.stdin.write(message.data) 10 | }); 11 | 12 | // Handle normal output 13 | proc.stdout.on('data', (data) => { 14 | console.log(data.toString()); 15 | }); 16 | 17 | // Handle error output 18 | proc.stderr.on('data', (data) => { 19 | console.error(data.toString()); 20 | }); 21 | 22 | // Handle process exit 23 | proc.on('close', (code) => { 24 | process.exit(code) 25 | }); 26 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import startAPIServer, { APIProcess } from "./api.js"; 2 | import { 3 | retrieveNativePHPConfig, 4 | retrievePhpIniSettings, 5 | serveApp, 6 | startScheduler, 7 | } from "./php.js"; 8 | import { appendCookie } from "./utils.js"; 9 | import state from "./state.js"; 10 | 11 | export async function startPhpApp() { 12 | const result = await serveApp( 13 | state.randomSecret, 14 | state.electronApiPort, 15 | state.phpIni 16 | ); 17 | 18 | state.phpPort = result.port; 19 | 20 | await appendCookie(); 21 | 22 | return result.process; 23 | } 24 | 25 | export function runScheduler() { 26 | startScheduler(state.randomSecret, state.electronApiPort, state.phpIni); 27 | } 28 | 29 | export function startAPI(): Promise { 30 | return startAPIServer(state.randomSecret); 31 | } 32 | 33 | export { retrieveNativePHPConfig, retrievePhpIniSettings }; 34 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/state.ts: -------------------------------------------------------------------------------- 1 | import {BrowserWindow, Tray, UtilityProcess} from "electron"; 2 | import Store from "electron-store"; 3 | import { notifyLaravel } from "./utils.js"; 4 | 5 | const settingsStore = new Store(); 6 | settingsStore.onDidAnyChange((newValue, oldValue) => { 7 | // Only notify of the changed key/value pair 8 | const changedKey = Object.keys(newValue).find( 9 | (key) => newValue[key] !== oldValue[key] 10 | ); 11 | 12 | if (changedKey) { 13 | notifyLaravel("events", { 14 | event: "Native\\Laravel\\Events\\Settings\\SettingChanged", 15 | payload: { 16 | key: changedKey, 17 | value: newValue[changedKey] || null, 18 | }, 19 | }); 20 | } 21 | }); 22 | 23 | interface State { 24 | electronApiPort: number | null; 25 | activeMenuBar: any; 26 | tray: Tray | null; 27 | php: string | null; 28 | phpPort: number | null; 29 | phpIni: any; 30 | caCert: string | null; 31 | icon: string | null; 32 | processes: Record}>; 33 | windows: Record; 34 | randomSecret: string; 35 | store: Store; 36 | findWindow: (id: string) => BrowserWindow | null; 37 | dockBounce: number; 38 | } 39 | 40 | function generateRandomString(length: number) { 41 | let result = ""; 42 | const characters = 43 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 44 | const charactersLength = characters.length; 45 | 46 | for (let i = 0; i < length; i += 1) { 47 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 48 | } 49 | 50 | return result; 51 | } 52 | 53 | export default { 54 | electronApiPort: null, 55 | activeMenuBar: null, 56 | tray: null, 57 | php: null, 58 | phpPort: null, 59 | phpIni: null, 60 | caCert: null, 61 | icon: null, 62 | store: settingsStore, 63 | randomSecret: generateRandomString(32), 64 | processes: {}, 65 | windows: {}, 66 | findWindow(id: string) { 67 | return this.windows[id] || null; 68 | }, 69 | } as State; 70 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/src/server/utils.ts: -------------------------------------------------------------------------------- 1 | import { session } from 'electron'; 2 | import state from './state.js'; 3 | import axios from 'axios'; 4 | 5 | export async function appendCookie() { 6 | const cookie = { 7 | url: `http://localhost:${state.phpPort}`, 8 | name: "_php_native", 9 | value: state.randomSecret, 10 | }; 11 | 12 | await session.defaultSession.cookies.set(cookie); 13 | } 14 | 15 | export async function notifyLaravel(endpoint: string, payload = {}) { 16 | if (endpoint === 'events') { 17 | broadcastToWindows('native-event', payload); 18 | } 19 | 20 | try { 21 | await axios.post( 22 | `http://127.0.0.1:${state.phpPort}/_native/api/${endpoint}`, 23 | payload, 24 | { 25 | headers: { 26 | "X-NativePHP-Secret": state.randomSecret, 27 | }, 28 | } 29 | ); 30 | } catch (e) { 31 | // 32 | } 33 | } 34 | 35 | export function broadcastToWindows(event, payload) { 36 | Object.values(state.windows).forEach(window => { 37 | window.webContents.send(event, payload); 38 | }); 39 | 40 | if (state.activeMenuBar?.window) { 41 | state.activeMenuBar.window.webContents.send(event, payload); 42 | } 43 | } 44 | 45 | /** 46 | * Remove null and undefined values from an object 47 | */ 48 | export function trimOptions(options: any): any { 49 | Object.keys(options).forEach(key => options[key] == null && delete options[key]); 50 | 51 | return options; 52 | } 53 | 54 | export function appendWindowIdToUrl(url, id) { 55 | return url + (url.indexOf('?') === -1 ? '?' : '&') + '_windowId=' + id; 56 | } 57 | 58 | export function goToUrl(url, windowId) { 59 | state.windows[windowId]?.loadURL(appendWindowIdToUrl(url, windowId)); 60 | } 61 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/tests/api.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 2 | import startAPIServer, { APIProcess } from "../src/server/api"; 3 | import axios from "axios"; 4 | 5 | vi.mock('electron-updater', () => ({ 6 | autoUpdater: { 7 | checkForUpdates: vi.fn(), 8 | quitAndInstall: vi.fn(), 9 | addListener: vi.fn(), 10 | }, 11 | })); 12 | 13 | let apiServer: APIProcess; 14 | 15 | describe('API test', () => { 16 | beforeEach(async () => { 17 | vi.resetModules(); 18 | apiServer = await startAPIServer('randomSecret'); 19 | axios.defaults.baseURL = `http://localhost:${apiServer.port}`; 20 | }); 21 | 22 | afterEach(async () => { 23 | await new Promise((resolve) => { 24 | apiServer.server.close(() => resolve()); 25 | }); 26 | }); 27 | 28 | it('starts API server on port 4000', async () => { 29 | expect(apiServer.port).toBe(4000); 30 | }); 31 | 32 | it('uses the next available API port', async () => { 33 | const nextApiProcess = await startAPIServer('randomSecret'); 34 | expect(nextApiProcess.port).toBe(apiServer.port + 1); 35 | 36 | nextApiProcess.server.close(); 37 | }); 38 | 39 | it('protects API endpoints with a secret', async () => { 40 | try { 41 | await axios.get('/api/process'); 42 | } catch (error) { 43 | expect(error.response.status).toBe(403); 44 | } 45 | 46 | let response; 47 | try { 48 | response = await axios.get('/api/process', { 49 | headers: { 50 | 'x-nativephp-secret': 'randomSecret', 51 | } 52 | }); 53 | } finally { 54 | expect(response.status).toBe(200); 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/tests/mocking.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import {mockForNodeRequire} from "vitest-mock-commonjs"; 3 | 4 | vi.mock('electron'); 5 | 6 | import electron, { app, powerMonitor } from 'electron'; // Import the module you want to test 7 | 8 | describe('My Electron Module Tests', () => { 9 | // it('should return an object for electron', () => { 10 | // expect(typeof electron).toBe('object'); 11 | // }); 12 | 13 | it('should return the correct path', () => { 14 | const path = app.getPath('user'); // Call the mocked method 15 | expect(path).toBe('path'); // Assert the return value 16 | }); 17 | 18 | it('should check if the app is packed', async () => { 19 | const isPackaged = app.isPackaged; // Call the mocked method 20 | expect(isPackaged).toBe(false); // Assert the return value 21 | }); 22 | 23 | it('should add a listener to power monitor', () => { 24 | const callback = vi.fn(); 25 | powerMonitor.addListener('some-event', callback); // Call the mocked method 26 | 27 | // Verify that addListener was called with the correct parameters 28 | expect(powerMonitor.addListener).toHaveBeenCalledWith('some-event', callback); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // vitest-setup.ts 2 | 3 | import { vi } from 'vitest'; 4 | import { mockForNodeRequire } from "vitest-mock-commonjs" 5 | import express from 'express'; 6 | 7 | // Mock electron 8 | mockForNodeRequire('electron', () => ({ 9 | app: { 10 | getPath: vi.fn(), 11 | on: vi.fn(), 12 | quit: vi.fn(), 13 | getName: vi.fn(), 14 | getVersion: vi.fn(), 15 | focus: vi.fn(), 16 | hide: vi.fn(), 17 | dock: { 18 | setMenu: vi.fn(), 19 | show: vi.fn(), 20 | hide: vi.fn(), 21 | setBadge: vi.fn(), 22 | bounce: vi.fn(), 23 | cancelBounce: vi.fn(), 24 | }, 25 | isPackaged: false, 26 | getAppPath: vi.fn().mockReturnValue('/fake/app/path'), 27 | }, 28 | clipboard: { 29 | writeText: vi.fn(), 30 | readText: vi.fn(), 31 | writeImage: vi.fn(), 32 | readImage: vi.fn(), 33 | }, 34 | BrowserWindow: vi.fn().mockImplementation(() => ({ 35 | loadURL: vi.fn(), 36 | loadFile: vi.fn(), 37 | on: vi.fn(), 38 | setMenu: vi.fn(), 39 | setMenuBarVisibility: vi.fn(), 40 | webContents: { 41 | on: vi.fn(), 42 | send: vi.fn(), 43 | }, 44 | show: vi.fn(), 45 | hide: vi.fn(), 46 | close: vi.fn(), 47 | maximize: vi.fn(), 48 | minimize: vi.fn(), 49 | restore: vi.fn(), 50 | isMaximized: vi.fn(), 51 | isMinimized: vi.fn(), 52 | setProgressBar: vi.fn(), 53 | })), 54 | ipcMain: { 55 | on: vi.fn(), 56 | handle: vi.fn(), 57 | removeHandler: vi.fn(), 58 | }, 59 | screen: { 60 | getPrimaryDisplay: vi.fn().mockReturnValue({ 61 | id: 1, 62 | bounds: { x: 0, y: 0, width: 1920, height: 1080 }, 63 | workArea: { x: 0, y: 0, width: 1920, height: 1040 }, 64 | scaleFactor: 1, 65 | rotation: 0, 66 | }), 67 | getAllDisplays: vi.fn().mockReturnValue([{ 68 | id: 1, 69 | bounds: { x: 0, y: 0, width: 1920, height: 1080 }, 70 | workArea: { x: 0, y: 0, width: 1920, height: 1040 }, 71 | scaleFactor: 1, 72 | rotation: 0, 73 | }]), 74 | }, 75 | 76 | dialog: { 77 | showOpenDialogSync: vi.fn(() => ['open dialog result']), 78 | showSaveDialogSync: vi.fn(() => ['save dialog result']), 79 | showMessageBoxSync: vi.fn(() => 1), 80 | showErrorBox: vi.fn(), 81 | }, 82 | globalShortcut: { 83 | register: vi.fn(), 84 | unregister: vi.fn(), 85 | isRegistered: vi.fn(), 86 | unregisterAll: vi.fn(), 87 | }, 88 | Notification: vi.fn().mockImplementation(() => ({ 89 | show: vi.fn(), 90 | on: vi.fn(), 91 | })), 92 | Menu: { 93 | buildFromTemplate: vi.fn().mockReturnValue({ 94 | popup: vi.fn(), 95 | closePopup: vi.fn(), 96 | append: vi.fn(), 97 | insert: vi.fn(), 98 | getMenuItemById: vi.fn(), 99 | }), 100 | setApplicationMenu: vi.fn(), 101 | getApplicationMenu: vi.fn(), 102 | }, 103 | Tray: vi.fn().mockImplementation(() => ({ 104 | setContextMenu: vi.fn(), 105 | on: vi.fn(), 106 | setImage: vi.fn(), 107 | setToolTip: vi.fn(), 108 | destroy: vi.fn(), 109 | })), 110 | MenuItem: vi.fn(), 111 | shell: { 112 | openExternal: vi.fn().mockResolvedValue(true), 113 | openPath: vi.fn().mockResolvedValue({ success: true }), 114 | showItemInFolder: vi.fn(), 115 | }, 116 | powerMonitor: { 117 | on: vi.fn(), 118 | addListener: vi.fn(), 119 | removeAllListeners: vi.fn(), 120 | }, 121 | nativeTheme: { 122 | on: vi.fn(), 123 | shouldUseDarkColors: false, 124 | }, 125 | safeStorage: { 126 | encryptString: vi.fn((str) => Buffer.from(str).toString('base64')), 127 | decryptString: vi.fn((buffer) => Buffer.from(buffer, 'base64').toString()), 128 | }, 129 | })); 130 | 131 | // Mock @electron/remote 132 | vi.mock('@electron/remote', () => ({ 133 | initialize: vi.fn(), 134 | enable: vi.fn(), 135 | getCurrentWindow: vi.fn().mockReturnValue({ 136 | loadURL: vi.fn(), 137 | loadFile: vi.fn(), 138 | on: vi.fn(), 139 | setMenu: vi.fn(), 140 | show: vi.fn(), 141 | hide: vi.fn(), 142 | close: vi.fn(), 143 | maximize: vi.fn(), 144 | minimize: vi.fn(), 145 | restore: vi.fn(), 146 | isMaximized: vi.fn(), 147 | isMinimized: vi.fn(), 148 | setProgressBar: vi.fn(), 149 | webContents: { 150 | on: vi.fn(), 151 | send: vi.fn(), 152 | }, 153 | }), 154 | app: { 155 | getPath: vi.fn(), 156 | getName: vi.fn(), 157 | getVersion: vi.fn(), 158 | getAppPath: vi.fn().mockReturnValue('/fake/app/path'), 159 | }, 160 | dialog: { 161 | showOpenDialog: vi.fn().mockResolvedValue({ filePaths: [] }), 162 | showSaveDialog: vi.fn().mockResolvedValue({ filePath: '' }), 163 | }, 164 | })); 165 | 166 | // Mock electron-store with onDidAnyChange method 167 | vi.mock('electron-store', () => { 168 | return { 169 | default: vi.fn().mockImplementation(() => ({ 170 | get: vi.fn(), 171 | set: vi.fn(), 172 | has: vi.fn(), 173 | delete: vi.fn(), 174 | clear: vi.fn(), 175 | onDidAnyChange: vi.fn().mockImplementation(callback => { 176 | // Return an unsubscribe function 177 | return () => {}; 178 | }), 179 | store: {}, 180 | path: '/fake/path/to/store.json', 181 | })), 182 | }; 183 | }); 184 | 185 | // Create empty router mocks for all API routes 186 | const createRouterMock = () => { 187 | const router = express.Router(); 188 | return { default: router }; 189 | }; 190 | 191 | // Mock individual route files directly to avoid the @electron/remote issue 192 | // vi.mock('../src/server/api/clipboard.js', () => createRouterMock()); 193 | // vi.mock('../src/server/api/app.js', () => createRouterMock()); 194 | // vi.mock('../src/server/api/screen.js', () => createRouterMock()); 195 | // vi.mock('../src/server/api/dialog.js', () => createRouterMock()); 196 | // vi.mock('../src/server/api/debug.js', () => createRouterMock()); 197 | // vi.mock('../src/server/api/broadcasting.js', () => createRouterMock()); 198 | // vi.mock('../src/server/api/system.js', () => createRouterMock()); 199 | // vi.mock('../src/server/api/globalShortcut.js', () => createRouterMock()); 200 | // vi.mock('../src/server/api/notification.js', () => createRouterMock()); 201 | // vi.mock('../src/server/api/dock.js', () => createRouterMock()); 202 | // vi.mock('../src/server/api/menu.js', () => createRouterMock()); 203 | vi.mock('../src/server/api/menuBar.js', () => createRouterMock()); 204 | vi.mock('../src/server/api/window.js', () => createRouterMock()); 205 | // vi.mock('../src/server/api/process.js', () => createRouterMock()); 206 | vi.mock('../src/server/api/contextMenu.js', () => createRouterMock()); 207 | // vi.mock('../src/server/api/settings.js', () => createRouterMock()); 208 | // vi.mock('../src/server/api/shell.js', () => createRouterMock()); 209 | // vi.mock('../src/server/api/progressBar.js', () => createRouterMock()); 210 | vi.mock('../src/server/api/powerMonitor.js', () => createRouterMock()); 211 | vi.mock('../src/server/api/childProcess.js', () => createRouterMock()); 212 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it, vi} from 'vitest'; 2 | import {notifyLaravel} from "../src/server/utils"; 3 | import state from "../src/server/state"; 4 | import axios from "axios"; 5 | 6 | vi.mock('axios'); 7 | vi.mock('electron-store'); 8 | 9 | describe('Utils test', () => { 10 | 11 | it('notifies laravel', async () => { 12 | state.phpPort = 8000; 13 | state.randomSecret = 'i-am-secret'; 14 | 15 | await notifyLaravel('endpoint', {payload: 'payload'}); 16 | 17 | expect(axios.post).toHaveBeenCalledWith( 18 | `http://127.0.0.1:8000/_native/api/endpoint`, 19 | {payload: 'payload'}, 20 | { 21 | headers: { 22 | "X-NativePHP-Secret": 'i-am-secret', 23 | } 24 | } 25 | ); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "node16", 5 | "moduleResolution": "node16", 6 | "esModuleInterop": true, 7 | "outDir": "./dist", 8 | "sourceMap": false, 9 | "target": "ES6", 10 | "noImplicitAny": false, 11 | "preserveConstEnums": true, 12 | "removeComments": true, 13 | "typeRoots": [ 14 | "../node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2019", 18 | "dom" 19 | ], 20 | "types": [ 21 | "node", 22 | ] 23 | }, 24 | "include": [ 25 | "./src/**/*.ts", 26 | "./src/**/*.mts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/electron-plugin/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: ['./tests/setup.ts'], 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /resources/js/electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; 3 | 4 | export default defineConfig({ 5 | main: { 6 | build: { 7 | rollupOptions: { 8 | plugins: [ 9 | { 10 | name: 'watch-external', 11 | buildStart() { 12 | this.addWatchFile(join(process.env.APP_PATH, 'app', 'Providers', 'NativeAppServiceProvider.php')); 13 | } 14 | } 15 | ] 16 | }, 17 | }, 18 | plugins: [externalizeDepsPlugin()] 19 | }, 20 | preload: { 21 | plugins: [externalizeDepsPlugin()] 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /resources/js/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import eslintPluginUnicorn from "eslint-plugin-unicorn"; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | { 10 | files: ["**/*.{js,mjs,cjs,ts}"], 11 | }, 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.builtin, 16 | ...globals.browser, 17 | ...globals.node, 18 | }, 19 | }, 20 | }, 21 | pluginJs.configs.recommended, 22 | ...tseslint.configs.recommended, 23 | eslintPluginPrettierRecommended, 24 | // { 25 | // languageOptions: { 26 | // globals: globals.builtin, 27 | // }, 28 | // plugins: { 29 | // unicorn: eslintPluginUnicorn, 30 | // }, 31 | // rules: { 32 | // 'unicorn/prefer-module': 'error', 33 | // }, 34 | // }, 35 | ]; 36 | -------------------------------------------------------------------------------- /resources/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NativePHP", 3 | "version": "1.0.0", 4 | "description": "A NativePHP Electron application", 5 | "main": "./out/main/index.js", 6 | "author": "NativePHP", 7 | "homepage": "https://nativephp.com", 8 | "type": "module", 9 | "engines": { 10 | "node": ">=22.0.0" 11 | }, 12 | "scripts": { 13 | "format": "prettier --write .", 14 | "lint": "eslint . --fix", 15 | "start": "electron-vite preview", 16 | "dev": "cross-env node php.js && electron-vite dev --watch", 17 | "build": "electron-vite build", 18 | "postinstall": "node ./node_modules/electron-builder/cli.js install-app-deps", 19 | "publish:all": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js --win --mac --linux --config --x64 --arm64 -p always", 20 | "publish:win": "cross-env npm run publish:win-x64", 21 | "publish:win-x64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p always --win --config --x64", 22 | "publish:mac": "cross-env npm run publish:mac-arm64 -- --x64", 23 | "publish:mac-arm64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p always --mac --config --arm64 -p always", 24 | "publish:mac-x86": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p always --mac --config --x64 -p always", 25 | "publish:linux": "cross-env npm run publish:linux-x64", 26 | "publish:linux-x64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js --linux --config --x64 -p always", 27 | "build:all": "cross-env npm run build:mac && cross-env npm run build:win && cross-env npm run build:linux", 28 | "build:win": "cross-env npm run build:win-x64", 29 | "build:win-x64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p never --win --config --x64", 30 | "build:mac": "cross-env npm run build:mac-arm64 -- --x64", 31 | "build:mac-arm64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p never --mac --config --arm64", 32 | "build:mac-x86": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p never --mac --config --x64", 33 | "build:linux": "cross-env npm run build:linux-x64", 34 | "build:linux-x64": "cross-env npm run build && cross-env node ./node_modules/electron-builder/cli.js -p never --linux --config --x64", 35 | "plugin:build": "rimraf electron-plugin/dist/ && node node_modules/typescript/bin/tsc --project electron-plugin", 36 | "plugin:build:watch": "npm run plugin:build -- -W", 37 | "plugin:test": "vitest run --root electron-plugin --coverage.all", 38 | "plugin:test:watch": "vitest watch --root electron-plugin --coverage.all" 39 | }, 40 | "dependencies": { 41 | "@electron-toolkit/preload": "^3.0.1", 42 | "@electron-toolkit/utils": "^4.0.0", 43 | "@electron/remote": "^2.1.2", 44 | "axios": "^1.7.9", 45 | "body-parser": "^1.20.3", 46 | "electron-context-menu": "^4.0.4", 47 | "electron-store": "^10.0.0", 48 | "electron-updater": "^6.3.9", 49 | "electron-window-state": "^5.0.3", 50 | "express": "^4.21.2", 51 | "fs-extra": "^11.2.0", 52 | "get-port": "^7.1.0", 53 | "kill-sync": "^1.0.3", 54 | "menubar": "^9.5.1", 55 | "nodemon": "^3.1.9", 56 | "ps-node": "^0.1.6", 57 | "tree-kill": "^1.2.2", 58 | "yauzl": "^3.2.0" 59 | }, 60 | "devDependencies": { 61 | "@babel/plugin-proposal-decorators": "^7.25.9", 62 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9", 63 | "@babel/plugin-proposal-function-sent": "^7.25.9", 64 | "@babel/plugin-proposal-numeric-separator": "^7.18.6", 65 | "@babel/plugin-proposal-throw-expressions": "^7.25.9", 66 | "@babel/plugin-transform-object-assign": "^7.25.9", 67 | "@babel/preset-env": "^7.26.0", 68 | "@babel/preset-typescript": "^7.26.0", 69 | "@electron/notarize": "^3.0.0", 70 | "@eslint/js": "^9.17.0", 71 | "@rushstack/eslint-patch": "^1.10.4", 72 | "@types/express": "^5.0.0", 73 | "@types/node": "^22.10.2", 74 | "@types/ps-node": "^0.1.3", 75 | "@typescript-eslint/eslint-plugin": "^8.18.1", 76 | "@typescript-eslint/parser": "^8.18.1", 77 | "@vue/eslint-config-prettier": "^10.1.0", 78 | "cross-env": "^7.0.3", 79 | "electron": "^32.2.7", 80 | "electron-builder": "^26.0.14", 81 | "electron-chromedriver": "^32.2.6", 82 | "electron-vite": "^3.0.0", 83 | "eslint": "^9.17.0", 84 | "eslint-config-prettier": "^10.0.0", 85 | "eslint-plugin-prettier": "^5.2.1", 86 | "eslint-plugin-unicorn": "^59.0.0", 87 | "globals": "^16.0.0", 88 | "less": "^4.2.1", 89 | "prettier": "^3.4.2", 90 | "rimraf": "^6.0.1", 91 | "stylelint": "^16.12.0", 92 | "stylelint-config-recommended": "^16.0.0", 93 | "stylelint-config-sass-guidelines": "^12.1.0", 94 | "ts-node": "^10.9.2", 95 | "tslib": "^2.8.1", 96 | "typescript": "^5.7.2", 97 | "typescript-eslint": "^8.18.1", 98 | "vite": "^6.2.1", 99 | "vitest": "^3.0.0", 100 | "vitest-mock-commonjs": "^1.0.2" 101 | }, 102 | "exports": "./electron-plugin/dist/index.js", 103 | "imports": { 104 | "#plugin": "./electron-plugin/dist/index.js" 105 | }, 106 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 107 | } 108 | -------------------------------------------------------------------------------- /resources/js/php.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import fs_extra from 'fs-extra'; 3 | const { copySync, removeSync, ensureDirSync } = fs_extra; 4 | import { join } from "path"; 5 | import unzip from "yauzl"; 6 | 7 | 8 | const isBuilding = Boolean(process.env.NATIVEPHP_BUILDING); 9 | const phpBinaryPath = process.env.NATIVEPHP_PHP_BINARY_PATH; 10 | const phpVersion = process.env.NATIVEPHP_PHP_BINARY_VERSION; 11 | 12 | // Differentiates for Serving and Building 13 | const isArm64 = isBuilding ? process.argv.includes('--arm64') : process.arch.includes('arm64'); 14 | const isWindows = isBuilding ? process.argv.includes('--win') : process.platform.includes('win32'); 15 | const isLinux = isBuilding ? process.argv.includes('--linux') : process.platform.includes('linux'); 16 | const isDarwin = isBuilding ? process.argv.includes('--mac') : process.platform.includes('darwin'); 17 | 18 | // false because string mapping is done in is{OS} checks 19 | const platform = { 20 | os: false, 21 | arch: false, 22 | phpBinary: 'php' 23 | }; 24 | 25 | if (isWindows) { 26 | platform.os = 'win'; 27 | platform.phpBinary += '.exe'; 28 | platform.arch = 'x64'; 29 | } 30 | 31 | if (isLinux) { 32 | platform.os = 'linux'; 33 | platform.arch = 'x64'; 34 | } 35 | 36 | if (isDarwin) { 37 | platform.os = 'mac'; 38 | platform.arch = 'x86'; 39 | } 40 | 41 | if (isArm64) { 42 | platform.arch = 'arm64'; 43 | } 44 | 45 | // isBuilding overwrites platform to the desired architecture 46 | if (isBuilding) { 47 | // Only one will be used by the configured build commands in package.json 48 | platform.arch = process.argv.includes('--x64') ? 'x64' : platform.arch; 49 | platform.arch = process.argv.includes('--x86') ? 'x86' : platform.arch; 50 | platform.arch = process.argv.includes('--arm64') ? 'arm64' : platform.arch; 51 | } 52 | 53 | const phpVersionZip = 'php-' + phpVersion + '.zip'; 54 | const binarySrcDir = join(phpBinaryPath, platform.os, platform.arch, phpVersionZip); 55 | const binaryDestDir = join(import.meta.dirname, 'resources/php'); 56 | 57 | console.log('Binary Source: ', binarySrcDir); 58 | console.log('Binary Filename: ', platform.phpBinary); 59 | console.log('PHP version: ' + phpVersion); 60 | 61 | if (platform.phpBinary) { 62 | try { 63 | console.log('Unzipping PHP binary from ' + binarySrcDir + ' to ' + binaryDestDir); 64 | removeSync(binaryDestDir); 65 | 66 | ensureDirSync(binaryDestDir); 67 | 68 | // Unzip the files 69 | unzip.open(binarySrcDir, {lazyEntries: true}, function (err, zipfile) { 70 | if (err) throw err; 71 | zipfile.readEntry(); 72 | zipfile.on("entry", function (entry) { 73 | zipfile.openReadStream(entry, function (err, readStream) { 74 | if (err) throw err; 75 | 76 | const binaryPath = join(binaryDestDir, platform.phpBinary); 77 | const writeStream = fs.createWriteStream(binaryPath); 78 | 79 | readStream.pipe(writeStream); 80 | 81 | writeStream.on("close", function() { 82 | console.log('Copied PHP binary to ', binaryPath); 83 | 84 | // Add execute permissions 85 | fs.chmod(binaryPath, 0o755, (err) => { 86 | if (err) { 87 | console.log(`Error setting permissions: ${err}`); 88 | } 89 | }); 90 | 91 | zipfile.readEntry(); 92 | }); 93 | }); 94 | }); 95 | }); 96 | } catch (e) { 97 | console.error('Error copying PHP binary', e); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /resources/js/resources/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !icon.png -------------------------------------------------------------------------------- /resources/js/resources/IconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativePHP/electron/82a8a51e0aa80d0962ee5e04b36b3b2c0ba8701a/resources/js/resources/IconTemplate.png -------------------------------------------------------------------------------- /resources/js/resources/IconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativePHP/electron/82a8a51e0aa80d0962ee5e04b36b3b2c0ba8701a/resources/js/resources/IconTemplate@2x.png -------------------------------------------------------------------------------- /resources/js/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NativePHP/electron/82a8a51e0aa80d0962ee5e04b36b3b2c0ba8701a/resources/js/resources/icon.png -------------------------------------------------------------------------------- /resources/js/src/main/index.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron' 2 | import NativePHP from '#plugin' 3 | import path from 'path' 4 | import defaultIcon from '../../resources/icon.png?asset&asarUnpack' 5 | import certificate from '../../resources/cacert.pem?asset&asarUnpack' 6 | 7 | let phpBinary = process.platform === 'win32' ? 'php.exe' : 'php'; 8 | 9 | phpBinary = path.join(import.meta.dirname, '../../resources/php', phpBinary).replace("app.asar", "app.asar.unpacked"); 10 | 11 | /** 12 | * Turn on the lights for the NativePHP app. 13 | */ 14 | NativePHP.bootstrap( 15 | app, 16 | defaultIcon, 17 | phpBinary, 18 | certificate 19 | ); 20 | -------------------------------------------------------------------------------- /resources/js/src/preload/index.js: -------------------------------------------------------------------------------- 1 | import { electronAPI } from '@electron-toolkit/preload' 2 | import * as remote from '@electron/remote/index.js' 3 | 4 | window.electron = electronAPI 5 | window.remote = remote; 6 | -------------------------------------------------------------------------------- /src/Commands/BuildCommand.php: -------------------------------------------------------------------------------- 1 | buildOS = $this->selectOs($this->argument('os')); 58 | 59 | $this->buildCommand = 'build'; 60 | if ($this->buildOS != 'all') { 61 | $arch = $this->selectArchitectureForOs($this->buildOS, $this->argument('arch')); 62 | 63 | $this->buildOS .= $arch != 'all' ? "-{$arch}" : ''; 64 | } 65 | 66 | if ($this->option('publish')) { 67 | $this->buildCommand = 'publish'; 68 | } 69 | 70 | if ($this->hasBundled()) { 71 | $this->buildBundle(); 72 | } else { 73 | $this->warnUnsecureBuild(); 74 | $this->buildUnsecure(); 75 | } 76 | } 77 | 78 | private function buildBundle(): void 79 | { 80 | $this->setAppNameAndVersion(); 81 | 82 | $this->updateElectronDependencies(); 83 | 84 | $this->newLine(); 85 | intro('Copying Bundle to build directory...'); 86 | $this->copyBundleToBuildDirectory(); 87 | $this->keepRequiredDirectories(); 88 | 89 | $this->newLine(); 90 | $this->copyCertificateAuthorityCertificate(); 91 | 92 | $this->newLine(); 93 | intro('Copying app icons...'); 94 | $this->installIcon(); 95 | 96 | $this->buildOrPublish(); 97 | } 98 | 99 | private function buildUnsecure(): void 100 | { 101 | $this->preProcess(); 102 | 103 | $this->setAppNameAndVersion(); 104 | 105 | $this->updateElectronDependencies(); 106 | 107 | $this->newLine(); 108 | intro('Copying App to build directory...'); 109 | $this->copyToBuildDirectory(); 110 | 111 | $this->newLine(); 112 | $this->copyCertificateAuthorityCertificate(); 113 | 114 | $this->newLine(); 115 | intro('Cleaning .env file...'); 116 | $this->cleanEnvFile(); 117 | 118 | $this->newLine(); 119 | intro('Copying app icons...'); 120 | $this->installIcon(); 121 | 122 | $this->newLine(); 123 | intro('Pruning vendor directory'); 124 | $this->pruneVendorDirectory(); 125 | 126 | $this->buildOrPublish(); 127 | 128 | $this->postProcess(); 129 | } 130 | 131 | protected function getEnvironmentVariables(): array 132 | { 133 | return array_merge( 134 | [ 135 | 'APP_PATH' => $this->sourcePath(), 136 | 'APP_URL' => config('app.url'), 137 | 'NATIVEPHP_BUILDING' => true, 138 | 'NATIVEPHP_PHP_BINARY_VERSION' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, 139 | 'NATIVEPHP_PHP_BINARY_PATH' => $this->sourcePath($this->phpBinaryPath()), 140 | 'NATIVEPHP_APP_NAME' => config('app.name'), 141 | 'NATIVEPHP_APP_ID' => config('nativephp.app_id'), 142 | 'NATIVEPHP_APP_VERSION' => config('nativephp.version'), 143 | 'NATIVEPHP_APP_COPYRIGHT' => config('nativephp.copyright'), 144 | 'NATIVEPHP_APP_FILENAME' => Str::slug(config('app.name')), 145 | 'NATIVEPHP_APP_AUTHOR' => config('nativephp.author'), 146 | 'NATIVEPHP_UPDATER_CONFIG' => json_encode(Updater::builderOptions()), 147 | 'NATIVEPHP_DEEPLINK_SCHEME' => config('nativephp.deeplink_scheme'), 148 | // Notarization 149 | 'NATIVEPHP_APPLE_ID' => config('nativephp-internal.notarization.apple_id'), 150 | 'NATIVEPHP_APPLE_ID_PASS' => config('nativephp-internal.notarization.apple_id_pass'), 151 | 'NATIVEPHP_APPLE_TEAM_ID' => config('nativephp-internal.notarization.apple_team_id'), 152 | ], 153 | Updater::environmentVariables(), 154 | ); 155 | } 156 | 157 | private function updateElectronDependencies(): void 158 | { 159 | $this->newLine(); 160 | intro('Updating Electron dependencies...'); 161 | Process::path(__DIR__.'/../../resources/js/') 162 | ->env($this->getEnvironmentVariables()) 163 | ->forever() 164 | ->run('npm ci', function (string $type, string $output) { 165 | echo $output; 166 | }); 167 | } 168 | 169 | private function buildOrPublish(): void 170 | { 171 | $this->newLine(); 172 | intro((($this->buildCommand == 'publish') ? 'Publishing' : 'Building')." for {$this->buildOS}"); 173 | Process::path(__DIR__.'/../../resources/js/') 174 | ->env($this->getEnvironmentVariables()) 175 | ->forever() 176 | ->tty(SymfonyProcess::isTtySupported() && ! $this->option('no-interaction')) 177 | ->run("npm run {$this->buildCommand}:{$this->buildOS}", function (string $type, string $output) { 178 | echo $output; 179 | }); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Commands/DevelopCommand.php: -------------------------------------------------------------------------------- 1 | option('no-dependencies')) { 32 | $this->installNPMDependencies( 33 | force: true, 34 | installer: $this->option('installer'), 35 | withoutInteraction: $this->option('no-interaction') 36 | ); 37 | } 38 | 39 | note('Starting NativePHP app'); 40 | 41 | if (PHP_OS_FAMILY === 'Darwin') { 42 | $this->patchPlist(); 43 | } 44 | 45 | $this->setAppNameAndVersion(developmentMode: true); 46 | 47 | $this->installIcon(); 48 | 49 | $this->copyCertificateAuthorityCertificate(); 50 | 51 | $this->runDeveloper( 52 | installer: $this->option('installer'), 53 | skip_queue: $this->option('no-queue'), 54 | withoutInteraction: $this->option('no-interaction') 55 | ); 56 | } 57 | 58 | /** 59 | * Patch Electron's Info.plist to show the correct app name 60 | * during development. 61 | */ 62 | protected function patchPlist(): void 63 | { 64 | $pList = file_get_contents(__DIR__.'/../../resources/js/node_modules/electron/dist/Electron.app/Contents/Info.plist'); 65 | 66 | // Change the CFBundleName to the correct app name 67 | $pattern = '/(CFBundleName<\/key>\s+)(.*?)(<\/string>)/m'; 68 | $pList = preg_replace($pattern, '$1'.config('app.name').'$3', $pList); 69 | 70 | $pattern = '/(CFBundleDisplayName<\/key>\s+)(.*?)(<\/string>)/m'; 71 | $pList = preg_replace($pattern, '$1'.config('app.name').'$3', $pList); 72 | 73 | file_put_contents(__DIR__.'/../../resources/js/node_modules/electron/dist/Electron.app/Contents/Info.plist', $pList); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | option('no-interaction'); 30 | 31 | $this->call('vendor:publish', ['--tag' => 'nativephp-provider']); 32 | $this->call('vendor:publish', ['--tag' => 'nativephp-config']); 33 | 34 | $this->installComposerScript(); 35 | 36 | $installer = $this->getInstaller($this->option('installer')); 37 | 38 | $this->installNPMDependencies( 39 | force: $this->option('force'), 40 | installer: $installer, 41 | withoutInteraction: $withoutInteraction 42 | ); 43 | 44 | $shouldPromptForServe = ! $withoutInteraction && ! $this->option('force'); 45 | 46 | if ($shouldPromptForServe && confirm('Would you like to start the NativePHP development server', false)) { 47 | $this->call('native:serve', [ 48 | '--installer' => $installer, 49 | '--no-dependencies', 50 | '--no-interaction' => $withoutInteraction, 51 | ]); 52 | } 53 | 54 | outro('NativePHP scaffolding installed successfully.'); 55 | } 56 | 57 | private function installComposerScript() 58 | { 59 | info('Installing `composer native:dev` script alias...'); 60 | 61 | $composer = json_decode(file_get_contents(base_path('composer.json'))); 62 | throw_unless($composer, RuntimeException::class, "composer.json couldn't be parsed"); 63 | 64 | $composerScripts = $composer->scripts ?? (object) []; 65 | 66 | if ($composerScripts->{'native:dev'} ?? false) { 67 | note('native:dev script already installed... skipping.'); 68 | 69 | return; 70 | } 71 | 72 | $composerScripts->{'native:dev'} = [ 73 | 'Composer\\Config::disableProcessTimeout', 74 | 'npx concurrently -k -c "#93c5fd,#c4b5fd" "php artisan native:serve --no-interaction" "npm run dev" --names=app,vite', 75 | ]; 76 | 77 | data_set($composer, 'scripts', $composerScripts); 78 | 79 | file_put_contents( 80 | base_path('composer.json'), 81 | json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL 82 | ); 83 | 84 | note('native:dev script installed!'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | info('Building and publishing NativePHP app…'); 24 | 25 | $os = $this->selectOs($this->argument('os')); 26 | 27 | $arch = null; 28 | 29 | if ($os != 'all') { 30 | $arch = $this->selectArchitectureForOs($os, $this->argument('arch')); 31 | } 32 | 33 | Artisan::call('native:build', ['os' => $os, 'arch' => $arch, '--publish' => true], $this->output); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/ResetCommand.php: -------------------------------------------------------------------------------- 1 | exists($nativeServeResourcePath)) { 28 | $this->line('Clearing: '.$nativeServeResourcePath); 29 | $filesystem->remove($nativeServeResourcePath); 30 | $filesystem->mkdir($nativeServeResourcePath); 31 | } 32 | 33 | // Removing the bundling directories 34 | $bundlingPath = base_path('build/'); 35 | $this->line('Clearing: '.$bundlingPath); 36 | 37 | if ($filesystem->exists($bundlingPath)) { 38 | $filesystem->remove($bundlingPath); 39 | } 40 | 41 | // Removing the built path 42 | $builtPath = base_path('dist/'); 43 | $this->line('Clearing: '.$builtPath); 44 | 45 | if ($filesystem->exists($builtPath)) { 46 | $filesystem->remove($builtPath); 47 | } 48 | 49 | if ($this->option('with-app-data')) { 50 | 51 | foreach ([true, false] as $developmentMode) { 52 | $appName = $this->setAppNameAndVersion($developmentMode); 53 | 54 | // Eh, just in case, I don't want to delete all user data by accident. 55 | if (! empty($appName)) { 56 | $appDataPath = $this->appDataDirectory($appName); 57 | $this->line('Clearing: '.$appDataPath); 58 | 59 | if ($filesystem->exists($appDataPath)) { 60 | $filesystem->remove($appDataPath); 61 | } 62 | } 63 | } 64 | } 65 | 66 | return 0; 67 | } 68 | 69 | protected function appDataDirectory(string $name): string 70 | { 71 | /* 72 | * Platform Location 73 | * macOS ~/Library/Application Support 74 | * Linux $XDG_CONFIG_HOME or ~/.config 75 | * Windows %APPDATA% 76 | */ 77 | 78 | return match (PHP_OS_FAMILY) { 79 | 'Darwin' => $_SERVER['HOME'].'/Library/Application Support/'.$name, 80 | 'Linux' => $_SERVER['XDG_CONFIG_HOME'] ?? $_SERVER['HOME'].'/.config/'.$name, 81 | 'Windows' => $_SERVER['APPDATA'].'/'.$name, 82 | default => $_SERVER['HOME'].'/.config/'.$name, 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ElectronServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('nativephp-electron') 22 | ->hasConfigFile('nativephp') 23 | ->hasCommands([ 24 | InstallCommand::class, 25 | DevelopCommand::class, 26 | BuildCommand::class, 27 | PublishCommand::class, 28 | BundleCommand::class, 29 | ResetCommand::class, 30 | ]); 31 | } 32 | 33 | public function packageRegistered(): void 34 | { 35 | $this->app->bind('nativephp.updater', function (Application $app) { 36 | return new UpdaterManager($app); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Facades/Updater.php: -------------------------------------------------------------------------------- 1 | overrideKeys, config('nativephp.cleanup_env_keys', [])); 26 | 27 | $envFile = $this->buildPath(app()->environmentFile()); 28 | 29 | $contents = collect(file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)) 30 | // Remove cleanup keys 31 | ->filter(function (string $line) use ($cleanUpKeys) { 32 | $key = str($line)->before('='); 33 | 34 | return ! $key->is($cleanUpKeys) 35 | && ! $key->startsWith('#'); 36 | }) 37 | // Set defaults (other config overrides are handled in the NativeServiceProvider) 38 | // The Log channel needs to be configured before anything else. 39 | ->push('LOG_CHANNEL=stack') 40 | ->push('LOG_STACK=daily') 41 | ->push('LOG_DAILY_DAYS=3') 42 | ->push('LOG_LEVEL=warning') 43 | ->join("\n"); 44 | 45 | file_put_contents($envFile, $contents); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Traits/CopiesBundleToBuildDirectory.php: -------------------------------------------------------------------------------- 1 | exists($this->sourcePath(self::$bundlePath)); 18 | } 19 | 20 | public function copyBundleToBuildDirectory(): bool 21 | { 22 | $filesystem = new Filesystem; 23 | 24 | $this->line('Copying secure app bundle to build directory...'); 25 | $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); 26 | $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); 27 | 28 | // Clean and create build directory 29 | $filesystem->remove($this->buildPath()); 30 | $filesystem->mkdir($this->buildPath()); 31 | 32 | $filesToCopy = [ 33 | self::$bundlePath, 34 | // '.env', 35 | ]; 36 | foreach ($filesToCopy as $file) { 37 | $filesystem->copy($this->sourcePath($file), $this->buildPath($file), true); 38 | } 39 | // $this->keepRequiredDirectories(); 40 | 41 | return true; 42 | } 43 | 44 | public function warnUnsecureBuild(): void 45 | { 46 | warning('==================================================================='); 47 | warning(' * * * INSECURE BUILD * * *'); 48 | warning('==================================================================='); 49 | warning('Secure app bundle not found! Building with exposed source files.'); 50 | warning('See https://nativephp.com/docs/publishing/building#security'); 51 | warning('==================================================================='); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Traits/CopiesCertificateAuthority.php: -------------------------------------------------------------------------------- 1 | phpBinaryPath())); 25 | } 26 | 27 | $certificateFileName = 'cacert.pem'; 28 | $certFilePath = Path::join($phpBinaryDirectory, $certificateFileName); 29 | 30 | if (! file_exists($certFilePath)) { 31 | warning('CA Certificate not found at '.$certFilePath.'. Skipping copy.'); 32 | 33 | return; 34 | } 35 | 36 | $copied = copy( 37 | $certFilePath, 38 | Path::join(base_path('vendor/nativephp/electron/resources/js/resources'), $certificateFileName) 39 | ); 40 | 41 | if (! $copied) { 42 | // It returned false, but doesn't give a reason why. 43 | throw new \Exception('copy() failed for an unknown reason.'); 44 | } 45 | } catch (\Throwable $e) { 46 | error('Failed to copy CA Certificate: '.$e->getMessage()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Traits/CopiesToBuildDirectory.php: -------------------------------------------------------------------------------- 1 | sourcePath(); 62 | $buildPath = $this->buildPath(); 63 | $filesystem = new Filesystem; 64 | 65 | $patterns = array_merge( 66 | $this->cleanupExcludeFiles, 67 | config('nativephp.cleanup_exclude_files', []), 68 | ); 69 | 70 | // Clean and create build directory 71 | $filesystem->remove($buildPath); 72 | $filesystem->mkdir($buildPath); 73 | 74 | // A filtered iterator that will exclude files matching our skip patterns 75 | $directory = new RecursiveDirectoryIterator( 76 | $sourcePath, 77 | RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS 78 | ); 79 | 80 | $filter = new RecursiveCallbackFilterIterator($directory, function ($current) use ($patterns) { 81 | $relativePath = substr($current->getPathname(), strlen($this->sourcePath()) + 1); 82 | $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); // Windows 83 | 84 | // Check each skip pattern against the current file/directory 85 | foreach ($patterns as $pattern) { 86 | 87 | // fnmatch supports glob patterns like "*.txt" or "cache/*" 88 | if (fnmatch($pattern, $relativePath)) { 89 | return false; 90 | } 91 | } 92 | 93 | return true; 94 | }); 95 | 96 | // Now we walk all directories & files and copy them over accordingly 97 | $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST); 98 | 99 | foreach ($iterator as $item) { 100 | $target = $buildPath.DIRECTORY_SEPARATOR.substr($item->getPathname(), strlen($sourcePath) + 1); 101 | 102 | if ($item->isDir()) { 103 | if (! is_dir($target)) { 104 | mkdir($target, 0755, true); 105 | } 106 | 107 | continue; 108 | } 109 | 110 | try { 111 | copy($item->getPathname(), $target); 112 | 113 | if (PHP_OS_FAMILY !== 'Windows') { 114 | $perms = fileperms($item->getPathname()); 115 | if ($perms !== false) { 116 | chmod($target, $perms); 117 | } 118 | } 119 | } catch (Throwable $e) { 120 | warning('[WARNING] '.$e->getMessage()); 121 | } 122 | } 123 | 124 | $this->keepRequiredDirectories(); 125 | 126 | return true; 127 | } 128 | 129 | private function keepRequiredDirectories() 130 | { 131 | // Electron build removes empty folders, so we have to create dummy files 132 | // dotfiles unfortunately don't work. 133 | $filesystem = new Filesystem; 134 | $buildPath = $this->buildPath(); 135 | 136 | $filesystem->dumpFile("{$buildPath}/storage/framework/cache/_native.json", '{}'); 137 | $filesystem->dumpFile("{$buildPath}/storage/framework/sessions/_native.json", '{}'); 138 | $filesystem->dumpFile("{$buildPath}/storage/framework/testing/_native.json", '{}'); 139 | $filesystem->dumpFile("{$buildPath}/storage/framework/views/_native.json", '{}'); 140 | $filesystem->dumpFile("{$buildPath}/storage/app/public/_native.json", '{}'); 141 | $filesystem->dumpFile("{$buildPath}/storage/logs/_native.json", '{}'); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Traits/Developer.php: -------------------------------------------------------------------------------- 1 | getInstallerAndCommand(installer: $installer, type: 'dev'); 14 | 15 | note("Running the dev script with {$installer}..."); 16 | 17 | $this->executeCommand( 18 | command: $command, 19 | skip_queue: $skip_queue, 20 | type: 'serve', 21 | withoutInteraction: $withoutInteraction 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Traits/ExecuteCommand.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'NATIVEPHP_PHP_BINARY_VERSION' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, 22 | 'NATIVEPHP_PHP_BINARY_PATH' => base_path($this->phpBinaryPath()), 23 | ], 24 | 'serve' => [ 25 | 'APP_PATH' => base_path(), 26 | 'NATIVEPHP_PHP_BINARY_VERSION' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, 27 | 'NATIVEPHP_PHP_BINARY_PATH' => base_path($this->phpBinaryPath()), 28 | 'NATIVE_PHP_SKIP_QUEUE' => $skip_queue, 29 | 'NATIVEPHP_BUILDING' => false, 30 | ], 31 | ]; 32 | 33 | note('Fetching latest dependencies…'); 34 | 35 | Process::path(__DIR__.'/../../resources/js/') 36 | ->env($envs[$type]) 37 | ->forever() 38 | ->tty(! $withoutInteraction && PHP_OS_FAMILY != 'Windows') 39 | ->run($command, function (string $type, string $output) { 40 | if ($this->getOutput()->isVerbose()) { 41 | echo $output; 42 | } 43 | }); 44 | } 45 | 46 | protected function getCommandArrays(string $type = 'install'): array 47 | { 48 | $commands = [ 49 | 'install' => [ 50 | 'npm' => 'npm install', 51 | 'yarn' => 'yarn', 52 | 'pnpm' => 'pnpm install', 53 | ], 54 | 'dev' => [ 55 | 'npm' => 'npm run dev', 56 | 'yarn' => 'yarn dev', 57 | 'pnpm' => 'pnpm run dev', 58 | ], 59 | ]; 60 | 61 | return $commands[$type]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Traits/HandlesZephpyr.php: -------------------------------------------------------------------------------- 1 | finish('/'); 14 | } 15 | 16 | private function checkAuthenticated() 17 | { 18 | intro('Checking authentication…'); 19 | 20 | return Http::acceptJson() 21 | ->withToken(config('nativephp-internal.zephpyr.token')) 22 | ->get($this->baseUrl().'api/v1/user')->successful(); 23 | } 24 | 25 | private function checkForZephpyrKey() 26 | { 27 | $this->key = config('nativephp-internal.zephpyr.key'); 28 | 29 | if (! $this->key) { 30 | $this->line(''); 31 | $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); 32 | $this->line(''); 33 | $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); 34 | $this->line(base_path('.env')); 35 | $this->line(''); 36 | $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); 37 | $this->info('Check out '.$this->baseUrl().''); 38 | $this->line(''); 39 | 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | private function checkForZephpyrToken() 47 | { 48 | if (! config('nativephp-internal.zephpyr.token')) { 49 | $this->line(''); 50 | $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); 51 | $this->line(''); 52 | $this->line('Add your Zephpyr API token to your .env file (ZEPHPYR_TOKEN):'); 53 | $this->line(base_path('.env')); 54 | $this->line(''); 55 | $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); 56 | $this->info('Check out '.$this->baseUrl().''); 57 | $this->line(''); 58 | 59 | return false; 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Traits/HasPreAndPostProcessing.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 20 | return; 21 | } 22 | 23 | intro('Running pre-process commands...'); 24 | 25 | $this->runProcess($config); 26 | 27 | outro('Pre-process commands completed.'); 28 | } 29 | 30 | public function postProcess(): void 31 | { 32 | $config = collect(config('nativephp.postbuild')); 33 | 34 | if ($config->isEmpty()) { 35 | return; 36 | } 37 | 38 | intro('Running post-process commands...'); 39 | 40 | $this->runProcess($config); 41 | 42 | outro('Post-process commands completed.'); 43 | } 44 | 45 | private function runProcess(Collection $configCommands): void 46 | { 47 | $configCommands->each(function ($command) { 48 | note("Running command: {$command}"); 49 | 50 | if (is_array($command)) { 51 | $command = implode(' && ', $command); 52 | } 53 | 54 | $result = Process::path(base_path()) 55 | ->timeout(300) 56 | ->tty(\Symfony\Component\Process\Process::isTtySupported()) 57 | ->run($command, function (string $type, string $output) { 58 | echo $output; 59 | }); 60 | 61 | if (! $result->successful()) { 62 | error("Command failed: {$command}"); 63 | 64 | return; 65 | } 66 | 67 | note("Command successful: {$command}"); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Traits/Installer.php: -------------------------------------------------------------------------------- 1 | installDependencies(withoutInteraction: $withoutInteraction); 23 | } else { 24 | $this->installDependencies(installer: $installer, withoutInteraction: $withoutInteraction); 25 | } 26 | $this->output->newLine(); 27 | } 28 | } 29 | 30 | protected function installDependencies(?string $installer = null, bool $withoutInteraction = false): void 31 | { 32 | [$installer, $command] = $this->getInstallerAndCommand(installer: $installer); 33 | 34 | note("Installing NPM dependencies using the {$installer} package manager..."); 35 | $this->executeCommand(command: $command, withoutInteraction: $withoutInteraction); 36 | } 37 | 38 | protected function getInstallerAndCommand(?string $installer, $type = 'install'): array 39 | { 40 | $commands = $this->getCommandArrays(type: $type); 41 | $installer = $this->getInstaller(installer: $installer); 42 | 43 | return [$installer, $commands[$installer]]; 44 | } 45 | 46 | protected function getInstaller(string $installer): int|string 47 | { 48 | if (! array_key_exists($installer, $this->getCommandArrays())) { 49 | error("Invalid installer ** {$installer} ** provided."); 50 | $keys = array_keys($this->getCommandArrays()); 51 | $techs = implode(', ', $keys); 52 | $installer = select('Choose one of the following installers: '.$techs, $keys, 0); 53 | } 54 | 55 | return $installer; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Traits/InstallsAppIcon.php: -------------------------------------------------------------------------------- 1 | binaryPackageDirectory().'bin/'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Traits/OsAndArch.php: -------------------------------------------------------------------------------- 1 | availableOs; 13 | } 14 | 15 | protected function getDefaultOs(): string 16 | { 17 | return match (PHP_OS_FAMILY) { 18 | 'Windows' => 'win', 19 | 'Darwin' => 'mac', 20 | 'Linux' => 'linux', 21 | default => 'all', 22 | }; 23 | } 24 | 25 | protected function selectOs(?string $os): string 26 | { 27 | $os = $os ?? false; 28 | if (! in_array($this->argument('os'), $this->getAvailableOs()) || ! $os) { 29 | $os = select( 30 | label: 'Please select the operating system to build for', 31 | options: $this->getAvailableOs(), 32 | default: $this->getDefaultOs(), 33 | ); 34 | } 35 | 36 | return $os; 37 | } 38 | 39 | /** 40 | * Get Arch for selected os 41 | * 42 | * Make this dynamic at some point 43 | */ 44 | protected function getArchitectureForOs(string $os): array 45 | { 46 | $archs = match ($os) { 47 | 'win' => ['x64'], 48 | 'mac' => ['x86', 'arm64'], 49 | 'linux' => ['x64'], 50 | default => throw new \InvalidArgumentException('Invalid OS'), 51 | }; 52 | 53 | return [...$archs, 'all']; 54 | } 55 | 56 | // Depends on the currenty available php executables 57 | protected function selectArchitectureForOs(string $os, ?string $arch): string 58 | { 59 | $arch = $arch ?? false; 60 | if (! in_array($this->argument('arch'), ($a = $this->getArchitectureForOs($os))) || ! $arch) { 61 | $arch = select( 62 | label: 'Please select Processor Architecture', 63 | options: $a, 64 | default: 'all' 65 | ); 66 | } 67 | 68 | return $arch; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Traits/PatchesPackagesJson.php: -------------------------------------------------------------------------------- 1 | slug(); 13 | 14 | /* 15 | * Suffix the app name with '-dev' if it's a development build 16 | * this way, when the developer test his freshly built app, 17 | * configs, migrations won't be mixed up with the production app 18 | */ 19 | if ($developmentMode) { 20 | $name .= '-dev'; 21 | } 22 | 23 | $packageJson['name'] = $name; 24 | $packageJson['version'] = config('nativephp.version'); 25 | $packageJson['description'] = config('nativephp.description'); 26 | $packageJson['author'] = config('nativephp.author'); 27 | $packageJson['homepage'] = config('nativephp.website'); 28 | 29 | file_put_contents($packageJsonPath, json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 30 | 31 | return $name; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Traits/PrunesVendorDirectory.php: -------------------------------------------------------------------------------- 1 | buildPath()) 19 | ->timeout(300) 20 | ->run('composer install --no-dev', function (string $type, string $output) { 21 | echo $output; 22 | }); 23 | 24 | $filesystem = new Filesystem; 25 | $filesystem->remove([ 26 | $this->buildPath('/vendor/bin'), 27 | $this->buildPath('/vendor/nativephp/php-bin'), 28 | ]); 29 | 30 | // Remove custom php binary package directory 31 | $binaryPackageDirectory = $this->binaryPackageDirectory(); 32 | if (! empty($binaryPackageDirectory) && $filesystem->exists($this->buildPath($binaryPackageDirectory))) { 33 | $filesystem->remove($this->buildPath($binaryPackageDirectory)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Updater/Contracts/Updater.php: -------------------------------------------------------------------------------- 1 | $this->config['token'], 15 | ]; 16 | } 17 | 18 | public function builderOptions(): array 19 | { 20 | return [ 21 | 'provider' => 'github', 22 | 'repo' => $this->config['repo'], 23 | 'owner' => $this->config['owner'], 24 | 'vPrefixedTagName' => $this->config['vPrefixedTagName'], 25 | 'private' => $this->config['private'], 26 | 'channel' => $this->config['channel'], 27 | 'releaseType' => $this->config['releaseType'], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Updater/S3Provider.php: -------------------------------------------------------------------------------- 1 | $this->config['key'], 15 | 'AWS_SECRET_ACCESS_KEY' => $this->config['secret'], 16 | ]; 17 | } 18 | 19 | public function builderOptions(): array 20 | { 21 | return [ 22 | 'provider' => 's3', 23 | 'endpoint' => $this->config['endpoint'], 24 | 'region' => $this->config['region'], 25 | 'bucket' => $this->config['bucket'], 26 | 'path' => $this->config['path'], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Updater/SpacesProvider.php: -------------------------------------------------------------------------------- 1 | $this->config['key'], 15 | 'DO_SECRET_KEY' => $this->config['secret'], 16 | ]; 17 | } 18 | 19 | public function builderOptions(): array 20 | { 21 | return [ 22 | 'provider' => 'spaces', 23 | 'name' => $this->config['name'], 24 | 'region' => $this->config['region'], 25 | 'path' => $this->config['path'], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Updater/UpdaterManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 32 | } 33 | 34 | /** 35 | * Get a updater provider instance by name, wrapped in a repository. 36 | * 37 | * @param string|null $name 38 | * @return \Native\Electron\Updater\Contracts\Updater 39 | */ 40 | public function provider($name = null) 41 | { 42 | $name = $name ?: $this->getDefaultDriver(); 43 | 44 | return $this->providers[$name] ??= $this->resolve($name); 45 | } 46 | 47 | /** 48 | * Get a updater provider instance. 49 | * 50 | * @param string|null $driver 51 | * @return \Native\Electron\Updater\Contracts\Updater 52 | */ 53 | public function driver($driver = null) 54 | { 55 | return $this->resolve($driver); 56 | } 57 | 58 | /** 59 | * Resolve the given store. 60 | * 61 | * @param string $name 62 | * @return \Native\Electron\Updater\Contracts\Updater 63 | * 64 | * @throws \InvalidArgumentException 65 | */ 66 | public function resolve($name) 67 | { 68 | $config = $this->getConfig($name); 69 | 70 | if (is_null($config)) { 71 | throw new InvalidArgumentException("NativePHP updater provider [{$name}] is not defined."); 72 | } 73 | 74 | $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; 75 | 76 | if (method_exists($this, $driverMethod)) { 77 | return $this->{$driverMethod}($config); 78 | } 79 | 80 | throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); 81 | } 82 | 83 | /** 84 | * Get the updater provider configuration. 85 | * 86 | * @param ?string $name 87 | * @return array|null 88 | */ 89 | protected function getConfig($name) 90 | { 91 | if (! is_null($name) && $name !== 'null') { 92 | return $this->app['config']["nativephp.updater.providers.{$name}"]; 93 | } 94 | 95 | return ['driver' => 'null']; 96 | } 97 | 98 | /** 99 | * Get the default updater driver name. 100 | * 101 | * @return string 102 | */ 103 | public function getDefaultDriver() 104 | { 105 | return $this->app['config']['nativephp.updater.default']; 106 | } 107 | 108 | /** 109 | * Set the default updater driver name. 110 | * 111 | * @param string $name 112 | * @return void 113 | */ 114 | public function setDefaultDriver($name) 115 | { 116 | $this->app['config']['nativephp.updater.default'] = $name; 117 | } 118 | 119 | /** 120 | * Set the application instance used by the manager. 121 | * 122 | * @param \Illuminate\Contracts\Foundation\Application $app 123 | * @return $this 124 | */ 125 | public function setApplication($app) 126 | { 127 | $this->app = $app; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Create an instance of the spaces updater driver. 134 | * 135 | * @return \Native\Electron\Updater\Contracts\Updater 136 | */ 137 | protected function createSpacesDriver(array $config) 138 | { 139 | return new SpacesProvider($config); 140 | } 141 | 142 | /** 143 | * Create an instance of the spaces updater driver. 144 | * 145 | * @return \Native\Electron\Updater\Contracts\Updater 146 | */ 147 | protected function createS3Driver(array $config) 148 | { 149 | return new S3Provider($config); 150 | } 151 | 152 | /** 153 | * Create an instance of the GitHub updater driver. 154 | * 155 | * @return \Native\Electron\Updater\Contracts\Updater 156 | */ 157 | protected function createGitHubDriver(array $config) 158 | { 159 | return new GitHubProvider($config); 160 | } 161 | 162 | /** 163 | * Dynamically call the default updater instance. 164 | * 165 | * @param string $method 166 | * @param array $parameters 167 | * @return mixed 168 | */ 169 | public function __call($method, $parameters) 170 | { 171 | return $this->provider()->$method(...$parameters); 172 | } 173 | } 174 | --------------------------------------------------------------------------------