├── .gitignore ├── LICENSE ├── README.md ├── dist ├── assets │ ├── defaultConfig.yaml │ ├── defaultmodules.zip │ ├── logo.svg │ ├── welcome_slidethemes.png │ ├── welcome_slidetricks.png │ └── welcome_slidewidgets.png └── bin │ ├── Hyper Spacer.exe │ └── ShellBasics.dll ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── @types │ └── hyper.d.ts ├── console.html ├── extension.html ├── index.html ├── loading.html ├── main.ts ├── main │ ├── checkdir.ts │ ├── createwindows.ts │ └── ipc.ts ├── render │ ├── components │ │ ├── ProgressRing.tsx │ │ ├── extensions │ │ │ ├── Downloader.tsx │ │ │ ├── PackageItem.tsx │ │ │ ├── TitleBar.tsx │ │ │ └── pages │ │ │ │ ├── Home.tsx │ │ │ │ └── Welcome.tsx │ │ ├── settings │ │ │ ├── dropZone.tsx │ │ │ ├── input.tsx │ │ │ ├── pages │ │ │ │ ├── about.tsx │ │ │ │ ├── default.tsx │ │ │ │ ├── themes.tsx │ │ │ │ └── widgets.tsx │ │ │ ├── refreshPrompt.tsx │ │ │ ├── sidebar │ │ │ │ ├── item.tsx │ │ │ │ └── sidebar.tsx │ │ │ └── titlebar.tsx │ │ └── widgets │ │ │ ├── widgetItem.tsx │ │ │ └── widgetUpload.tsx │ ├── ipc.ts │ ├── style │ │ ├── index.css │ │ ├── input.module.css │ │ ├── loading.css │ │ └── settings.module.css │ ├── utils.ts │ └── windows │ │ ├── console.tsx │ │ ├── extension.tsx │ │ ├── index.tsx │ │ ├── loading.tsx │ │ └── settings.tsx ├── settings.html └── shared │ ├── config.ts │ ├── logger.ts │ ├── theme.ts │ └── widget.ts ├── tailwind.config.js ├── tsconfig.json ├── utils.ts └── yarn-error.log /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .parcel-cache/ 3 | dist/*.html 4 | dist/*.css 5 | dist/*.css.map 6 | dist/*.js 7 | dist/*.js.map 8 | yarn.lock 9 | node_modules/ 10 | out/ 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kangabru 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | # Hyperbar - Hackable modern topbar for windows. 8 | 9 | Hyper is built entirely on webtech and aims to be customizable to the extreme.
10 | Hyper by itself does nothing, all of it's power is concentrated on widgets that extends it's capacity. 11 | 12 | Visit my [Discord server](https://discord.gg/xCmjx4SSZq)
13 | 14 | [Usage preview](https://i.wisp.run/f/uVevoM.mp4)
15 | [Widget installation preview](https://i.wisp.run/f/4JkmFX.mp4)
16 | [Widget development preview](https://i.wisp.run/f/sCmAtH.mp4) 17 | ## 🏃‍♀️ How to run hyper 18 |
19 | 20 | ### As a developer 21 | - git clone this repository. 22 | - navigate to the now clonned repository. 23 | - on your terminal, install the dependencies 24 | ```bash 25 | yarn #or npm i 26 | ``` 27 | - start hyper 28 | ```bash 29 | yarn start #or npm run start 30 | ``` 31 |
32 | 33 | ### As a normal user 34 | - [Download the latest release](https://github.com/hyperts/hyper/releases) 35 | - Run hyper.exe 36 | - Agree to install some default widgets `or` [Download widgets from community](https://github.com/hyperts/communitywidgets) 37 | 38 |
39 | 40 | ## ⚡ Making widgets/themes 41 | You can [download the example widget](https://github.com/hyperts/basewidget) and start from there. 42 | 43 | Making themes are simple as making a new folder under your hyperfolder (~/.hyperbar/themes) and including a index.css file there, you can also duplicate the default theme and edit from there. 44 | -------------------------------------------------------------------------------- /dist/assets/defaultConfig.yaml: -------------------------------------------------------------------------------- 1 | general: 2 | name: General 3 | items: 4 | behavior: 5 | name: Behavior 6 | description: Change how hyper reacts to your desktop. 7 | icon: Monitor 8 | fields: 9 | hide-taskbar: 10 | name: Hide Taskbar 11 | description: Hides windows taskbar when Hyper is initialized 12 | type: checkbox 13 | value: true 14 | reserve-space: 15 | name: Reserve Space 16 | description: Reserves space for Hyper, preveting non full-screen windows to 17 | maximize over the bar. 18 | type: checkbox 19 | value: true 20 | always-on-top: 21 | name: Always on Top 22 | description: Prevents other programs from overlaping the bar (Disabling this 23 | also hides the window when pressing the show desktop button) 24 | type: checkbox 25 | value: true 26 | position: 27 | name: Position 28 | description: Pick spacing and position for hyper. 29 | icon: Layout 30 | fields: 31 | dock-pos: 32 | name: Dock Position 33 | description: Which border of the monitor hyper should be attached to 34 | type: selection 35 | options: 36 | - top 37 | - bottom 38 | value: top 39 | vertical-margin: 40 | name: Vertical Margin 41 | description: Vertical margin from monitor edge 42 | type: number 43 | value: 0 44 | horizontal-margin: 45 | name: Horizontal Margin 46 | description: Horizontal margin from monitor edge 47 | type: number 48 | value: 0 49 | misc: 50 | name: Debug 51 | description: Show/Hide debug options and menus. 52 | icon: Settings 53 | fields: 54 | watch-widgets: 55 | name: Watch for widget changes 56 | description: When enabled, hyper will reload itself when a change is detected on 57 | widget files 58 | type: checkbox 59 | value: false 60 | watch-themes: 61 | name: Watch for theme changes 62 | description: When enabled, hyper will reload the theme when a change is detected 63 | on themes directory 64 | type: checkbox 65 | value: false 66 | context-menu: 67 | name: Context menu 68 | description: When enabled, the hyper context menu will include debug actions. 69 | type: checkbox 70 | value: true 71 | appearence: 72 | name: Appearence 73 | items: 74 | color: 75 | name: Color 76 | description: Change the global color scheme for supported themes. 77 | icon: Droplet 78 | fields: 79 | accent: 80 | name: Accent 81 | description: Accent color used on supported themes 82 | type: color 83 | value: "#f45000" 84 | primary: 85 | name: Primary 86 | description: Predominant color used on supported themes 87 | type: color 88 | value: "#000000" 89 | secondary: 90 | name: Secondary 91 | description: Secondary color used on supported themes 92 | type: color 93 | value: "#161616" 94 | composition: 95 | name: Composition 96 | description: Change the composition effect behind the main bar 97 | icon: Codesandbox 98 | fields: 99 | effect: 100 | name: Composition Effect 101 | description: The desired effect 102 | type: acrylic 103 | options: 104 | - acrylic 105 | - blur 106 | - transparent 107 | - none 108 | value: transparent 109 | tint: 110 | name: Color tint 111 | description: Color tinted to composition effect 112 | type: color 113 | value: "#21212120" 114 | sizes: 115 | name: Sizes 116 | description: Tiny monitor? no problems, Change bar height and padding 117 | icon: Crop 118 | fields: 119 | height: 120 | name: Height 121 | description: Total height of the bar 122 | type: number 123 | value: 32 124 | padding: 125 | name: Padding 126 | description: Internal margin (theme needs to support) 127 | type: number 128 | value: 6 129 | theme: 130 | name: Theme 131 | description: Theming options 132 | icon: Paperclip 133 | fields: 134 | selected: 135 | name: Active Theme 136 | description: Selects the active theme 137 | type: selection 138 | options: 139 | - default 140 | value: default -------------------------------------------------------------------------------- /dist/assets/defaultmodules.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/assets/defaultmodules.zip -------------------------------------------------------------------------------- /dist/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dist/assets/welcome_slidethemes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/assets/welcome_slidethemes.png -------------------------------------------------------------------------------- /dist/assets/welcome_slidetricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/assets/welcome_slidetricks.png -------------------------------------------------------------------------------- /dist/assets/welcome_slidewidgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/assets/welcome_slidewidgets.png -------------------------------------------------------------------------------- /dist/bin/Hyper Spacer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/bin/Hyper Spacer.exe -------------------------------------------------------------------------------- /dist/bin/ShellBasics.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/dist/bin/ShellBasics.dll -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.9.1", 3 | "authors": "nodge", 4 | "name": "hyper", 5 | "description": "Hyper windows customization", 6 | "main": "dist/main.js", 7 | "scripts": { 8 | "preinstall": "npx npm-force-resolutions", 9 | "prestart": "npm run typecheck", 10 | "start": "npm run render:start | npm run main:start", 11 | "prebuild": "npm run clean && npm run typecheck", 12 | "build": "npm run render:build && npm run main:build && electron:build", 13 | "render:start": "npm run render:watch", 14 | "render:watch": "parcel watch ./src/index.html ./src/settings.html ./src/loading.html ./src/extension.html ./src/console.html --target electron --public-url ./ --no-hmr", 15 | "render:build": "parcel build ./src/index.html ./src/settings.html ./src/loading.html ./src/extension.html ./src/console.html --target electron --public-url ./", 16 | "main:start": "npm run main:watch | npm run electron:start ", 17 | "main:watch": "parcel watch src/main.ts --target node --no-hmr", 18 | "main:build": "parcel build src/main.ts --target node", 19 | "electron:start": "electron-forge start", 20 | "electron:build": "electron-forge make", 21 | "electron:package": "electron-forge package", 22 | "typecheck": "tsc --noEmit", 23 | "clean": "rimraf dist" 24 | }, 25 | "browserslist": [ 26 | "last 2 chrome versions", 27 | "last 2 firefox versions" 28 | ], 29 | "devDependencies": { 30 | "@electron-forge/cli": "^6.0.0-beta.54", 31 | "@electron-forge/maker-squirrel": "^6.0.0-beta.54", 32 | "@electron-forge/maker-zip": "^6.0.0-beta.54", 33 | "@types/adm-zip": "^0.4.34", 34 | "@types/feather-icons": "^4.7.0", 35 | "@types/node": "^14.14.35", 36 | "@types/react": "^17.0.5", 37 | "@types/react-dom": "^17.0.5", 38 | "@types/ws": "^8.2.0", 39 | "autoprefixer": "^9.8.6", 40 | "electron": "^12.0.1", 41 | "electron-reload": "^1.5.0", 42 | "electron-squirrel-startup": "^1.0.0", 43 | "parcel-bundler": "^1.12.4", 44 | "rimraf": "^2.7.1", 45 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", 46 | "typescript": "^4.1.3" 47 | }, 48 | "dependencies": { 49 | "adm-zip": "^0.5.9", 50 | "axios": "^0.21.1", 51 | "chokidar": "^3.5.2", 52 | "dot-prop": "^6.0.1", 53 | "electron-log": "^4.4.1", 54 | "ewc": "^0.0.1", 55 | "feather-icons": "^4.28.0", 56 | "framer-motion": "^4.1.17", 57 | "perfect-scrollbar": "^1.5.1", 58 | "react": "^17.0.2", 59 | "react-color-palette": "^6.1.0", 60 | "react-dom": "^17.0.2", 61 | "react-dropzone": "^11.4.2", 62 | "react-feather": "^2.0.9", 63 | "react-fetch-hook": "^1.8.5", 64 | "three-dots": "^0.2.1", 65 | "ws": "^8.3.0", 66 | "yaml": "^1.10.2" 67 | }, 68 | "resolutions": { 69 | "node-forge": "0.10.0" 70 | }, 71 | "config": { 72 | "forge": { 73 | "packagerConfig": { 74 | "dir": "dist", 75 | "protocols": [ 76 | { 77 | "name": "Hyper Exntension point", 78 | "schemes": [ 79 | "hyper-install" 80 | ] 81 | } 82 | ] 83 | }, 84 | "makers": [ 85 | { 86 | "name": "@electron-forge/maker-squirrel", 87 | "config": { 88 | "name": "hyper" 89 | } 90 | }, 91 | { 92 | "name": "@electron-forge/maker-zip", 93 | "platforms": [ 94 | "darwin" 95 | ] 96 | }, 97 | { 98 | "name": "@electron-forge/maker-deb", 99 | "config": { 100 | "mimeType": [ 101 | "x-scheme-handler/hyper-install" 102 | ] 103 | } 104 | }, 105 | { 106 | "name": "@electron-forge/maker-rpm", 107 | "config": {} 108 | } 109 | ] 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")], 3 | }; -------------------------------------------------------------------------------- /src/@types/hyper.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ConfigTable = { 3 | [key: string]: ConfigCategory 4 | } 5 | 6 | export type ConfigCategory = { 7 | name: string 8 | items: { 9 | [key: string] : ConfigItem 10 | } 11 | } 12 | 13 | export type ConfigItem = { 14 | name: string 15 | description: string, 16 | icon: string 17 | fields: { 18 | [key: string]: ConfigField 19 | } 20 | } 21 | 22 | export type ConfigField = { 23 | name: string 24 | description: string 25 | value: boolean | number | string 26 | } & ({ 27 | type: "selection" 28 | options: string[] 29 | } | { 30 | type: "checkbox" | "number" | "color" | "text" 31 | options?: string[] 32 | }) 33 | 34 | export type HSWWData = { 35 | 36 | } & { 37 | Event: "window.opened" 38 | ProcessId: number 39 | Name: string 40 | WindowHandle: string 41 | } | { 42 | Event: "window.opened" 43 | ProcessId: number 44 | Name: string 45 | WindowHandle: string 46 | } | { 47 | Event: "window.renamed" 48 | ProcessId: number 49 | Name: string 50 | WindowHandle: string 51 | } | { 52 | Event: "WindowList" 53 | Handle: number 54 | Title: string 55 | ProcessId: number 56 | ProcessName: string 57 | } -------------------------------------------------------------------------------- /src/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hyper debug console 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/extension.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hyper ⚡ Extension Point 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hyper ⚡ 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hyper ⚡ 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app, protocol, dialog } from 'electron'; 2 | import path, { dirname } from 'path'; 3 | import WidgetRepository, { WidgetObject } from './shared/widget'; 4 | import { Config } from './shared/config' 5 | import { homedir } from 'os'; 6 | import { createWindows, createSplash, createDebugWindow } from './main/createwindows'; 7 | import startIPC from './main/ipc'; 8 | 9 | import './main/checkdir'; 10 | 11 | // import log from 'electron-log' 12 | // import {join} from 'path' 13 | import ThemeRepository from './shared/theme'; 14 | import {makeAppBar} from '../utils' 15 | import { Logger } from './shared/logger'; 16 | 17 | // const logger = log.scope('WIDGET') 18 | // const logg = log.scope('MAIN') 19 | // log.transports.file.resolvePath = () => join(homedir(), '.hyperlogs/main.log'); 20 | export const windows = {} 21 | let loadedWidgets: WidgetObject[] = [] 22 | export const widgetReference: {[key: string]: WidgetObject} = {} 23 | 24 | 25 | if (!app.isPackaged) require('electron-reload')(__dirname, { 26 | electron: process.platform === 'win32' 27 | ? path.join(dirname(__dirname), "node_modules", "electron", "dist", "electron.exe") 28 | : path.join(dirname(__dirname), 'node_modules', '.bin', 'electron') 29 | }) 30 | 31 | const gotTheLock = app.requestSingleInstanceLock() 32 | 33 | 34 | if (!gotTheLock) { 35 | dialog.showErrorBox('Failed to initialize Hyper', `Hyper attempted to kill other instances of itself and failed\n - Probably:\n -- Another instance of hyper is open, try and close it via a task manager.\n -- A Widget is trying to control hyper initializing process, remove your latest installed widget\n -- Hyper developers are insane, contact them.\n\n -- Restarting hyper may resolve your problem`) 36 | app.quit() 37 | } 38 | 39 | 40 | 41 | const logg = new Logger('MAIN') 42 | 43 | const loggWidget = new Logger('[MAIN] WIDGET') 44 | 45 | dialog.showErrorBox = (title, err) =>{ 46 | logg.error(`Not managed exception in: ${title}\n      Error information:\n       ${err}`) 47 | } 48 | 49 | app.on('ready', async ()=>{ 50 | protocol.registerFileProtocol('assets', (request, callback) => { 51 | const url = request.url.substr(9) 52 | callback({ path: path.normalize(`${__dirname}/assets/${url}`) }) 53 | }) 54 | 55 | protocol.registerFileProtocol('theme', (request, callback) => { 56 | const url = request.url.substr(7) 57 | const themeConfig = new Config('appearence.items.theme.fields.selected') 58 | callback({ path: path.normalize(`${homedir()}/.hyperbar/themes/${themeConfig.get('value')}${url}`) }) 59 | }) 60 | 61 | protocol.registerFileProtocol('widgets', (request, callback) => { 62 | const url = request.url.substr(10) 63 | if (!loadedWidgets) { 64 | loggWidget.error(`Invalid attempt to access widget protocol\n - url:${url}\n - Widget not loaded`) 65 | return 66 | } 67 | const searchRegExp = /\\/gi; 68 | const replaceWith = '/'; 69 | 70 | const probableName = url.replace(searchRegExp, replaceWith).split('/')[0] 71 | const entryMatch = widgetReference?.[probableName] 72 | 73 | if (!entryMatch) { 74 | loggWidget.error(`Invalid attempt to access widget protocol\n - url:${url}\n - Widget not found in reference`) 75 | return 76 | } 77 | 78 | const newURL = url.replace(probableName, entryMatch.directory) 79 | callback({ path: path.normalize(`${homedir()}/.hyperbar/widgets/${newURL}`) }) 80 | }) 81 | 82 | if (process.defaultApp) { 83 | if (process.argv.length >= 3) { 84 | app.setAsDefaultProtocolClient('hyper-install', process.execPath, [path.resolve(process.argv[1], process.argv[2])]) 85 | } 86 | 87 | } else { 88 | app.setAsDefaultProtocolClient('hyper-install') 89 | } 90 | 91 | createSplash(windows) // Loading the splashscreen before doing Sync procedures 92 | 93 | if (new Config().getValue('general','misc', "console-window")) { 94 | createDebugWindow(windows) 95 | } 96 | 97 | const widgetRepository = new WidgetRepository(); 98 | widgetRepository.loadWidgetsInPaths() 99 | 100 | const themeRepository = new ThemeRepository(); 101 | themeRepository.setVars() 102 | 103 | // Giving 2 seconds for any hanging rule in main. 104 | setTimeout(async () => { 105 | await makeAppBar() 106 | createWindows(windows) // Creates main app windows [Main, Settings] 107 | startIPC(windows) 108 | loadedWidgets = widgetRepository.loadedWidgets 109 | widgetRepository.loadedWidgets.forEach( widget =>{ 110 | widgetReference[widget.name] = widget 111 | widget.default() 112 | loggWidget.debug(`Extension/Widget loaded: ${widget?.name} v${widget?.version} by:${widget.author}`) 113 | }) 114 | widgetRepository.watchWidgets() 115 | }, 2000); 116 | 117 | }) -------------------------------------------------------------------------------- /src/main/checkdir.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; 3 | import Zip from 'adm-zip' 4 | 5 | const homePath : String = homedir() 6 | 7 | let firstRun = false 8 | 9 | if ( !existsSync(`${homePath}\\.hyperbar`) ) { 10 | mkdirSync(`${homePath}\\.hyperbar`) 11 | mkdirSync(`${homePath}\\.hyperbar\\widgets`) 12 | mkdirSync(`${homePath}\\.hyperbar\\themes`) 13 | const zipFile = new Zip(`${__dirname}\\assets\\defaultmodules.zip`) 14 | zipFile.extractAllTo(`${homePath}\\.hyperbar`, true) 15 | } 16 | 17 | if (firstRun || !existsSync(`${homePath}\\.hyperbar\\config.yaml`)) { 18 | writeFileSync(`${homePath}\\.hyperbar\\config.yaml`, readFileSync(`${__dirname}/assets/defaultConfig.yaml`).toString()) 19 | } -------------------------------------------------------------------------------- /src/main/createwindows.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, shell } from "electron"; 2 | import { Config } from '../shared/config'; 3 | import { stringToHex, generateBounds, removeAppBar } from '../../utils'; 4 | import ewc from 'ewc'; 5 | 6 | 7 | import { Logger } from "../shared/logger"; 8 | const logger = new Logger('WINDOW MANAGER') 9 | 10 | export async function createWindows( windows: {[key: string]:BrowserWindow|null } ) { 11 | 12 | const configBehavior = new Config("general.items.behavior.fields") 13 | const configComposition = new Config("appearence.items.composition.fields") 14 | const mainBounds = generateBounds() 15 | 16 | windows.main = new BrowserWindow({ 17 | width: mainBounds.width, 18 | height: mainBounds.height, 19 | frame: false, 20 | x: mainBounds.x, 21 | y: mainBounds.y, 22 | minimizable: false, 23 | alwaysOnTop: configBehavior.get("always-on-top.value"), 24 | thickFrame: false, 25 | backgroundColor: '#00000000', 26 | hasShadow: false, 27 | resizable: false, 28 | skipTaskbar: true, 29 | focusable: true, 30 | fullscreenable: false, 31 | show: true, 32 | webPreferences: { 33 | nodeIntegration: true, 34 | contextIsolation: false, 35 | } 36 | }) 37 | 38 | windows.main.loadFile('./dist/index.html') 39 | 40 | setInterval(()=>{ 41 | const currentPos = windows?.main?.getPosition() 42 | if (currentPos && currentPos[0] != mainBounds.x || currentPos && currentPos[1] != mainBounds.y) { 43 | windows.main?.setPosition(mainBounds.x, mainBounds.y) 44 | } 45 | }, 5000) 46 | 47 | switch (configComposition.get("effect.value")) { 48 | case "acrylic": 49 | ewc.setAcrylic(windows.main, stringToHex( configComposition.get("tint.value"))) 50 | break; 51 | case "blur": 52 | ewc.setBlurBehind(windows.main, stringToHex(configComposition.get("tint.value"))) 53 | break; 54 | case "transparent": 55 | ewc.setTransparentGradient(windows.main, 0x0000000) 56 | break; 57 | default: 58 | windows.main.setBackgroundColor( configComposition.get("tint.value") ) 59 | } 60 | 61 | windows.main.webContents.on('new-window', function (e, url) { 62 | e.preventDefault() 63 | shell.openExternal(url) 64 | }); 65 | 66 | windows.main.on('ready-to-show', function(){ 67 | // if (windows.main) { 68 | // windows.main.show() 69 | // } 70 | 71 | if (windows.splash) { 72 | windows.splash.close() 73 | windows.splash = null 74 | } 75 | 76 | logger.debug("Hyper loaded") 77 | if (new Config().getValue('general','misc', "console-window")) { 78 | windows.main?.webContents.openDevTools({ 79 | mode: 'detach' 80 | }) 81 | } 82 | }) 83 | 84 | 85 | windows.main.on('close', (e)=>{ 86 | removeAppBar() 87 | e.preventDefault(); 88 | app.quit(); 89 | process.exit(0); 90 | }) 91 | 92 | if (app.isPackaged) { windows.main.setMenuBarVisibility(false) } 93 | } 94 | 95 | export function createSplash(windows: {[key: string]:BrowserWindow}) { 96 | logger.debug("Creating load screen window") 97 | 98 | windows.splash = new BrowserWindow({ 99 | width: 600, 100 | height: 300, 101 | minimizable: false, 102 | alwaysOnTop: true, 103 | thickFrame: false, 104 | frame: false, 105 | transparent: true, 106 | backgroundColor: '#00000000', 107 | hasShadow: false, 108 | resizable: false, 109 | skipTaskbar: false, 110 | focusable: true, 111 | fullscreenable: false, 112 | webPreferences: { 113 | nodeIntegration: true, 114 | contextIsolation: false, 115 | } 116 | }) 117 | ewc.setTransparentGradient(windows.splash, 0x0000000) 118 | windows.splash.loadFile('./dist/loading.html') 119 | } 120 | 121 | export function createSettingsWindow(windows: {[key: string]:BrowserWindow|null }) { 122 | if (!windows.settings) { 123 | windows.settings = new BrowserWindow({ 124 | width: 900, 125 | height: 600, 126 | show: true, 127 | frame: false, 128 | minimizable: true, 129 | thickFrame: false, 130 | hasShadow: false, 131 | backgroundColor: '#00000000', 132 | resizable: false, 133 | minWidth: 800, 134 | minHeight: 500, 135 | skipTaskbar: false, 136 | focusable: true, 137 | fullscreenable: false, 138 | webPreferences: { 139 | nodeIntegration: true, 140 | contextIsolation: false, 141 | } 142 | }) 143 | 144 | windows.settings.webContents.on('new-window', function (e, url) { 145 | e.preventDefault() 146 | shell.openExternal(url) 147 | }); 148 | 149 | windows.settings.loadFile('./dist/settings.html') 150 | ewc.setAcrylic(windows.settings, 0x21212120) 151 | 152 | windows.settings.on('ready-to-show', function(){ 153 | logger.info('Settings window registered & Ready to show') 154 | }) 155 | 156 | windows.settings.on('close', (e)=>{ 157 | delete windows.settings 158 | }) 159 | } else { 160 | windows?.settings.show() 161 | } 162 | } 163 | 164 | export function createExtensionWindow(windows: {[key:string]:BrowserWindow|null}, firstTime: boolean) { 165 | if (!windows.extension) { 166 | windows.extension = new BrowserWindow({ 167 | width: 900, 168 | height: 600, 169 | show: true, 170 | frame: false, 171 | minimizable: true, 172 | thickFrame: false, 173 | hasShadow: false, 174 | backgroundColor: '#00000000', 175 | minWidth: 800, 176 | minHeight: 500, 177 | skipTaskbar: false, 178 | focusable: true, 179 | fullscreenable: true, 180 | maximizable: true, 181 | webPreferences: { 182 | nodeIntegration: true, 183 | contextIsolation: false, 184 | } 185 | }) 186 | } else { 187 | windows?.extension.show() 188 | } 189 | 190 | windows.extension.on('close', (e)=>{ 191 | delete windows.extension 192 | }) 193 | 194 | windows.extension.on('ready-to-show', function(){ 195 | if (!firstTime) { 196 | windows.extension?.webContents.send('extensionWindowHideTutorial') 197 | } 198 | }) 199 | 200 | windows.extension.loadFile('./dist/extension.html') 201 | 202 | windows.extension.on('ready-to-show', function(){ 203 | logger.info('Extension window registered & Ready to show') 204 | }) 205 | } 206 | 207 | export function createDebugWindow(windows: {[key:string]:BrowserWindow|null}) { 208 | if (!windows.console) { 209 | windows.console = new BrowserWindow({ 210 | width: 1200, 211 | height: 600, 212 | show: true, 213 | frame: true, 214 | autoHideMenuBar: true, 215 | webPreferences: { 216 | nodeIntegration: true, 217 | contextIsolation: false, 218 | } 219 | }) 220 | } else { 221 | windows?.console.show() 222 | } 223 | 224 | windows.console.on('close', (e)=>{ 225 | delete windows.console 226 | }) 227 | 228 | windows.console.loadFile('./dist/console.html') 229 | 230 | windows.console.on('ready-to-show', function(){ 231 | logger.info('Console / Debugger window ready to show') 232 | }) 233 | } -------------------------------------------------------------------------------- /src/main/ipc.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow, ipcMain, screen, Menu} from 'electron' 2 | import { createSettingsWindow, createExtensionWindow, createDebugWindow } from './createwindows'; 3 | import { WebSocketServer } from 'ws' 4 | import {HSWWData} from '../@types/hyper' 5 | import log from 'electron-log' 6 | import {homedir} from 'os' 7 | import {join} from 'path' 8 | import {Config} from '../shared/config' 9 | import { Logger } from '../shared/logger'; 10 | const logger = new Logger('[MAIN] IPC') 11 | 12 | log.transports.file.resolvePath = () => join(homedir(), '.hyperlogs/main.log'); 13 | 14 | function startIPC(windows: {[key: string]: BrowserWindow}) { 15 | logger.debug("Initializing") 16 | 17 | const wss = new WebSocketServer({ port: 49737}) 18 | 19 | wss.on('connection', function (ws) { 20 | 21 | ws.on('message', function (message: string) { 22 | try { 23 | const data = JSON.parse(message) as HSWWData 24 | Object.keys(windows).forEach( name => { 25 | const windowObj = windows[name] 26 | if (windowObj) { 27 | windowObj.webContents.send(`hws_${data.Event}`, data) 28 | } 29 | }) 30 | } catch { 31 | logger.error("Invalid JSON received via IPC WS") 32 | } 33 | }) 34 | 35 | wss.on('close', function (this) { 36 | logger.error("Connection closed -> <-") 37 | }) 38 | 39 | // Listen to sendSocket only when socket state is READY (1) 40 | ipcMain.on('sendSocketMessage', (event, {event_name, data_message}: {event_name: string, data_message: string}) => { 41 | wss.clients.forEach( client => { 42 | client.send(JSON.stringify({event_name, data_message})) 43 | }) 44 | }) 45 | }) 46 | 47 | app.on('window-all-closed', () => { 48 | app.quit() 49 | }) 50 | 51 | ipcMain.on('openSettings', () => { 52 | createSettingsWindow(windows) 53 | }) 54 | 55 | ipcMain.on('openExtensions', (e, tutorial?: boolean) => { 56 | createExtensionWindow(windows, tutorial ?? false) 57 | }) 58 | 59 | ipcMain.on('showWelcomeScreen', ()=>{ 60 | createExtensionWindow(windows, true) 61 | }) 62 | 63 | ipcMain.on('extensionWindowHideTutorial', (e) =>{ 64 | if (windows.extension) { 65 | windows.extension.webContents.send('extensionWindowHideTutorial') 66 | return 67 | } 68 | }) 69 | 70 | ipcMain.on('refreshExtensionWindow', () => { 71 | windows?.extension?.close() 72 | createExtensionWindow(windows, false) 73 | }) 74 | 75 | ipcMain.on('closeExtensionWindow', () => { 76 | windows?.extension?.close() 77 | }) 78 | 79 | ipcMain.on('openConsole', ()=>{ 80 | createDebugWindow(windows) 81 | }) 82 | 83 | 84 | ipcMain.on('h_logmessage', (e,data)=> { 85 | windows?.console.webContents.send('h_logmessage', data) 86 | }) 87 | 88 | 89 | ipcMain.on('closeSettings', () => { 90 | windows?.settings?.close() 91 | }) 92 | 93 | ipcMain.on('updateComposition', (_, composition) => { 94 | 95 | }) 96 | 97 | ipcMain.on('moveSettingsWindow', (e, {mouseX, mouseY}: {mouseX: number, mouseY:number} ) => { 98 | const { x, y } = screen.getCursorScreenPoint() 99 | windows.settings.setPosition(x - mouseX, y - mouseY) 100 | }); 101 | 102 | ipcMain.on('moveExtensionWindow', (e, {mouseX, mouseY}: {mouseX: number, mouseY:number} ) => { 103 | const { x, y } = screen.getCursorScreenPoint() 104 | windows.extension.setPosition(x - mouseX, y - mouseY) 105 | }); 106 | 107 | ipcMain.on('refreshApp', () => { 108 | app?.relaunch() 109 | app?.exit() 110 | }) 111 | 112 | ipcMain.on('closeApp', () => { 113 | app?.exit() 114 | }) 115 | 116 | ipcMain.on('forceReload', () => { 117 | for (const windowName in windows) { 118 | const window = windows[windowName] 119 | window?.webContents?.reloadIgnoringCache() 120 | } 121 | }) 122 | 123 | ipcMain.on('getCursorPosition', (e, window) => { 124 | const {x, y} = screen.getCursorScreenPoint() 125 | if (window) { 126 | window.webContents.send('sendCursorPosition', {x, y}) 127 | return 128 | } 129 | ipcMain.emit('sendCursorPosition', {x, y}) 130 | }) 131 | 132 | ipcMain.on('getScreenSize', (e, window) =>{ 133 | const primaryDisplay = screen.getPrimaryDisplay() 134 | const { width: w, height: h } = primaryDisplay.workAreaSize 135 | if (window) { 136 | window.webContents.send('sendScreenSize', {w, h}) 137 | return 138 | } 139 | ipcMain.emit('sendScreenSize', {w, h}) 140 | }) 141 | 142 | ipcMain.on('show-context-menu', (event) => { 143 | // Create context menu on message 144 | let template = [ 145 | { label: 'Restart', click: () => { ipcMain.emit('refreshApp') } }, 146 | { label: 'Exit', click: () => { ipcMain.emit('closeApp') } }, 147 | { label: 'Debug Console', click:()=>{ipcMain.emit('openConsole')}}, 148 | { type: 'separator' }, 149 | { label: 'Install Themes/Extensions', click: () =>{ ipcMain.emit('openExtensions', true) } }, 150 | { label: 'Settings', click: () => { ipcMain.emit('openSettings') } }, 151 | { type: 'separator' }, 152 | { label: 'Hyper - Beta release'} 153 | ] 154 | 155 | const config = new Config() 156 | // If the user settings doesn't allow to display debug settings 157 | // then we just remove it from the array. 158 | if (!config.getValue('general', 'misc', "context-menu")) { 159 | delete template[0] 160 | // delete template[1] 161 | delete template[2] 162 | // delete template[3] 163 | } 164 | //@ts-expect-error 165 | const menu = Menu.buildFromTemplate(template) 166 | menu.popup() 167 | }) 168 | } 169 | 170 | export default startIPC; -------------------------------------------------------------------------------- /src/render/components/ProgressRing.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | 3 | interface Props { 4 | radius: number 5 | stroke: number 6 | progress: number 7 | } 8 | 9 | import React from 'react'; 10 | 11 | const ProgressRing = ({radius, stroke, progress}: Props) => { 12 | 13 | const normalizedRadius = radius - stroke * 2 14 | const circumference = normalizedRadius * 2 * Math.PI 15 | const strokeDashoffset = circumference - progress / 100 * circumference; 16 | 17 | 18 | return ( 19 | 23 | 34 | 35 | ); 36 | }; 37 | 38 | export default ProgressRing -------------------------------------------------------------------------------- /src/render/components/extensions/Downloader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Axios from 'axios' 3 | import {createWriteStream} from 'fs' 4 | import ProgressRing from '../ProgressRing' 5 | import {Download, Tool} from 'react-feather' 6 | 7 | import WidgetRepository from '../../../shared/widget' 8 | import ThemeRepository from '../../../shared/theme' 9 | 10 | const widgetRepository = new WidgetRepository() 11 | const themeRepository = new ThemeRepository() 12 | 13 | enum DownloadStatus { 14 | WAITING = 0, 15 | IN_PROGRESS = 1, 16 | DONE = 2, 17 | FAILED = 3, 18 | ERROR = 4, 19 | INSTALLING = 5 20 | } 21 | 22 | interface Props { 23 | onFinish: () => void 24 | onFail?: () => void 25 | repoType: string 26 | url: string 27 | name: string, 28 | size: number 29 | } 30 | 31 | const Downloader = ({onFail, onFinish, url, name, repoType, size}: Props) => { 32 | const path = repoType == 'widgets' ? widgetRepository.widgetPaths[0] : themeRepository.themesPath[0] 33 | 34 | const [progress, setProgress] = useState(0) 35 | const [status, setStatus] = useState(DownloadStatus.WAITING) 36 | 37 | async function doDownload() { 38 | 39 | if (status === DownloadStatus.IN_PROGRESS){ return } 40 | if (status === DownloadStatus.ERROR){ return } 41 | if (status === DownloadStatus.INSTALLING){ return } 42 | 43 | if (!url || url === '') { 44 | setStatus(DownloadStatus.ERROR) 45 | return 46 | } 47 | 48 | if (status === DownloadStatus.WAITING) { setStatus(DownloadStatus.IN_PROGRESS) } 49 | 50 | console.table({ 51 | url, 52 | repoType, 53 | name 54 | }) 55 | 56 | const response = await Axios({ 57 | url, 58 | method: 'GET', 59 | responseType: 'blob', 60 | onDownloadProgress: (progressEvent) => { 61 | const current = progressEvent?.currentTarget?.response.length 62 | let percentCompleted = Math.floor(current / size * 100) 63 | setProgress(percentCompleted) 64 | } 65 | }) 66 | 67 | if (!response){ 68 | setStatus(DownloadStatus.ERROR) 69 | onFail?.() 70 | } 71 | 72 | const writer = createWriteStream(`${path}\\${name}.zip`) 73 | //@ts-expect-error 74 | const ws = new WritableStream(writer) 75 | 76 | let blob = new Blob([response.data], {type: 'application/zip'}) 77 | const stream = blob.stream(); 78 | 79 | stream.pipeTo(ws) 80 | .then(()=>{ 81 | // Now we install & load :D 82 | setStatus(DownloadStatus.INSTALLING) 83 | setProgress(100) 84 | writer.close() 85 | setTimeout(() => { 86 | widgetRepository.installWidget(`${path}\\${name}.zip`, ()=>{ 87 | setStatus(DownloadStatus.DONE) 88 | }) 89 | }, 300); 90 | }) 91 | } 92 | 93 | 94 | doDownload() 95 | 96 | if (status === DownloadStatus.IN_PROGRESS || status === DownloadStatus.INSTALLING) { 97 | console.log("STATUS:", status, "PROGRESS:", progress) 98 | return
99 |
100 | 101 | {status === DownloadStatus.INSTALLING ? : } 102 |
103 |

{status === DownloadStatus.INSTALLING ? 'Installing' : 'Downloading'} {name}

104 |
105 | } 106 | return <> 107 | }; 108 | 109 | export default Downloader; -------------------------------------------------------------------------------- /src/render/components/extensions/PackageItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import useFetch from "react-fetch-hook" 3 | import {DownloadCloud, AlertTriangle, RefreshCcw} from 'react-feather' 4 | import { WidgetInfo } from '../../../shared/widget' 5 | import WidgetRepository from '../../../shared/widget' 6 | import ThemeRepository from '../../../shared/theme' 7 | import Downloader from './Downloader'; 8 | 9 | const widgetRepository = new WidgetRepository() 10 | const themeRepository = new ThemeRepository() 11 | // Better to fetch everything again, user might have installed manually 12 | widgetRepository.loadWidgetsInPaths() 13 | themeRepository.loadThemes() 14 | interface Props { 15 | repoType: "widgets" | "themes" | string // string so we don't have to edit this when expanding for tests 16 | user: string 17 | pkg: string 18 | onDownload: () => void 19 | } 20 | 21 | enum InstallStatus { 22 | NOT_INSTALLED = 0, 23 | OUTDATED = 1, 24 | INSTALLED = 2, 25 | FAILED = 3 26 | } 27 | 28 | interface ReleaseInfo { 29 | assets: [ 30 | { 31 | url: string, 32 | name: string, 33 | label: string | undefined, 34 | browser_download_url: string, 35 | size: number 36 | } 37 | ] 38 | } 39 | 40 | function isInstalled(pkgName: string, pkgVersion: string, pkgType: string): InstallStatus { 41 | 42 | switch(pkgType) { 43 | case 'widgets': { 44 | const matches = widgetRepository.loadedWidgets.filter( item => { 45 | return item.name === pkgName 46 | }) 47 | if (matches.length > 0) { 48 | if (Number(matches[0].version) < Number(pkgVersion)) { 49 | return InstallStatus.OUTDATED 50 | } else { 51 | return InstallStatus.INSTALLED 52 | } 53 | } 54 | 55 | return InstallStatus.NOT_INSTALLED 56 | } 57 | case 'themes': { 58 | const matches = themeRepository.installedThemes.filter( item => { 59 | return item.name === pkgName 60 | }) 61 | 62 | if (matches.length > 0) { 63 | if (Number(matches[0].version) < Number(pkgVersion)) { 64 | return InstallStatus.OUTDATED 65 | } else { 66 | return InstallStatus.INSTALLED 67 | } 68 | } 69 | 70 | return InstallStatus.NOT_INSTALLED 71 | } 72 | } 73 | 74 | return InstallStatus.FAILED 75 | } 76 | 77 | const PackageList = ({repoType, user, pkg, onDownload}:Props) => { 78 | const [isDownloading, setDownloading] = useState(false) 79 | //@ts-expect-error 80 | const { isLoading, data, error }: {isLoading: boolean, data:WidgetInfo, error:useFetch.UseFetchError} = useFetch(`https://raw.githubusercontent.com/${user}/${pkg}/master/package.json`) 81 | //@ts-expect-error 82 | const { isLoading: isLoadingReleases, data: releaseData, error: releaseError }: {isLoading: boolean, data:ReleaseInfo, error:useFetch.UseFetchError} = useFetch(`https://api.github.com/repos/${user}/${pkg}/releases/latest`) 83 | 84 | if (isLoading) { 85 | return
86 | 😁 Loading repository data 87 |
88 | } 89 | 90 | if (error) { 91 | return
92 | 😔 Skipping invalid package: {user}.{pkg} 93 |
94 | } 95 | 96 | const installed = isInstalled(data.name, data.version, repoType) 97 | 98 | return <> 99 | { 100 | isDownloading && !isLoadingReleases 101 | ? {}} size={releaseData?.assets[0]?.size} name={data.name} repoType={repoType} url={releaseData?.assets[0]?.browser_download_url}/> 102 | : <> // Maybe display some package size information in the future? 103 | } 104 |
105 | 106 |
107 |

108 | {data.name ?? 'Invalid Name'} 109 | v{data.version ?? 'Unknown version'} 110 |

111 |

112 | {data.description ?? 'No description'} 113 |

114 |

115 | by: {data.author} 116 | /@{user} 117 |

118 |
119 |
121 | 156 |
157 |
158 | 159 | } 160 | 161 | export default PackageList -------------------------------------------------------------------------------- /src/render/components/extensions/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {X} from 'react-feather' 3 | import {ipcRenderer} from 'electron' 4 | 5 | interface Props { 6 | title: string 7 | } 8 | 9 | let animationId: number; 10 | let mouseX: number; 11 | let mouseY: number; 12 | 13 | function onMouseDown(e: any) { 14 | mouseX = e.clientX; 15 | mouseY = e.clientY; 16 | 17 | document.addEventListener('mouseup', onMouseUp) 18 | requestAnimationFrame(moveWindow); 19 | } 20 | 21 | function onMouseUp(e: any) { 22 | ipcRenderer.send('windowMoved'); 23 | document.removeEventListener('mouseup', onMouseUp) 24 | cancelAnimationFrame(animationId) 25 | } 26 | 27 | function moveWindow() { 28 | ipcRenderer.send('moveExtensionWindow', { mouseX, mouseY }); 29 | animationId = requestAnimationFrame(moveWindow); 30 | } 31 | 32 | const TitleBar = (props: Props) => { 33 | return ( 34 |
35 |
36 | 37 |
38 |
39 | {props.title} 40 |
41 |
42 | { 43 | ipcRenderer.send('closeExtensionWindow') 44 | }}/> 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default TitleBar; -------------------------------------------------------------------------------- /src/render/components/extensions/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import useFetch from "react-fetch-hook" 3 | import {Box, Feather} from 'react-feather' 4 | import PackageItem from '../PackageItem' 5 | 6 | 7 | type RepoItem = { 8 | user: string, 9 | package: string 10 | } 11 | type RepoData = { 12 | [key: string] : RepoItem[] 13 | } 14 | 15 | const Home = () => { 16 | const [repoType, setRepoType] = useState('widgets') 17 | //@ts-expect-error -- For real? 18 | const { isLoading: isLoadingRepository, data: repositoryData, error }: {isLoading: boolean, data: RepoData} = useFetch('https://raw.githubusercontent.com/hyperts/hyperassets/master/communityrepository.json') 19 | 20 | // const [downloadProgress, setDownloadProgress] = useState() 21 | 22 | if (isLoadingRepository) { 23 | return <>Loading repository data 24 | } 25 | 26 | if (error){ 27 | return
28 |
29 | 😔 Unable to reach hyper repositories: Check your internet connection 30 |
31 |
32 | } 33 | 34 | return <> 35 |
36 | 44 | 52 |
53 |
54 | { 55 | repositoryData[repoType].map( repo => { 56 | return { 61 | }} 62 | key={`${repo.package}.${repo.user}.item` 63 | }/> 64 | }) 65 | } 66 |
67 | 68 | } 69 | 70 | export default Home -------------------------------------------------------------------------------- /src/render/components/extensions/pages/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import * as Icon from 'react-feather' 3 | import { ipcRenderer } from 'electron'; 4 | 5 | const {ChevronLeft, ChevronRight} = Icon 6 | 7 | const Pages = [ 8 | { 9 | icon: 'Box', 10 | title: 'Widgets', 11 | text:

Hyper is modular by core.
12 | Minimalist by default and Extensible to the limit. 13 |

14 | Everything works around extensions called widgets 15 | In the next pages you can pick some extensions

, 16 | image: 'assets://welcome_slidewidgets.png', 17 | color: '#7614F5' 18 | }, 19 | { 20 | icon: 'Feather', 21 | title: 'Themes', 22 | text:

Made with only modern CSS, the look can be drastically modified by installing themes.
23 |
24 | Themes are injected into every component, making possible to customize absolutely everything.

, 25 | image: 'assets://welcome_slidethemes.png', 26 | color: '#000' 27 | }, 28 | { 29 | icon: 'Zap', 30 | title: 'Tricks', 31 | image: 'assets://welcome_slidetricks.png', 32 | color: '#7614F5', 33 | text:

34 | • Hold CTRL and right click anywhere in the bar to show options.

35 | • Enable DebugMode in settings when editting themes/making widgets.

36 | • Widgets can have their own window and properties, making hyper infinitely extensible. 37 |

38 | } 39 | ] 40 | 41 | const Welcome = () => { 42 | const [currentPage, setcurrentPage] = useState(0) 43 | const pageObj = Pages[currentPage] 44 | //@ts-ignore 45 | const Glyph : ElementType = Icon[pageObj.icon] 46 | return ( 47 |
48 |
49 |

Welcome to Hyper.

50 | Since it's your first time, i have to explain some things to you. 51 |
52 |
53 |
54 |
{pageObj.title}
55 |
{pageObj.text}
56 |
57 |
62 |
{ 64 | setcurrentPage(currentPage - 1 <= 0 ? 0 : currentPage - 1) 65 | }} 66 | > 67 | 68 |
69 |
{ 71 | setcurrentPage(currentPage + 1 > Pages.length - 1 ? Pages.length - 1 : currentPage + 1) 72 | if (currentPage + 1 > Pages.length - 1 ) { 73 | ipcRenderer.send('refreshExtensionWindow') 74 | } 75 | }} 76 | > 77 | 78 |
79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | export default Welcome; -------------------------------------------------------------------------------- /src/render/components/settings/dropZone.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo} from 'react'; 2 | import {useDropzone} from 'react-dropzone'; 3 | import { Upload } from 'react-feather'; 4 | 5 | const baseStyle = { 6 | flex: 1, 7 | display: 'flex', 8 | flexDirection: 'column', 9 | alignItems: 'center', 10 | padding: '20px', 11 | borderWidth: 2, 12 | borderRadius: 6, 13 | borderColor: '#272727', 14 | borderStyle: 'dashed', 15 | backgroundColor: '#212121', 16 | outline: 'none', 17 | transition: 'all .24s ease-in-out', 18 | color: 'rgb(107, 114, 128)' 19 | }; 20 | 21 | const activeStyle = { 22 | borderColor: '#212121', 23 | color: '#ffffff' 24 | }; 25 | 26 | const acceptStyle = { 27 | borderColor: '#1FDC98', 28 | color: '#ffffff' 29 | }; 30 | 31 | const rejectStyle = { 32 | borderColor: '#CF6679' 33 | }; 34 | 35 | interface UploadInterfaceProps { 36 | onDrop: (files: File[]) => void; 37 | } 38 | 39 | function StyledDropzone(props: UploadInterfaceProps) { 40 | 41 | const onDrop = useCallback((files: File[]) => { 42 | props.onDrop(files) 43 | }, []) 44 | 45 | 46 | const { 47 | getRootProps, 48 | getInputProps, 49 | isDragActive, 50 | isDragAccept, 51 | isDragReject 52 | } = useDropzone({ 53 | accept: 'zip,application/octet-stream,application/zip,application/x-zip,application/x-zip-compressed', 54 | onDrop, 55 | maxFiles: 1 56 | }); 57 | 58 | const style = useMemo(() => ({ 59 | ...baseStyle, 60 | ...(isDragActive ? activeStyle : {}), 61 | ...(isDragAccept ? acceptStyle : {}), 62 | ...(isDragReject ? rejectStyle : {}) 63 | }), [ 64 | isDragActive, 65 | isDragReject, 66 | isDragAccept 67 | ]); 68 | 69 | return ( 70 |
71 | {/*@ts-expect-error */} 72 |
73 | 74 | { 75 | isDragAccept ? 76 |

⚡ Drop it!

: 77 |

Drop/click to install (ZIP only)

78 | } 79 |
80 |
81 | ); 82 | } 83 | 84 | export default StyledDropzone -------------------------------------------------------------------------------- /src/render/components/settings/input.tsx: -------------------------------------------------------------------------------- 1 | import { Config } from '../../../shared/config'; 2 | import React from 'react'; 3 | import { ColorPicker, useColor } from "react-color-palette"; 4 | 5 | import '../../style/input.module.css' 6 | 7 | type InputProps = { 8 | value: any; 9 | type: string; 10 | category: string; 11 | entry: string; 12 | field: string; 13 | options?: Array; 14 | change?: () => void; 15 | } 16 | 17 | 18 | function Input({ category, entry, field, value, type, options, change }: InputProps) { 19 | 20 | 21 | const config = new Config() 22 | 23 | function applyChange(newValue: any) { 24 | change?.() 25 | config.set(category, entry, field, newValue, (Err) => console.log) 26 | config.save(()=>{ 27 | change?.() 28 | }) 29 | } 30 | 31 | 32 | function renderForm() { 33 | switch (type) { 34 | case "color": 35 | const [color, setColor] = useColor("hex", value); 36 | 37 | if (color.hex !== value) { 38 | applyChange(color.hex) 39 | } 40 | 41 | return <> 42 |
43 | 44 |
45 | 46 | break; 47 | case "text": 48 | return <> 49 |
50 |
51 | { 58 | console.log("Field", e.target.value) 59 | change?.() 60 | applyChange(e.target.value) 61 | }} 62 | /> 63 |
64 |
65 | 66 | break; 67 | case "checkbox": 68 | return <> 69 | 82 | 83 | case "number": 84 | return <> 85 |
86 |
87 | 101 | { 107 | applyChange(Number(e.target.value)) 108 | }} 109 | /> 110 | 124 |
125 |
126 | 127 | case "selection": 128 | return <> 129 |
130 |
131 | 135 | 149 |
150 | 151 |
152 | 153 | default: 154 | return <> 155 |
😞 Field type not recognized
156 | 157 | } 158 | } 159 | 160 | 161 | return ( 162 |
163 | {renderForm()} 164 |
165 | ); 166 | } 167 | 168 | export default Input; -------------------------------------------------------------------------------- /src/render/components/settings/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function About() { 4 | 5 | return <> 6 | :) 7 | 8 | } 9 | 10 | export default About -------------------------------------------------------------------------------- /src/render/components/settings/pages/default.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType} from 'react'; 2 | import { Config } from '../../../../shared/config'; 3 | import * as Icon from 'react-feather'; 4 | import type { ConfigItem } from'../../../../@types/hyper' 5 | 6 | import Input from '../input'; 7 | 8 | type PageProps = { 9 | active: string, 10 | change?: () => void; 11 | } 12 | 13 | function ConfigPage( {active, change}: PageProps ) { 14 | const config = new Config() 15 | const { category, entry } = { category: active.split('.')[0], entry: active.split('.')[1] } 16 | 17 | 18 | function generateFields(entryData: ConfigItem) { 19 | return Object.keys(entryData.fields).map( field =>{ 20 | const fieldData = entryData.fields[field] 21 | 22 | return <> 23 |
24 |

{fieldData.name}

25 |

{fieldData.description}

26 | 27 |
28 | 29 | }) 30 | } 31 | 32 | function generateEntry() { 33 | 34 | const entryData = config.getEntry(category, entry) 35 | 36 | //@ts-ignore 37 | const Glyph : ElementType = Icon[entryData.icon] 38 | 39 | if (!entryData.fields) { 40 | return <> 41 |
42 |
43 |
44 |
45 |
46 |

🤷‍♂️ There's nothing here

47 |

There's no fields for this config page

48 |
49 |
50 | 51 | } 52 | return <> 53 |
54 |

{Glyph && }{entryData.name}

55 |

{entryData.description}

56 |
57 | {generateFields(entryData)} 58 |
59 |
60 | 61 | 62 | } 63 | 64 | if (!config.data) { 65 | return <> 66 |
67 |
68 |
69 |
70 |
71 |

😭 Oh, my...

72 |

This config page is invalid, call the devs

73 |
74 |
75 | 76 | } 77 | 78 | return <> 79 | {generateEntry()} 80 | 81 | } 82 | 83 | export default ConfigPage -------------------------------------------------------------------------------- /src/render/components/settings/pages/themes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ThemesPageProps { 4 | change?: () => void; 5 | } 6 | 7 | function Themes(props:ThemesPageProps) { 8 | 9 | return <> 10 | 11 | 12 | } 13 | 14 | export default Themes -------------------------------------------------------------------------------- /src/render/components/settings/pages/widgets.tsx: -------------------------------------------------------------------------------- 1 | // TODO: Allow widget creators to pick icon and/or image. 2 | 3 | import React, { useState } from 'react'; 4 | import Dropzone from '../dropZone'; 5 | import * as Icon from 'react-feather'; 6 | import WidgetRepository from './../../../../shared/widget' 7 | import WidgetItem from '../../widgets/widgetItem' 8 | 9 | interface WidgetPageProps { 10 | change?: () => void; 11 | } 12 | 13 | function Widgets(props: WidgetPageProps) { 14 | const [loading, setLoading] = useState(false) 15 | const DetectedWidgets = new WidgetRepository() 16 | DetectedWidgets.loadWidgetsInPaths() 17 | 18 | // //@ts-ignore 19 | // const Glyph : ElementType = Icon[icon] 20 | 21 | return
22 |
23 |

