mirror of
https://github.com/cupcakearmy/unpixel.git
synced 2025-09-10 12:40:45 +00:00
initial commit
This commit is contained in:
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'))
|
Reference in New Issue
Block a user