mirror of
https://github.com/cupcakearmy/unpixel.git
synced 2024-12-21 23:56:28 +00:00
initial commit
This commit is contained in:
commit
606832a141
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.cache
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
node_modules
|
||||||
|
.parcel-cache
|
BIN
assets/tray.png
Normal file
BIN
assets/tray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
14
electron-builder.yml
Normal file
14
electron-builder.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
appId: "io.nicco.app.unpixel"
|
||||||
|
# productName: "UnPixel"
|
||||||
|
files:
|
||||||
|
- assets
|
||||||
|
- dist
|
||||||
|
directories:
|
||||||
|
output: "./out"
|
||||||
|
mac:
|
||||||
|
target: "dmg"
|
||||||
|
identity: null
|
||||||
|
win:
|
||||||
|
target: "nsis"
|
||||||
|
linux:
|
||||||
|
category: "Utility"
|
61
package.json
Normal file
61
package.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "unpixel",
|
||||||
|
"productName": "UnPixel",
|
||||||
|
"description": "Help your eyes take a break",
|
||||||
|
"author": {
|
||||||
|
"name": "Niccolo Borgioli",
|
||||||
|
"email": "hi@nicco.io",
|
||||||
|
"url": "https://nicco.io"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./dist/back/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"watch:front": "parcel watch --target front --no-hmr --no-cache ./src/front/banner/index.html ./src/front/settings/index.html",
|
||||||
|
"watch:back": "parcel watch --target back --no-hmr --no-cache ./src/back/index.ts",
|
||||||
|
"dev": "run-p watch:*",
|
||||||
|
"build:front": "parcel build --target front ./src/front/banner/index.html ./src/front/settings/index.html",
|
||||||
|
"build:back": "parcel build --target back ./src/back/index.ts",
|
||||||
|
"build": "run-s build:*",
|
||||||
|
"pack": "electron-builder -mwl",
|
||||||
|
"pack:dev": "electron-builder -m",
|
||||||
|
"dist": "rm -rf ./dist && run-s build pack"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"last 2 Chrome versions"
|
||||||
|
],
|
||||||
|
"targets": {
|
||||||
|
"back": {
|
||||||
|
"distDir": "./dist/back",
|
||||||
|
"context": "electron-main"
|
||||||
|
},
|
||||||
|
"front": {
|
||||||
|
"distDir": "./dist/front",
|
||||||
|
"context": "electron-renderer",
|
||||||
|
"publicUrl": "../"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"auto-launch": "^5.0.5",
|
||||||
|
"dayjs": "^1.10.4",
|
||||||
|
"electron-log": "^4.3.2",
|
||||||
|
"electron-store": "^7.0.2",
|
||||||
|
"hiq": "^4.1.4",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"spectre.css": "^0.5.9",
|
||||||
|
"tachyons": "^4.12.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0",
|
||||||
|
"@types/react": "^17.0.3",
|
||||||
|
"@types/react-dom": "^17.0.3",
|
||||||
|
"electron": "^12.0.0",
|
||||||
|
"electron-builder": "^22.10.5",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"parcel": "next",
|
||||||
|
"postcss": "^8.2.9",
|
||||||
|
"typescript": "^4.2.3"
|
||||||
|
}
|
||||||
|
}
|
84
src/back/banner.ts
Normal file
84
src/back/banner.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
|
||||||
|
import { DEV } from '.'
|
||||||
|
import Settings from './settings'
|
||||||
|
import TrayUtility from './tray'
|
||||||
|
|
||||||
|
export default class Banner {
|
||||||
|
static interval: ReturnType<typeof setInterval>
|
||||||
|
static window: BrowserWindow | null = null
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (this.interval) return
|
||||||
|
this.interval = setInterval(this.check, 1000)
|
||||||
|
ipcMain.on('close', () => {
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static check() {
|
||||||
|
const paused: boolean = Settings.load('paused')
|
||||||
|
if (paused) {
|
||||||
|
TrayUtility.setStatus('Paused')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const every = Settings.load('every')
|
||||||
|
const now = dayjs()
|
||||||
|
const lastRun = Settings.load('lastRun')
|
||||||
|
const diff = every - now.diff(dayjs(lastRun), 'minutes')
|
||||||
|
TrayUtility.setStatus(`Next break: ${diff}m`)
|
||||||
|
if (diff < 1) {
|
||||||
|
Banner.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static open() {
|
||||||
|
if (this.window) return
|
||||||
|
|
||||||
|
logger.debug('Showing banner')
|
||||||
|
const options: BrowserWindowConstructorOptions = {
|
||||||
|
frame: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
width: 1200,
|
||||||
|
height: 600,
|
||||||
|
}
|
||||||
|
if (!DEV) {
|
||||||
|
Object.assign(options, {
|
||||||
|
resizable: false,
|
||||||
|
movable: false,
|
||||||
|
simpleFullscreen: true,
|
||||||
|
fullscreen: true,
|
||||||
|
transparent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.window = new BrowserWindow(options)
|
||||||
|
|
||||||
|
const entry = join(__dirname, '../front/banner/index.html')
|
||||||
|
this.window.loadFile(entry)
|
||||||
|
|
||||||
|
if (!DEV) {
|
||||||
|
this.window.maximize()
|
||||||
|
this.window.setAlwaysOnTop(true, 'floating', 99)
|
||||||
|
this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
|
||||||
|
this.window.setFullScreenable(false)
|
||||||
|
} else {
|
||||||
|
this.window.webContents.toggleDevTools()
|
||||||
|
}
|
||||||
|
this.window.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
static close() {
|
||||||
|
if (this.window) {
|
||||||
|
Settings.save('lastRun', Date.now())
|
||||||
|
this.window.close()
|
||||||
|
this.window = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/back/index.ts
Normal file
33
src/back/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
|
||||||
|
import TrayUtility from './tray'
|
||||||
|
import Settings from './settings'
|
||||||
|
import Banner from './banner'
|
||||||
|
|
||||||
|
export const DEV = !app.isPackaged
|
||||||
|
|
||||||
|
// Disable gpu
|
||||||
|
app.disableHardwareAcceleration()
|
||||||
|
app.commandLine.appendSwitch('disable-software-rasterizer')
|
||||||
|
|
||||||
|
logger.catchErrors({ showDialog: true })
|
||||||
|
logger.log('Starting')
|
||||||
|
app
|
||||||
|
.whenReady()
|
||||||
|
.then(() => {
|
||||||
|
logger.log('Initializing')
|
||||||
|
if (!DEV) app.dock.hide()
|
||||||
|
TrayUtility.init()
|
||||||
|
Settings.init()
|
||||||
|
Banner.init()
|
||||||
|
logger.log('Done')
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// Prevent closing of the app
|
||||||
|
})
|
84
src/back/settings.ts
Normal file
84
src/back/settings.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { BrowserWindow, ipcMain } from 'electron'
|
||||||
|
import Store from 'electron-store'
|
||||||
|
import { join } from 'path'
|
||||||
|
import AutoLaunch from 'auto-launch'
|
||||||
|
|
||||||
|
import { productName } from '../../package.json'
|
||||||
|
|
||||||
|
const autoLaunch = new AutoLaunch({ name: productName, mac: { useLaunchAgent: true } })
|
||||||
|
|
||||||
|
import { DEV } from '.'
|
||||||
|
|
||||||
|
const store = new Store()
|
||||||
|
const defaults = {
|
||||||
|
every: 20,
|
||||||
|
duration: 20,
|
||||||
|
boot: true,
|
||||||
|
paused: false,
|
||||||
|
lastRun: 0,
|
||||||
|
autoClose: false,
|
||||||
|
}
|
||||||
|
export type SettingKeys = keyof typeof defaults
|
||||||
|
const IntNormalizer = (x: any) => parseInt(x)
|
||||||
|
const BoolNormalizer = (x: any) => !!x
|
||||||
|
const normalizers: Record<SettingKeys, (x: any) => any> = {
|
||||||
|
every: IntNormalizer,
|
||||||
|
duration: IntNormalizer,
|
||||||
|
boot: BoolNormalizer,
|
||||||
|
autoClose: BoolNormalizer,
|
||||||
|
paused: BoolNormalizer,
|
||||||
|
lastRun: IntNormalizer,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Settings {
|
||||||
|
static win: BrowserWindow | null = null
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
ipcMain.on('save', (e, { key, value }) => {
|
||||||
|
this.save(key, value)
|
||||||
|
})
|
||||||
|
ipcMain.on('load', (e, { key }) => {
|
||||||
|
e.returnValue = this.load(key)
|
||||||
|
})
|
||||||
|
if (Settings.load('boot')) {
|
||||||
|
autoLaunch.enable()
|
||||||
|
}
|
||||||
|
Settings.save('lastRun', Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
static save<T extends SettingKeys>(key: T, value: typeof defaults[T]) {
|
||||||
|
const normalized = normalizers[key](value)
|
||||||
|
store.set(key, normalized)
|
||||||
|
if (key === 'boot') {
|
||||||
|
normalized ? autoLaunch.enable() : autoLaunch.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static load<T extends SettingKeys>(key: T) {
|
||||||
|
const saved = store.get(key) as typeof defaults[T] | undefined
|
||||||
|
return saved ?? defaults[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
static open() {
|
||||||
|
if (this.win) return
|
||||||
|
this.win = new BrowserWindow({
|
||||||
|
width: 400,
|
||||||
|
height: 485,
|
||||||
|
center: true,
|
||||||
|
resizable: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const entry = join(__dirname, '../front/settings/index.html')
|
||||||
|
Settings.win.loadFile(entry)
|
||||||
|
|
||||||
|
if (DEV) {
|
||||||
|
Settings.win.setSize(800, 485)
|
||||||
|
Settings.win.setResizable(true)
|
||||||
|
Settings.win.webContents.openDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/back/tray.ts
Normal file
64
src/back/tray.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Tray, Menu, nativeImage } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import Banner from './banner'
|
||||||
|
import Settings from './settings'
|
||||||
|
|
||||||
|
enum Items {
|
||||||
|
Status = 'status',
|
||||||
|
Pause = 'pause',
|
||||||
|
Run = 'run',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TrayUtility {
|
||||||
|
static menu: Parameters<typeof Menu['buildFromTemplate']>[0] = [
|
||||||
|
{ label: 'Status', type: 'normal', enabled: false, id: Items.Status },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Take a break now',
|
||||||
|
type: 'normal',
|
||||||
|
id: Items.Run,
|
||||||
|
click: () => Banner.open(),
|
||||||
|
},
|
||||||
|
{ label: 'Pause', type: 'checkbox', id: Items.Pause },
|
||||||
|
{ label: 'Settings', type: 'normal', click: () => Settings.open() },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Quit', type: 'normal', role: 'quit' },
|
||||||
|
]
|
||||||
|
|
||||||
|
static tray: Tray | null = null
|
||||||
|
|
||||||
|
static setStatus(status: string) {
|
||||||
|
this.menu[0].label = status
|
||||||
|
this.tray.setContextMenu(this.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private static build() {
|
||||||
|
const menu = Menu.buildFromTemplate(this.menu)
|
||||||
|
for (const item of menu.items) {
|
||||||
|
if (item.id === Items.Pause) {
|
||||||
|
let initial = Settings.load('paused')
|
||||||
|
item.checked = initial
|
||||||
|
item.click = () => {
|
||||||
|
initial = !initial
|
||||||
|
item.checked = initial
|
||||||
|
Settings.save('paused', initial)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
|
static init() {
|
||||||
|
if (!this.tray) {
|
||||||
|
const file = path.join(__dirname, '../../assets/tray.png')
|
||||||
|
const icon = nativeImage.createFromPath(file).resize({
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
})
|
||||||
|
this.tray = new Tray(icon)
|
||||||
|
this.tray.setContextMenu(this.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
src/front/assets/chime.mp3
Normal file
BIN
src/front/assets/chime.mp3
Normal file
Binary file not shown.
60
src/front/banner/index.css
Normal file
60
src/front/banner/index.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
@import 'tachyons/css/tachyons.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #111;
|
||||||
|
color: #eee;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
font-size: min(5vw, 5rem);
|
||||||
|
text-align: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
border-bottom: 0.5em solid currentColor;
|
||||||
|
padding-bottom: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
font-size: min(8rem, 25vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
background: #ffffff12;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25em 1em;
|
||||||
|
color: currentColor;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
bottom: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #2c2cce;
|
||||||
|
}
|
13
src/front/banner/index.html
Normal file
13
src/front/banner/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Look Away</title>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||||
|
<link rel="stylesheet" href="./index.css" charset="utf-8" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main></main>
|
||||||
|
<script src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
88
src/front/banner/index.tsx
Normal file
88
src/front/banner/index.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
console.log('test')
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import chime from 'url:../assets/chime.mp3'
|
||||||
|
|
||||||
|
const useKeyPress = (handler) => {
|
||||||
|
const handlerRef = useRef<(e: KeyboardEvent) => void>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = handler
|
||||||
|
}, [handler])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fn = (event) => handlerRef.current(event)
|
||||||
|
window.addEventListener('keydown', fn)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', fn)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const Banner: React.FC = () => {
|
||||||
|
const close = useCallback(() => {
|
||||||
|
ipcRenderer.send('close')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const [auto, setAuto] = useState(false)
|
||||||
|
const [countdown, setCountdown] = useState<null | number>(null)
|
||||||
|
|
||||||
|
const handler = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (done) close()
|
||||||
|
},
|
||||||
|
[countdown, done]
|
||||||
|
)
|
||||||
|
useKeyPress(handler)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (done && auto) setTimeout(() => close(), 1500)
|
||||||
|
}, [done, auto])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown === null) return
|
||||||
|
else if (countdown > 0) {
|
||||||
|
setTimeout(() => setCountdown(countdown - 1), 1000)
|
||||||
|
} else {
|
||||||
|
const audio = new Audio(chime)
|
||||||
|
audio.play()
|
||||||
|
setDone(true)
|
||||||
|
}
|
||||||
|
}, [countdown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const autoClose = ipcRenderer.sendSync('load', { key: 'autoClose' })
|
||||||
|
setAuto(autoClose)
|
||||||
|
const time = ipcRenderer.sendSync('load', { key: 'duration' })
|
||||||
|
setCountdown(time)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="ma0 mb4">Look Away</h1>
|
||||||
|
<div className="code countdown">{countdown}</div>
|
||||||
|
<div className="tile message">
|
||||||
|
Look at least <b>6 meters</b> away. <br />
|
||||||
|
<small>You will hear a sound when you are done.</small>
|
||||||
|
</div>
|
||||||
|
<button className="tile" onClick={close}>
|
||||||
|
{done ? (
|
||||||
|
<span>
|
||||||
|
Close me
|
||||||
|
<br />
|
||||||
|
<small className="code f6">or press any key</small>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`I'm weak, close me now`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<Banner />, document.querySelector('main'))
|
17
src/front/base.css
Normal file
17
src/front/base.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* @import 'hiq/dist/hiq.css'; */
|
||||||
|
@import 'spectre.css/dist/spectre.min.css';
|
||||||
|
@import 'tachyons/css/tachyons.min.css';
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='range'] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
36
src/front/settings/About.tsx
Normal file
36
src/front/settings/About.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { shell } from 'electron'
|
||||||
|
|
||||||
|
const Link: React.FC<{ text: string; link: string }> = ({ text, link }) => {
|
||||||
|
const fn = useCallback(
|
||||||
|
(e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
shell.openExternal(link)
|
||||||
|
},
|
||||||
|
[link]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<a onClick={fn} href={link}>
|
||||||
|
{' '}
|
||||||
|
{text}{' '}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const About: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="ma0 mb2">About</h3>
|
||||||
|
<p>
|
||||||
|
UnPixel aims at helping you following the 20/20/20 rule to alleviate stress on the eyes caused by Computer
|
||||||
|
Vision Syndrome (CVS).
|
||||||
|
<br />
|
||||||
|
Read more
|
||||||
|
<Link text="here" link="https://en.wikipedia.org/wiki/Computer_vision_syndrome" /> and
|
||||||
|
<Link text="here." link="https://www.aoa.org/healthy-eyes/eye-and-vision-conditions/computer-vision-syndrome" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default About
|
54
src/front/settings/Field.tsx
Normal file
54
src/front/settings/Field.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
every: ['Alert every me', 'minutes'],
|
||||||
|
duration: ['For', 'seconds'],
|
||||||
|
boot: ['Start on boot'],
|
||||||
|
autoClose: ['Close window after countdown'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const Field: React.FC<{ setting: keyof typeof labels }> = ({ setting: key }) => {
|
||||||
|
const label = labels[key]
|
||||||
|
|
||||||
|
const [value, setValue] = useState<null | number | boolean>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initial = ipcRenderer.sendSync('load', { key })
|
||||||
|
setValue(initial)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value === null) return
|
||||||
|
ipcRenderer.send('save', { key, value })
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return value === null ? null : (
|
||||||
|
<div className="ma0 mt0">
|
||||||
|
{typeof value === 'boolean' ? (
|
||||||
|
<label className="form-switch">
|
||||||
|
<input type="checkbox" id={key} onChange={(e) => setValue(e.target.checked)} checked={value} />
|
||||||
|
<i className="form-icon"></i> {label[0]}
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={key}>
|
||||||
|
{label[0]} <b>{value}</b> {label[1]}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="mt0 mb3"
|
||||||
|
type="range"
|
||||||
|
id={key}
|
||||||
|
min="1"
|
||||||
|
max="60"
|
||||||
|
step="1"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Field
|
12
src/front/settings/Footer.tsx
Normal file
12
src/front/settings/Footer.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { version } from '../../../package.json'
|
||||||
|
|
||||||
|
const Footer: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="tc mt4 f6 o-40">
|
||||||
|
<span className="code">version: {version}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
24
src/front/settings/Settings.tsx
Normal file
24
src/front/settings/Settings.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
|
||||||
|
import '../base.css'
|
||||||
|
|
||||||
|
import Field from './Field'
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="ma0 mb2">Settings</h3>
|
||||||
|
<form>
|
||||||
|
<fieldset className="ma0 pa0">
|
||||||
|
<Field setting="every" />
|
||||||
|
<Field setting="duration" />
|
||||||
|
<Field setting="autoClose" />
|
||||||
|
<Field setting="boot" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
12
src/front/settings/index.html
Normal file
12
src/front/settings/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Settings</title>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main></main>
|
||||||
|
<script src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
src/front/settings/index.tsx
Normal file
20
src/front/settings/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
|
||||||
|
import '../base.css'
|
||||||
|
|
||||||
|
import About from './About'
|
||||||
|
import Settings from './Settings'
|
||||||
|
import Footer from './Footer'
|
||||||
|
|
||||||
|
const Main = () => {
|
||||||
|
return (
|
||||||
|
<div className="pa4">
|
||||||
|
<About />
|
||||||
|
<Settings />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<Main />, window.document.querySelector('main'))
|
Loading…
Reference in New Issue
Block a user