experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #6 from autonome/modulus

mo(dul)ar(ization), etc

authored by

Dietrich Ayala and committed by
GitHub
8f48b393 ab92181d

+373 -560
+52 -12
README.md
··· 2 2 3 3 Please meet Peek, a web user agent application designed for using the web where, when and how you want it. 4 4 5 - ** WARNING** 5 + ** WARNING: THIS IS VACATIONWARE ** 6 + 6 7 * Peek is not a web browser! Yet! And likely never will be a browser as you would expect from browsers to date. There are no tabs, and no windows in the tabbed-browser-like sense of them. If that's what you're looking for, there are a few decent browsers for you to choose from. 7 8 * Peek is not safe for general use yet! It is a crude proof of concept I whipped up while on vacation. While I have thoughts on security model and user interface, I have not written it up into a proper security model yet. 8 9 ··· 38 39 * Windows and tabs should have died a long time ago, a mixed metaphor constraining the ability of the web to grow/thrive/change and meet user needs 39 40 * Security user interface must be a clear articulation of risks and trade-offs, and users should own the decisions 40 41 41 - # Architecture / Implementation 42 + ## Design 43 + 44 + * Escape IZUI 45 + * IZUI: inverse zooming user interface 46 + * ZUIs navigate by starting from a known root and user navigates by zooming ever further in 47 + * Escape starts anywhere, and instead of navigating by zooming in, all interfaces can zoom out to reset 48 + * allows unbounded and diverse entry points with predictable behavior 49 + * consistent path to familiar ground 50 + 51 + Escape navigation model 52 + * navigation base can start at any level in stack 53 + * forward navigations are added on top of stack 54 + * backwards navigations walk the stack in reverse 55 + 56 + ## Architecture / Implementation 42 57 43 58 Peek is designed to be modular and configurable around the idea that parts of it can run in different environments. 44 59 ··· 47 62 * I'd like to have a decentralized compute option for running your scripts outside of your clients and syncing the data 48 63 * Want cloud storage for all config and data, esp infinite history, so can do fun things with it 49 64 50 - ## Desktop App 65 + ### Desktop App 51 66 52 67 Proof of concept is Electron. By far the best option today for cross-platform desktop apps which need a web rendering engine. There's really nothing else remotely suited (yet). 53 68 ··· 59 74 60 75 ### Usage 61 76 62 - * `cmd/ctl+Escape` or launch app to open settings 63 - * Configure Peeks/Slides/Scripts in settings 64 - * `Opt+0-9` to open Peeks 65 - * `Opt+←→↑↓` to open Slides 77 + * Settings 78 + * In app, `cmd/ctl+r,` or launch app to open settings, or click tray 79 + * Configure Peeks/Slides/Scripts in settings 80 + * Peeks 81 + * `Opt+0-9` to open Peeks 82 + * Slides 83 + * `Opt+←→↑↓` to open Slides 66 84 67 - ## Mobile 85 + ### Mobile 68 86 69 - TBD 87 + * Quick access to Script output and manual runs, as widgets (or output from cloud runners?) 88 + * Peeks still totes useful here - on mobile is more like "quick dial" features 70 89 71 - ## Cloud 90 + ### Cloud 72 91 73 92 * Going full crypto payments for distributed compute on this one. 74 93 75 94 ## Roadmap 76 95 77 96 Next 78 - * Modularize feature types, eyeing the extensibility model 97 + * ✅ Modularize feature types, eyeing the extensibility model 98 + * Settings Cleanup 99 + * ✅ move settings window to features/settings 100 + * why no settings 101 + * make features enable/disable-able 102 + * move feature list and enablement to storage 103 + * re-enable label previews, eg "Peek {key} - {address}" 104 + * App cleanup 105 + * ✅ main window vs settings 106 + * ✅ change settings shortcut from global+esc to app+comma 107 + * Default features cleanup 108 + * enable/disable individual peeks/slides 109 + * Window lifecycle 110 + * modularize window open/close/hide/show across all windows 111 + * hide/show window vs create fresh 112 + * Web content 113 + * isolate web loading code, have features load content that way instead of raw BrowserWindow 79 114 * Basic command bar to open pages 80 115 * Basic history store 81 116 82 117 After that 83 - * Extension model 118 + * Extension model? 84 119 * Ubiquity-like 85 120 * Panorama-like 121 + * Tray 122 + * Scratchpad 123 + * Identity 124 + * Contacts 125 + * Collaboration 86 126 87 127 Further 88 128 * Implement the Firefox "awesomebar" scoring and search algorithm so that Peek *learns* you
-17
config.js
··· 1 - const Store = require('electron-store'); 2 - 3 - const store = new Store(); 4 - store.clear(); 5 - 6 - store.set('prefs', { 7 - globalKeyCmd: 'CommandOrControl+Escape', 8 - peekKeyPrefix: 'Option+' 9 - }); 10 - 11 - store.set('peeks', [ 12 - ]); 13 - 14 - store.set('scripts', [ 15 - ]); 16 - 17 - module.exports = store;
-8
content-main.js
··· 1 - console.log('content-main'); 2 - 3 - setTimeout(() => { 4 - const s = "selector: '.percent > span:nth-child(2)"; 5 - const r = document.querySelector(s); 6 - const value = r ? r.textContent : null; 7 - console.log('cs val', value; 8 - }, 1000);
+243
features/input/input.js
··· 1 + // input/input.js 2 + (async () => { 3 + 4 + console.log('input/input'); 5 + 6 + const labels = { 7 + featureType: 'input', 8 + featureDisplay: 'Input', 9 + prefs: { 10 + shortcutKey: 'Input shortcut', 11 + } 12 + }; 13 + 14 + const { 15 + BrowserWindow, 16 + globalShortcut, 17 + } = require('electron'); 18 + 19 + const path = require('path'); 20 + 21 + let _store = null; 22 + let _data = {}; 23 + 24 + const prefsSchema = { 25 + "$schema": "https://json-schema.org/draft/2020-12/schema", 26 + "$id": "peek.input.prefs.schema.json", 27 + "title": "Input preferences", 28 + "description": "Peek app Input user preferences", 29 + "type": "object", 30 + "properties": { 31 + "shortcutKey": { 32 + "description": "Global OS hotkey to open input", 33 + "type": "string", 34 + "default": "Option+Space" 35 + }, 36 + }, 37 + "required": [ "shortcutKey"] 38 + }; 39 + 40 + const itemSchema = { 41 + "$schema": "https://json-schema.org/draft/2020-12/schema", 42 + "$id": "peek.input.entry.schema.json", 43 + "title": "Peek - input entry", 44 + "description": "Peek input entry", 45 + "type": "object", 46 + "properties": { 47 + "keyNum": { 48 + "description": "Number on keyboard to open this peek, 0-9", 49 + "type": "integer", 50 + "minimum": 0, 51 + "maximum": 9, 52 + "default": 0 53 + }, 54 + "title": { 55 + "description": "Name of the peek - user defined label", 56 + "type": "string", 57 + "default": "New Peek" 58 + }, 59 + "address": { 60 + "description": "URL to load", 61 + "type": "string", 62 + "default": "https://example.com" 63 + }, 64 + "persistState": { 65 + "description": "Whether to persist local state or load page into empty container - defaults to false", 66 + "type": "boolean", 67 + "default": false 68 + }, 69 + "keepLive": { 70 + "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 71 + "type": "boolean", 72 + "default": false 73 + }, 74 + "allowSound": { 75 + "description": "Whether to allow the page to emit sound or not (eg for background music player peeks - defaults to false", 76 + "type": "boolean", 77 + "default": false 78 + }, 79 + "height": { 80 + "description": "User-defined height of peek page", 81 + "type": "integer", 82 + "default": 600 83 + }, 84 + "width": { 85 + "description": "User-defined width of peek page", 86 + "type": "integer", 87 + "default": 800 88 + }, 89 + }, 90 + "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound", 91 + "height", "width" ] 92 + }; 93 + 94 + const listSchema = { 95 + type: 'array', 96 + items: { "$ref": "#/$defs/entry" } 97 + }; 98 + 99 + // TODO: schemaize 0-9 constraints for peeks 100 + const schemas = { 101 + prefs: prefsSchema, 102 + item: itemSchema, 103 + items: listSchema 104 + }; 105 + 106 + const _defaults = { 107 + prefs: { 108 + shortcutKey: 'Option+Space' 109 + }, 110 + }; 111 + 112 + let _windows = {}; 113 + 114 + const openInputWindow = (api, item) => { 115 + // TODO: implement me 116 + return; 117 + 118 + const height = item.height || 600; 119 + const width = item.width || 800; 120 + 121 + let win = null; 122 + 123 + const windowKey = labels.featureType + item.keyNum; 124 + 125 + if (_windows[windowKey]) { 126 + console.log(labels.featureType, item.keyNum, 'using stored window'); 127 + win = _windows[windowKey]; 128 + win.show(); 129 + } 130 + else { 131 + console.log(labels.featureType, item.keyNum, 'creating new window'); 132 + 133 + win = new BrowserWindow({ 134 + height, 135 + width, 136 + center: true, 137 + skipTaskbar: true, 138 + autoHideMenuBar: true, 139 + titleBarStyle: 'hidden', 140 + webPreferences: { 141 + preload: api.preloadPath, 142 + // isolate content and do not persist it 143 + partition: Date.now() 144 + } 145 + }); 146 + 147 + //_windows[windowKey] = win; 148 + } 149 + 150 + const onGoAway = () => { 151 + /* 152 + if (item.keepLive) { 153 + _windows[windowKey] = win; 154 + win.hide(); 155 + } 156 + else { 157 + win.destroy(); 158 + } 159 + */ 160 + win.destroy(); 161 + } 162 + win.on('blur', onGoAway); 163 + win.on('close', onGoAway); 164 + 165 + win.webContents.send('window', { type: labels.featureType, id: win.id, data: item }); 166 + 167 + win.loadURL(item.address); 168 + }; 169 + 170 + const initStore = (store, data) => { 171 + const sp = store.get('prefs'); 172 + if (!sp) { 173 + store.set('prefs', data.prefs); 174 + } 175 + }; 176 + 177 + const initShortcut = (api, prefs) => { 178 + const shortcut = prefs.shortcutKey; 179 + 180 + if (globalShortcut.isRegistered(shortcut)) { 181 + globalShortcut.unregister(shortcut); 182 + } 183 + 184 + const ret = globalShortcut.register(shortcut, () => { 185 + openInputWindow(api); 186 + }); 187 + 188 + if (!ret) { 189 + console.error('Unable to register shortcut', shortcut); 190 + } 191 + }; 192 + 193 + const init = (api, store) => { 194 + _store = store; 195 + _api = api; 196 + 197 + initStore(_store, _defaults); 198 + 199 + 200 + _data = { 201 + get prefs() { return _store.get('prefs'); }, 202 + //get items() { return _store.get('items'); }, 203 + }; 204 + 205 + initShortcut(api, _data.prefs); 206 + }; 207 + 208 + const onChange = (changed, old) => { 209 + console.log(labels.featureType, 'onChange', changed); 210 + 211 + // TODO only update store if changed 212 + // and re-init 213 + if (changed.prefs) { 214 + console.log('input: updating prefs', changed.prefs); 215 + _store.set('prefs', changed.prefs); 216 + } 217 + 218 + if (changed.items) { 219 + _store.set('items', changed.items); 220 + } 221 + }; 222 + 223 + // ui config 224 + const config = { 225 + // allow user to create new items 226 + allowNew: false, 227 + // fields that are view only 228 + disabled: ['keyNum'], 229 + }; 230 + 231 + module.exports = { 232 + init: init, 233 + config, 234 + labels, 235 + schemas, 236 + data: { 237 + get prefs() { return _store.get('prefs'); }, 238 + }, 239 + onChange 240 + }; 241 + 242 + 243 + })();
+3 -2
features/peeks/peeks.js
··· 21 21 const path = require('path'); 22 22 23 23 let _store = null; 24 + let _data = {}; 24 25 25 26 const prefsSchema = { 26 27 "$schema": "https://json-schema.org/draft/2020-12/schema", 27 28 "$id": "peek.peeks.prefs.schema.json", 28 - "title": "Peek - peeks", 29 - "description": "Peek app Peeks prefs", 29 + "title": "Peeks preferences", 30 + "description": "Peeks user preferences", 30 31 "type": "object", 31 32 "properties": { 32 33 "shortcutKeyPrefix": {
features/scripts/.scripts.js.swp

This is a binary file and will not be displayed.

+4 -2
features/scripts/scripts.js
··· 25 25 const prefsSchema = { 26 26 "$schema": "https://json-schema.org/draft/2020-12/schema", 27 27 "$id": "peek.scripts.prefs.schema.json", 28 - "title": "Peek - Scripts prefs", 29 - "description": "Peek app Scripts prefs", 28 + "title": "Scripts preferences", 29 + "description": "Scripts user preferences", 30 30 "type": "object", 31 31 "properties": { 32 32 }, ··· 112 112 prefs: { 113 113 }, 114 114 items: [ 115 + /* 115 116 { 116 117 id: 'peek:script:localhost:test', 117 118 title: 'localhost test', ··· 123 124 storehistory: false, 124 125 notifyOnChange: false 125 126 }, 127 + */ 126 128 ] 127 129 }; 128 130
+2 -2
features/slides/slides.js
··· 26 26 const prefsSchema = { 27 27 "$schema": "https://json-schema.org/draft/2020-12/schema", 28 28 "$id": "peek.slides.prefs.schema.json", 29 - "title": "Peek - Slides prefs", 30 - "description": "Peek app Slides prefs", 29 + "title": "Slides prefs", 30 + "description": "Peek app Slides user preferences", 31 31 "type": "object", 32 32 "properties": { 33 33 "shortcutKeyPrefix": {
+69 -169
index.js
··· 3 3 4 4 console.log('main'); 5 5 6 + const DEBUG = process.env.DEBUG; 7 + 6 8 // Modules to control application life and create native browser window 7 9 const { 8 10 electron, ··· 19 21 20 22 const path = require('path'); 21 23 const preloadPath = path.join(__dirname, 'preload.js'); 22 - 23 24 const Store = require('electron-store'); 24 25 25 - const features = { 26 - peeks: require('./features/peeks/peeks'), 27 - slides: require('./features/slides/slides'), 28 - scripts: require('./features/scripts/scripts'), 29 - }; 30 - 31 - const labels = { 32 - app: { 33 - key: 'peek', 34 - title: 'Peek' 35 - }, 36 - tray: { 37 - tooltip: 'Click to open' 38 - } 39 - }; 40 - 41 - const ICON_RELATIVE_PATH = 'assets/icons/AppIcon.appiconset/Icon-App-20x20@2x.png'; 42 - const ICON_PATH = path.join(__dirname, ICON_RELATIVE_PATH); 43 - 26 + // ***** Developer / Error handling / Etc ***** 44 27 const isDev = require('electron-is-dev'); 45 28 46 29 if (isDev) { ··· 59 42 const unhandled = require('electron-unhandled'); 60 43 unhandled(); 61 44 45 + // ***** System / OS / Theme / Etc ***** 46 + 62 47 // system dark mode handling 63 48 ipcMain.handle('dark-mode:toggle', () => { 64 49 if (nativeTheme.shouldUseDarkColors) { ··· 73 58 nativeTheme.themeSource = 'system'; 74 59 }); 75 60 76 - let _windows = []; 77 - 78 - // main window 79 - let _win = null; 61 + // ***** App / Strings / Etc ***** 80 62 81 - // tray 82 - let _tray = null; 63 + const features = { 64 + settings: require('./features/settings/settings'), 65 + input: require('./features/input/input'), 66 + peeks: require('./features/peeks/peeks'), 67 + slides: require('./features/slides/slides'), 68 + scripts: require('./features/scripts/scripts'), 69 + }; 83 70 84 - const getMainWindow = () => { 85 - if (_win === null || _win.isDestroyed()) { 86 - _win = createMainWindow(); 71 + const labels = { 72 + app: { 73 + key: 'peek', 74 + title: 'Peek' 75 + }, 76 + tray: { 77 + tooltip: 'Click to open' 87 78 } 88 - return _win; 89 79 }; 90 80 91 - const createMainWindow = () => { 92 - console.log('createMainWindow, preloadPath', preloadPath); 93 - // Create the browser window. 94 - const mainWindow = new BrowserWindow({ 95 - width: 800, 96 - height: 600, 97 - webPreferences: { 98 - preload: preloadPath 99 - } 100 - }); 81 + // ***** Caches ***** 101 82 102 - /* 103 - mainWindow.on('close', (e) => { 104 - console.log('onClose - just hiding'); 105 - e.preventDefault(); 106 - mainWindow.hide(); 107 - }); 108 - */ 109 - 110 - // and load the index.html of the app. 111 - mainWindow.loadFile('main.html'); 112 - 113 - // Open the DevTools. 114 - mainWindow.webContents.openDevTools() 83 + // vestigial? 84 + let _windows = []; 115 85 116 - mainWindow.webContents.send('window', { 117 - path: path.join(__dirname), 118 - id: mainWindow.id, 119 - type: 'main', 120 - }); 86 + // main window 87 + let _win = null; 121 88 122 - _windows.push(mainWindow); 123 - /* 124 - mainWindow.on('closed', () => { 125 - const idx = _windows.findIndex(mainWindow); 126 - //_windows. 127 - }); 128 - */ 129 - 130 - return mainWindow; 131 - }; 132 - 133 - // 89 + // TODO: make this open settings? 134 90 app.on('activate', () => { 135 91 // On macOS it's common to re-create a window in the app when the 136 92 // dock icon is clicked and there are no other windows open. 137 93 if (BrowserWindow.getAllWindows().length === 0) { 138 - getMainWindow().show(); 94 + //getMainWindow().show(); 139 95 } 140 96 }); 97 + 98 + // ***** Tray ***** 99 + 100 + const ICON_RELATIVE_PATH = 'assets/icons/AppIcon.appiconset/Icon-App-20x20@2x.png'; 101 + const ICON_PATH = path.join(__dirname, ICON_RELATIVE_PATH); 102 + 103 + let _tray = null; 141 104 142 105 const initTray = () => { 143 106 if (!_tray || _tray.isDestroyed()) { ··· 150 113 return _tray; 151 114 }; 152 115 116 + // ***** Data ***** 117 + 153 118 const getData = () => { 154 119 let rollup = { 155 - prefs: { 156 - schema: prefsSchema, 157 - data: appStore.get('prefs') 158 - }, 159 120 features: [] 160 121 }; 161 122 162 123 Object.keys(features).forEach(k => { 163 124 const feature = features[k]; 125 + 126 + //console.log('feature', feature); 127 + 164 128 rollup.features.push({ 165 129 config: feature.config, 166 130 labels: feature.labels, ··· 175 139 const updateData = newData => { 176 140 console.log('updateData', newData); 177 141 178 - if (newData.prefs) { 179 - appStore.set('prefs', newData.prefs); 180 - } 181 - 182 142 Object.keys(newData).forEach(k => { 143 + console.log('updateData: key exists?', k); 183 144 if (features[k]) { 145 + console.log('updateData: yes, updating with', newData[k]); 184 146 features[k].onChange(newData[k]); 185 147 } 186 148 }); 187 149 }; 188 150 189 - const prefsSchema = { 190 - "$schema": "https://json-schema.org/draft/2020-12/schema", 191 - "$id": "peek.prefs.schema.json", 192 - "title": "Peek - prefs", 193 - "description": "Peek user preferences", 194 - "type": "object", 195 - "properties": { 196 - "globalKeyCmd": { 197 - "description": "Global OS hotkey to load app", 198 - "type": "string", 199 - "default": "CommandOrControl+Escape" 200 - } 201 - }, 202 - "required": [ "globalKeyCmd" ] 203 - }; 204 - 205 - const initPrefs = store => { 206 - const defaults = { 207 - globalKeyCmd: 'CommandOrControl+Escape', 208 - }; 209 - 210 - let prefs = appStore.get('prefs'); 211 - if (!prefs) { 212 - store.set('prefs', defaults); 213 - prefs = store.get('prefs'); 214 - } 215 - 216 - // register global activation shortcut 217 - if (globalShortcut.isRegistered(prefs.globalKeyCmd)) { 218 - globalShortcut.unregister(prefs.globalKeyCmd); 219 - } 220 - 221 - const onGlobalKeyCmd = () => getMainWindow().show(); 222 - 223 - const ret = globalShortcut.register(prefs.globalKeyCmd, onGlobalKeyCmd); 224 - 225 - if (!ret) { 226 - console.error('Unable to register global key command.') 227 - } 228 - }; 229 - 230 - const initFeatures = (features) => { 231 - 151 + // initialized all bits which need updating if the data changes 152 + // can be called repeatedly to refresh on changes 153 + const initFeatures = () => { 154 + console.log('initFeatures'); 232 155 // TODO: allow features to register 233 156 // as app level prefs for enable/disable 234 157 ··· 238 161 preloadPath, 239 162 }; 240 163 241 - const featureContainerPrefix = 'peekFeature'; 164 + const datastorePrefix = 'peekFeature'; 242 165 243 166 Object.keys(features).forEach(k => { 167 + console.log('main:initFeatures()', k); 244 168 const feature = features[k]; 245 - const storeName = `${featureContainerPrefix}${feature.labels.featureType}`; 169 + 170 + if (!feature.labels) { 171 + console.error('feature?', feature) 172 + } 173 + const storeName = `${datastorePrefix}${feature.labels.featureType}`; 246 174 247 175 // have to make per feature stores for now, pfftt 248 176 // maybe fine, better isolation ··· 253 181 watch: true 254 182 }); 255 183 256 - featureStore.onDidAnyChange(newData => { 257 - initData(); 258 - //win.webContents.send('configchange', {}); 259 - }); 184 + if (DEBUG) { 185 + //console.log('main: clearing datastore', k) 186 + featureStore.clear(); 187 + } 260 188 261 189 feature.init(api, featureStore); 262 - }); 263 - }; 264 - 265 - // initialized all bits which need updating if the data changes 266 - // can be called repeatedly to refresh on changes 267 - const initData = () => { 268 - // initialize app prefs 269 - initPrefs(appStore); 270 - 271 - initFeatures(features); 272 190 273 - /* 274 - // initialize slides 275 - if (data.slides.length > 0) { 276 - initSlides(prefs.slideKeyPrefix, data.slides); 277 - } 278 - 279 - // initialize scripts 280 - if (data.scripts.length > 0) { 281 - initScripts(data.scripts); 282 - } 283 - */ 191 + featureStore.onDidAnyChange(initFeatures); 192 + }); 284 193 }; 285 194 286 - const appStore = new Store({ 287 - name: labels.app.key, 288 - // TODO: re-enable schemas 289 - //schema: fullSchema, 290 - watch: true 291 - }); 292 - 293 - // DEBUG 294 - appStore.clear(); 295 - 296 195 // app load 297 196 const onReady = () => { 298 197 console.log('onReady'); 299 198 300 - // create main app window on app start 301 - const win = getMainWindow(); 302 - 303 199 // keep app out of dock and tab switcher 304 200 if (app.dock) { 305 201 app.dock.hide(); ··· 307 203 308 204 initTray(); 309 205 310 - initData(); 206 + initFeatures(features); 207 + 208 + // open settings on startup for now 209 + if (BrowserWindow.getAllWindows().length === 0) { 210 + features.settings.open(); 211 + } 311 212 312 - appStore.onDidAnyChange(newData => { 313 - initData(); 314 - win.webContents.send('configchange', {}); 315 - }); 316 213 }; 317 214 318 215 app.whenReady().then(onReady); 319 216 320 217 // when renderer is ready, send over user data 321 218 ipcMain.on('getconfig', (ev, data) => { 322 - getMainWindow().webContents.send('config', getData()) 219 + console.log('main: getconfig') 220 + //ev.sender.hostWebContents.send('config', getData()) 221 + ev.reply('config', getData()) 323 222 }); 324 223 325 224 // listen for updates ··· 357 256 // explicitly with Cmd + Q. 358 257 app.on('window-all-closed', () => { 359 258 console.log('window-all-closed', process.platform); 360 - // 259 + /* 361 260 if (!_win.isDestroyed()) { 362 261 console.log('wac: killingit'); 363 262 _win.destroy(); 364 263 _win = null; 365 264 } 265 + */ 366 266 /* 367 267 if (_win.isVisible()) { 368 268 console.log('win is visible, hide it');
-33
main.css
··· 1 - body { 2 - font-family: -apple-system, BlinkMacSystemFont, helvetica neue, helvetica, sans-serif; 3 - font-feature-settings: "tnum"; 4 - font-size: 12.4px; 5 - font-variant-numeric: tabular-nums; 6 - margin: 12px; 7 - } 8 - 9 - body > div { 10 - margin-bottom: 10px; 11 - } 12 - 13 - body > div > div { 14 - margin-bottom: 10px; 15 - } 16 - 17 - h1 { 18 - margin-top: 1px; 19 - margin-bottom: 2px; 20 - } 21 - 22 - .houseofpane { 23 - display: flex; 24 - flex-wrap: wrap; 25 - justify-content: space-between; 26 - } 27 - 28 - /* tweakpanes */ 29 - .houseofpane > div > div { 30 - min-width: 380px; 31 - max-width: 380px; 32 - margin-bottom: 10px; 33 - }
-38
main.html
··· 1 - <!--index.html--> 2 - 3 - <!DOCTYPE html> 4 - <html> 5 - <head> 6 - <meta charset="UTF-8"> 7 - <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 8 - <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 9 - <title>peek</title> 10 - <link rel="stylesheet" href="main.css"> 11 - </head> 12 - <body> 13 - <div> 14 - <h1>peek</h1> 15 - </div> 16 - 17 - <div class="houseofpane"> 18 - <div> 19 - <div class="prefs"></div> 20 - <div class="scripts"></div> 21 - </div> 22 - <div> 23 - <div class="peeks"></div> 24 - <div class="slides"></div> 25 - </div> 26 - </div> 27 - 28 - <div> 29 - Node.js <span id="node-version"></span><br> 30 - Chromium <span id="chrome-version"></span><br> 31 - Electron <span id="electron-version"></span><br> 32 - </div> 33 - 34 - <script type=module src="./node_modules/tweakpane/dist/tweakpane.js"></script> 35 - <script type=module src="./renderer.js"></script> 36 - 37 - </body> 38 - </html>
-277
renderer.js
··· 1 - console.log('renderer'); 2 - 3 - // TODO: move to proper l10n 4 - const labels = { 5 - prefs: { 6 - paneTitle: 'Preferences', 7 - globalKeyCmd: 'App activation shortcut', 8 - }, 9 - /* 10 - peeks: { 11 - paneTitle: 'Peeks', 12 - type: 'Peek', 13 - testBtn: 'Try (❌)', 14 - }, 15 - slides: { 16 - paneTitle: 'Slides', 17 - testBtn: 'Try (❌)', 18 - }, 19 - scripts: { 20 - paneTitle: 'Scripts', 21 - testBtn: 'Try (❌)', 22 - newFolder: 'Add new script', 23 - addBtn: 'Add', 24 - delBtn: 'Delete', 25 - } 26 - */ 27 - }; 28 - 29 - // TODO: capture and internally navigate out of panes 30 - window.addEventListener('keyup', e => { 31 - //console.log('renderer', 'onkeyup', e); 32 - if (e.key == 'Escape') { 33 - //ipcRenderer.send('esc', ''); 34 - } 35 - }); 36 - 37 - // send changes back to main process 38 - // it will notify us when saved 39 - // and we'll reload entirely 😐 40 - const updateToMain = data => { 41 - console.log('renderer: updating to main', data); 42 - window.app.setConfig(data); 43 - }; 44 - 45 - let panes = []; 46 - 47 - const init = cfg => { 48 - console.log('renderer: init'); 49 - console.log('renderer: cfg', cfg); 50 - 51 - let { prefs, features } = cfg; 52 - 53 - const containerEl = document.querySelector('.houseofpane'); 54 - 55 - // blow away panes if this is an update 56 - if (panes.length > 0) { 57 - panes.forEach(p => { 58 - p.pane.dispose(); 59 - }); 60 - panes = []; 61 - } 62 - 63 - // prefs pane 64 - const el = containerEl.querySelector('.prefs'); 65 - const onChange = newData => { 66 - updateToMain({ prefs: newData }); 67 - }; 68 - 69 - const prefsPane = initValuesPane( 70 - el, 71 - labels.prefs, 72 - cfg.prefs.schema, 73 - cfg.prefs.data, 74 - onChange); 75 - panes.push({ el, pane: prefsPane }); 76 - 77 - cfg.features.forEach(feature => { 78 - const type = feature.labels.featureType; 79 - const el = containerEl.querySelector('.' + type); 80 - 81 - const allowNew = feature.config.allowNew || false; 82 - const disabled = feature.config.disabled || []; 83 - 84 - /* 85 - const disabled = { 86 - scripts: ['previousValue'], 87 - slides: ['screenEdge'], 88 - }; 89 - */ 90 - 91 - const onChange = newData => { 92 - const p = {}; 93 - p[type] = { items: newData }; 94 - updateToMain(p); 95 - }; 96 - 97 - const pane = initFeaturePane( 98 - el, 99 - feature, 100 - onChange 101 - ); 102 - 103 - panes.push({ 104 - el, 105 - pane 106 - }); 107 - }); 108 - }; 109 - 110 - const fillPaneFromSchema = (pane, labels, schema, data, onChange, disabled) => { 111 - const props = schema.properties; 112 - Object.keys(props).forEach(k => { 113 - // schema for property 114 - const s = props[k]; 115 - 116 - // value (or default) 117 - const v = 118 - (data && data.hasOwnProperty(k)) 119 - ? data[k] 120 - : props[k].default; 121 - 122 - const params = {}; 123 - const opts = {}; 124 - 125 - // dedecimalize 126 - if (s.type == 'integer') { 127 - opts.step = 1; 128 - } 129 - 130 - // disabled fields 131 - if (disabled.includes(k)) { 132 - opts.disabled = true; 133 - } 134 - 135 - params[k] = v; 136 - 137 - const input = pane.addInput(params, k, opts); 138 - // TODO: consider inline state management 139 - input.on('change', ev => { 140 - // TODO: validate against schema 141 - console.log('change', k, ev.value) 142 - //data[k] = ev.value; 143 - }); 144 - }); 145 - }; 146 - 147 - // TODO: fuckfuckfuck 148 - // https://github.com/cocopon/tweakpane/issues/431 149 - const exportPaneData = pane => { 150 - const children = pane.rackApi_.children.filter(p => p.children); 151 - const val = pane.rackApi_.children.filter(p => p.children).map(paneChild => { 152 - return paneChild.children.reduce((obj, field) => { 153 - const k = field.label; 154 - if (!k) { 155 - return obj; 156 - } 157 - 158 - let v = null; 159 - 160 - const input = field.element.querySelector('.tp-txtv_i') 161 - if (input) { 162 - v = input.value; 163 - } 164 - 165 - const checkbox = field.element.querySelector('.tp-ckbv_i'); 166 - if (checkbox) { 167 - v = checkbox.checked; 168 - } 169 - 170 - // TODO: drop fields not supported for now 171 - if (v) { 172 - obj[k] = v; 173 - } 174 - 175 - return obj; 176 - }, {}); 177 - }); 178 - return val; 179 - }; 180 - 181 - const initValuesPane = (container, labels, schema, values, onChange) => { 182 - const pane = new Tweakpane.Pane({ 183 - container: container, 184 - title: labels.paneTitle 185 - }); 186 - 187 - fillPaneFromSchema(pane, labels, schema, values, onChange, []); 188 - 189 - const update = (ev) => { 190 - // TODO: this won't work forever 191 - // gotta fix when tweakpane state export exists 192 - // also, gotta add accelerator validation 193 - values[ev.presetKey] = ev.value; 194 - onChange(values); 195 - }; 196 - 197 - // handle changes to existing entries 198 - pane.on('change', update); 199 - 200 - return pane; 201 - }; 202 - 203 - const initFeaturePane = (container, feature, onChange) => { 204 - const { config, labels, schemas, data } = feature; 205 - 206 - const pane = new Tweakpane.Pane({ 207 - container: container, 208 - title: labels.featureDisplay 209 - }); 210 - 211 - const update = (all) => { 212 - const newData = exportPaneData(pane); 213 - // remove "new item" entry if not 214 - if (!all) { 215 - newData.pop(); 216 - } 217 - onChange(newData); 218 - }; 219 - 220 - // add prefs 221 - 222 - // add items 223 - data.items.forEach(item => { 224 - const folder = pane.addFolder({ 225 - title: item.title, 226 - expanded: false 227 - }); 228 - 229 - fillPaneFromSchema(folder, labels, schemas.item, item, onChange, config.disabled); 230 - 231 - // TODO: implement 232 - //folder.addButton({title: labels.testBtn}); 233 - 234 - if (config.allowNew) { 235 - const delBtn = folder.addButton({title: labels.delBtn}); 236 - delBtn.on('click', () => { 237 - pane.remove(folder); 238 - // TODO: https://github.com/cocopon/tweakpane/issues/533 239 - update(); 240 - }); 241 - } 242 - 243 - folder.on('change', () => update(!config.allowNew)); 244 - }); 245 - 246 - /* 247 - if (config.allowNew) { 248 - // add new item entry 249 - const folder = pane.addFolder({ 250 - title: labels.newFolder, 251 - expanded: false 252 - }); 253 - 254 - //fillPaneFromSchema(folder, labels, schema); 255 - fillPaneFromSchema(folder, labels, schema, {}, onChange, disabled); 256 - 257 - const btn = pane.addButton({title: labels.addBtn}); 258 - 259 - // handle adds of new entries 260 - btn.on('click', () => { 261 - update(true); 262 - }); 263 - } 264 - */ 265 - 266 - return pane; 267 - }; 268 - 269 - // listen for data changes 270 - window.app.onConfigChange(() => { 271 - console.log('onconfigchange'); 272 - window.app.getConfig.then(init); 273 - }); 274 - 275 - // initialization: get data and load ui 276 - window.app.getConfig.then(init); 277 -