1
0
mirror of https://github.com/kremalicious/blowfish.git synced 2024-11-15 09:35:14 +01:00

Merge pull request #10 from kremalicious/feature/touchbar-electron

Electron Touch Bar
This commit is contained in:
Matthias Kretschmann 2019-05-25 03:21:31 +02:00 committed by GitHub
commit db3919154f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 225 additions and 181 deletions

View File

@ -26,6 +26,7 @@
- [Features](#features) - [Features](#features)
- [Download](#download) - [Download](#download)
- [Development](#development) - [Development](#development)
- [Configuration](#configuration)
- [Build packages](#build-packages) - [Build packages](#build-packages)
- [License](#license) - [License](#license)
@ -39,9 +40,10 @@
- re-fetches everything automatically every minute - re-fetches everything automatically every minute
- balances are fetched via etherscan.io API - balances are fetched via etherscan.io API
- spot prices are fetched from coingecko.com API - spot prices are fetched from coingecko.com API
- detects system locale for number formatting
- detects dark appearance setting and switches to dark theme automatically (macOS only) - detects dark appearance setting and switches to dark theme automatically (macOS only)
- detects system accent color and uses it as primary color (macOS & Windows only) - detects system accent color and uses it as primary color (macOS & Windows only)
- Touch Bar support (macOS only)
- detects system locale for number formatting
- currently highly optimized for macOS, your mileage on Windows or Linux may vary - currently highly optimized for macOS, your mileage on Windows or Linux may vary
## Download ## Download
@ -57,6 +59,8 @@ Alternatively, you can [build the app on your system](#build-packages).
## Development ## Development
The main app is a React app in `src/renderer/` wrapped within an Electron app defined in `src/main/`.
Clone, and run: Clone, and run:
```bash ```bash
@ -70,6 +74,18 @@ npm install
npm start npm start
``` ```
## Configuration
The app has a settings screen where you can add your account addresses.
When building the app yourself, you can configure more in the `src/config.js` file:
| Key | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `conversions` | Array defining the currencies the Ocean balance is converted to. Every currency listed here will appear in the ticker buttons. |
| `refreshInterval` | Defines the interval prices and balances are refetched. |
| `oceanTokenContract` | Contract address of the Ocean Token. You should not change this. |
## Build packages ## Build packages
```bash ```bash

View File

@ -3,7 +3,7 @@
"productName": "Blowfish", "productName": "Blowfish",
"version": "1.0.2", "version": "1.0.2",
"description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.", "description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.",
"main": "./src/main.js", "main": "./src/main/index.js",
"scripts": { "scripts": {
"test": "eslint ./src/**/*.{js,jsx} && stylelint ./src/**/*.css", "test": "eslint ./src/**/*.{js,jsx} && stylelint ./src/**/*.css",
"start": "webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js", "start": "webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js",
@ -21,28 +21,23 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.3.1", "@coingecko/cryptoformat": "^0.3.1",
"@reach/router": "^1.2.1",
"ethereum-address": "0.0.4", "ethereum-address": "0.0.4",
"ms": "^2.1.1", "ms": "^2.1.1"
"react": "^16.8.6",
"react-blockies": "^1.4.1",
"react-dom": "^16.8.6",
"react-pose": "^4.0.8",
"react-touchbar-electron": "0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.4", "@babel/core": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4", "@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-transform-runtime": "^7.4.4", "@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.4", "@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.5",
"@reach/router": "^1.2.1",
"@svgr/webpack": "^4.2.0", "@svgr/webpack": "^4.2.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.3", "copy-webpack-plugin": "^5.0.3",
"css-loader": "^2.1.1", "css-loader": "^2.1.1",
"electron": "^6.0.0-beta.3", "electron": "^5.0.2",
"electron-builder": "^20.40.2", "electron-builder": "^20.40.2",
"electron-devtools-installer": "^2.2.4", "electron-devtools-installer": "^2.2.4",
"electron-store": "^3.2.0", "electron-store": "^3.2.0",
@ -53,10 +48,14 @@
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"prettier": "^1.17.0", "prettier": "^1.17.0",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"react": "^16.8.6",
"react-blockies": "^1.4.1",
"react-dom": "^16.8.6",
"react-pose": "^4.0.8",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"stylelint": "^10.0.1", "stylelint": "^10.0.1",
"stylelint-config-standard": "^18.3.0", "stylelint-config-standard": "^18.3.0",
"webpack": "^4.31.0", "webpack": "^4.32.2",
"webpack-cli": "^3.3.2", "webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.3.1" "webpack-dev-server": "^3.3.1"
}, },
@ -65,6 +64,7 @@
"appId": "com.kremalicious.blowfish", "appId": "com.kremalicious.blowfish",
"files": [ "files": [
"./build/**/*", "./build/**/*",
"./src/main/**/*",
"./src/*.js", "./src/*.js",
"package.json" "package.json"
], ],

View File

@ -1,49 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { TouchBar, Button } from 'react-touchbar-electron'
import { cryptoFormatter } from '../../utils'
import { AppContext } from '../store/createContext'
const TouchbarItems = ({ prices, currency, toggleCurrencies, accentColor }) => (
<>
<Button
label={cryptoFormatter(1, 'ocean')}
onClick={() => toggleCurrencies('ocean')}
backgroundColor={currency === 'ocean' ? accentColor : '#141414'}
/>
{Object.keys(prices).map(key => (
<Button
key={key}
label={cryptoFormatter(prices[key], key)}
onClick={() => toggleCurrencies(key)}
backgroundColor={
currency !== 'ocean' && currency === key ? accentColor : '#141414'
}
/>
))}
</>
)
TouchbarItems.propTypes = {
prices: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired,
toggleCurrencies: PropTypes.func.isRequired,
accentColor: PropTypes.string
}
export default class Touchbar extends PureComponent {
render() {
return (
<TouchBar>
<TouchbarItems
prices={this.context.prices}
currency={this.context.currency}
toggleCurrencies={this.context.toggleCurrencies}
accentColor={this.context.accentColor}
/>
</TouchBar>
)
}
}
Touchbar.contextType = AppContext

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
prices: ['eur', 'usd', 'btc', 'eth'], conversions: ['eur', 'usd', 'btc', 'eth'],
refreshInterval: '1m', refreshInterval: '1m',
oceanTokenContract: '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41' oceanTokenContract: '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41'
} }

View File

@ -1,9 +1,9 @@
const path = require('path') const path = require('path')
const { app, BrowserWindow, systemPreferences } = require('electron') const { app, BrowserWindow, systemPreferences, ipcMain } = require('electron')
const { touchBarWrapper } = require('react-touchbar-electron') const pkg = require('../../package.json')
const pkg = require('../package.json')
const buildMenu = require('./menu') const buildMenu = require('./menu')
const { rgbaToHex } = require('./utils') const { buildTouchbar, updateTouchbar } = require('./touchbar')
const { rgbaToHex } = require('../utils')
let mainWindow let mainWindow
@ -20,6 +20,84 @@ if (
const width = 620 const width = 620
const height = 440 const height = 440
const createWindow = async () => {
const isDarkMode = systemPreferences.isDarkMode()
mainWindow = new BrowserWindow({
width,
height,
minWidth: width,
minHeight: height,
acceptFirstMouse: true,
titleBarStyle: 'hiddenInset',
fullscreenWindowTitle: true,
backgroundColor: isDarkMode ? '#141414' : '#fff',
frame: false,
show: false,
title: pkg.productName,
webPreferences: {
nodeIntegration: true,
scrollBounce: true
}
})
mainWindow.loadURL(
isDev
? 'http://localhost:8080'
: `file://${path.join(__dirname, '../../build/index.html')}`
)
createWindowEvents(mainWindow)
installDevTools(mainWindow)
mainWindow.once('ready-to-show', () => {
mainWindow.show()
mainWindow.focus()
})
mainWindow.on('closed', () => {
mainWindow = null
})
// Load menubar
buildMenu(mainWindow)
// Load touchbar
if (process.platform === 'darwin') {
const accentColor = getAccentColor()
buildTouchbar(mainWindow, accentColor)
ipcMain.on('prices-updated', (event, pricesNew) => {
updateTouchbar(pricesNew, mainWindow, accentColor)
})
ipcMain.on('currency-updated', (event, pricesNew, currentCurrency) => {
updateTouchbar(pricesNew, mainWindow, accentColor, currentCurrency)
})
}
}
app.on('ready', () => {
createWindow()
mainWindow.webContents.on('dom-ready', () => {
switchTheme()
switchAccentColor()
})
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
const installDevTools = async mainWindow => { const installDevTools = async mainWindow => {
if (isDev) { if (isDev) {
const { const {
@ -66,80 +144,17 @@ const createWindowEvents = mainWindow => {
) )
} }
const createWindow = async () => {
const isDarkMode = systemPreferences.isDarkMode()
mainWindow = new BrowserWindow({
width,
height,
minWidth: width,
minHeight: height,
acceptFirstMouse: true,
titleBarStyle: 'hiddenInset',
fullscreenWindowTitle: true,
backgroundColor: isDarkMode ? '#141414' : '#fff',
frame: false,
show: false,
title: pkg.productName,
webPreferences: {
nodeIntegration: true,
scrollBounce: true
}
})
mainWindow.loadURL(
isDev
? 'http://localhost:8080'
: `file://${path.join(__dirname, '../build/index.html')}`
)
createWindowEvents(mainWindow)
installDevTools(mainWindow)
mainWindow.once('ready-to-show', () => {
mainWindow.show()
mainWindow.focus()
})
mainWindow.on('closed', () => {
mainWindow = null
})
// Load menubar
buildMenu(mainWindow)
// Load touchbar
process.platform === 'darwin' && touchBarWrapper(mainWindow)
}
app.on('ready', () => {
createWindow()
mainWindow.webContents.on('dom-ready', () => {
switchTheme()
switchAccentColor()
})
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
// //
// Accent color setting // Accent color setting
// macOS & Windows // macOS & Windows
// //
const switchAccentColor = () => { const getAccentColor = () => {
const systemAccentColor = systemPreferences.getAccentColor() const systemAccentColor = systemPreferences.getAccentColor()
const accentColor = rgbaToHex(systemAccentColor) return rgbaToHex(systemAccentColor)
}
const switchAccentColor = () => {
const accentColor = getAccentColor()
mainWindow.webContents.send('accent-color', accentColor) mainWindow.webContents.send('accent-color', accentColor)
} }

View File

@ -1,6 +1,6 @@
const { app, Menu } = require('electron') const { app, Menu } = require('electron')
const { openUrl } = require('./utils') const { openUrl } = require('../utils')
const { homepage } = require('../package.json') const { homepage } = require('../../package.json')
const buildMenu = mainWindow => { const buildMenu = mainWindow => {
const template = [ const template = [

54
src/main/touchbar.js Normal file
View File

@ -0,0 +1,54 @@
const { TouchBar } = require('electron')
const { cryptoFormatter } = require('../utils')
const { conversions } = require('../config')
const { TouchBarButton } = TouchBar
const createButton = (
value,
key,
mainWindow,
accentColor,
currentCurrency = 'ocean'
) =>
new TouchBarButton({
label: cryptoFormatter(value, key),
click: () => mainWindow.webContents.send('setCurrency', key),
backgroundColor: key === currentCurrency ? accentColor : '#141414'
})
const buildTouchbar = (mainWindow, accentColor) => {
const touchBar = new TouchBar({
items: [
createButton(1, 'ocean', mainWindow, accentColor),
...conversions.map(key => createButton(0, key, mainWindow, accentColor))
]
})
mainWindow.setTouchBar(touchBar)
}
const updateTouchbar = (
pricesNew,
mainWindow,
accentColor,
currentCurrency = 'ocean'
) => {
const items = pricesNew.map(item => {
return createButton(
item[1],
item[0],
mainWindow,
accentColor,
currentCurrency
)
})
const touchBar = new TouchBar({
items: [...items]
})
mainWindow.setTouchBar(touchBar)
}
module.exports = { buildTouchbar, updateTouchbar }

View File

@ -8,7 +8,6 @@ import Home from './screens/Home'
import Preferences from './screens/Preferences' import Preferences from './screens/Preferences'
import './App.css' import './App.css'
import { defaultAnimation } from './components/Animations' import { defaultAnimation } from './components/Animations'
import Touchbar from './components/Touchbar'
// //
// Disable zooming // Disable zooming
@ -51,8 +50,6 @@ export default class App extends PureComponent {
<Preferences path="/preferences" /> <Preferences path="/preferences" />
</PosedRouter> </PosedRouter>
</div> </div>
<Touchbar />
</> </>
) )
} }

View File

@ -11,8 +11,10 @@ export default class Ticker extends PureComponent {
static contextType = AppContext static contextType = AppContext
items = activeStyle => items = activeStyle =>
Object.keys(this.context.prices).map((key, i) => ( // convert Map to array first, cause for...of or forEach returns undefined,
<Item key={i} className="number-unit"> // so it cannot be mapped to a collection of elements
[...this.context.prices.entries()].map(([key, value]) => (
<Item key={key} className="number-unit">
<button <button
className="label label--price" className="label label--price"
onClick={() => this.context.toggleCurrencies(key)} onClick={() => this.context.toggleCurrencies(key)}
@ -23,7 +25,7 @@ export default class Ticker extends PureComponent {
: {} : {}
} }
> >
{cryptoFormatter(this.context.prices[key], key)} {cryptoFormatter(value, key)}
</button> </button>
</Item> </Item>
)) ))

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { AppContext } from '../store/createContext' import { AppContext } from '../store/createContext'
import Balance from './Balance' import Balance from './Balance'
import { prices } from '../config' import { conversions } from '../../config'
const calculateTotalBalance = (accounts, currency) => { const calculateTotalBalance = (accounts, currency) => {
const balanceTotalArray = [] const balanceTotalArray = []
@ -23,15 +23,15 @@ export default class Total extends PureComponent {
static contextType = AppContext static contextType = AppContext
render() { render() {
const conversions = Object.assign( const conversionsBalance = Object.assign(
...prices.map(key => ({ ...conversions.map(key => ({
[key]: calculateTotalBalance(this.context.accounts, key) [key]: calculateTotalBalance(this.context.accounts, key)
})) }))
) )
const balanceNew = { const balanceNew = {
ocean: calculateTotalBalance(this.context.accounts, 'ocean'), ocean: calculateTotalBalance(this.context.accounts, 'ocean'),
...conversions ...conversionsBalance
} }
return ( return (

View File

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 871 B

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import { render } from 'react-dom' import { render } from 'react-dom'
import { TouchBarProvider } from 'react-touchbar-electron'
import AppProvider from './store/AppProvider' import AppProvider from './store/AppProvider'
import App from './App' import App from './App'
@ -13,9 +12,7 @@ document.body.appendChild(root)
render( render(
<AppProvider> <AppProvider>
<TouchBarProvider> <App />
<App />
</TouchBarProvider>
</AppProvider>, </AppProvider>,
document.getElementById('root') document.getElementById('root')
) )

View File

@ -4,8 +4,14 @@ import ms from 'ms'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import Store from 'electron-store' import Store from 'electron-store'
import { AppContext } from './createContext' import { AppContext } from './createContext'
import fetchData from '../util/fetch' import fetchData from '../utils/fetch'
import { refreshInterval, prices, oceanTokenContract } from '../config' import { refreshInterval, conversions, oceanTokenContract } from '../../config'
// construct initial prices Map to get consistent
// order for Ticker and Touchbar
let pricesMap = new Map()
pricesMap.set('ocean', 1)
conversions.map(key => pricesMap.set(key, 0))
export default class AppProvider extends PureComponent { export default class AppProvider extends PureComponent {
static propTypes = { static propTypes = {
@ -19,18 +25,26 @@ export default class AppProvider extends PureComponent {
accounts: [], accounts: [],
currency: 'ocean', currency: 'ocean',
needsConfig: false, needsConfig: false,
prices: Object.assign(...prices.map(key => ({ [key]: 0 }))), prices: pricesMap,
toggleCurrencies: currency => this.setState({ currency }), toggleCurrencies: currency => this.toggleCurrencies(currency),
setBalances: () => this.setBalances(), setBalances: () => this.setBalances(),
accentColor: '' accentColor: ''
} }
async componentDidMount() { async componentDidMount() {
// listener for accent color
ipcRenderer.on('accent-color', (event, accentColor) => { ipcRenderer.on('accent-color', (event, accentColor) => {
this.setState({ accentColor }) this.setState({ accentColor })
}) })
await this.fetchAndSetPrices() // listener for touchbar
ipcRenderer.on('setCurrency', (evt, currency) =>
this.state.toggleCurrencies(currency)
)
const newPrizes = await this.fetchAndSetPrices()
this.setState({ prices: newPrizes })
await this.setBalances() await this.setBalances()
setInterval(this.fetchAndSetPrices, ms(refreshInterval)) setInterval(this.fetchAndSetPrices, ms(refreshInterval))
@ -66,19 +80,17 @@ export default class AppProvider extends PureComponent {
} }
fetchAndSetPrices = async () => { fetchAndSetPrices = async () => {
const currencies = prices.join(',') const currencies = conversions.join(',')
const json = await fetchData( const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}` `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}`
) )
const newPrizes = Object.assign( let newPrices = new Map(this.state.prices) // make a shallow copy of the Map
...prices.map(key => ({ conversions.map(key => newPrices.set(key, json['ocean-protocol'][key])) // modify the copy
ocean: 1,
[key]: json['ocean-protocol'][key]
}))
)
this.setState({ prices: newPrizes }) ipcRenderer.send('prices-updated', Array.from(newPrices)) // convert Map to array, ipc messages seem to kill it
this.setState({ prices: newPrices })
return newPrices
} }
setBalances = async () => { setBalances = async () => {
@ -89,9 +101,9 @@ export default class AppProvider extends PureComponent {
for (const account of accountsPref) { for (const account of accountsPref) {
const oceanBalance = await this.getBalance(account) const oceanBalance = await this.getBalance(account)
const conversions = Object.assign( const conversionsBalance = Object.assign(
...prices.map(key => ({ ...conversions.map(key => ({
[key]: oceanBalance * this.state.prices[key] || 0 [key]: oceanBalance * this.state.prices.get(key) || 0
})) }))
) )
@ -99,7 +111,7 @@ export default class AppProvider extends PureComponent {
address: account, address: account,
balance: { balance: {
ocean: oceanBalance, ocean: oceanBalance,
...conversions ...conversionsBalance
} }
} }
@ -111,6 +123,12 @@ export default class AppProvider extends PureComponent {
} }
} }
toggleCurrencies(currency) {
const pricesNew = Array.from(this.state.prices)
ipcRenderer.send('currency-updated', pricesNew, currency)
this.setState({ currency })
}
render() { render() {
return ( return (
<AppContext.Provider value={this.state}> <AppContext.Provider value={this.state}>

View File

@ -15,7 +15,7 @@ const rgbaToHex = color => {
} }
const locale = const locale =
typeof navigator !== 'undefined' ? navigator.language : app.getLocale() typeof navigator !== 'undefined' ? navigator.language : () => app.getLocale()
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
const numberFormatter = value => const numberFormatter = value =>
@ -39,7 +39,7 @@ const cryptoFormatter = (value, currency) => {
if (currency === 'ocean') { if (currency === 'ocean') {
return formatOcean(value) return formatOcean(value)
} else { } else {
return formatCurrency(value, currency.toUpperCase(), locale.split('-')[0]) return formatCurrency(value, currency.toUpperCase(), locale)
.replace(/BTC/, 'Ƀ') .replace(/BTC/, 'Ƀ')
.replace(/ETH/, 'Ξ') .replace(/ETH/, 'Ξ')
} }

View File

@ -2,11 +2,10 @@ const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin') const CopyPlugin = require('copy-webpack-plugin')
// Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up const defaultInclude = [path.resolve(__dirname, 'src', 'renderer')]
const defaultInclude = [path.resolve(__dirname, 'src')]
module.exports = { module.exports = {
entry: path.resolve(__dirname, 'src') + '/app/index.js', entry: path.resolve(__dirname, 'src', 'renderer', 'index.js'),
output: { output: {
path: path.resolve(__dirname, 'build'), path: path.resolve(__dirname, 'build'),
filename: 'bundle.js', filename: 'bundle.js',
@ -33,11 +32,6 @@ module.exports = {
test: /\.svg$/, test: /\.svg$/,
use: ['@svgr/webpack'], use: ['@svgr/webpack'],
include: defaultInclude include: defaultInclude
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: ['file-loader?name=font/[name]__[hash:base64:5].[ext]'],
include: defaultInclude
} }
] ]
}, },
@ -48,7 +42,7 @@ module.exports = {
plugins: [ plugins: [
new HtmlWebpackPlugin(), new HtmlWebpackPlugin(),
new CopyPlugin([ new CopyPlugin([
{ from: './src/app/images/icon.*', to: './', flatten: true } { from: './src/renderer/images/icon.*', to: './', flatten: true }
]) ])
] ]
} }