Installed widgets

24 |

This list contains your installed and detected widgets

25 | {DetectedWidgets.loadedWidgets.map(widget => { 26 | if (!widget) { return } 27 | return { 36 | const widgetRepository = new WidgetRepository() 37 | widgetRepository.uninstallWidget(widget, () => { 38 | window.location.reload() 39 | props.change?.() 40 | })} 41 | } 42 | /> 43 | })} 44 |
45 |
46 | {/* 47 |
48 | 49 |
*/} 50 | { 51 | setLoading(true) 52 | const widgetRepository = new WidgetRepository() 53 | widgetRepository.installWidget(files[0].path, ()=>{ 54 | setLoading(false) 55 | props.change?.() 56 | }) 57 | }}/> 58 | 59 | { 60 | loading && <> 61 | } 62 |
63 | } 64 | 65 | export default Widgets -------------------------------------------------------------------------------- /src/render/components/settings/refreshPrompt.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { motion, useAnimation } from "framer-motion"; 3 | import { refresh } from '../../ipc' 4 | 5 | import { RefreshCw, ChevronUp } from 'react-feather'; 6 | 7 | 8 | function refreshPrompt() { 9 | const [collapsed, setCollapsed] = useState(false) 10 | const controls = useAnimation() 11 | const arrowControls = useAnimation() 12 | 13 | const sequence = async () => { 14 | await controls.start({ y: 0, x: "-50%", transition: { duration: 0 } }) 15 | await controls.start({ y: -60, transition: { duration: .5 } }) 16 | await controls.start({ y: -25, transition: { duration: .3, delay: 2 } }) 17 | setCollapsed(true) 18 | return await arrowControls.start({ opacity: 100, transition: { duration: .3 } }) 19 | } 20 | 21 | 22 | useEffect(() => { 23 | sequence() 24 | }, []) 25 | 26 | return <> 27 | 31 | { 34 | controls.start({ 35 | y: !collapsed ? -25 : -60, 36 | transition: { duration: .3 } 37 | }) 38 | await arrowControls.start({ height: 2, opacity: !collapsed ? 100 : 0, transition: { duration: .3 } }) 39 | 40 | setCollapsed(!collapsed) 41 | }} 42 | animate={arrowControls} 43 | > 44 | 45 | 46 |
47 |

