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 #15 from autonome/webunfucked

migratoryness

authored by

Dietrich Ayala and committed by
GitHub
f266dbed 4062c9f3

+344 -109
+56
animate.js
··· 1 + 2 + const animateWindow = (win, slide) => { 3 + return new Promise((res, rej) => { 4 + const { size, bounds } = screen.getPrimaryDisplay(); 5 + 6 + // get x/y field 7 + const coord = slide.screenEdge == 'Left' || slide.screenEdge == 'Right' ? 'x' : 'y'; 8 + 9 + const dim = coord == 'x' ? 'width' : 'height'; 10 + 11 + const winBounds = win.getBounds(); 12 + 13 + // created window at x/y taking animation into account 14 + let pos = winBounds[coord]; 15 + 16 + const speedMs = 150; 17 + const timerInterval = 10; 18 + 19 + let tick = 0; 20 + const numTicks = parseInt(speedMs / timerInterval); 21 + 22 + const offset = slide[dim] / numTicks; 23 + 24 + //console.log('numTicks', numTicks, 'widthChunk', offset); 25 + 26 + const timer = setInterval(() => { 27 + tick++; 28 + 29 + if (tick >= numTicks) { 30 + clearInterval(timer); 31 + res(); 32 + } 33 + 34 + const winBounds = win.getBounds(); 35 + 36 + if (slide.screenEdge == 'Right' || slide.screenEdge == 'Down') { 37 + // new position is current position +/- offset 38 + pos = pos - offset; 39 + } 40 + 41 + const grownEnough = winBounds[dim] <= slide[dim]; 42 + const newDim = grownEnough ? 43 + winBounds[dim] + offset 44 + : winBounds[dim]; 45 + 46 + const newBounds = {}; 47 + newBounds[coord] = parseInt(pos, 10); 48 + newBounds[dim] = parseInt(newDim, 10); 49 + 50 + // set new bounds 51 + win.setBounds(newBounds); 52 + 53 + }, timerInterval); 54 + }); 55 + }; 56 +
+12
features/cmd/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
features/cmd/cmd.js features/cmd/background.js
+12
features/core/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
+22
features/core/background.js
··· 1 + const debug = 1; 2 + 3 + const features = [ 4 + //'features/cmd/background.html', 5 + //'features/groups/background.html', 6 + //'features/peeks/background.html', 7 + //'features/scripts/background.html', 8 + 'features/settings/background.html', 9 + //'features/slides/background.html' 10 + ]; 11 + 12 + const initFeature = file => { 13 + const params = { 14 + debug, 15 + file, 16 + keepLive: true, 17 + show: debug 18 + }; 19 + window.app.openWindow(params, () => console.log('win opened')); 20 + }; 21 + 22 + features.forEach(initFeature);
+12
features/groups/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
features/groups/groups.js features/groups/background.js
+12
features/peeks/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
features/peeks/peeks.js features/peeks/background.js
+12
features/scripts/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
features/scripts/scripts.js features/scripts/background.js
+12
features/settings/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
+2 -3
features/settings/content.js
··· 1 - console.log('renderer'); 1 + console.log('settings/content'); 2 2 3 3 // TODO: capture and internally navigate out of panes 4 4 window.addEventListener('keyup', e => { ··· 19 19 let panes = []; 20 20 21 21 const init = cfg => { 22 - console.log('renderer: init'); 23 - //console.log('renderer: cfg', cfg); 22 + console.log('settings: init'); 24 23 25 24 let { prefs, features } = cfg; 26 25
+25 -25
features/settings/settings.js features/settings/background.js
··· 3 3 4 4 console.log('settings/settings'); 5 5 6 + //const debug = window.location.search.indexOf('debug') > 0; 7 + const debug = 1; 8 + 9 + if (debug) { 10 + console.log('clearing storage') 11 + localStorage.clear(); 12 + } 13 + 6 14 const labels = { 7 15 featureType: 'settings', 8 16 featureDisplay: 'Settings', ··· 10 18 shortcutKey: 'Settings shortcut', 11 19 } 12 20 }; 13 - 14 - const { 15 - BrowserWindow, 16 - globalShortcut, 17 - } = require('electron'); 18 - 19 - const path = require('path'); 20 21 21 22 let _store = null; 22 23 let _data = {}; ··· 65 66 const width = prefs.width || 800; 66 67 67 68 const params = { 69 + debug, 68 70 type: labels.featureType, 69 71 file: 'features/settings/content.html', 70 72 height, 71 73 width 72 74 }; 73 75 74 - _api.openWindow(params); 76 + api.openWindow(params); 75 77 }; 76 78 77 79 const initStore = (store, data) => { 78 - const sp = store.get('prefs'); 80 + const sp = store.getItem('prefs'); 79 81 if (!sp) { 80 - store.set('prefs', data.prefs); 82 + store.setItem('prefs', JSON.stringify(data.prefs)); 81 83 } 82 84 }; 83 85 84 86 const initShortcut = (api, prefs) => { 85 87 const shortcut = prefs.shortcutKey; 86 - 87 - if (globalShortcut.isRegistered(shortcut)) { 88 - globalShortcut.unregister(shortcut); 89 - } 90 - 91 - const ret = globalShortcut.register(shortcut, () => { 88 + api.shortcuts.register(shortcut, () => { 92 89 openSettingsWindow(api, prefs); 93 90 }); 94 - 95 - if (!ret) { 96 - console.error('Unable to register shortcut', shortcut); 97 - } 98 91 }; 99 92 100 93 const init = (api, store) => { 101 94 console.log('settings: init'); 102 95 103 96 _store = store; 104 - _api = api; 105 97 106 98 initStore(_store, _defaults); 107 99 108 100 _data = { 109 - get prefs() { return _store.get('prefs'); }, 110 - //get items() { return _store.get('items'); }, 101 + get prefs() { return JSON.parse(_store.getItem('prefs')); }, 102 + get items() { return JSON.parse(_store.getItem('items')); } 111 103 }; 112 104 105 + console.log('data', _data); 106 + 113 107 initShortcut(api, _data.prefs); 114 108 }; 115 109 ··· 119 113 // TODO only update store if changed 120 114 // and re-init 121 115 if (changed.prefs) { 122 - _store.set('prefs', changed.prefs); 116 + _store.setItem('prefs', JSON.stringify(changed.prefs)); 123 117 } 124 118 125 119 if (changed.items) { 126 - _store.set('items', changed.items); 120 + _store.setItem('items', JSON.stringif(changed.items)); 127 121 } 128 122 }; 129 123 ··· 139 133 openSettingsWindow(_api, _data.prefs); 140 134 }; 141 135 136 + /* 142 137 module.exports = { 143 138 init: init, 144 139 config, ··· 152 147 open, 153 148 onChange 154 149 }; 150 + */ 151 + 152 + window.addEventListener('load', () => { 153 + init(window.app, localStorage); 154 + }); 155 155 156 156 })();
+12
features/slides/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:core:background</title> 8 + </head> 9 + <body> 10 + <script type=module src="./background.js"></script> 11 + </body> 12 + </html>
features/slides/slides.js features/slides/background.js
+100 -78
index.js
··· 9 9 const { 10 10 electron, 11 11 app, 12 - BrowserView, 13 12 BrowserWindow, 14 13 globalShortcut, 15 14 ipcMain, 16 15 Menu, 17 16 nativeTheme, 18 - Notification, 19 17 Tray 20 18 } = require('electron'); 21 19 22 20 const path = require('path'); 23 21 const preloadPath = path.join(__dirname, 'preload.js'); 24 - const Store = require('electron-store'); 22 + 23 + const webCoreAddress = 'features/core/background.html'; 25 24 26 25 // ***** Developer / Error handling / Etc ***** 27 26 const isDev = require('electron-is-dev'); ··· 42 41 const unhandled = require('electron-unhandled'); 43 42 unhandled(); 44 43 45 - // ***** System / OS / Theme / Etc ***** 44 + // ***** Features / Strings ***** 45 + 46 + const labels = { 47 + app: { 48 + key: 'peek', 49 + title: 'Peek' 50 + }, 51 + tray: { 52 + tooltip: 'Click to open' 53 + } 54 + }; 55 + 56 + // ***** System / OS / Theme ***** 46 57 47 58 // system dark mode handling 48 59 ipcMain.handle('dark-mode:toggle', () => { ··· 58 69 nativeTheme.themeSource = 'system'; 59 70 }); 60 71 61 - // ***** App / Strings / Etc ***** 62 - 63 - const features = { 64 - settings: require('./features/settings/settings'), 65 - cmd: require('./features/cmd/cmd'), 66 - slides: require('./features/slides/slides'), 67 - peeks: require('./features/peeks/peeks'), 68 - scripts: require('./features/scripts/scripts'), 69 - groups: require('./features/groups/groups'), 70 - }; 71 - 72 - const labels = { 73 - app: { 74 - key: 'peek', 75 - title: 'Peek' 76 - }, 77 - tray: { 78 - tooltip: 'Click to open' 79 - } 80 - }; 81 - 82 - // ***** Caches ***** 83 - 84 - // TODO: make this open settings? 72 + // TODO: when does this actually hit on each OS? 85 73 app.on('activate', () => { 86 74 // On macOS it's common to re-create a window in the app when the 87 75 // dock icon is clicked and there are no other windows open. ··· 102 90 _tray = new Tray(ICON_PATH); 103 91 _tray.setToolTip(labels.tray.tooltip); 104 92 _tray.on('click', () => { 105 - features.settings.open(); 93 + //features.settings.open(); 106 94 }); 107 95 } 108 96 return _tray; 97 + }; 98 + 99 + // ***** Caches ***** 100 + 101 + let _windows = new Set(); 102 + 103 + const windowCache = { 104 + cache: [], 105 + add: entry => windowCache.cache.push(entry), 106 + byId: id => windowCache.cache.find(w => w.id == id), 107 + byKey: key => windowCache.cache.find(w => w.key == key) 109 108 }; 110 109 111 110 // ***** Data ***** ··· 179 178 }); 180 179 181 180 if (DEBUG) { 182 - //console.log('main: clearing datastore', k) 181 + console.log('main: clearing datastore', k) 183 182 featureStore.clear(); 184 183 } 185 184 ··· 189 188 }); 190 189 }; 191 190 192 - // app load 191 + // Electron app load 193 192 const onReady = () => { 194 193 console.log('onReady'); 195 194 ··· 198 197 app.dock.hide(); 199 198 } 200 199 200 + // initialize system tray 201 + // mostly just useful to know if the app is running or not 201 202 initTray(); 202 203 203 - initFeatures(features); 204 + //initFeatures(features); 204 205 206 + openWindow({ 207 + file: webCoreAddress, 208 + show: false, 209 + debug: DEBUG 210 + }) 211 + 212 + /* 205 213 // open settings on startup for now 206 214 if (BrowserWindow.getAllWindows().length === 0) { 207 215 features.settings.open(); 208 216 } 217 + */ 218 + 219 + registerShortcut('Option+q', onQuit); 209 220 }; 210 221 211 222 app.whenReady().then(onReady); 212 223 213 - // when renderer is ready, send over user data 214 - ipcMain.on('getconfig', (ev, data) => { 215 - console.log('main: getconfig') 216 - //ev.sender.hostWebContents.send('config', getData()) 217 - ev.reply('config', getData()) 224 + // ***** API ***** 225 + 226 + ipcMain.on('registershortcut', (ev, msg) => { 227 + registerShortcut(msg.shortcut, () => { 228 + console.log('shorcut executed', msg.shortcut, msg.replyTopic) 229 + ev.reply(msg.replyTopic, {}); 230 + }); 218 231 }); 219 232 220 - // listen for updates 221 - ipcMain.on('setconfig', (event, newData) => { 222 - // TODO: if any shortcuts changed, unregister the old ones 233 + ipcMain.on('unregistershortcut', (ev, msg) => { 234 + if (globalShortcut.isRegistered(msg.shortcut)) { 235 + globalShortcut.unregister(msg.shortcut); 236 + } 237 + }); 223 238 224 - // write to datastore 225 - updateData(newData); 239 + ipcMain.on('openwindow', (ev, msg) => { 240 + openWindow(msg.params, output => { 241 + if (msg.replyTopic) { 242 + ev.reply(msg.replyTopic, { output }); 243 + } 244 + }); 226 245 }); 227 246 228 247 // generic dispatch - messages only from trusted code (💀) 229 - ipcMain.on('sendmessage', (event, msg) => { 248 + ipcMain.on('sendmessage', (ev, msg) => { 230 249 console.log('sendmsg', msg); 231 - 232 - if (!msg.hasOwnProperty('feature')) { 233 - console.error('sendMessage', 'no feature property in message'); 234 - return; 235 - } 236 - 237 - const fkey = msg.feature; 238 - 239 - if (Object.keys(features).findIndex(k => k==fkey) == -1) { 240 - console.error('sendMessage', 'no matching feature'); 241 - return; 242 - } 243 - 244 - const feature = features[fkey]; 245 - 246 - if (!feature.hasOwnProperty('onMessage')) { 247 - console.error('sendMessage', 'feature has no message handler for', fkey); 248 - return; 249 - } 250 - 251 - feature.onMessage(msg.data); 252 250 }); 253 251 254 252 // ipc ESC handler 255 - ipcMain.on('esc', (event, title) => { 253 + ipcMain.on('esc', (ev, title) => { 256 254 console.log('esc'); 257 255 258 256 const fwin = BrowserWindow.getFocusedWindow(); ··· 265 263 } 266 264 }); 267 265 268 - const windowCache = { 269 - cache: [], 270 - add: entry => windowCache.cache.push(entry), 271 - byId: id => windowCache.cache.find(w => w.id == id), 272 - byKey: key => windowCache.cache.find(w => w.key == key) 266 + ipcMain.on('console', (ev, msg) => { 267 + console.log('renderer:', msg); 268 + }); 269 + 270 + // ***** Helpers ***** 271 + 272 + const registerShortcut = (shortcut, callback) => { 273 + console.log('registerShortcut', shortcut) 274 + 275 + if (globalShortcut.isRegistered(shortcut)) { 276 + globalShortcut.unregister(shortcut); 277 + } 278 + 279 + const ret = globalShortcut.register(shortcut, callback); 280 + 281 + if (!ret) { 282 + console.error('Unable to register shortcut', shortcut); 283 + return new Error("Failed in some way", { cause: err }); 284 + } 273 285 }; 274 286 287 + // window opener 275 288 const openWindow = (params) => { 276 289 if (params.keepLive == true) { 277 290 const entry = windowCache.byKey(params.windowKey); ··· 317 330 } 318 331 }); 319 332 320 - const win = new BrowserWindow(winPreferences); 333 + let win = new BrowserWindow(winPreferences); 321 334 322 335 // if persisting window, cache the caller's key and window id 323 336 if (params.keepLive == true) { ··· 339 352 win.on('blur', onGoAway); 340 353 win.on('close', onGoAway); 341 354 342 - //win.webContents.send('window', { type: labels.featureType, id: win.id}); 355 + win.on('closed', () => { 356 + _windows.delete(win); 357 + win = null; 358 + }); 343 359 344 360 if (params.debug) { 345 361 win.webContents.openDevTools(); ··· 355 371 console.error('openWindow: neither address nor file!'); 356 372 } 357 373 374 + //win.webContents.send('window', { type: labels.featureType, id: win.id}); 375 + broadcastToWindows('window', { type: labels.featureType, id: win.id}); 376 + 358 377 if (params.script) { 359 378 const script = params.script; 360 379 const domEvent = script.domEvent || 'dom-ready'; ··· 376 395 } 377 396 }; 378 397 398 + // send message to all windows 399 + const broadcastToWindows = (topic, msg) => { 400 + _windows.forEach(win => { 401 + win.webContents.send(topic, msg); 402 + }); 403 + }; 404 + 379 405 // Quit when all windows are closed, except on macOS. There, it's common 380 406 // for applications and their menu bar to stay active until the user quits 381 407 // explicitly with Cmd + Q. ··· 386 412 } 387 413 }); 388 414 389 - const onQuit = () => { 390 - console.log('onquit'); 391 415 392 - // Unregister all shortcuts on app close 393 - globalShortcut.unregisterAll(); 394 - 395 - // Close all persisent windows 416 + const onQuit = () => { 417 + // Close all persisent windows? 396 418 397 419 app.quit(); 398 420 };
+55 -3
preload.js
··· 1 - console.log('preload'); 1 + const d = msg => { 2 + ipcRenderer.send('console', msg); 3 + }; 2 4 3 5 const { 4 6 contextBridge, 7 + globalShortcut, 5 8 ipcRenderer 6 9 } = require('electron') 7 10 11 + /* 12 + const EventEmitter = require('node:events'); 13 + class LocalEmitter extends EventEmitter {}; 14 + const pubsub = new LocalEmitter(); 15 + */ 16 + 17 + /* 8 18 ipcRenderer.on('window', (ev, msg) => { 9 - console.log('preload: onwindow', msg); 19 + d('preload: onwindow', msg); 10 20 const { type, id, data } = msg; 11 21 if (type == 'main') { 12 22 handleMainWindow(); 13 23 } 14 24 }); 25 + */ 15 26 16 27 17 - // all window types close on escape 28 + // all visible window types close on escape 18 29 window.addEventListener('keyup', e => { 19 30 if (e.key == 'Escape') { 20 31 ipcRenderer.send('esc', ''); ··· 23 34 24 35 let api = {}; 25 36 37 + api.shortcuts = { 38 + register: (shortcut, cb) => { 39 + console.log('registering', shortcut, 'for', window.location) 40 + const replyTopic = `${shortcut}` 41 + ipcRenderer.send('registershortcut', { 42 + shortcut, 43 + replyTopic 44 + }); 45 + ipcRenderer.on(replyTopic, cb); 46 + }, 47 + unregister: shortcut => { 48 + console.log('unregistering', shortcut, 'for', window.location) 49 + ipcRenderer.send('registershortcut', { 50 + shortcut 51 + }); 52 + } 53 + }; 54 + 55 + api.openWindow = (params, callback) => { 56 + console.log('openwindow', params, 'for', window.location) 57 + const replyTopic = 'huh'; 58 + ipcRenderer.send('openwindow', { 59 + params, 60 + replyTopic 61 + }); 62 + if (callback) { 63 + ipcRenderer.on(replyTopic, params.callback); 64 + } 65 + }; 66 + 67 + /* 26 68 api.onConfigChange = callback => { 27 69 // noop if not an internal app file 28 70 const isMain = window.location.protocol == 'file:'; ··· 68 110 69 111 ipcRenderer.send('sendmessage', msg); 70 112 }; 113 + */ 71 114 72 115 contextBridge.exposeInMainWorld('app', api); 73 116 117 + window.addEventListener('load', () => { 118 + console.log('preloaded'); 119 + d('preload loaded'); 120 + }); 121 + 122 + 123 + /* 74 124 const handleMainWindow = () => { 125 + d('handleMainWindow'); 75 126 window.addEventListener('load', () => { 76 127 const replaceText = (selector, text) => { 77 128 const element = document.getElementById(selector) ··· 83 134 } 84 135 }); 85 136 }; 137 + */