├── .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 |
{
135 | if(installed === InstallStatus.INSTALLED || installed === InstallStatus.FAILED) { return }
136 | setDownloading(true)
137 | onDownload?.()
138 | }}>
139 | {installed === InstallStatus.NOT_INSTALLED
140 | ?
141 | :installed === InstallStatus.FAILED
142 | ?
143 | :installed === InstallStatus.OUTDATED
144 | ?
145 | : <>>
146 | }
147 | {installed === InstallStatus.INSTALLED
148 | ? 'Installed'
149 | :installed === InstallStatus.OUTDATED
150 | ? 'Update'
151 | :installed === InstallStatus.NOT_INSTALLED
152 | ? 'Install'
153 | : 'ERROR'
154 | }
155 |
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 | {
39 | if (repoType !== 'widgets'){ setRepoType('widgets') }
40 | }}
41 | >
42 | Widgets
43 |
44 | {
47 | if (repoType !== 'themes'){ setRepoType('themes') }
48 | }}
49 | >
50 | Themes
51 |
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 |
70 |
71 |
{
72 | change?.()
73 | applyChange(e.target.checked)
74 | }} defaultChecked={value} />
75 |
77 |
80 |
81 |
82 | >
83 | case "number":
84 | return <>
85 |
86 |
87 | {
90 | const element = document.getElementById(`${category}.${entry}.${field}`)
91 | // @ts-ignore // We need to ignore this, the solution for that is a stupid type swap, which i'm not going to do
92 | let num = Number(element.value);
93 | num--;
94 | // @ts-ignore
95 | element.value = num
96 | applyChange(num)
97 | }}
98 | >
99 | −
100 |
101 | {
107 | applyChange(Number(e.target.value))
108 | }}
109 | />
110 | {
113 | const element = document.getElementById(`${category}.${entry}.${field}`)
114 | //@ts-ignore // I'm not doing type swap, sorry.
115 | let num = Number(element.value);
116 | num++;
117 | // @ts-ignore
118 | element.value = num
119 | applyChange(num)
120 | }}
121 | >
122 | +
123 |
124 |
125 |
126 | >
127 | case "selection":
128 | return <>
129 |
130 |
131 |
132 | {value}
133 |
134 |
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 |
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 |
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 |
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 |
56 |
57 |
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 |
55 |
56 |
57 | Settings
58 |
59 | Hyper
60 |
65 | {this.generateCategories()}
66 | {this.props.children}
67 |
68 |
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 ?
41 |
42 |
43 | :
47 |
48 |
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 | {
140 | if (filter.level === MessageLevel.WARN){
141 | setFilter({})
142 | } else {
143 | setFilter({level: MessageLevel.WARN})}
144 | }
145 | }>
146 | {history.filter(line=>{
147 | return line.level === MessageLevel.WARN
148 | }).length}
149 |
150 | {
151 | if (filter.level === MessageLevel.ERROR){
152 | setFilter({})
153 | } else {
154 | setFilter({level: MessageLevel.ERROR})}
155 | }
156 | }>
157 | {history.filter(line=>{
158 | return line.level === MessageLevel.ERROR
159 | }).length}
160 |
161 | {
162 | if (filter.level === MessageLevel.SUCCESS){
163 | setFilter({})
164 | } else {
165 | setFilter({level: MessageLevel.SUCCESS})}
166 | }
167 | }>
168 | {history.filter(line=>{
169 | return line.level === MessageLevel.SUCCESS
170 | }).length}
171 |
172 | {
173 | if (filter.level === MessageLevel.DEBUG){
174 | setFilter({})
175 | } else {
176 | setFilter({level: MessageLevel.DEBUG})}
177 | }
178 | }>
179 | {history.filter(line=>{
180 | return line.level === MessageLevel.DEBUG
181 | }).length}
182 |
183 | {
184 | if (filter.level === MessageLevel.INFO){
185 | setFilter({})
186 | } else {
187 | setFilter({level: MessageLevel.INFO})}
188 | }
189 | }>
190 | {history.filter(line=>{
191 | return line.level === MessageLevel.INFO
192 | }).length}
193 |
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 |
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 | }
--------------------------------------------------------------------------------