50 | Refresh to apply 51 |

52 | 58 |
59 | 60 |
61 | 62 | } 63 | 64 | export default refreshPrompt -------------------------------------------------------------------------------- /src/render/components/settings/sidebar/item.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType } from 'react'; 2 | 3 | import * as Icon from 'react-feather'; 4 | 5 | type ItemProps = { 6 | name: string, 7 | category: string, 8 | entry: string, 9 | active?: string, 10 | click?: (category: string)=> void, 11 | icon: string, 12 | key?: string 13 | } 14 | 15 | function Item( {name, click, active, icon, category, entry } : ItemProps) { 16 | //@ts-ignore 17 | const Glyph : ElementType = Icon[icon] 18 | 19 | return <> 20 |
  • { 32 | console.log("Clicked", name) 33 | if (click) { click(`${category}.${entry}`) } 34 | }} 35 | > 36 | { Glyph && } 37 | {name} 38 |
  • 39 | 40 | } 41 | 42 | export default Item -------------------------------------------------------------------------------- /src/render/components/settings/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PerfectScrollbar from 'perfect-scrollbar'; 3 | import { Config } from '../../../../shared/config'; 4 | import type { ConfigTable, ConfigCategory } from'../../../../@types/hyper' 5 | 6 | import '../../../style/index.css'; 7 | import '../../../style/settings.module.css'; 8 | 9 | import Item from './item'; 10 | 11 | const config = new Config() 12 | 13 | type SidebarProps = { 14 | active: string; 15 | onChange?: (page: string)=> void; 16 | } 17 | class Sidebar extends Component { 18 | 19 | generateEntries(category: string) { 20 | const categoryData = config.data[category] as ConfigCategory 21 | 22 | return Object.keys(categoryData.items).map( entry => { 23 | const entryData = categoryData.items[entry] 24 | return 25 | }) 26 | } 27 | 28 | generateCategories() { 29 | return Object.keys(config.data as ConfigTable).map( entry => { 30 | const category = config.data[entry] as ConfigCategory 31 | 32 | return ( 33 | <> 34 | {category.name} 35 |
      36 | {this.generateEntries(entry)} 37 |
    38 | 39 | ) 40 | }) 41 | } 42 | 43 | componentDidMount(){ 44 | new PerfectScrollbar('#container',{ 45 | wheelSpeed: 2, 46 | wheelPropagation: true, 47 | minScrollbarLength: 20 48 | }) 49 | } 50 | 51 | render() { 52 | return ( 53 |
    54 | 69 |
    70 | ); 71 | } 72 | } 73 | 74 | export default Sidebar; -------------------------------------------------------------------------------- /src/render/components/settings/titlebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {X} from 'react-feather' 3 | 4 | import {ipcRenderer} from 'electron' 5 | 6 | let animationId: number; 7 | let mouseX: number; 8 | let mouseY: number; 9 | 10 | function onMouseDown(e: any) { 11 | mouseX = e.clientX; 12 | mouseY = e.clientY; 13 | 14 | document.addEventListener('mouseup', onMouseUp) 15 | requestAnimationFrame(moveWindow); 16 | } 17 | 18 | function onMouseUp(e: any) { 19 | ipcRenderer.send('windowMoved'); 20 | document.removeEventListener('mouseup', onMouseUp) 21 | cancelAnimationFrame(animationId) 22 | } 23 | 24 | function moveWindow() { 25 | ipcRenderer.send('moveSettingsWindow', { mouseX, mouseY }); 26 | animationId = requestAnimationFrame(moveWindow); 27 | } 28 | 29 | function Titlebar() { 30 | return <> 31 |
    41 | { ipcRenderer.send('closeSettings') } } 51 | /> 52 | 53 |
    54 | 55 | } 56 | 57 | export default Titlebar -------------------------------------------------------------------------------- /src/render/components/widgets/widgetItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Icon from 'react-feather'; 3 | 4 | interface Props { 5 | image?: string 6 | name: string 7 | description: string 8 | author?: string 9 | repository?: string 10 | installed?: boolean 11 | version?: string 12 | onClick?: () => void 13 | uninstall?: () => void 14 | install?: () => void 15 | } 16 | 17 | const WidgetItem = (props: Props) => { 18 | const defaultImage = "https://i.imgur.com/vccRW0H.png" 19 | 20 | return <> 21 |
    22 | 23 |
    24 |

    {props.name} {props.version && `[${props.version}]`}

    25 | {props.description} 26 | {props.author ?? 'Unknown author'} 27 |
    28 |
    29 | {props.repository && 30 | 34 | 35 | 36 | } 37 | {props.installed ? 43 | : 49 | } 50 |
    51 |
    52 | ; 53 | }; 54 | 55 | export default WidgetItem; -------------------------------------------------------------------------------- /src/render/components/widgets/widgetUpload.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperts/Hyper/30f1a4a6c0878b9c2b55898d08f8716ceff119c6/src/render/components/widgets/widgetUpload.tsx -------------------------------------------------------------------------------- /src/render/ipc.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from "electron" 2 | 3 | 4 | export function openSettings() { 5 | ipcRenderer.send('openSettings') 6 | } 7 | 8 | export function openContext() { 9 | ipcRenderer.send("show-context-menu") 10 | } 11 | 12 | export function refresh() { 13 | ipcRenderer.send('refreshApp') 14 | } 15 | 16 | export function openExtensions() { 17 | ipcRenderer.send('openExtensions', true) 18 | } -------------------------------------------------------------------------------- /src/render/style/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind screens; 5 | 6 | @layer utilities { 7 | .row { 8 | @apply flex flex-row items-center; 9 | } 10 | .col { 11 | @apply flex flex-col items-center; 12 | } 13 | } 14 | 15 | body, html { 16 | background: transparent !important; 17 | font-family: 'Montserrat', sans-serif; 18 | } 19 | /* Firefox */ 20 | * { 21 | scrollbar-width: thin; 22 | scrollbar-color: #272727 #121212; 23 | } 24 | 25 | /* Chrome, Edge, and Safari */ 26 | *::-webkit-scrollbar { 27 | width: 6px; 28 | } 29 | 30 | *::-webkit-scrollbar-track { 31 | background: #121212; 32 | } 33 | 34 | *::-webkit-scrollbar-thumb { 35 | background-color: #272727; 36 | border-radius: 10px; 37 | border: none; 38 | } -------------------------------------------------------------------------------- /src/render/style/input.module.css: -------------------------------------------------------------------------------- 1 | 2 | .toggle-path { 3 | transition: background 0.3s ease-in-out; 4 | } 5 | .toggle-circle { 6 | left: 0.25rem; 7 | top: 0.1rem; 8 | transition: all 0.3s ease-in-out; 9 | } 10 | 11 | input:checked ~ .toggle-circle { 12 | transform: translateX(100%); 13 | } 14 | 15 | input:checked ~ .toggle-path { 16 | background-color:#7614F5; 17 | } 18 | 19 | input:checked ~ .toggle-circle { 20 | background-color:#121212 !important; 21 | } 22 | 23 | /* Numbers */ 24 | input[type='number']::-webkit-inner-spin-button, 25 | input[type='number']::-webkit-outer-spin-button { 26 | -webkit-appearance: none; 27 | margin: 0; 28 | } 29 | 30 | .custom-number-input input:focus { 31 | outline: none !important; 32 | } 33 | 34 | .custom-number-input button:focus { 35 | outline: none !important; 36 | } 37 | 38 | 39 | /* Dropdown */ 40 | .dropdown:hover .dropdown-menu { 41 | display: block; 42 | z-index: 99; 43 | border-radius: 6px; 44 | } 45 | .dropdown-menu li:first-child a { 46 | border-radius: 6px 6px 0 0; 47 | } 48 | 49 | .dropdown-menu li:last-child a { 50 | border-radius: 0 0 6px 6px; 51 | } 52 | /* Color selector */ 53 | .rcp-light { 54 | --rcp-background: #ffffff; 55 | --rcp-input-text: #111111; 56 | --rcp-input-border: rgba(0, 0, 0, 0.1); 57 | --rcp-input-label: #717171; 58 | } 59 | 60 | .rcp-dark { 61 | --rcp-background: #181818; 62 | --rcp-input-text: #f3f3f3; 63 | --rcp-input-border: rgba(255, 255, 255, 0.1); 64 | --rcp-input-label: #999999; 65 | } 66 | 67 | .rcp { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | 72 | background-color: var(--rcp-background); 73 | border-radius: 6px; 74 | box-sizing: border-box; 75 | } 76 | 77 | .rcp-body { 78 | display: flex; 79 | flex-direction: column; 80 | align-items: center; 81 | justify-content: center; 82 | gap: 20px; 83 | width: 100%; 84 | 85 | padding: 20px; 86 | } 87 | 88 | .rcp-saturation { 89 | position: relative; 90 | 91 | width: 100%; 92 | background-image: linear-gradient(transparent, black), linear-gradient(to right, white, transparent); 93 | border-radius: 6px 6px 0 0; 94 | 95 | user-select: none; 96 | } 97 | 98 | .rcp-saturation-cursor { 99 | position: absolute; 100 | 101 | width: 20px; 102 | height: 20px; 103 | 104 | border: 2px solid #000; 105 | border-radius: 50%; 106 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15); 107 | box-sizing: border-box; 108 | 109 | transform: translate(-10px, -10px); 110 | } 111 | 112 | .rcp-hue { 113 | position: relative; 114 | 115 | width: 100%; 116 | height: 12px; 117 | 118 | background-image: linear-gradient( 119 | to right, 120 | rgb(255, 0, 0), 121 | rgb(255, 255, 0), 122 | rgb(0, 255, 0), 123 | rgb(0, 255, 255), 124 | rgb(0, 0, 255), 125 | rgb(255, 0, 255), 126 | rgb(255, 0, 0) 127 | ); 128 | border-radius: 10px; 129 | 130 | user-select: none 131 | } 132 | 133 | .rcp-hue-cursor { 134 | position: absolute; 135 | 136 | width: 20px; 137 | height: 20px; 138 | 139 | border: 2px solid #000; 140 | border-radius: 50%; 141 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; 142 | box-sizing: border-box; 143 | 144 | transform: translate(-10px, -4px); 145 | } 146 | 147 | .rcp-alpha { 148 | position: relative; 149 | 150 | width: 100%; 151 | height: 12px; 152 | 153 | border-radius: 10px; 154 | 155 | user-select: none; 156 | } 157 | 158 | .rcp-alpha-cursor { 159 | position: absolute; 160 | 161 | width: 20px; 162 | height: 20px; 163 | 164 | border: 2px solid #000; 165 | border-radius: 50%; 166 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; 167 | box-sizing: border-box; 168 | 169 | transform: translate(-10px, -4px); 170 | } 171 | 172 | .rcp-fields { 173 | display: grid; 174 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 175 | gap: 10px; 176 | 177 | width: 100%; 178 | } 179 | 180 | .rcp-fields-element { 181 | display: flex; 182 | flex-direction: column; 183 | align-items: center; 184 | gap: 5px; 185 | 186 | width: 100%; 187 | } 188 | 189 | .hex-element { 190 | grid-row: 1; 191 | } 192 | 193 | .hex-element:nth-child(3n) { 194 | grid-column: 1 / -1; 195 | } 196 | 197 | .rcp-fields-element-input { 198 | width: 100%; 199 | 200 | font-size: 14px; 201 | font-weight: 600; 202 | 203 | color: var(--rcp-input-text); 204 | text-align: center; 205 | 206 | background: none; 207 | border: 2px solid; 208 | border-color: var(--rcp-input-border); 209 | border-radius: 5px; 210 | box-sizing: border-box; 211 | 212 | outline: none; 213 | 214 | padding: 10px; 215 | } 216 | 217 | .rcp-fields-element-label { 218 | font-size: 14px; 219 | font-weight: 600; 220 | 221 | color: var(--rcp-input-label); 222 | text-transform: uppercase; 223 | } 224 | -------------------------------------------------------------------------------- /src/render/style/loading.css: -------------------------------------------------------------------------------- 1 | @keyframes flash{ 2 | 0%, to { opacity: 1; } 3 | 4% { opacity: 0; } 4 | 6% { opacity: .6; } 5 | 8% { opacity: .2; } 6 | 10% { opacity: .9; } 7 | } 8 | 9 | @keyframes float{ 10 | 0%, 100% { transform: translateY(0); } 11 | 50% { transform: translateY(5px); } 12 | } 13 | 14 | .dragger { 15 | -webkit-app-region: drag 16 | } 17 | 18 | .lightningBolt { 19 | position: absolute; 20 | z-index: 99; 21 | width: 70px; 22 | margin: 0 auto; 23 | left: 0; 24 | right: 0; 25 | position: absolute; 26 | animation: flash 3s infinite, float 3s ease-in-out infinite; 27 | } 28 | 29 | .lightningBolt:before, 30 | .lightningBolt:after { 31 | content: ''; 32 | position: absolute; 33 | } 34 | .lightningBolt:before { 35 | width: 0; 36 | height: 0; 37 | left: -4px; 38 | top: 5px; 39 | border-style: solid; 40 | border-width: 15px 3px 14px 60px; 41 | border-color: transparent transparent #fbeb36 transparent; 42 | transform: rotate(-60deg) scaleX(.5); 43 | } 44 | .lightningBolt:after { 45 | width: 0; 46 | height: 0; 47 | left: 4px; 48 | top: -10px; 49 | border-style: solid; 50 | border-width: 15px 3px 14px 60px; 51 | border-color: transparent transparent #fbeb36 transparent; 52 | transform: rotate(-60deg) scaleY(-1) scaleX(-.5); 53 | } 54 | 55 | *, *::after, *::before { 56 | -webkit-user-select: none; 57 | user-select: none; 58 | -webkit-user-drag: none; 59 | -webkit-app-region: no-drag; 60 | cursor: default; 61 | } -------------------------------------------------------------------------------- /src/render/style/settings.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | 3 | } 4 | /* 5 | * Container style 6 | */ 7 | .ps { 8 | overflow: hidden !important; 9 | overflow-anchor: none; 10 | -ms-overflow-style: none; 11 | touch-action: auto; 12 | -ms-touch-action: auto; 13 | } 14 | 15 | /* 16 | * Scrollbar rail styles 17 | */ 18 | .ps__rail-x { 19 | display: none; 20 | opacity: 0; 21 | transition: background-color .2s linear, opacity .2s linear; 22 | -webkit-transition: background-color .2s linear, opacity .2s linear; 23 | height: 15px; 24 | /* there must be 'bottom' or 'top' for ps__rail-x */ 25 | bottom: 0px; 26 | /* please don't change 'position' */ 27 | position: absolute; 28 | } 29 | 30 | .ps__rail-y { 31 | display: none; 32 | opacity: 0; 33 | transition: background-color .2s linear, opacity .2s linear; 34 | -webkit-transition: background-color .2s linear, opacity .2s linear; 35 | width: 15px; 36 | /* there must be 'right' or 'left' for ps__rail-y */ 37 | right: 0; 38 | /* please don't change 'position' */ 39 | position: absolute; 40 | } 41 | 42 | .ps--active-x > .ps__rail-x, 43 | .ps--active-y > .ps__rail-y { 44 | display: block; 45 | background-color: transparent; 46 | } 47 | 48 | .ps:hover > .ps__rail-x, 49 | .ps:hover > .ps__rail-y, 50 | .ps--focus > .ps__rail-x, 51 | .ps--focus > .ps__rail-y, 52 | .ps--scrolling-x > .ps__rail-x, 53 | .ps--scrolling-y > .ps__rail-y { 54 | opacity: 0.6; 55 | } 56 | 57 | .ps .ps__rail-x:hover, 58 | .ps .ps__rail-y:hover, 59 | .ps .ps__rail-x:focus, 60 | .ps .ps__rail-y:focus, 61 | .ps .ps__rail-x.ps--clicking, 62 | .ps .ps__rail-y.ps--clicking { 63 | background-color: transparent; 64 | opacity: 0.9; 65 | } 66 | 67 | /* 68 | * Scrollbar thumb styles 69 | */ 70 | .ps__thumb-x { 71 | background-color: #2E2E2E; 72 | border-radius: 6px; 73 | transition: background-color .2s linear, height .2s ease-in-out; 74 | -webkit-transition: background-color .2s linear, height .2s ease-in-out; 75 | height: 6px; 76 | /* there must be 'bottom' for ps__thumb-x */ 77 | bottom: 2px; 78 | /* please don't change 'position' */ 79 | position: absolute; 80 | } 81 | 82 | .ps__thumb-y { 83 | background-color: #2E2E2E; 84 | border-radius: 6px; 85 | transition: background-color .2s linear, width .2s ease-in-out; 86 | -webkit-transition: background-color .2s linear, width .2s ease-in-out; 87 | width: px; 88 | /* there must be 'right' for ps__thumb-y */ 89 | right: 2px; 90 | /* please don't change 'position' */ 91 | position: absolute; 92 | } 93 | 94 | .ps__rail-x:hover > .ps__thumb-x, 95 | .ps__rail-x:focus > .ps__thumb-x, 96 | .ps__rail-x.ps--clicking .ps__thumb-x { 97 | background-color: #2E2E2E; 98 | height: 11px; 99 | } 100 | 101 | .ps__rail-y:hover > .ps__thumb-y, 102 | .ps__rail-y:focus > .ps__thumb-y, 103 | .ps__rail-y.ps--clicking .ps__thumb-y { 104 | background-color: #2E2E2E; 105 | width: 8px; 106 | } 107 | 108 | /* MS supports */ 109 | @supports (-ms-overflow-style: none) { 110 | .ps { 111 | overflow: auto !important; 112 | } 113 | } 114 | 115 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 116 | .ps { 117 | overflow: auto !important; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/render/utils.ts: -------------------------------------------------------------------------------- 1 | import WidgetRepository from '../shared/widget' 2 | import ThemeRepository from '../shared/theme' 3 | 4 | export function loadWidgets() { 5 | const widgetController = new WidgetRepository() 6 | widgetController.loadWidgetsInPaths() 7 | widgetController.watchWidgets() 8 | widgetController.loadStyles() 9 | return widgetController.loadedWidgets 10 | } 11 | 12 | export function loadThemes() { 13 | const themeController = new ThemeRepository() 14 | themeController.loadThemes() 15 | themeController.setup() 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/render/windows/console.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { ipcRenderer } from 'electron' 4 | import { MessageLevel, MessagePayload } from '../../shared/logger'; 5 | 6 | import '../style/index.css' 7 | 8 | const styles = { 9 | [MessageLevel.SUCCESS]: { 10 | color: '#709e70' 11 | }, 12 | [MessageLevel.INFO]: { 13 | color: '#A0A4A1' 14 | }, 15 | [MessageLevel.DEBUG]: { 16 | color: '#666b6f' 17 | }, 18 | [MessageLevel.WARN] : { 19 | color: '#996555' 20 | }, 21 | [MessageLevel.ERROR]: { 22 | color: '#8c2b24' 23 | }, 24 | time: { 25 | backgroundColor: '#272c29', 26 | color: '#687D68' 27 | } 28 | } 29 | 30 | const LevelStrings = { 31 | [MessageLevel.ERROR]: 'Error', 32 | [MessageLevel.WARN]: 'Warn', 33 | [MessageLevel.SUCCESS]: 'Success', 34 | [MessageLevel.DEBUG]: 'Debug', 35 | [MessageLevel.INFO]: 'Info', 36 | } 37 | 38 | const preRenderHistory: any[] = [] 39 | 40 | ipcRenderer.on('h_logmessage', (e, data) =>{ 41 | const message: MessagePayload = data ?? e 42 | console.log('Message is here', message) 43 | preRenderHistory.push(message) 44 | }) 45 | 46 | function App() { 47 | const [filter, setFilter]: [any, (item: any)=>void] = useState({}) 48 | const [history] = useState(preRenderHistory) 49 | const [lastUpdate, setLastUpdate] = useState(new Date()) 50 | const [selectedLine, setSelectedLine]: [undefined | MessagePayload, any] = useState(undefined) 51 | const containerEl = useRef(null); 52 | 53 | useEffect(()=>{ 54 | ipcRenderer.send('h_consoleready') 55 | ipcRenderer.on('h_logmessage', (e, data) =>{ 56 | setLastUpdate(new Date()) 57 | }) 58 | }, []) 59 | 60 | useEffect(()=>{ 61 | if (containerEl) { 62 | 63 | 64 | //@ts-expect-error -- Common, do i really need to make typing for you? isn't that obvious? 65 | containerEl?.current?.addEventListener('DOMNodeInserted', event => { 66 | const { currentTarget: target } = event; 67 | target.scroll({ top: target.scrollHeight, behavior:'smooth' }); 68 | 69 | }); 70 | } 71 | }) 72 | 73 | function filterDisplay(): any[] { 74 | let display: any[] = [] 75 | 76 | if (Object.keys(filter).length === 0) { display = history } 77 | else { 78 | display = history.filter( (item: MessagePayload) => { 79 | console.log(item.message, item.level, filter.level) 80 | if (filter.level) { 81 | return item.level === filter.level 82 | } 83 | if (filter.text && filter.text.trim() !== '') { 84 | return item.message.toLowerCase().includes(filter.text.toLowerCase()) || item.name.toLowerCase().includes(filter.text.toLowerCase()) 85 | } 86 | if (filter.name) { 87 | return item.name.includes(filter.name) 88 | } 89 | }) 90 | } 91 | return display 92 | } 93 | 94 | function renderLines() { 95 | console.log('filter', filter) 96 | return filterDisplay().map( (line, index) => { 97 | return
  • { 102 | if (selectedLine == line) { 103 | setSelectedLine(undefined) 104 | } else { 105 | setSelectedLine(line) 106 | } 107 | }} 108 | > 109 |
    110 | {line.time.toLocaleTimeString()} 111 |
    112 |
    113 | {line.name.toUpperCase()} 114 |
    115 |
    120 | {line.message.split('\n').map((str: string) =>

    {str}

    )} 121 |
    122 |
  • 123 | }) 124 | } 125 | 126 | return
    127 |
    128 |
    129 |
    Hyper Console - Last update: {lastUpdate.toLocaleTimeString()}
    130 |
    131 |
      132 | {renderLines()} 133 |
    134 |
    135 |
    136 | 150 | 161 | 172 | 183 | 194 | { 199 | if (e.target.value.trim() === '') { 200 | setFilter({}) 201 | } else { 202 | setFilter({ 203 | ...filter, 204 | text: e.target.value.trim() 205 | }) 206 | } 207 | }} 208 | /> 209 |
    210 |
    211 |
    212 |
    213 | Logger 214 | {/*@ts-expect-error This is internal tooling, i don't give a fuck.*/} 215 |
    {selectedLine ? selectedLine.name : '-'}
    216 |
    217 |
    218 | Level 219 | {/*@ts-expect-error This is internal tooling, i don't give a fuck.*/} 220 |
    {selectedLine ? LevelStrings[selectedLine.level] : '-'}
    221 |
    222 |
    223 | Message 224 | {/*@ts-expect-error This is internal tooling, i don't give a fuck.*/} 225 |
    {selectedLine ? selectedLine.message.split('\n').map((str: string) =>

    {str}

    ) : '-'}
    226 |
    227 |
    228 | Stack 229 |
    {selectedLine 230 | /*@ts-expect-error This is internal tooling, i don't give a fuck.*/ 231 | ? selectedLine.stack.map(str=>{ 232 | return

    {str}

    233 | }) 234 | : '-' 235 | } 236 |
    237 |
    238 |
    239 |
    240 |
    241 | } 242 | 243 | 244 | ReactDOM.render(, document.getElementById('root')) -------------------------------------------------------------------------------- /src/render/windows/extension.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react" 2 | import ReactDOM from "react-dom" 3 | import TitleBar from "../components/extensions/TitleBar"; 4 | import Welcome from "../components/extensions/pages/Welcome"; 5 | import Home from "../components/extensions/pages/Home"; 6 | 7 | import '../style/index.css'; 8 | import { ipcRenderer } from 'electron'; 9 | 10 | 11 | ReactDOM.render(, document.getElementById('root')) 12 | 13 | 14 | function App() { 15 | const [showTutorial, setShowTutorial] = useState(true) 16 | 17 | useEffect(()=>{ 18 | ipcRenderer.on('extensionWindowHideTutorial', () =>{ 19 | setShowTutorial(false) 20 | }) 21 | }, []) 22 | 23 | return
    24 | 25 | { showTutorial ? : } 26 |
    27 | } 28 | -------------------------------------------------------------------------------- /src/render/windows/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useLayoutEffect} from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | import '../style/index.css'; 5 | 6 | //@ts-ignore 7 | import {loadWidgets, loadThemes} from '../utils' 8 | import {openExtensions, openContext} from '../ipc' 9 | 10 | ReactDOM.render(, document.getElementById('root')) 11 | 12 | 13 | function App() { 14 | const loadedWidgets = loadWidgets() 15 | 16 | useLayoutEffect(() => { 17 | loadThemes() 18 | }, []) 19 | 20 | return
    { 24 | if (e.ctrlKey) { 25 | openContext() 26 | } 27 | }} 28 | style={{marginTop: 'var(--offset)'}} 29 | > 30 | { loadedWidgets.map(widget =>{ 31 | if (typeof widget.default === 'function') { 32 | //@ts-expect-error 33 | return 34 | } 35 | }) } 36 | {/* Using plain CSS, just in case there's also no themes loaded... for some reason */} 37 | {loadedWidgets.length <= 0 && 38 |
    50 | HYPERBAR - CLICK HERE TO START ⚡ 51 |
    } 52 |
    53 | } 54 | -------------------------------------------------------------------------------- /src/render/windows/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import axios from 'axios'; 4 | 5 | import '../style/index.css'; 6 | import '../style/loading.css' 7 | interface LoadingProps { 8 | 9 | } 10 | 11 | interface LoadingStates { 12 | artwork: {image: string, name: string, link: string, author: string}, 13 | phrase: string 14 | } 15 | 16 | class App extends React.Component { 17 | 18 | constructor(props: LoadingProps) { 19 | super(props) 20 | this.state = { 21 | artwork: {image:'', author: '', link: '', name: ''}, 22 | phrase: "" 23 | } 24 | } 25 | 26 | componentWillMount() { 27 | axios.get('https://raw.githubusercontent.com/hyperts/hyperassets/main/loadingartwork.json') // TODO: Move this to .env? 28 | .then((response)=>{ 29 | const artworkList = response.data 30 | const random = Math.floor(Math.random() * artworkList.length); 31 | this.setState({ 32 | artwork : { 33 | image: artworkList[random].image, 34 | name: artworkList[random].name, 35 | link: artworkList[random].link, 36 | author: artworkList[random].author 37 | } 38 | }) 39 | }) 40 | .catch((err)=>{ 41 | console.log(err) 42 | }) 43 | 44 | axios.get('https://raw.githubusercontent.com/hyperts/hyperassets/main/loadingquotes.json') 45 | .then((response)=>{ 46 | const phraseList = response.data 47 | const random = Math.floor(Math.random() * phraseList.length); 48 | this.setState({ 49 | phrase: phraseList[random] 50 | }) 51 | }) 52 | } 53 | 54 | render() { 55 | return <> 56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 |
    64 |
    65 |

    Hyper is loading

    66 |

    {this.state.phrase}

    67 |
    68 | Artwork: {this.state.artwork.name} by {this.state.artwork.author} 69 |
    70 |
    71 | 72 | } 73 | } 74 | 75 | ReactDOM.render( 76 | 77 | ,document.getElementById('root') 78 | ) -------------------------------------------------------------------------------- /src/render/windows/settings.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | 5 | import '../style/index.css'; 6 | 7 | import Titlebar from '../components/settings/titlebar'; 8 | import Sidebar from '../components/settings/sidebar/sidebar'; 9 | import RefreshPrompt from '../components/settings/refreshPrompt'; 10 | 11 | import About from '../components/settings/pages/about'; 12 | import Themes from '../components/settings/pages/themes'; 13 | import Widgets from '../components/settings/pages/widgets'; 14 | import Dynamic from '../components/settings/pages/default'; 15 | 16 | ReactDOM.render(, document.getElementById('root')) 17 | 18 | function App() { 19 | const [active, SetActive] = useState('hyper_about.special'); 20 | const [refreshPending, setPending] = useState(false) 21 | 22 | function settingChanged() { 23 | setPending(true) 24 | } 25 | 26 | function pageChange(category: string) { 27 | SetActive(category) 28 | } 29 | 30 | function renderPage() { 31 | switch (active) { 32 | case 'hyper_about.special': 33 | return 34 | break; 35 | case 'hyper_themes.special': 36 | return 37 | case 'hyper_widgets.special': 38 | return 39 | default: 40 | return 41 | } 42 | } 43 | 44 | return <> 45 | 46 |
    47 | 48 |
    49 | {renderPage()} 50 | {refreshPending && } 51 | 52 |
    53 |
    54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hyper ⚡ Settings 11 | 12 | 13 | 14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /src/shared/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { homedir } from 'os'; 3 | import YAML from 'yaml' 4 | import * as objSearch from 'dot-prop' 5 | import type { ConfigTable, ConfigCategory, ConfigItem, ConfigField } from '../@types/hyper' 6 | 7 | export class Config { 8 | 9 | /** 10 | * Returns data using any object path 11 | * ex: .get("mario.items") -- will return object with all objects from the category mario 12 | * @return {any} The path content 13 | */ 14 | public get: (configStr: string) => any; 15 | /** 16 | * Returns the value from specified field 17 | * @param {string} category The category which field belongs. 18 | * @param {string} field The field name 19 | * @return {string | boolean | number} The field value 20 | */ 21 | public getValue: (category: string, entry: string, field: string) => string | boolean | number; 22 | /** 23 | * Returns a category object with the provided key and 24 | * @return {ConfigEntry | undefined} The category object or undefined 25 | */ 26 | public getCategory: (category: string) => ConfigCategory; 27 | /** 28 | * Returns the item object with provided keys 29 | * @param {string} category The category which contains the wanted item 30 | * @param {string} entry The wanted entry 31 | * @return {ConfigItem} The item object 32 | */ 33 | public getEntry: (category: string, entry: string) => ConfigItem; 34 | /** 35 | * Returns the entry object with provided keys 36 | * @param {string} category The category which contains the wanted entry 37 | * @param {string} entry The wanted entry which the field belongs to 38 | * @param {string} field The field name 39 | * @return {ConfigField} The field object 40 | */ 41 | public getField: (category: string, entry: string, field:string) => ConfigField; 42 | /** 43 | * Set's a new value to the specified category and field value 44 | * @param {string} category Category where the desired entry 45 | * @param {string} entry Config entry where the desired field is 46 | * @param {string} field Field to change value 47 | * @param {string | boolean | number} value The value to be inserted 48 | * @param {(Error?: Error) => void} callback Action callback, will be called after the save procedure, may return Error. 49 | */ 50 | public set: (category: string, entry: string, field: string, value: string | boolean | number, callback: (Error?: Error)=> void) => void; 51 | /** 52 | * Saves the current config values to the user directory 53 | * Without running this, every change on config will be reverted back to normal state 54 | * @param {function} [callback] Callback function to run after saving 55 | */ 56 | public save: (callback?: () => void) => void; 57 | /** 58 | * Inserts a new category to the config table 59 | * @param {string} category The category name 60 | * @param {ConfigCategory} data The data to be inserted 61 | * @return {string} String with the object path (to be used withing config.get()) 62 | */ 63 | public insert: (category:string, data: ConfigCategory) => string; 64 | /** 65 | * Adds a new section under said category 66 | * A section is the block that holds fields, it's what the user sees on the sidebar 67 | * @param {string} category 68 | * @param {ConfigItem} data 69 | * @return {string} The oject path to the newly added entry 70 | */ 71 | public addEntry: (category: string, data: ConfigItem) => string; 72 | /** 73 | * Adds a new field under specified category 74 | * @param {string} category The category name 75 | * @param {string} entry The section inside category which holds the fields 76 | * @param {ConfigField} field The field with all of it's data to be added 77 | * @return {string} The object path to the newly added field 78 | */ 79 | public addField: (category:string, entry: string, data: ConfigField) => string; 80 | private path: string; 81 | private yaml: string; 82 | /** 83 | * The user config table itself or any other part of it based on the path used in constructor 84 | * If the constructor is empty, this will return the entire config table (ConfigTable type) 85 | */ 86 | readonly data: ConfigTable | ConfigCategory | ConfigItem | ConfigField | any; 87 | 88 | constructor (entry?: string) { 89 | this.path = `${homedir()}\\.hyperbar\\config.yaml` 90 | this.yaml = fs.readFileSync(this.path, 'utf8').toString() 91 | 92 | if (!entry) { 93 | this.data = YAML.parse(this.yaml) 94 | } else { 95 | this.data = objSearch.get(YAML.parse(this.yaml), entry) as any 96 | } 97 | 98 | this.get = function (configStr) { 99 | const stack = objSearch.get(this.data, configStr, undefined) 100 | return stack 101 | } 102 | 103 | this.getCategory = function (category: string) { 104 | const stack = objSearch.get(this.data, `${category}`) as ConfigCategory 105 | return stack 106 | } 107 | 108 | this.getEntry = function (category: string, entry: string) { 109 | const entryKey = entry.toLowerCase().split(' ').join('_') 110 | 111 | const stack = objSearch.get(this.data, `${category}.items.${entryKey}`) as ConfigItem 112 | return stack 113 | } 114 | 115 | this.getField = function(category: string, entry: string, field:string) { 116 | const stack = objSearch.get(this.data, `${category}.items.${entry}.fields.${field}`) as ConfigField 117 | return stack 118 | } 119 | 120 | this.getValue = function (category: string, entry: string, field: string) { 121 | const stack = objSearch.get(this.data, `${category}.items.${entry}.fields.${field}.value`) as string | number | boolean 122 | return stack 123 | } 124 | 125 | 126 | this.set = function (category, entry, field, value, callback) { 127 | if (this.data) { 128 | objSearch.set(this.data, `${category}.items.${entry}.fields.${field}.value`, value) 129 | } else { 130 | callback(new Error(`No data found for < ${category}.fields.${field} >`)) 131 | } 132 | if (callback) { callback() } 133 | } 134 | 135 | this.insert = function( category,{name} ) { 136 | const categoryKey = category.toLocaleLowerCase().split(' ').join('_') 137 | 138 | if (!this.data[categoryKey]) { 139 | this.data[categoryKey] = { 140 | name: name, 141 | items: { 142 | } 143 | } 144 | } 145 | 146 | return categoryKey 147 | 148 | } 149 | 150 | this.addEntry = function( category, {name, description, icon, fields}) { 151 | const categoryKey = category 152 | 153 | const explodedKey = name.split(" ") 154 | const key = explodedKey.join("_").toLowerCase() 155 | 156 | objSearch.set(this.data, `${categoryKey}.items.${key}`, { 157 | name, 158 | description, 159 | icon, 160 | fields: fields || {} 161 | }) 162 | 163 | return `${categoryKey}.items.${key}` 164 | } 165 | 166 | this.addField = function(category, entry, {name, description, type, value, options}) { 167 | const categoryKey = category 168 | 169 | const explodedEntry = name.split(" ") 170 | const entryKey = explodedEntry.join("_").toLowerCase() 171 | 172 | const explodedKey = name.split(" ") 173 | const key = explodedKey.join("_").toLowerCase() 174 | 175 | 176 | objSearch.set(this.data, `${categoryKey}.items.${entryKey}.fields.${key}`, { 177 | name, 178 | description, 179 | type, 180 | value, 181 | options 182 | }) 183 | 184 | return `${categoryKey}.items.${entry}.fields.${key}` 185 | } 186 | 187 | this.save = function (callback) { 188 | fs.writeFileSync(this.path, YAML.stringify(this.data), 'utf8') 189 | if (callback) { callback() } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, ipcRenderer } from 'electron'; 2 | import {Config} from '../shared/config' 3 | 4 | export type MessagePayload = {message: string, level: number, name: string, stack: string[], time: Date} 5 | 6 | function stringify(message: any[]){ 7 | const messageArray: string[] = [] 8 | 9 | message.forEach( str => { 10 | if (typeof str === 'string') { messageArray.push(str)} 11 | else { messageArray.push(JSON.stringify(str))} 12 | }) 13 | 14 | return messageArray.join(' ') 15 | } 16 | 17 | export class Logger { 18 | public name: string 19 | private isRenderer: boolean 20 | private queue: any[] 21 | private locked: boolean 22 | /** 23 | * Initializes a new logger instance. 24 | * @param name Identifier of this instance. 25 | **/ 26 | constructor (name: string) { 27 | this.name = name 28 | this.isRenderer = process.type === 'renderer' 29 | this.queue = [] 30 | this.locked = true 31 | if (!this.isRenderer) { 32 | this.listen() 33 | } 34 | } 35 | 36 | //@ts-ignore 37 | private send({message, level}: {message: any[], level: MessageLevel}) { 38 | 39 | if (!new Config().getValue('general','misc', "console-window")) { 40 | return 41 | } 42 | const stackLines: string[] = []; 43 | 44 | const error = new Error() 45 | if (error.stack) { 46 | (error.stack).split('at ').forEach( (stackLine, index) => { 47 | if (index < 2) { return } 48 | stackLines.push(stackLine.trim()) 49 | }) 50 | } 51 | 52 | const data = { 53 | message: stringify(message), 54 | level, 55 | name: this.name, 56 | stack: stackLines, 57 | time: new Date() 58 | } 59 | 60 | if (this.isRenderer) { 61 | ipcRenderer.send('h_logmessage', data) 62 | } else { 63 | const { windows }:{windows: {[key:string]:Electron.BrowserWindow}} = require('../main') 64 | 65 | if (!windows.console) { 66 | this.queue.push(data) 67 | const queueChecker = setInterval(()=>{ 68 | console.log("Waiting for console window to be ready!") 69 | 70 | if (windows.console && !this.locked) {clearInterval(queueChecker)} // If it's unlocked, remove the checked and continue. 71 | else { return } // Otherwise we stop here 72 | 73 | this.queue.forEach( item => { 74 | windows?.console.webContents.send('h_logmessage', item) 75 | }) 76 | }, 100) 77 | 78 | return 79 | } 80 | 81 | windows?.console.webContents.send('h_logmessage', data) 82 | } 83 | } 84 | 85 | private listen() { 86 | ipcMain.on('h_consoleready', ()=> { 87 | this.locked = false 88 | }) 89 | } 90 | 91 | public info(...args: any[]) { 92 | this.send({ 93 | message: args, 94 | level: MessageLevel.INFO 95 | }) 96 | } 97 | 98 | public warn(...args: any[]) { 99 | this.send({ 100 | message: args, 101 | level: MessageLevel.WARN, 102 | }) 103 | } 104 | 105 | public error(...args: any[]) { 106 | this.send({ 107 | message: args, 108 | level: MessageLevel.ERROR 109 | }) 110 | } 111 | 112 | public silly(...args: any[]) { 113 | this.send({ 114 | message: args, 115 | level: MessageLevel.DEBUG, 116 | }) 117 | } 118 | 119 | public debug(...args: any[]) { 120 | this.send({ 121 | message: args, 122 | level: MessageLevel.DEBUG, 123 | }) 124 | } 125 | 126 | public success(...args: any[]) { 127 | this.send({ 128 | message: args, 129 | level: MessageLevel.SUCCESS, 130 | }) 131 | } 132 | } 133 | 134 | export enum MessageLevel { 135 | DEBUG = 1, 136 | INFO = 2, 137 | WARN = 3, 138 | ERROR = 4, 139 | SUCCESS = 5 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/shared/theme.ts: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar' 2 | import {ipcRenderer, app} from 'electron' 3 | import { Config } from './config' 4 | import { readdirSync, existsSync, readFileSync, lstatSync } from 'fs' 5 | import log from 'electron-log' 6 | import { join } from 'path' 7 | import { homedir } from 'os' 8 | const logger = log.scope('THEME') 9 | 10 | type CssVar = { 11 | name: string; 12 | value: string 13 | } 14 | 15 | type Theme = { 16 | name: string; 17 | author?: string; 18 | repository?: string; 19 | dependency?: string[]; 20 | version?: string; 21 | supportedWidgets?: string[]; 22 | file: string; 23 | folder: string; 24 | } 25 | 26 | class ThemeRepository { 27 | private isRenderer: boolean 28 | private config: Config 29 | public varList: CssVar[] 30 | public installedThemes: Theme[] 31 | public activeTheme: Theme | undefined; 32 | 33 | constructor() { 34 | this.config = new Config() 35 | this.isRenderer = process && process.type === 'renderer' 36 | this.varList = [] 37 | this.installedThemes = [] 38 | } 39 | 40 | public setVars() { 41 | const dockedTop = this.config.getValue('general', 'position', 'dock-pos') === 'top' 42 | const barHeight = this.config.getValue('appearence', 'sizes', 'height') as number 43 | const barPadding = this.config.getValue('appearence', 'sizes', 'padding') as number 44 | const offset = (dockedTop 45 | ? barHeight < 39 46 | ? (barHeight - 39) * - 1 47 | : 0 48 | : 0) 49 | const accentColor = this.config.getValue('appearence', 'color', 'accent') as string 50 | const primaryColor = this.config.getValue('appearence', 'color', 'primary') as string 51 | const secondaryColor = this.config.getValue('appearence', 'color', 'secondary') as string 52 | 53 | this.varList.push({ name: 'barsize', value: `${barHeight}px` }) 54 | this.varList.push({ name: 'padding', value: `${barPadding}px` }) 55 | this.varList.push({ name: 'offset', value: `${offset}px` }) 56 | 57 | this.varList.push({ name: 'accent', value: accentColor }) 58 | this.varList.push({ name: 'primary', value: primaryColor }) 59 | this.varList.push({ name: 'secondary', value: secondaryColor }) 60 | 61 | if (!this.isRenderer) { 62 | app.on('browser-window-created', (e, window) => { 63 | const cssStr = this.varList.map( variable => { 64 | return `--${variable.name}: ${variable.value}` 65 | }) 66 | window.webContents.insertCSS(` 67 | :root { 68 | ${cssStr.join(';')} 69 | } 70 | `) 71 | }) 72 | 73 | return 74 | } 75 | } 76 | 77 | setup() { 78 | this.setVars() 79 | this.varList.forEach(variable => { 80 | document.documentElement.style.setProperty(`--${variable.name}`, variable.value) 81 | }) 82 | const style = document.createElement('link') 83 | style.setAttribute('rel', 'stylesheet') 84 | style.setAttribute('href', `theme://index.css`) 85 | 86 | document.head.appendChild(style) 87 | 88 | this.watchTheme() 89 | } 90 | 91 | install() { 92 | 93 | } 94 | 95 | uninstall() { 96 | 97 | } 98 | 99 | listThemes() { 100 | return this.installedThemes 101 | } 102 | 103 | validate() { 104 | const currentThemeList = this.config.getField('general', 'appearence', 'theme').options as string[] 105 | let tempList: string[] = [] 106 | this.installedThemes.forEach(theme => { 107 | const path = theme.file.split('\\themes\\') 108 | const directory = path[path.length - 1].split('\\')[0] 109 | 110 | if (!currentThemeList.includes(directory)) { 111 | tempList = [...tempList, directory] 112 | } 113 | 114 | if ( this.config.getValue('general', 'appearence', 'theme') === directory) { 115 | this.activeTheme = theme 116 | } 117 | }) 118 | 119 | this.config.data.appearence.items.theme.fields.selected.options = tempList 120 | this.config.save() 121 | } 122 | 123 | private loadTheme(theme: string, path: string) { 124 | const themePath = join(path, theme); 125 | const themePathManifest = join(themePath, 'manifest.json'); 126 | 127 | if (!existsSync(themePath)) { 128 | logger.warn(`Path [${themePath}] is invalid`); 129 | return; 130 | } 131 | 132 | if (!existsSync(themePathManifest)) { 133 | logger.warn(`Failed to load [${themePath}]\n > ${themePathManifest} does not exist`); 134 | return; 135 | } 136 | 137 | const stats = lstatSync(path) 138 | 139 | if (stats && !stats.isDirectory()) { 140 | logger.warn(`Detected invalid file in widgets path.\n - You forgot to extract the widget?\n - Widget files must be on their own folder, not directly on widgets directory.\n - This file will not be loaded.`) 141 | } 142 | 143 | 144 | const themeInfo = JSON.parse(readFileSync(themePathManifest).toString()); 145 | themeInfo.file = themePathManifest 146 | 147 | this.installedThemes.push(themeInfo) 148 | } 149 | 150 | loadThemes() { 151 | this.themesPath.forEach(path => { 152 | const themes = readdirSync(path); 153 | 154 | themes.forEach(theme => { 155 | this.loadTheme(theme, path); 156 | }) 157 | }); 158 | } 159 | 160 | watchTheme() { 161 | 162 | const watchThemes = this.config.getValue('general', 'misc', 'watch-themes') 163 | if (!watchThemes) { return } 164 | 165 | const watcher = chokidar.watch(this.themesPath) 166 | 167 | watcher.on('change', () => { ipcRenderer.send('forceReload') }) 168 | 169 | } 170 | 171 | get themesPath() { 172 | const paths = [ 173 | join(homedir(), "./.hyperbar/themes"), 174 | ]; 175 | 176 | return paths; 177 | } 178 | 179 | } 180 | 181 | export default ThemeRepository -------------------------------------------------------------------------------- /src/shared/widget.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { homedir } from 'os'; 3 | import { readdirSync, existsSync, readFileSync, lstatSync, unlinkSync, rmdirSync } from 'fs'; 4 | import { Config } from './config'; 5 | import chokidar from 'chokidar' 6 | import Zip from 'adm-zip'; 7 | // import log from 'electron-log' 8 | import { ipcRenderer } from 'electron'; 9 | import { Logger } from './logger'; 10 | 11 | const logger = new Logger('WIDGET CONTROLLER') 12 | // log.transports.file.resolvePath = () => join(homedir(), '.hyperlogs/main.log'); 13 | 14 | function rimraf(dir_path: string) { 15 | if (existsSync(dir_path)) { 16 | readdirSync(dir_path).forEach(function(entry) { 17 | var entry_path = join(dir_path, entry); 18 | if (lstatSync(entry_path).isDirectory()) { 19 | rimraf(entry_path); 20 | } else { 21 | unlinkSync(entry_path); 22 | } 23 | }); 24 | rmdirSync(dir_path); 25 | } 26 | } 27 | 28 | export type WidgetInfo = { 29 | image?: string, 30 | name:string, 31 | author?:string, 32 | description:string, 33 | main?: string, 34 | renderer?: string, 35 | version: string, 36 | widgetPath?: string, 37 | repository?: string 38 | directory: string 39 | file: string 40 | } 41 | export interface WidgetObject extends WidgetInfo { 42 | default: () => void; 43 | styles?: string[]; 44 | } 45 | class WidgetRepository { 46 | public loadedWidgets: WidgetObject[]; 47 | private isRenderer: boolean; 48 | 49 | constructor() { 50 | this.loadedWidgets = []; 51 | this.isRenderer = false 52 | } 53 | 54 | getWidgetContext(widgetInfo: WidgetInfo) { 55 | if (!this.isRenderer) { 56 | const electron = require('electron') 57 | const directory = widgetInfo.file.split('\\widgets\\') 58 | 59 | return { 60 | config: new Config(), 61 | api: { 62 | show: electron.app.show, 63 | ipcMain: electron.ipcMain, 64 | browserWindow: electron.BrowserWindow, 65 | Menu: electron.Menu, 66 | shell: electron.shell, 67 | windows: require('../main').windows, 68 | directory: directory[directory.length -1].split('\\')[0], 69 | }, 70 | logger: new Logger(`[MAIN] ${widgetInfo.name}`) 71 | } 72 | } else { 73 | const electron = require('electron') 74 | 75 | return { 76 | config: new Config(), 77 | api: { 78 | ipcRenderer: electron.ipcRenderer, 79 | }, 80 | logger: new Logger(`[RENDER] ${widgetInfo.name}`) 81 | } 82 | 83 | } 84 | } 85 | 86 | requireWidget(widgetInfo: WidgetObject) { 87 | try { 88 | const widgetExecutor = require(widgetInfo.file) as WidgetObject; 89 | const directory = widgetInfo.file.split('\\widgets\\') 90 | 91 | widgetInfo.default = widgetExecutor.default.bind( this.getWidgetContext(widgetInfo), ) 92 | widgetInfo.directory = directory[directory.length -1].split('\\')[0] 93 | widgetInfo.styles = widgetExecutor.styles 94 | this.loadedWidgets.push(widgetInfo); 95 | } catch (err) { 96 | logger.error(`Failed loading [${widgetInfo.file}] - ${err}`); 97 | } 98 | } 99 | 100 | loadWidget(widget: string, path: string) { 101 | const widgetPath = join(path, widget); 102 | const widgetPathPackageJson = join(widgetPath, 'package.json'); 103 | 104 | if (!existsSync(widgetPath)) { 105 | logger.warn(`Path [${widgetPath}] is invalid`); 106 | return; 107 | } 108 | 109 | if (!existsSync(widgetPathPackageJson)) { 110 | logger.warn(`Failed to load [${widgetPath}]\n > ${widgetPathPackageJson} does not exist`); 111 | return; 112 | } 113 | 114 | const stats = lstatSync(path) 115 | 116 | if (stats && !stats.isDirectory()) { 117 | logger.warn(`Detected invalid file in widgets path.\n - You forgot to extract the widget?\n - Widget files must be on their own folder, not directly on widgets directory.\n - This file will not be loaded.`) 118 | } 119 | 120 | let widgetInfo = JSON.parse( readFileSync(widgetPathPackageJson).toString() ); 121 | 122 | widgetInfo = Object.assign(widgetInfo, { 123 | widgetPath, 124 | file: join(widgetPath, this.isRenderer ? widgetInfo.renderer : widgetInfo.main) 125 | }) 126 | 127 | if (widgetInfo.hypersettings && widgetInfo.hypersettings.fields && Object.keys(widgetInfo.hypersettings.fields).length > 0) { 128 | const config = new Config() 129 | if (!config.get('widgets')) { 130 | config.insert('widgets', { 131 | name: "Widgets", 132 | items: {} 133 | }) 134 | } 135 | 136 | if (!config.getEntry('widgets', widgetInfo.hypersettings.name)) { 137 | config.addEntry('widgets', { 138 | name: widgetInfo.hypersettings.name, 139 | description: widgetInfo.hypersettings.description, 140 | icon: widgetInfo.hypersettings.icon, 141 | fields: widgetInfo.hypersettings.fields 142 | }) 143 | 144 | config.save() 145 | } 146 | } 147 | 148 | this.requireWidget(widgetInfo); 149 | } 150 | 151 | 152 | loadWidgetsInPaths() { 153 | this.isRenderer = process && process.type === 'renderer' 154 | // TODO: Detect first run 155 | // TODO: Load default widgets from hyper repository 156 | 157 | this.widgetPaths.forEach(path => { 158 | const widgets = readdirSync(path); 159 | 160 | widgets.forEach(widget => { 161 | this.loadWidget(widget, path); 162 | }) 163 | }); 164 | } 165 | 166 | get widgetPaths() { 167 | const paths = [ 168 | join(homedir(), "./.hyperbar/widgets"), 169 | ]; 170 | 171 | return paths; 172 | } 173 | 174 | installWidget(path: string, callback?: (installedWidget: WidgetInfo) => void ) { 175 | if (!existsSync(path)) { logger.error(`Tried to install invalid widget file - Corrupted or missing :: ${path}`); return false; } 176 | if (!path.endsWith('.zip')) { logger.error(`This file is not a compressed folder - Skipping :: ${path}`); return false; } 177 | 178 | const zipFile = new Zip(path) 179 | let widgetData: string = "" 180 | 181 | if (zipFile.getEntries().length <= 0) { logger.error(`Empty or corrupt zip file :: ${path}`); return false; } 182 | 183 | zipFile.getEntries().forEach(function (zipEntry: any) { // TODO: Declaration files for adm-zip 184 | if (zipEntry.entryName.endsWith("package.json")) { 185 | widgetData = zipFile.readAsText(zipEntry) 186 | } 187 | }); 188 | 189 | if (widgetData === "") { logger.error(`Tried to install non widget zip :: ${path}`); return false; } 190 | 191 | zipFile.extractAllTo(this.widgetPaths[0], true) 192 | 193 | logger.debug(`Installed widget - ${JSON.parse(widgetData).name}`) 194 | 195 | if (this.isRenderer) { 196 | this.loadWidgetsInPaths() 197 | this.watchWidgets() 198 | this.loadStyles() 199 | ipcRenderer.send('forceReload') 200 | } else { 201 | this.loadWidgetsInPaths() 202 | this.watchWidgets() 203 | this.loadedWidgets.forEach( widget =>{ 204 | widget.default() 205 | }) 206 | } 207 | 208 | callback?.(JSON.parse(widgetData)) 209 | 210 | return JSON.parse(widgetData) 211 | } 212 | 213 | uninstallWidget(widget: WidgetObject, callback?: () => void) { 214 | const directory = widget.file.split('\\widgets\\') 215 | const widgetPath = join(homedir(), '.hyperbar', 'widgets', directory[directory.length -1].split('\\')[0]) 216 | const widgetPathPackageJson = join(widgetPath, 'package.json'); 217 | if (!existsSync(widgetPath)) { 218 | logger.error(`Path [${widgetPath}] is invalid`); 219 | return; 220 | } 221 | 222 | if (!existsSync(widgetPathPackageJson)) { 223 | logger.error(`Failed to remove [${widgetPath}]\n > ${widgetPathPackageJson} does not exist`); 224 | return; 225 | } 226 | 227 | const stats = lstatSync(widgetPath) 228 | 229 | if (stats && !stats.isDirectory()) { 230 | logger.warn(`Detected invalid file in widgets path.\n - You forgot to extract the widget?\n - Widget files must be on their own folder, not directly on widgets directory.\n - This file will not be loaded.`) 231 | } 232 | 233 | let widgetInfo = JSON.parse( readFileSync(widgetPathPackageJson).toString() ); 234 | 235 | const config = new Config() 236 | 237 | const entryName = widgetInfo.hypersettings.name.toLowerCase().split(' ').join('_') 238 | 239 | if (config.getEntry('widgets', entryName)) { 240 | delete config.data.widgets.items[entryName] 241 | if (config.data.widgets.items.length <= 0) { 242 | delete config.data.widgets 243 | } 244 | config.save() 245 | } 246 | 247 | rimraf(widgetPath) 248 | callback?.() 249 | } 250 | 251 | watchWidgets() { 252 | const config = new Config() 253 | const watchWidgets = config.getValue('general', 'misc', 'watch-widgets') 254 | if (!watchWidgets) { return } 255 | 256 | const watchList: string[] = [] 257 | this.loadedWidgets.forEach( widget => { 258 | watchList.push(widget.file) 259 | if (widget.styles) { 260 | widget.styles.forEach( style =>{ 261 | logger.success(`Watching style: ${widget.name}/${style}`) 262 | const directory = widget.file.split('\\widgets\\') 263 | watchList.push(`${join(homedir(), "./.hyperbar/widgets", directory[directory.length -1].split('\\')[0], style)}`) 264 | }) 265 | } 266 | }) 267 | 268 | const watcher = chokidar.watch(watchList) 269 | logger.info("Total widget entries being watched:", watchList.length ) 270 | 271 | const electron = require('electron'); 272 | 273 | watcher.on('change', () =>{ 274 | 275 | if(this.isRenderer) { 276 | electron.ipcRenderer.send('forceReload') 277 | } else { 278 | electron.app.relaunch() 279 | electron.app.exit() 280 | } 281 | }) 282 | } 283 | 284 | loadStyles() { 285 | this.loadedWidgets.map( widget =>{ 286 | widget.styles?.forEach( style => { 287 | const head = document.querySelector('head') 288 | if (head) { head.innerHTML += ``; } 289 | }) 290 | }) 291 | } 292 | } 293 | 294 | export default WidgetRepository -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A tailwinds config file used to generate atomic utility css classes. 3 | * See: https://tailwindcss.com/docs/configuration/ 4 | * Def: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js 5 | */ 6 | 7 | module.exports = { 8 | purge: [ 9 | "./src/**/*.html", 10 | "./src/**/*.js", 11 | "./src/**/*.jsx", 12 | "./src/**/*.ts", 13 | "./src/**/*.tsx", 14 | ], 15 | theme: { 16 | extend: { 17 | colors: { 18 | "bg": "#121212", 19 | "primary": "#212121", 20 | "secondary": "#272727", 21 | "navbar": "#2E2E2E", 22 | "accent": "#7614F5", 23 | "error": "#CF6679", 24 | "success": "#1FDC98", 25 | "warning": "#D5AC5C", 26 | "subtle": "#464646" 27 | } 28 | }, 29 | }, 30 | variants: { 31 | backgroundColor: ['hover', 'focus'], 32 | textColor: ['hover', 'focus'], 33 | borderColor: ['focus', 'hover'], 34 | fontWeight:['hover', 'focus'], 35 | padding:['hover', 'focus'] 36 | }, 37 | plugins: [], 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": false, 11 | "typeRoots": ["./src/@types", "./node_modules/@types"], 12 | "jsx": "react" 13 | }, 14 | "exclude": [ 15 | "dist", 16 | "node_modules", 17 | ] 18 | } -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import { screen } from 'electron'; 2 | import log from 'electron-log' 3 | import {homedir} from 'os' 4 | import {join, resolve} from 'path' 5 | import { execSync, execFile } from 'child_process' 6 | 7 | import { Config } from './src/shared/config'; 8 | import { Logger } from './src/shared/logger'; 9 | const logger = new Logger('WINDOW MANAGER') 10 | 11 | log.transports.file.resolvePath = () => join(homedir(), '.hyperlogs/main.log'); 12 | 13 | export function stringToHex(color: string){ 14 | return Number(color.replace("#", "0x")) || 0x000000 15 | } 16 | 17 | export function generateBounds() { 18 | const configSizes = new Config("appearence.items.sizes.fields") 19 | const configDock = new Config("general.items.position.fields") 20 | 21 | const { width, height } = screen.getPrimaryDisplay().workAreaSize 22 | 23 | logger.debug(`Detected Sreen Size: ${width}x${height}`) 24 | 25 | const dockedTop = configDock.get('dock-pos.value') == "top" 26 | const barHeight = Number(configSizes.get("height.value")) 27 | const horizontalMargin = Number(configDock.get("horizontal-margin.value")) 28 | const verticalMargin = Number(configDock.get("vertical-margin.value")) 29 | 30 | const calculated = { 31 | width: width - horizontalMargin * 2, 32 | height: barHeight, 33 | x: horizontalMargin, 34 | y: dockedTop 35 | ? barHeight < 39 36 | ? barHeight - 39 + verticalMargin 37 | : verticalMargin 38 | // I don't know why 11, ask microsoft 39 | : height - barHeight - verticalMargin 40 | } 41 | 42 | return calculated 43 | 44 | } 45 | 46 | export function removeAppBar(){ 47 | const exeLocation = resolve(__dirname, 'bin', 'Hyper Spacer.exe') 48 | endAppBar() 49 | execFile(`${exeLocation}`, ["0"]) // Change size or set to 0 50 | } 51 | 52 | export function endAppBar() { 53 | try { execSync('taskkill /T /F /IM "Hyper Spacer.exe"') } catch {logger.info("Hyper Spacer is not running") } 54 | } 55 | 56 | export function makeAppBar() { 57 | const promise = new Promise((resolvePromise, rejectPromise) => { 58 | 59 | const config = new Config() 60 | const shouldDock = config.getValue('general', 'behavior', 'reserve-space') 61 | const reservedSpace = config.getValue('appearence', 'sizes', 'height') as number 62 | const reservedMargin = config.getValue('general', 'position', 'vertical-margin') as number 63 | const dockPos = config.getValue('general', 'position', 'dock-pos') 64 | logger.info("Initializing space reservation") 65 | endAppBar() 66 | setTimeout(()=>{ 67 | const exeLocation = resolve(__dirname, 'bin', 'Hyper Spacer.exe') 68 | execFile(`${exeLocation}`, [String(reservedSpace + reservedMargin * 2),`${shouldDock ? dockPos == "top" ? 'Top' : 'Bottom' : 0}`]) // Change size or set to 0 69 | logger.success("Space reserved!", String(reservedSpace + reservedMargin * 2),`${shouldDock ? dockPos == "top" ? 'Top' : 'Bottom' : 0}`) 70 | resolvePromise(true) 71 | }, 100) 72 | }) 73 | 74 | return promise 75 | } --------------------------------------------------------------------------------