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 #4 from autonome/slides

Slides and bunch of other stuff

authored by

Dietrich Ayala and committed by
GitHub
f5d1d54d 51186deb

+692 -189
+37 -9
README.md
··· 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 5 ** WARNING** 6 - * Peek is not a web browser! There are no tabs, and no windows in the browser sense of them. If that's what you're looking for, there are a few decent browsers for you to choose from. 6 + * 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 7 * 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 8 9 9 <img width="969" alt="CleanShot 2023-04-03 at 18 50 22@2x" src="https://user-images.githubusercontent.com/50103/229501558-7084d66e-962a-4c0f-a10e-11787ef3ce68.png"> ··· 12 12 13 13 You can use Peek in three ways, with more coming: 14 14 15 - * Peeks - Keyboard activated modal chromeless web pages mapped to `Opt+0-9` 16 - * Slides - Gesture activated modal chromeless web pages which slide in from left/right/bottom/top 15 + * Peeks - Keyboard activated modal chromeless web pages 16 + * Slides - Keyboard or gesture activated modal chromeless web pages which slide in from any screen edges 17 17 * Scripts - Scripts periodically executed against a web page in the background which extract data and notify on changes 18 18 19 19 ### Peeks ··· 59 59 60 60 ### Usage 61 61 62 - * `cmd/ctl+Escape` to open settings 63 - * `opt+0-9` to open Peeks 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 64 66 65 67 ## Mobile 66 68 ··· 70 72 71 73 * Going full crypto payments for distributed compute on this one. 72 74 73 - ## Future 75 + ## Roadmap 76 + 77 + Next 78 + * Modularize feature types, eyeing the extensibility model 79 + * Basic command bar to open pages 80 + * Basic history store 74 81 75 - * GCLI - not just a command bar, but like the Ubiquity extension 76 - * Lossless personal encrypted archive of web history 82 + After that 83 + * Extension model 84 + * Ubiquity-like 85 + * Panorama-like 86 + 87 + Further 77 88 * Implement the Firefox "awesomebar" scoring and search algorithm so that Peek *learns* you 78 89 * Extension model designed for web user agent user interface experimentation 79 - * Panorama 90 + * Infinite lossless personal encrypted archive of web history 80 91 81 92 ## Development 82 93 ··· 84 95 yarn install 85 96 yarn start 86 97 ``` 98 + 99 + ## Resources 100 + 101 + Agregore ext protocol impl 102 + * where all are registered: https://github.com/AgregoreWeb/agregore-browser/blob/master/app/protocols/index.js#L74 103 + * how convert the fetch APIs to be compatible with the streaming protocol handler API in electron: https://github.com/AgregoreWeb/agregore-browser/blob/master/app/protocols/fetch-to-handler.js 104 + * where register IPFS: https://github.com/AgregoreWeb/agregore-browser/blob/electron-23/app/protocols/ipfs-protocol.js 105 + 106 + Browsers 107 + * Min browser architecture - https://github.com/minbrowser/min/wiki/Architecture 108 + * Dot browser https://www.dothq.org/en-US 109 + 110 + Misc 111 + * https://github.com/Rajaniraiyn/awesome-electron-browsers 112 + * https://github.com/mawie81/electron-window-state 113 + * https://antonfisher.com/posts/2020/12/27/how-to-animate-native-electron-window/ 114 + * https://stackoverflow.com/questions/44818508/how-do-i-move-a-frameless-window-in-electron-without-using-webkit-app-region 87 115 88 116 ## History 89 117
+151 -34
defaults.js
··· 7 7 "title": "Peek - prefs", 8 8 "description": "Peek user preferences", 9 9 "type": "object", 10 - "properties": { 10 + "properties": { 11 11 "globalKeyCmd": { 12 12 "description": "Global OS hotkey to load app", 13 13 "type": "string", ··· 18 18 "type": "string", 19 19 "default": "Option+" 20 20 }, 21 + "slideKeyPrefix": { 22 + "description": "Global OS hotkey prefix to trigger slides - will be followed by U/D/L/R arrows", 23 + "type": "string", 24 + "default": "Option+" 25 + }, 21 26 }, 22 27 "required": [ "globalKeyCmd", "peekKeyPrefix"] 23 28 }; ··· 28 33 "title": "Peek - page peek", 29 34 "description": "Peek page peek", 30 35 "type": "object", 31 - "properties": { 36 + "properties": { 32 37 "keyNum": { 33 38 "description": "Number on keyboard to open this peek, 0-9", 34 39 "type": "integer", 40 + "minimum": 0, 41 + "maximum": 9, 35 42 "default": 0 36 43 }, 37 44 "title": { ··· 40 47 "default": "New Peek" 41 48 }, 42 49 "address": { 43 - "description": "URL to execute script against", 50 + "description": "URL to load", 44 51 "type": "string", 45 52 "default": "https://example.com" 46 53 }, ··· 71 78 }, 72 79 }, 73 80 "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound", 74 - "height", "width" ] 81 + "height", "width" ] 82 + }; 83 + 84 + const slideSchema = { 85 + "$schema": "https://json-schema.org/draft/2020-12/schema", 86 + "$id": "peek.slide.schema.json", 87 + "title": "Peek - page slide", 88 + "description": "Peek page slide", 89 + "type": "object", 90 + "properties": { 91 + "screenEdge": { 92 + "description": "Edge of screen or arrow key to open this slide, up/down/left/right", 93 + "type": "string", 94 + "oneOf": [ 95 + { "format": "Up" }, 96 + { "format": "Down" }, 97 + { "format": "Left" }, 98 + { "format": "Right" } 99 + ], 100 + "default": "Right" 101 + }, 102 + "title": { 103 + "description": "Name of the slide - user defined label", 104 + "type": "string", 105 + "default": "New Slide" 106 + }, 107 + "address": { 108 + "description": "URL to load", 109 + "type": "string", 110 + "default": "https://example.com" 111 + }, 112 + "persistState": { 113 + "description": "Whether to persist local state or load page into empty container - defaults to false", 114 + "type": "boolean", 115 + "default": false 116 + }, 117 + "keepLive": { 118 + "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 119 + "type": "boolean", 120 + "default": false 121 + }, 122 + "allowSound": { 123 + "description": "Whether to allow the page to emit sound or not (eg for background music player slides - defaults to false", 124 + "type": "boolean", 125 + "default": false 126 + }, 127 + "height": { 128 + "description": "User-defined height of slide page", 129 + "type": "integer", 130 + "default": 600 131 + }, 132 + "width": { 133 + "description": "User-defined width of slide page", 134 + "type": "integer", 135 + "default": 800 136 + }, 137 + }, 138 + "required": [ "screenEdge", "title", "address", "persistState", "keepLive", "allowSound", 139 + "height", "width" ] 75 140 }; 76 141 77 142 const scriptSchema = { ··· 80 145 "title": "Peek - script", 81 146 "description": "Peek background script", 82 147 "type": "object", 83 - "properties": { 148 + "properties": { 84 149 "id": { 85 150 "description": "The unique identifier for a script", 86 151 "type": "string", ··· 106 171 "type": "string", 107 172 "default": "body > h1" 108 173 }, 109 - "value": { 174 + "property": { 110 175 "description": "Which element property to return - currently 'textContent' is the only supported value", 111 176 "type": "string", 112 177 "default": "textContent" ··· 115 180 "description": "How often to execute the script, in milliseconds - defaults to five minutes", 116 181 "type": "integer", 117 182 "default": 300000, 118 - "minimum": 0 183 + "minimum": 0 119 184 }, 120 185 "storeHistory": { 121 186 "description": "Whether to store historic values - defaults to false", 122 187 "type": "boolean", 123 188 "default": false 124 189 }, 190 + "notifyOnChange": { 191 + "description": "Whether to notify using local OS notifications when script value changes", 192 + "type": "boolean", 193 + "default": true 194 + }, 195 + "previousValue": { 196 + "description": "The most recently fetched result of script exection", 197 + "type": "string", 198 + "default": "", 199 + }, 125 200 }, 126 - "required": [ "id", "title", "address", "version", "selector", "value", 127 - "interval", "storeHistory" ] 201 + "required": [ "id", "title", "address", "version", "selector", "property", 202 + "interval", "notifyOnChange", "storeHistory" ] 128 203 }; 129 204 130 205 const schemas = { 131 - prefs: prefsSchema, 132 - peek: peekSchema, 133 - script: scriptSchema 206 + prefs: prefsSchema, 207 + peeks: peekSchema, 208 + slides: slideSchema, 209 + scripts: scriptSchema 134 210 }; 135 211 212 + // TODO: schemaize 0-9 constraints for peeks and UPLR constraints for slides 136 213 const fullSchema = { 137 214 prefs: prefsSchema, 138 215 peek: peekSchema, 216 + slide: slideSchema, 139 217 script: scriptSchema, 140 218 peeks: { 141 219 type: 'array', 142 - items: { 143 - type: 'peek' 144 - } 220 + items: { "$ref": "#/$defs/peek" } 221 + }, 222 + slides: { 223 + type: 'array', 224 + items: { "$ref": "#/$defs/slide" } 145 225 }, 146 226 scripts: { 147 227 type: 'array', 148 - items: { 149 - type: 'script' 150 - } 228 + items: { "$ref": "#/$defs/script" } 151 229 }, 152 230 }; 153 231 154 232 const defaults = { 155 233 prefs: { 156 234 globalKeyCmd: 'CommandOrControl+Escape', 157 - peekKeyPrefix: 'Option+' 235 + peekKeyPrefix: 'Option+', 236 + slideKeyPrefix: 'Option+' 158 237 }, 159 - peeks: [ 238 + peeks: [], 239 + slides: [ 160 240 { 161 - keyNum: 0, 162 - title: 'localhost', 241 + screenEdge: 'Up', 242 + title: 'Slide from top', 163 243 address: 'http://localhost/', 164 244 persistState: false, 165 245 keepLive: false, 166 246 allowSound: false, 167 - height: '', 168 - width: '', 247 + height: 600, 248 + width: 800, 169 249 }, 170 250 { 171 - keyNum: 1, 172 - title: 'everytimezone', 173 - address: 'https://everytimezone.com/', 251 + screenEdge: 'Down', 252 + title: 'Slide from bottom', 253 + address: 'http://localhost/', 174 254 persistState: false, 175 255 keepLive: false, 176 256 allowSound: false, 177 - height: '', 178 - width: '', 179 - } 257 + height: 600, 258 + width: 800, 259 + }, 260 + { 261 + screenEdge: 'Left', 262 + title: 'Slide from left', 263 + address: 'http://localhost/', 264 + persistState: false, 265 + keepLive: false, 266 + allowSound: false, 267 + height: 600, 268 + width: 800, 269 + }, 270 + { 271 + screenEdge: 'Right', 272 + title: 'Slide from right', 273 + address: 'http://localhost/', 274 + persistState: false, 275 + keepLive: false, 276 + allowSound: false, 277 + height: 600, 278 + width: 800, 279 + }, 180 280 ], 181 281 scripts: [ 182 282 { ··· 185 285 address: 'http://localhost/', 186 286 version: '1', 187 287 selector: 'body > h1', 188 - value: 'textContent', 288 + property: 'textContent', 189 289 interval: 300000, 190 - storehistory: false 290 + storehistory: false, 291 + notifyOnChange: false 191 292 }, 192 293 ] 193 294 }; 194 295 296 + for (var i = 0; i != 10; i++) { 297 + defaults.peeks.push({ 298 + keyNum: i, 299 + title: `Peek key ${i}`, 300 + address: 'http://localhost/', 301 + persistState: false, 302 + keepLive: false, 303 + allowSound: false, 304 + height: 600, 305 + width: 800, 306 + }); 307 + } 308 + 195 309 const set = data => { 196 310 store.set('prefs', data.prefs); 197 311 store.set('peeks', data.peeks); 312 + store.set('slides', data.slides); 198 313 store.set('scripts', data.scripts); 199 314 }; 200 315 ··· 212 327 console.log('initializing datastore'); 213 328 store.set('prefs', defaults.prefs); 214 329 store.set('peeks', defaults.peeks); 330 + store.set('slides', defaults.slides); 215 331 store.set('scripts', defaults.scripts); 216 332 } 217 333 218 334 module.exports = { 219 - schemas, 220 - data: { 335 + schemas, 336 + data: { 221 337 get prefs() { return store.get('prefs'); }, 222 338 get peeks() { return store.get('peeks'); }, 339 + get slides() { return store.get('slides'); }, 223 340 get scripts() { return store.get('scripts'); } 224 341 }, 225 342 set,
+353 -42
index.js
··· 13 13 ipcMain, 14 14 Menu, 15 15 nativeTheme, 16 + Notification, 17 + screen, 16 18 Tray 17 19 } = require('electron'); 18 20 ··· 26 28 tooltip: 'Click to open Peek' 27 29 } 28 30 }; 29 - 30 - // keep app out of dock and tab switcher 31 - app.dock.hide(); 32 31 33 32 // load data 34 33 let { data, schemas, set, watch } = require('./defaults'); ··· 70 69 71 70 let _windows = []; 72 71 let _peekWins = {}; 72 + let _slideWins = {}; 73 73 74 + // main window 74 75 let _win = null; 76 + // tray 77 + let _tray = null; 78 + 75 79 const getMainWindow = () => { 76 - //console.log('getMainWindow', _win === null); 77 - if (_win === null) { 80 + if (_win === null || _win.isDestroyed()) { 78 81 _win = createMainWindow(); 79 82 } 80 83 return _win; 81 84 }; 82 85 83 86 const createMainWindow = () => { 87 + console.log('createMainWindow'); 84 88 // Create the browser window. 85 89 const mainWindow = new BrowserWindow({ 86 90 width: 800, ··· 89 93 preload: path.join(__dirname, 'preload.js') 90 94 } 91 95 }); 96 + 97 + /* 98 + mainWindow.on('close', (e) => { 99 + console.log('onClose - just hiding'); 100 + e.preventDefault(); 101 + mainWindow.hide(); 102 + }); 103 + */ 92 104 93 105 // and load the index.html of the app. 94 106 mainWindow.loadFile('main.html'); ··· 109 121 }); 110 122 111 123 const initTray = () => { 112 - const tray = new Tray(ICON_PATH); 113 - tray.setToolTip(labels.tray.tooltip); 114 - tray.on('click', () => { 115 - getMainWindow().show(); 116 - }); 117 - return tray; 124 + if (!_tray || _tray.isDestroyed()) { 125 + _tray = new Tray(ICON_PATH); 126 + _tray.setToolTip(labels.tray.tooltip); 127 + _tray.on('click', () => { 128 + getMainWindow().show(); 129 + }); 130 + } 131 + return _tray; 118 132 }; 119 133 120 134 const execContentScript = (script, cb) => { 121 135 const view = new BrowserView({ 122 136 webPreferences: { 137 + preload: path.join(__dirname, 'preload.js'), 123 138 // isolate content and do not persist it 124 139 partition: Date.now() 125 140 } 126 141 }); 127 142 128 - //win.setBrowserView(view) 129 - //view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) 143 + view.webContents.send('window', { 144 + id: 'view', 145 + type: 'script', 146 + data: script 147 + }); 148 + 130 149 view.webContents.loadURL(script.address); 131 150 132 151 const str = ` ··· 147 166 }); 148 167 }; 149 168 169 + let _intervals = []; 170 + 150 171 const initScripts = scripts => { 151 - return; 172 + //console.log('initScripts', scripts); 173 + 174 + // blow it all away for now 175 + // someday make it right proper just cancel/update changed and add new 176 + _intervals.forEach(clearInterval); 177 + 152 178 // debounce me somehow so not shooting em all off 153 179 // at once every time app starts 154 180 scripts.forEach(script => { 155 - const r = execContentScript(script, (res) => { 156 - console.log('cs r', res); 157 - }); 181 + setInterval(() => { 182 + //console.log('interval hit', script.title); 183 + const r = execContentScript(script, (res) => { 184 + //console.log('cs r', res); 185 + 186 + if (script.previousValue != res) { 187 + // update stored value 188 + const previousValue = script.previousValue; 189 + script.previousValue = res; 190 + const idx = data.scripts.findIndex(el => el.id == script.id); 191 + if (idx >= 0) { 192 + data.scripts[idx] = script; 193 + set(data); 194 + } 195 + else { 196 + console.log('errrrr, wat'); 197 + } 198 + 199 + // notification 200 + // add to schema and support per script 201 + /* 202 + const title = `Peek :: Script :: ${script.title}`; 203 + const body = [ 204 + `Script result changed for ${script.title}:`, 205 + `- Old: ${previousValue}`, 206 + `- New: ${res}` 207 + ].join('\n'); 208 + 209 + new Notification({ title, body }).show(); 210 + */ 211 + } 212 + }); 213 + }, script.interval); 158 214 }); 159 215 }; 160 216 161 217 const initGlobalShortcuts = prefs => { 218 + if (globalShortcut.isRegistered(prefs.globalKeyCmd)) { 219 + globalShortcut.unregister(prefs.globalKeyCmd); 220 + } 221 + 162 222 // register global activation shortcut 163 - if (!globalShortcut.isRegistered(prefs.globalKeyCmd)) { 164 - const onActivate = () => { 165 - getMainWindow().show(); 166 - }; 223 + const onGlobalKeyCmd = () => getMainWindow().show(); 167 224 168 - const ret = globalShortcut.register(prefs.globalKeyCmd, onActivate); 225 + const ret = globalShortcut.register(prefs.globalKeyCmd, onGlobalKeyCmd); 169 226 170 - if (!ret) { 171 - console.error('Unable to register global key command.') 172 - } 227 + if (!ret) { 228 + console.error('Unable to register global key command.') 173 229 } 174 230 }; 175 231 ··· 196 252 autoHideMenuBar: true, 197 253 titleBarStyle: 'hidden', 198 254 webPreferences: { 199 - preload: path.join(__dirname, 'peek-preload.js'), 255 + preload: path.join(__dirname, 'preload.js'), 200 256 // isolate content and do not persist it 201 - partition: Date.now() 257 + //partition: Date.now() 202 258 } 203 259 }); 204 260 } 205 261 206 262 const onGoAway = () => { 263 + /* 207 264 if (peek.keepLive) { 208 265 _peekWins[key] = win; 209 266 win.hide(); ··· 211 268 else { 212 269 win.destroy(); 213 270 } 271 + */ 272 + win.destroy(); 214 273 } 215 274 win.on('blur', onGoAway); 216 275 win.on('close', onGoAway); ··· 235 294 }); 236 295 */ 237 296 238 - //win.setBounds({ x: 0, y: 0, width, height }) 297 + win.webContents.send('window', { 298 + path: path.join(__dirname), 299 + id: win.id, 300 + type: 'peek', 301 + data: peek 302 + }); 303 + 239 304 win.loadURL(peek.address); 240 305 }; 241 306 242 307 const initPeeks = (cmdPrefix, peeks) => { 243 308 peeks.forEach((p, i) => { 244 - if (!globalShortcut.isRegistered(cmdPrefix + `${i}`)) { 245 - const ret = globalShortcut.register(cmdPrefix + `${i}`, () => { 246 - showPeek(p); 309 + if (globalShortcut.isRegistered(cmdPrefix + `${i}`)) { 310 + globalShortcut.unregister(cmdPrefix + `${i}`) 311 + } 312 + 313 + const ret = globalShortcut.register(cmdPrefix + `${i}`, () => { 314 + showPeek(p); 315 + }); 316 + 317 + if (!ret) { 318 + console.error('Unable to register peek'); 319 + } 320 + }); 321 + }; 322 + 323 + const animateSlide = (win, slide) => { 324 + return new Promise((res, rej) => { 325 + const { size, bounds } = screen.getPrimaryDisplay(); 326 + 327 + // get x/y field 328 + const coord = slide.screenEdge == 'Left' || slide.screenEdge == 'Right' ? 'x' : 'y'; 329 + 330 + const dim = coord == 'x' ? 'width' : 'height'; 331 + 332 + const winBounds = win.getBounds(); 333 + 334 + // created window at x/y taking animation into account 335 + let pos = winBounds[coord]; 336 + 337 + const speedMs = 150; 338 + const timerInterval = 10; 339 + 340 + let tick = 0; 341 + const numTicks = parseInt(speedMs / timerInterval); 342 + 343 + const offset = slide[dim] / numTicks; 344 + 345 + //console.log('numTicks', numTicks, 'widthChunk', offset); 346 + 347 + const timer = setInterval(() => { 348 + tick++; 349 + 350 + if (tick >= numTicks) { 351 + clearInterval(timer); 352 + res(); 353 + } 354 + 355 + const winBounds = win.getBounds(); 356 + 357 + if (slide.screenEdge == 'Right' || slide.screenEdge == 'Down') { 358 + // new position is current position +/- offset 359 + pos = pos - offset; 360 + } 361 + 362 + const grownEnough = winBounds[dim] <= slide[dim]; 363 + const newDim = grownEnough ? 364 + winBounds[dim] + offset 365 + : winBounds[dim]; 366 + 367 + const newBounds = {}; 368 + newBounds[coord] = parseInt(pos, 10); 369 + newBounds[dim] = parseInt(newDim, 10); 370 + 371 + // set new bounds 372 + win.setBounds(newBounds); 373 + 374 + }, timerInterval); 375 + }); 376 + }; 377 + 378 + const showSlide = (slide) => { 379 + let win = null; 380 + 381 + const key = 'slide' + slide.screenEdge; 382 + 383 + // TODO: fix stored+live windows 384 + if (_slideWins[key]) { 385 + console.log('slide', slide.screenEdge, 'using stored window'); 386 + win = _slideWins[key]; 387 + win.show(); 388 + } 389 + else { 390 + 391 + const { size, bounds } = screen.getPrimaryDisplay(); 392 + 393 + let x, y, height, width, center = null; 394 + 395 + switch(slide.screenEdge) { 396 + case 'Up': 397 + // horizontally center 398 + x = (size.width - slide.width) / 2; 399 + 400 + // y starts at screen top and stays there 401 + y = 0; 402 + 403 + width = slide.width; 404 + height = 1; 405 + break; 406 + case 'Down': 407 + // horizonally center 408 + x = (size.width - slide.width) / 2; 409 + 410 + // y ends up at window height from bottom 411 + // 412 + // eg: y = size.height - slide.height; 413 + // 414 + // but starts at screen bottom 415 + y = size.height; 416 + 417 + width = slide.width; 418 + height = 1; 419 + break; 420 + case 'Left': 421 + // x starts and ends at at left screen edge 422 + // at left edge 423 + x = 0; 424 + 425 + // vertically center 426 + y = (size.height - slide.height) / 2; 427 + 428 + width = 1; 429 + height = slide.height; 430 + break; 431 + case 'Right': 432 + // x ends at at right screen edge - window size 433 + // 434 + // eg: x = size.width - slide.width; 435 + // 436 + // but starts at screen right edge, will animate in 437 + x = size.width; 438 + 439 + // vertically center 440 + y = (size.height - slide.height) / 2; 441 + 442 + width = 1; 443 + height = slide.height; 444 + break; 445 + default: 446 + center = true; 447 + console.log('waddafa'); 448 + } 449 + 450 + win = new BrowserWindow({ 451 + height, 452 + width, 453 + x, 454 + y, 455 + skipTaskbar: true, 456 + autoHideMenuBar: true, 457 + titleBarStyle: 'hidden', 458 + // maybe worth doing instead of animating width 459 + //enableLargerThanScreen: true, 460 + webPreferences: { 461 + preload: path.join(__dirname, 'preload.js'), 462 + // isolate content and do not persist it 463 + partition: Date.now() 464 + } 465 + }); 466 + 467 + //_slideWins[key] = win; 468 + } 469 + 470 + animateSlide(win, slide).then(); 471 + 472 + const onGoAway = () => { 473 + /* 474 + if (slide.keepLive) { 475 + _slideWins[key] = win; 476 + win.hide(); 477 + } 478 + else { 479 + win.destroy(); 480 + } 481 + */ 482 + win.destroy(); 483 + } 484 + win.on('blur', onGoAway); 485 + win.on('close', onGoAway); 486 + 487 + /* 488 + const str = ` 489 + window.addEventListener('keyup', e => { 490 + if (e.key == 'Escape') { 491 + console.log('peek script esc'); 492 + } 493 + }); 494 + 1; 495 + `; 496 + 497 + win.webContents.on('dom-ready', async () => { 498 + try { 499 + const r = await win.webContents.executeJavaScript(str); 500 + console.log(r); 501 + } catch(ex) { 502 + console.error('cs exec error', ex); 503 + } 504 + }); 505 + */ 506 + 507 + win.webContents.send('window', { 508 + path: path.join(__dirname), 509 + id: win.id, 510 + type: 'slide', 511 + data: slide 512 + }); 513 + 514 + //win.setBounds({ x: 0, y: 0, width, height }) 515 + win.loadURL(slide.address); 516 + }; 517 + 518 + const initSlides = (cmdPrefix, slides) => { 519 + slides.forEach(s => { 520 + if (!globalShortcut.isRegistered(cmdPrefix + `${s.screenEdge}`)) { 521 + const ret = globalShortcut.register(cmdPrefix + `${s.screenEdge}`, () => { 522 + showSlide(s); 247 523 }); 248 524 249 525 if (!ret) { 250 - console.error('Unable to register peek'); 526 + console.error('Unable to register slide'); 251 527 } 252 528 } 253 529 }); 254 530 }; 255 531 532 + // initialized all bits which need updating if the data changes 533 + // can be called repeatedly to refresh on changes 256 534 const initData = data => { 257 535 // initialize prefs 258 536 const prefs = data.prefs; 259 537 initGlobalShortcuts(prefs); 260 538 261 539 // initialize peeks 262 - const peeks = data.peeks; 263 - if (peeks.length > 0) { 264 - initPeeks(prefs.peekKeyPrefix, peeks); 540 + if (data.peeks.length > 0) { 541 + initPeeks(prefs.peekKeyPrefix, data.peeks); 542 + } 543 + 544 + // initialize slides 545 + if (data.slides.length > 0) { 546 + initSlides(prefs.slideKeyPrefix, data.slides); 265 547 } 266 548 267 549 // initialize scripts 268 - const scripts = data.scripts; 269 - if (scripts.length > 0) { 270 - initScripts(scripts); 550 + if (data.scripts.length > 0) { 551 + initScripts(data.scripts); 271 552 } 272 553 }; 273 554 555 + // app load 274 556 const onReady = () => { 557 + console.log('onReady'); 275 558 // create main app window on app start 276 559 const win = getMainWindow(); 277 560 561 + win.webContents.send('window', { 562 + path: path.join(__dirname), 563 + id: win.id, 564 + type: 'main', 565 + }); 566 + 278 567 initData(data); 279 568 569 + // keep app out of dock and tab switcher 570 + if (app.dock) { 571 + app.dock.hide(); 572 + } 573 + 280 574 initTray(); 281 575 282 576 watch(newData => { 283 577 initData(newData); 284 - getMainWindow().webContents.send('configchange', {}); 578 + win.webContents.send('configchange', {}); 285 579 }); 286 580 }; 287 581 ··· 307 601 ipcMain.on('esc', (event, title) => { 308 602 console.log('esc'); 309 603 const win = getMainWindow(); 310 - win.close(); 311 - _win = null; 604 + // 605 + if (!win.isDestroyed()) { 606 + console.log('esc: killingit'); 607 + win.close(); 608 + win.destroy(); 609 + _win = null; 610 + } 611 + // 312 612 /* 313 613 if (win.isVisible()) { 314 614 console.log('win is visible, hide it'); ··· 322 622 // explicitly with Cmd + Q. 323 623 app.on('window-all-closed', () => { 324 624 console.log('window-all-closed', process.platform); 325 - if (process.platform !== 'darwin') { 625 + // 626 + if (!_win.isDestroyed()) { 627 + console.log('wac: killingit'); 628 + _win.destroy(); 629 + _win = null; 630 + } 631 + // 632 + if (_win.isVisible()) { 633 + console.log('win is visible, hide it'); 634 + //_win.hide(); 635 + } 636 + else if (process.platform !== 'darwin') { 326 637 onQuit(); 327 638 } 328 639 });
+4 -3
main.html
··· 16 16 17 17 <div class="houseofpane"> 18 18 <div> 19 - <div class="shortcuts"></div> 20 - <div class="peeks"></div> 19 + <div class="prefs"></div> 20 + <div class="scripts"></div> 21 21 </div> 22 22 <div> 23 - <div class="scripts"></div> 23 + <div class="peeks"></div> 24 + <div class="slides"></div> 24 25 </div> 25 26 </div> 26 27
-11
peek-preload.js
··· 1 - 2 - 3 - console.log('peek preload'); 4 - 5 - /* 6 - window.addEventListener('keyup', e => { 7 - if (e.key == 'Escape') { 8 - console.log('peek preload esc'); 9 - } 10 - }); 11 - */
+45 -31
preload.js
··· 1 1 console.log('preload'); 2 - const {app, contextBridge, ipcRenderer} = require('electron') 3 2 4 - let api = {}; 5 - 6 - api.onConfigChange = callback => { 7 - ipcRenderer.on('configchange', (ev, msg) => { 8 - callback(msg); 9 - }); 10 - }; 11 - 12 - api.getConfig = new Promise((resolve, reject) => { 13 - // TODO: race potential 14 - ipcRenderer.once('config', (ev, msg) => { 15 - resolve(msg); 16 - }); 17 - ipcRenderer.send('getconfig'); 18 - }); 19 - 20 - api.setConfig = cfg => { 21 - //console.log('preload: setConfig', cfg); 22 - ipcRenderer.send('setconfig', cfg); 23 - }; 3 + const { 4 + contextBridge, 5 + ipcRenderer 6 + } = require('electron') 24 7 25 - contextBridge.exposeInMainWorld('app', api); 26 - 27 - window.addEventListener('DOMContentLoaded', () => { 28 - const replaceText = (selector, text) => { 29 - const element = document.getElementById(selector) 30 - if (element) element.innerText = text 31 - } 32 - 33 - for (const dependency of ['chrome', 'node', 'electron']) { 34 - replaceText(`${dependency}-version`, process.versions[dependency]) 8 + ipcRenderer.on('window', (ev, msg) => { 9 + console.log('preload: onwindow', msg); 10 + const { type, id, data } = msg; 11 + if (type == 'main') { 12 + handleMainWindow(); 35 13 } 36 14 }); 37 15 16 + // all window types close on escape 38 17 window.addEventListener('keyup', e => { 39 18 if (e.key == 'Escape') { 40 19 ipcRenderer.send('esc', ''); 41 20 } 42 21 }); 43 22 23 + const handleMainWindow = () => { 24 + let api = {}; 25 + 26 + api.onConfigChange = callback => { 27 + ipcRenderer.on('configchange', (ev, msg) => { 28 + callback(msg); 29 + }); 30 + }; 31 + 32 + api.getConfig = new Promise((resolve, reject) => { 33 + // TODO: race potential 34 + ipcRenderer.once('config', (ev, msg) => { 35 + resolve(msg); 36 + }); 37 + ipcRenderer.send('getconfig'); 38 + }); 39 + 40 + api.setConfig = cfg => { 41 + //console.log('preload: setConfig', cfg); 42 + ipcRenderer.send('setconfig', cfg); 43 + }; 44 + 45 + contextBridge.exposeInMainWorld('app', api); 46 + 47 + window.addEventListener('DOMContentLoaded', () => { 48 + const replaceText = (selector, text) => { 49 + const element = document.getElementById(selector) 50 + if (element) element.innerText = text 51 + } 52 + 53 + for (const dependency of ['chrome', 'node', 'electron']) { 54 + replaceText(`${dependency}-version`, process.versions[dependency]) 55 + } 56 + }); 57 + };
+102 -59
renderer.js
··· 2 2 3 3 // TODO: move to proper l10n 4 4 const labels = { 5 - shortcutsPane: { 6 - paneTitle: 'Keyboard Shortcuts', 7 - globalKeyCmd: 'Global activation shortcut', 5 + prefs: { 6 + paneTitle: 'Preferences', 7 + globalKeyCmd: 'App activation shortcut', 8 8 peekKeyPrefix: 'Peek shortcut prefix', 9 + slideKeyPrefix: 'Slide shortcut prefix', 9 10 }, 10 - peeksPane: { 11 + peeks: { 11 12 paneTitle: 'Peeks', 12 - testBtn: 'Try', 13 - newFolder: 'Add new peek', 14 - addBtn: 'Add', 15 - delBtn: 'Delete', 13 + type: 'Peek', 14 + testBtn: 'Try (❌)', 15 + }, 16 + slides: { 17 + paneTitle: 'Slides', 18 + testBtn: 'Try (❌)', 16 19 }, 17 - scriptsPane: { 20 + scripts: { 18 21 paneTitle: 'Scripts', 19 - testBtn: 'Try', 22 + testBtn: 'Try (❌)', 20 23 newFolder: 'Add new script', 21 24 addBtn: 'Add', 22 25 delBtn: 'Delete', ··· 56 59 panes = []; 57 60 } 58 61 59 - // build panes and wire up change handlers 60 - const el1 = containerEl.querySelector('.shortcuts'); 61 - const pane1 = initValuesPane(el1, labels.shortcutsPane, schemas.prefs, data.prefs, newPrefs => { 62 - data.prefs = newPrefs; 63 - updateToMain(data); 64 - }); 65 - panes.push({ el: el1, pane: pane1}); 62 + // janky but hey it all is soooo 63 + const initPane = (type) => { 64 + const el = containerEl.querySelector('.' + type); 65 + 66 + const initPane = type == 'prefs' ? initValuesPane : initListPane; 67 + 68 + const allowNew = type == 'scripts' || false; 69 + 70 + const disabled = { 71 + prefs: [], 72 + scripts: ['previousValue'], 73 + peeks: ['keyNum'], 74 + slides: ['screenEdge'], 75 + }; 76 + 77 + const labelMaker = entry => { 78 + if (type == 'peeks') { 79 + return `${data.prefs.peekKeyPrefix}${entry.keyNum} - ${entry.address}`; 80 + } 81 + else if (type == 'slides') { 82 + return `${entry.screenEdge} - ${entry.address}`; 83 + } 84 + return entry.title; 85 + }; 66 86 67 - const el2 = containerEl.querySelector('.peeks'); 68 - const pane2 = initListPane(el2, labels.peeksPane, schemas.peek, data.peeks, newPeeks => { 69 - data.peeks = newPeeks; 70 - updateToMain(data); 71 - }); 72 - panes.push({ el: el2, pane: pane2}); 87 + const onChange = newData => { 88 + data[type] = newData; 89 + updateToMain(data); 90 + }; 73 91 74 - const el3 = containerEl.querySelector('.scripts'); 75 - const pane3 = initListPane(el3, labels.scriptsPane, schemas.script, data.scripts, newScripts => { 76 - data.scripts = newScripts; 77 - updateToMain(data); 78 - }); 79 - panes.push({ el: el3, pane: pane3}); 80 - }; 92 + const pane = initPane( 93 + el, 94 + labels[type], 95 + schemas[type], 96 + data[type], 97 + onChange, 98 + allowNew, 99 + disabled[type], 100 + labelMaker 101 + ); 81 102 82 - // listen for data changes 83 - window.app.onConfigChange(() => { 84 - console.log('onconfigchange'); 85 - window.app.getConfig.then(init); 86 - }); 103 + panes.push({ 104 + el, 105 + pane 106 + }); 107 + }; 87 108 88 - // initialization: get data and load ui 89 - window.app.getConfig.then(init); 109 + ['prefs', 'peeks', 'slides', 'scripts'].forEach(initPane); 110 + }; 90 111 91 - const fillPaneFromSchema = (pane, labels, schema, data, onChange) => { 112 + const fillPaneFromSchema = (pane, labels, schema, data, onChange, disabled) => { 92 113 const props = schema.properties; 93 114 Object.keys(props).forEach(k => { 94 115 // schema for property ··· 103 124 const params = {}; 104 125 const opts = {}; 105 126 127 + // dedecimalize 106 128 if (s.type == 'integer') { 107 129 opts.step = 1; 108 130 } 109 131 132 + // disabled fields 133 + if (disabled.includes(k)) { 134 + opts.disabled = true; 135 + } 136 + 110 137 params[k] = v; 111 138 const input = pane.addInput(params, k, opts); 112 139 // TODO: consider inline state management ··· 152 179 return val; 153 180 }; 154 181 155 - const initValuesPane = (container, labels, schema, values, onChange) => { 182 + const initValuesPane = (container, labels, schema, values, onChange, allowNew, disabled, labelMaker) => { 156 183 const pane = new Tweakpane.Pane({ 157 184 container: container, 158 185 title: labels.paneTitle 159 186 }); 160 187 161 - fillPaneFromSchema(pane, labels, schema, values); 188 + fillPaneFromSchema(pane, labels, schema, values, onChange, disabled); 162 189 163 190 const update = (ev) => { 164 191 // TODO: this won't work forever ··· 174 201 return pane; 175 202 }; 176 203 177 - const initListPane = (container, labels, schema, items, onChange) => { 204 + const initListPane = (container, labels, schema, items, onChange, allowNew, disabled, labelMaker) => { 178 205 const pane = new Tweakpane.Pane({ 179 206 container: container, 180 207 title: labels.paneTitle ··· 191 218 192 219 items.forEach(entry => { 193 220 const folder = pane.addFolder({ 194 - title: entry.title, 221 + title: labelMaker(entry), 195 222 expanded: false 196 223 }); 197 224 198 - fillPaneFromSchema(folder, labels, schema, entry, onChange); 225 + fillPaneFromSchema(folder, labels, schema, entry, onChange, disabled); 199 226 200 227 // TODO: implement 201 228 folder.addButton({title: labels.testBtn}); 202 229 203 - const delBtn = folder.addButton({title: labels.delBtn}); 204 - delBtn.on('click', () => { 205 - pane.remove(folder); 206 - // https://github.com/cocopon/tweakpane/issues/533 207 - update(); 208 - }); 230 + if (allowNew) { 231 + const delBtn = folder.addButton({title: labels.delBtn}); 232 + delBtn.on('click', () => { 233 + pane.remove(folder); 234 + // TODO: https://github.com/cocopon/tweakpane/issues/533 235 + update(); 236 + }); 237 + } 209 238 210 239 folder.on('change', () => update()); 211 240 }); 212 241 213 - // add new item entry 214 - const folder = pane.addFolder({ 215 - title: labels.newFolder 216 - }); 242 + if (allowNew) { 243 + // add new item entry 244 + const folder = pane.addFolder({ 245 + title: labels.newFolder, 246 + expanded: false 247 + }); 217 248 218 - fillPaneFromSchema(folder, labels, schema); 249 + //fillPaneFromSchema(folder, labels, schema); 250 + fillPaneFromSchema(folder, labels, schema, {}, onChange, disabled); 219 251 220 - const btn = pane.addButton({title: labels.addBtn}); 252 + const btn = pane.addButton({title: labels.addBtn}); 221 253 222 - // handle adds of new entries 223 - btn.on('click', () => { 224 - update(true); 225 - }); 254 + // handle adds of new entries 255 + btn.on('click', () => { 256 + update(true); 257 + }); 258 + } 226 259 227 260 return pane; 228 261 }; 262 + 263 + // listen for data changes 264 + window.app.onConfigChange(() => { 265 + console.log('onconfigchange'); 266 + window.app.getConfig.then(init); 267 + }); 268 + 269 + // initialization: get data and load ui 270 + window.app.getConfig.then(init); 271 +