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 #5 from autonome/modulus

modularization

authored by

Dietrich Ayala and committed by
GitHub
ab92181d f5d1d54d

+1203 -817
-348
defaults.js
··· 1 - 2 - const Store = require('electron-store'); 3 - 4 - const prefsSchema = { 5 - "$schema": "https://json-schema.org/draft/2020-12/schema", 6 - "$id": "peek.prefs.schema.json", 7 - "title": "Peek - prefs", 8 - "description": "Peek user preferences", 9 - "type": "object", 10 - "properties": { 11 - "globalKeyCmd": { 12 - "description": "Global OS hotkey to load app", 13 - "type": "string", 14 - "default": "CommandOrControl+Escape" 15 - }, 16 - "peekKeyPrefix": { 17 - "description": "Global OS hotkey prefix to trigger peeks - will be followed by 0-9", 18 - "type": "string", 19 - "default": "Option+" 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 - }, 26 - }, 27 - "required": [ "globalKeyCmd", "peekKeyPrefix"] 28 - }; 29 - 30 - const peekSchema = { 31 - "$schema": "https://json-schema.org/draft/2020-12/schema", 32 - "$id": "peek.peek.schema.json", 33 - "title": "Peek - page peek", 34 - "description": "Peek page peek", 35 - "type": "object", 36 - "properties": { 37 - "keyNum": { 38 - "description": "Number on keyboard to open this peek, 0-9", 39 - "type": "integer", 40 - "minimum": 0, 41 - "maximum": 9, 42 - "default": 0 43 - }, 44 - "title": { 45 - "description": "Name of the peek - user defined label", 46 - "type": "string", 47 - "default": "New Peek" 48 - }, 49 - "address": { 50 - "description": "URL to load", 51 - "type": "string", 52 - "default": "https://example.com" 53 - }, 54 - "persistState": { 55 - "description": "Whether to persist local state or load page into empty container - defaults to false", 56 - "type": "boolean", 57 - "default": false 58 - }, 59 - "keepLive": { 60 - "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 61 - "type": "boolean", 62 - "default": false 63 - }, 64 - "allowSound": { 65 - "description": "Whether to allow the page to emit sound or not (eg for background music player peeks - defaults to false", 66 - "type": "boolean", 67 - "default": false 68 - }, 69 - "height": { 70 - "description": "User-defined height of peek page", 71 - "type": "integer", 72 - "default": 600 73 - }, 74 - "width": { 75 - "description": "User-defined width of peek page", 76 - "type": "integer", 77 - "default": 800 78 - }, 79 - }, 80 - "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound", 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" ] 140 - }; 141 - 142 - const scriptSchema = { 143 - "$schema": "https://json-schema.org/draft/2020-12/schema", 144 - "$id": "peek.script.schema.json", 145 - "title": "Peek - script", 146 - "description": "Peek background script", 147 - "type": "object", 148 - "properties": { 149 - "id": { 150 - "description": "The unique identifier for a script", 151 - "type": "string", 152 - "default": "peek:script:REPLACEME" 153 - }, 154 - "title": { 155 - "description": "Name of the script - user defined", 156 - "type": "string", 157 - "default": "New Script" 158 - }, 159 - "version": { 160 - "description": "Version number of the script", 161 - "type": "string", 162 - "default": "1.0.0" 163 - }, 164 - "address": { 165 - "description": "URL to execute script against", 166 - "type": "string", 167 - "default": "https://example.com" 168 - }, 169 - "selector": { 170 - "description": "CSS Selector for the script", 171 - "type": "string", 172 - "default": "body > h1" 173 - }, 174 - "property": { 175 - "description": "Which element property to return - currently 'textContent' is the only supported value", 176 - "type": "string", 177 - "default": "textContent" 178 - }, 179 - "interval": { 180 - "description": "How often to execute the script, in milliseconds - defaults to five minutes", 181 - "type": "integer", 182 - "default": 300000, 183 - "minimum": 0 184 - }, 185 - "storeHistory": { 186 - "description": "Whether to store historic values - defaults to false", 187 - "type": "boolean", 188 - "default": false 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 - }, 200 - }, 201 - "required": [ "id", "title", "address", "version", "selector", "property", 202 - "interval", "notifyOnChange", "storeHistory" ] 203 - }; 204 - 205 - const schemas = { 206 - prefs: prefsSchema, 207 - peeks: peekSchema, 208 - slides: slideSchema, 209 - scripts: scriptSchema 210 - }; 211 - 212 - // TODO: schemaize 0-9 constraints for peeks and UPLR constraints for slides 213 - const fullSchema = { 214 - prefs: prefsSchema, 215 - peek: peekSchema, 216 - slide: slideSchema, 217 - script: scriptSchema, 218 - peeks: { 219 - type: 'array', 220 - items: { "$ref": "#/$defs/peek" } 221 - }, 222 - slides: { 223 - type: 'array', 224 - items: { "$ref": "#/$defs/slide" } 225 - }, 226 - scripts: { 227 - type: 'array', 228 - items: { "$ref": "#/$defs/script" } 229 - }, 230 - }; 231 - 232 - const defaults = { 233 - prefs: { 234 - globalKeyCmd: 'CommandOrControl+Escape', 235 - peekKeyPrefix: 'Option+', 236 - slideKeyPrefix: 'Option+' 237 - }, 238 - peeks: [], 239 - slides: [ 240 - { 241 - screenEdge: 'Up', 242 - title: 'Slide from top', 243 - address: 'http://localhost/', 244 - persistState: false, 245 - keepLive: false, 246 - allowSound: false, 247 - height: 600, 248 - width: 800, 249 - }, 250 - { 251 - screenEdge: 'Down', 252 - title: 'Slide from bottom', 253 - address: 'http://localhost/', 254 - persistState: false, 255 - keepLive: false, 256 - allowSound: false, 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 - }, 280 - ], 281 - scripts: [ 282 - { 283 - id: 'peek:script:localhost:test', 284 - title: 'localhost test', 285 - address: 'http://localhost/', 286 - version: '1', 287 - selector: 'body > h1', 288 - property: 'textContent', 289 - interval: 300000, 290 - storehistory: false, 291 - notifyOnChange: false 292 - }, 293 - ] 294 - }; 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 - 309 - const set = data => { 310 - store.set('prefs', data.prefs); 311 - store.set('peeks', data.peeks); 312 - store.set('slides', data.slides); 313 - store.set('scripts', data.scripts); 314 - }; 315 - 316 - const store = new Store({ 317 - // TODO: re-enable schemas 318 - //schema: fullSchema, 319 - watch: true 320 - }); 321 - 322 - // DEBUG 323 - store.clear(); 324 - 325 - const tmp = store.get('prefs'); 326 - if (!tmp) { 327 - console.log('initializing datastore'); 328 - store.set('prefs', defaults.prefs); 329 - store.set('peeks', defaults.peeks); 330 - store.set('slides', defaults.slides); 331 - store.set('scripts', defaults.scripts); 332 - } 333 - 334 - module.exports = { 335 - schemas, 336 - data: { 337 - get prefs() { return store.get('prefs'); }, 338 - get peeks() { return store.get('peeks'); }, 339 - get slides() { return store.get('slides'); }, 340 - get scripts() { return store.get('scripts'); } 341 - }, 342 - set, 343 - watch: fn => { 344 - store.onDidAnyChange(newData => { 345 - fn(newData) 346 - }); 347 - } 348 - };
+266
features/peeks/peeks.js
··· 1 + // peeks/peek.js 2 + (async () => { 3 + 4 + console.log('peeks/peeks'); 5 + 6 + const labels = { 7 + featureType: 'peeks', 8 + featureDisplay: 'Peeks', 9 + itemType: 'peek', 10 + itemDisplay: 'Peek', 11 + prefs: { 12 + keyPrefix: 'Peek shortcut prefix', 13 + } 14 + }; 15 + 16 + const { 17 + BrowserWindow, 18 + globalShortcut, 19 + } = require('electron'); 20 + 21 + const path = require('path'); 22 + 23 + let _store = null; 24 + 25 + const prefsSchema = { 26 + "$schema": "https://json-schema.org/draft/2020-12/schema", 27 + "$id": "peek.peeks.prefs.schema.json", 28 + "title": "Peek - peeks", 29 + "description": "Peek app Peeks prefs", 30 + "type": "object", 31 + "properties": { 32 + "shortcutKeyPrefix": { 33 + "description": "Global OS hotkey prefix to trigger peeks - will be followed by 0-9", 34 + "type": "string", 35 + "default": "Option+" 36 + }, 37 + }, 38 + "required": [ "shortcutKeyPrefix"] 39 + }; 40 + 41 + const itemSchema = { 42 + "$schema": "https://json-schema.org/draft/2020-12/schema", 43 + "$id": "peek.peeks.peek.schema.json", 44 + "title": "Peek - page peek", 45 + "description": "Peek page peek", 46 + "type": "object", 47 + "properties": { 48 + "keyNum": { 49 + "description": "Number on keyboard to open this peek, 0-9", 50 + "type": "integer", 51 + "minimum": 0, 52 + "maximum": 9, 53 + "default": 0 54 + }, 55 + "title": { 56 + "description": "Name of the peek - user defined label", 57 + "type": "string", 58 + "default": "New Peek" 59 + }, 60 + "address": { 61 + "description": "URL to load", 62 + "type": "string", 63 + "default": "https://example.com" 64 + }, 65 + "persistState": { 66 + "description": "Whether to persist local state or load page into empty container - defaults to false", 67 + "type": "boolean", 68 + "default": false 69 + }, 70 + "keepLive": { 71 + "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 72 + "type": "boolean", 73 + "default": false 74 + }, 75 + "allowSound": { 76 + "description": "Whether to allow the page to emit sound or not (eg for background music player peeks - defaults to false", 77 + "type": "boolean", 78 + "default": false 79 + }, 80 + "height": { 81 + "description": "User-defined height of peek page", 82 + "type": "integer", 83 + "default": 600 84 + }, 85 + "width": { 86 + "description": "User-defined width of peek page", 87 + "type": "integer", 88 + "default": 800 89 + }, 90 + }, 91 + "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound", 92 + "height", "width" ] 93 + }; 94 + 95 + const listSchema = { 96 + type: 'array', 97 + items: { "$ref": "#/$defs/peek" } 98 + }; 99 + 100 + // TODO: schemaize 0-9 constraints for peeks 101 + const schemas = { 102 + prefs: prefsSchema, 103 + item: itemSchema, 104 + items: listSchema 105 + }; 106 + 107 + const _defaults = { 108 + prefs: { 109 + shortcutKeyPrefix: 'Option+' 110 + }, 111 + items: Array.from(Array(10)), 112 + }; 113 + 114 + for (var i = 0; i != 10; i++) { 115 + _defaults.items[i] = { 116 + keyNum: i, 117 + title: `Peek key ${i}`, 118 + address: 'https://example.com/', 119 + persistState: false, 120 + keepLive: false, 121 + allowSound: false, 122 + height: 600, 123 + width: 800, 124 + }; 125 + } 126 + 127 + let _windows = {}; 128 + 129 + const executeItem = (api, item) => { 130 + const height = item.height || 600; 131 + const width = item.width || 800; 132 + 133 + let win = null; 134 + 135 + const windowKey = labels.featureType + item.keyNum; 136 + 137 + if (_windows[windowKey]) { 138 + console.log(labels.featureType, item.keyNum, 'using stored window'); 139 + win = _windows[windowKey]; 140 + win.show(); 141 + } 142 + else { 143 + console.log(labels.featureType, item.keyNum, 'creating new window'); 144 + 145 + win = new BrowserWindow({ 146 + height, 147 + width, 148 + center: true, 149 + skipTaskbar: true, 150 + autoHideMenuBar: true, 151 + titleBarStyle: 'hidden', 152 + webPreferences: { 153 + preload: api.preloadPath, 154 + // isolate content and do not persist it 155 + partition: Date.now() 156 + } 157 + }); 158 + 159 + //_windows[windowKey] = win; 160 + } 161 + 162 + const onGoAway = () => { 163 + /* 164 + if (item.keepLive) { 165 + _windows[windowKey] = win; 166 + win.hide(); 167 + } 168 + else { 169 + win.destroy(); 170 + } 171 + */ 172 + win.destroy(); 173 + } 174 + win.on('blur', onGoAway); 175 + win.on('close', onGoAway); 176 + 177 + win.webContents.send('window', { type: labels.featureType, id: win.id, data: item }); 178 + 179 + win.loadURL(item.address); 180 + }; 181 + 182 + const initStore = (store, data) => { 183 + const sp = store.get('prefs'); 184 + if (!sp) { 185 + store.set('prefs', data.prefs); 186 + } 187 + 188 + const items = store.get('items'); 189 + if (!items) { 190 + store.set('items', data.items); 191 + } 192 + }; 193 + 194 + const initItems = (api, prefs, items) => { 195 + const cmdPrefix = prefs.shortcutKeyPrefix; 196 + 197 + items.forEach(item => { 198 + const shortcut = `${cmdPrefix}${item.keyNum}`; 199 + 200 + if (globalShortcut.isRegistered(shortcut)) { 201 + globalShortcut.unregister(shortcut); 202 + } 203 + 204 + const ret = globalShortcut.register(shortcut, () => { 205 + executeItem(api, item); 206 + }); 207 + 208 + if (!ret) { 209 + console.error('Unable to register shortcut', shortcut); 210 + } 211 + }); 212 + }; 213 + 214 + const init = (api, store) => { 215 + _store = store; 216 + _api = api; 217 + 218 + initStore(_store, _defaults); 219 + 220 + _data = { 221 + get prefs() { return _store.get('prefs'); }, 222 + get items() { return _store.get('items'); }, 223 + }; 224 + 225 + // initialize peeks 226 + if (_data.items.length > 0) { 227 + initItems(api, _data.prefs, _data.items); 228 + } 229 + }; 230 + 231 + const onChange = (changed, old) => { 232 + console.log(labels.featureType, 'onChange', changed); 233 + 234 + // TODO only update store if changed 235 + // and re-init 236 + if (changed.prefs) { 237 + _store.set('prefs', changed.prefs); 238 + } 239 + 240 + if (changed.items) { 241 + _store.set('items', changed.items); 242 + } 243 + }; 244 + 245 + // ui config 246 + const config = { 247 + // allow user to create new items 248 + allowNew: false, 249 + // fields that are view only 250 + disabled: ['keyNum'], 251 + }; 252 + 253 + module.exports = { 254 + init: init, 255 + config, 256 + labels, 257 + schemas, 258 + data: { 259 + get prefs() { return _store.get('prefs'); }, 260 + get items() { return _store.get('items'); }, 261 + }, 262 + onChange 263 + }; 264 + 265 + 266 + })();
features/scripts/.scripts.js.swp

This is a binary file and will not be displayed.

+279
features/scripts/scripts.js
··· 1 + // scripts/scripts.js 2 + (async () => { 3 + 4 + console.log('scripts/scripts'); 5 + 6 + const labels = { 7 + featureType: 'scripts', 8 + featureDisplay: 'Scripts', 9 + itemType: 'script', 10 + itemDisplay: 'Script', 11 + prefs: { 12 + } 13 + }; 14 + 15 + const { 16 + BrowserWindow, 17 + globalShortcut, 18 + screen, 19 + } = require('electron'); 20 + 21 + const path = require('path'); 22 + 23 + let _store = null; 24 + 25 + const prefsSchema = { 26 + "$schema": "https://json-schema.org/draft/2020-12/schema", 27 + "$id": "peek.scripts.prefs.schema.json", 28 + "title": "Peek - Scripts prefs", 29 + "description": "Peek app Scripts prefs", 30 + "type": "object", 31 + "properties": { 32 + }, 33 + "required": [] 34 + }; 35 + 36 + const itemSchema = { 37 + "$schema": "https://json-schema.org/draft/2020-12/schema", 38 + "$id": "peek.scripts.script.schema.json", 39 + "title": "Peek - script", 40 + "description": "Peek background script", 41 + "type": "object", 42 + "properties": { 43 + "id": { 44 + "description": "The unique identifier for a script", 45 + "type": "string", 46 + "default": "peek:script:REPLACEME" 47 + }, 48 + "title": { 49 + "description": "Name of the script - user defined", 50 + "type": "string", 51 + "default": "New Script" 52 + }, 53 + "version": { 54 + "description": "Version number of the script", 55 + "type": "string", 56 + "default": "1.0.0" 57 + }, 58 + "address": { 59 + "description": "URL to execute script against", 60 + "type": "string", 61 + "default": "https://example.com" 62 + }, 63 + "selector": { 64 + "description": "CSS Selector for the script", 65 + "type": "string", 66 + "default": "body > h1" 67 + }, 68 + "property": { 69 + "description": "Which element property to return - currently 'textContent' is the only supported value", 70 + "type": "string", 71 + "default": "textContent" 72 + }, 73 + "interval": { 74 + "description": "How often to execute the script, in milliseconds - defaults to five minutes", 75 + "type": "integer", 76 + "default": 300000, 77 + "minimum": 0 78 + }, 79 + "storeHistory": { 80 + "description": "Whether to store historic values - defaults to false", 81 + "type": "boolean", 82 + "default": false 83 + }, 84 + "notifyOnChange": { 85 + "description": "Whether to notify using local OS notifications when script value changes", 86 + "type": "boolean", 87 + "default": true 88 + }, 89 + "previousValue": { 90 + "description": "The most recently fetched result of script exection", 91 + "type": "string", 92 + "default": "", 93 + }, 94 + }, 95 + "required": [ "id", "title", "address", "version", "selector", "property", 96 + "interval", "notifyOnChange", "storeHistory" ] 97 + }; 98 + 99 + const listSchema = { 100 + type: 'array', 101 + items: { "$ref": "#/$defs/script" } 102 + }; 103 + 104 + // TODO: schemaize 0-9 constraints for peeks 105 + const schemas = { 106 + prefs: prefsSchema, 107 + item: itemSchema, 108 + items: listSchema 109 + }; 110 + 111 + const _defaults = { 112 + prefs: { 113 + }, 114 + items: [ 115 + { 116 + id: 'peek:script:localhost:test', 117 + title: 'localhost test', 118 + address: 'http://localhost/', 119 + version: '1', 120 + selector: 'body > h1', 121 + property: 'textContent', 122 + interval: 300000, 123 + storehistory: false, 124 + notifyOnChange: false 125 + }, 126 + ] 127 + }; 128 + 129 + let _windows = {}; 130 + 131 + const executeItem = (api, script, cb) => { 132 + const view = new BrowserWindow({ 133 + show: false, 134 + webPreferences: { 135 + preload: api.preloadPath, 136 + // isolate content and do not persist it 137 + partition: Date.now() 138 + } 139 + }); 140 + 141 + view.webContents.send('window', { 142 + id: 'view', 143 + type: 'script', 144 + data: script 145 + }); 146 + 147 + view.webContents.loadURL(script.address); 148 + 149 + const str = ` 150 + const s = "${script.selector}"; 151 + const r = document.querySelector(s); 152 + const value = r ? r.textContent : null; 153 + value; 154 + `; 155 + 156 + view.webContents.on('dom-ready', async () => { 157 + try { 158 + const r = await view.webContents.executeJavaScript(str); 159 + cb(r); 160 + } catch(ex) { 161 + console.error('cs exec error', ex); 162 + cb(null); 163 + } 164 + view.destroy(); 165 + }); 166 + }; 167 + 168 + const initStore = (store, data) => { 169 + const sp = store.get('prefs'); 170 + if (!sp) { 171 + store.set('prefs', data.prefs); 172 + } 173 + 174 + const items = store.get('items'); 175 + if (!items) { 176 + store.set('items', data.items); 177 + } 178 + }; 179 + 180 + let _intervals = []; 181 + 182 + const initItems = (api, prefs, items) => { 183 + // blow it all away for now 184 + // someday make it right proper just cancel/update changed and add new 185 + _intervals.forEach(clearInterval); 186 + 187 + // debounce me somehow so not shooting em all off 188 + // at once every time app starts 189 + items.forEach(item => { 190 + const interval = setInterval(() => { 191 + //console.log('interval hit', item.title); 192 + const r = executeItem(api, item, (res) => { 193 + //console.log('cs r', res); 194 + 195 + if (item.previousValue != res) { 196 + 197 + // update stored value 198 + item.previousValue = res; 199 + updateItem(item); 200 + 201 + // notification 202 + // add to schema and support per script 203 + /* 204 + const title = `Peek :: Script :: ${item.title}`; 205 + const body = [ 206 + `Script result changed for ${item.title}:`, 207 + `- Old: ${previousValue}`, 208 + `- New: ${res}` 209 + ].join('\n'); 210 + 211 + new Notification({ title, body }).show(); 212 + */ 213 + } 214 + }); 215 + }, item.interval); 216 + _intervals.push(interval); 217 + }); 218 + }; 219 + 220 + const init = (api, store) => { 221 + _store = store; 222 + _api = api; 223 + 224 + initStore(_store, _defaults); 225 + 226 + _data = { 227 + get prefs() { return _store.get('prefs'); }, 228 + get items() { return _store.get('items'); }, 229 + }; 230 + 231 + // initialize peeks 232 + if (_data.items.length > 0) { 233 + initItems(api, _data.prefs, _data.items); 234 + } 235 + }; 236 + 237 + const updateItem = (item) => { 238 + let items = _store.get('items'); 239 + const idx = items.findIndex(el => el.id == item.id); 240 + items[idx] = item; 241 + _store.set('items', items); 242 + }; 243 + 244 + const onChange = (changed, old) => { 245 + console.log(labels.featureType, 'onChange', changed); 246 + 247 + // TODO only update store if changed 248 + // and re-init 249 + if (changed.prefs) { 250 + _store.set('prefs', changed.prefs); 251 + } 252 + 253 + if (changed.items) { 254 + _store.set('items', changed.items); 255 + } 256 + }; 257 + 258 + // ui config 259 + const config = { 260 + // allow user to create new items 261 + allowNew: false, 262 + // fields that are view only 263 + disabled: ['screenEdge'], 264 + }; 265 + 266 + module.exports = { 267 + init: init, 268 + config, 269 + labels, 270 + schemas, 271 + data: { 272 + get prefs() { return _store.get('prefs'); }, 273 + get items() { return _store.get('items'); }, 274 + }, 275 + onChange 276 + }; 277 + 278 + 279 + })();
+433
features/slides/slides.js
··· 1 + // slides/slides.js 2 + (async () => { 3 + 4 + console.log('slides/slides'); 5 + 6 + const labels = { 7 + featureType: 'slides', 8 + featureDisplay: 'Slides', 9 + itemType: 'slide', 10 + itemDisplay: 'Slide', 11 + prefs: { 12 + keyPrefix: 'Slide shortcut prefix', 13 + } 14 + }; 15 + 16 + const { 17 + BrowserWindow, 18 + globalShortcut, 19 + screen, 20 + } = require('electron'); 21 + 22 + const path = require('path'); 23 + 24 + let _store = null; 25 + 26 + const prefsSchema = { 27 + "$schema": "https://json-schema.org/draft/2020-12/schema", 28 + "$id": "peek.slides.prefs.schema.json", 29 + "title": "Peek - Slides prefs", 30 + "description": "Peek app Slides prefs", 31 + "type": "object", 32 + "properties": { 33 + "shortcutKeyPrefix": { 34 + "description": "Global OS hotkey prefix to trigger slides - will be followed by up/down/left/right arrows", 35 + "type": "string", 36 + "default": "Option+" 37 + }, 38 + }, 39 + "required": [ "shortcutKeyPrefix"] 40 + }; 41 + 42 + const itemSchema = { 43 + "$schema": "https://json-schema.org/draft/2020-12/schema", 44 + "$id": "peek.slides.slide.schema.json", 45 + "title": "Peek - page slide", 46 + "description": "Peek page slide", 47 + "type": "object", 48 + "properties": { 49 + "screenEdge": { 50 + "description": "Edge of screen or arrow key to open this slide, up/down/left/right", 51 + "type": "string", 52 + "oneOf": [ 53 + { "format": "Up" }, 54 + { "format": "Down" }, 55 + { "format": "Left" }, 56 + { "format": "Right" } 57 + ], 58 + "default": "Right" 59 + }, 60 + "title": { 61 + "description": "Name of the slide - user defined label", 62 + "type": "string", 63 + "default": "New Slide" 64 + }, 65 + "address": { 66 + "description": "URL to load", 67 + "type": "string", 68 + "default": "https://example.com" 69 + }, 70 + "persistState": { 71 + "description": "Whether to persist local state or load page into empty container - defaults to false", 72 + "type": "boolean", 73 + "default": false 74 + }, 75 + "keepLive": { 76 + "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 77 + "type": "boolean", 78 + "default": false 79 + }, 80 + "allowSound": { 81 + "description": "Whether to allow the page to emit sound or not (eg for background music player slides - defaults to false", 82 + "type": "boolean", 83 + "default": false 84 + }, 85 + "height": { 86 + "description": "User-defined height of slide page", 87 + "type": "integer", 88 + "default": 600 89 + }, 90 + "width": { 91 + "description": "User-defined width of slide page", 92 + "type": "integer", 93 + "default": 800 94 + }, 95 + }, 96 + "required": [ "screenEdge", "title", "address", "persistState", "keepLive", "allowSound", 97 + "height", "width" ] 98 + }; 99 + 100 + const listSchema = { 101 + type: 'array', 102 + items: { "$ref": "#/$defs/slide" } 103 + }; 104 + 105 + // TODO: schemaize 0-9 constraints for peeks 106 + const schemas = { 107 + prefs: prefsSchema, 108 + item: itemSchema, 109 + items: listSchema 110 + }; 111 + 112 + const _defaults = { 113 + prefs: { 114 + shortcutKeyPrefix: 'Option+' 115 + }, 116 + items: [ 117 + { 118 + screenEdge: 'Up', 119 + title: 'Slide from top', 120 + address: 'http://localhost/', 121 + persistState: false, 122 + keepLive: false, 123 + allowSound: false, 124 + height: 600, 125 + width: 800, 126 + }, 127 + { 128 + screenEdge: 'Down', 129 + title: 'Slide from bottom', 130 + address: 'http://localhost/', 131 + persistState: false, 132 + keepLive: false, 133 + allowSound: false, 134 + height: 600, 135 + width: 800, 136 + }, 137 + { 138 + screenEdge: 'Left', 139 + title: 'Slide from left', 140 + address: 'http://localhost/', 141 + persistState: false, 142 + keepLive: false, 143 + allowSound: false, 144 + height: 600, 145 + width: 800, 146 + }, 147 + { 148 + screenEdge: 'Right', 149 + title: 'Slide from right', 150 + address: 'http://localhost/', 151 + persistState: false, 152 + keepLive: false, 153 + allowSound: false, 154 + height: 600, 155 + width: 800, 156 + }, 157 + ] 158 + }; 159 + 160 + let _windows = {}; 161 + 162 + const executeItem = (api, item) => { 163 + let win = null; 164 + 165 + const windowKey = labels.featureType + item.screenEdge; 166 + 167 + // TODO: fix stored+live windows 168 + if (_windows[windowKey]) { 169 + console.log(labels.featureType, slide.screenEdge, 'using stored window'); 170 + win = _windows[windowKey]; 171 + win.show(); 172 + } 173 + else { 174 + const { size, bounds } = screen.getPrimaryDisplay(); 175 + 176 + let x, y, height, width, center = null; 177 + 178 + switch(item.screenEdge) { 179 + case 'Up': 180 + // horizontally center 181 + x = (size.width - item.width) / 2; 182 + 183 + // y starts at screen top and stays there 184 + y = 0; 185 + 186 + width = item.width; 187 + height = 1; 188 + break; 189 + case 'Down': 190 + // horizonally center 191 + x = (size.width - item.width) / 2; 192 + 193 + // y ends up at window height from bottom 194 + // 195 + // eg: y = size.height - item.height; 196 + // 197 + // but starts at screen bottom 198 + y = size.height; 199 + 200 + width = item.width; 201 + height = 1; 202 + break; 203 + case 'Left': 204 + // x starts and ends at at left screen edge 205 + // at left edge 206 + x = 0; 207 + 208 + // vertically center 209 + y = (size.height - item.height) / 2; 210 + 211 + width = 1; 212 + height = item.height; 213 + break; 214 + case 'Right': 215 + // x ends at at right screen edge - window size 216 + // 217 + // eg: x = size.width - item.width; 218 + // 219 + // but starts at screen right edge, will animate in 220 + x = size.width; 221 + 222 + // vertically center 223 + y = (size.height - item.height) / 2; 224 + 225 + width = 1; 226 + height = item.height; 227 + break; 228 + default: 229 + center = true; 230 + console.log('waddafa'); 231 + } 232 + 233 + win = new BrowserWindow({ 234 + height, 235 + width, 236 + x, 237 + y, 238 + skipTaskbar: true, 239 + autoHideMenuBar: true, 240 + titleBarStyle: 'hidden', 241 + // maybe worth doing instead of animating width 242 + //enableLargerThanScreen: true, 243 + webPreferences: { 244 + preload: api.preloadPath, 245 + // isolate content and do not persist it 246 + partition: Date.now() 247 + } 248 + }); 249 + 250 + //_windows[windowKey] = win; 251 + } 252 + 253 + animateSlide(win, item).then(); 254 + 255 + const onGoAway = () => { 256 + /* 257 + if (item.keepLive) { 258 + _windows[key] = win; 259 + win.hide(); 260 + } 261 + else { 262 + win.destroy(); 263 + } 264 + */ 265 + win.destroy(); 266 + } 267 + win.on('blur', onGoAway); 268 + win.on('close', onGoAway); 269 + 270 + /* 271 + const str = ` 272 + window.addEventListener('keyup', e => { 273 + if (e.key == 'Escape') { 274 + console.log('peek script esc'); 275 + } 276 + }); 277 + 1; 278 + `; 279 + 280 + win.webContents.on('dom-ready', async () => { 281 + try { 282 + const r = await win.webContents.executeJavaScript(str); 283 + console.log(r); 284 + } catch(ex) { 285 + console.error('cs exec error', ex); 286 + } 287 + }); 288 + */ 289 + 290 + win.webContents.send('window', { type: labels.featureType, id: win.id, data: item }); 291 + 292 + win.loadURL(item.address); 293 + }; 294 + 295 + const initStore = (store, data) => { 296 + const sp = store.get('prefs'); 297 + if (!sp) { 298 + store.set('prefs', data.prefs); 299 + } 300 + 301 + const items = store.get('items'); 302 + if (!items) { 303 + store.set('items', data.items); 304 + } 305 + }; 306 + 307 + const initItems = (api, prefs, items) => { 308 + const cmdPrefix = prefs.shortcutKeyPrefix; 309 + 310 + items.forEach(item => { 311 + const shortcut = `${cmdPrefix}${item.screenEdge}`; 312 + 313 + if (globalShortcut.isRegistered(shortcut)) { 314 + globalShortcut.unregister(shortcut); 315 + } 316 + 317 + const ret = globalShortcut.register(shortcut, () => { 318 + executeItem(api, item); 319 + }); 320 + 321 + if (!ret) { 322 + console.error('Unable to register shortcut', shortcut); 323 + } 324 + }); 325 + }; 326 + 327 + const init = (api, store) => { 328 + _store = store; 329 + _api = api; 330 + 331 + initStore(_store, _defaults); 332 + 333 + _data = { 334 + get prefs() { return _store.get('prefs'); }, 335 + get items() { return _store.get('items'); }, 336 + }; 337 + 338 + // initialize peeks 339 + if (_data.items.length > 0) { 340 + initItems(api, _data.prefs, _data.items); 341 + } 342 + }; 343 + 344 + const onChange = (changed, old) => { 345 + console.log(labels.featureType, 'onChange', changed); 346 + 347 + // TODO only update store if changed 348 + // and re-init 349 + if (changed.prefs) { 350 + _store.set('prefs', changed.prefs); 351 + } 352 + 353 + if (changed.items) { 354 + _store.set('items', changed.items); 355 + } 356 + }; 357 + 358 + // ui config 359 + const config = { 360 + // allow user to create new items 361 + allowNew: false, 362 + // fields that are view only 363 + disabled: ['screenEdge'], 364 + }; 365 + 366 + module.exports = { 367 + init: init, 368 + config, 369 + labels, 370 + schemas, 371 + data: { 372 + get prefs() { return _store.get('prefs'); }, 373 + get items() { return _store.get('items'); }, 374 + }, 375 + onChange 376 + }; 377 + 378 + const animateSlide = (win, slide) => { 379 + return new Promise((res, rej) => { 380 + const { size, bounds } = screen.getPrimaryDisplay(); 381 + 382 + // get x/y field 383 + const coord = slide.screenEdge == 'Left' || slide.screenEdge == 'Right' ? 'x' : 'y'; 384 + 385 + const dim = coord == 'x' ? 'width' : 'height'; 386 + 387 + const winBounds = win.getBounds(); 388 + 389 + // created window at x/y taking animation into account 390 + let pos = winBounds[coord]; 391 + 392 + const speedMs = 150; 393 + const timerInterval = 10; 394 + 395 + let tick = 0; 396 + const numTicks = parseInt(speedMs / timerInterval); 397 + 398 + const offset = slide[dim] / numTicks; 399 + 400 + //console.log('numTicks', numTicks, 'widthChunk', offset); 401 + 402 + const timer = setInterval(() => { 403 + tick++; 404 + 405 + if (tick >= numTicks) { 406 + clearInterval(timer); 407 + res(); 408 + } 409 + 410 + const winBounds = win.getBounds(); 411 + 412 + if (slide.screenEdge == 'Right' || slide.screenEdge == 'Down') { 413 + // new position is current position +/- offset 414 + pos = pos - offset; 415 + } 416 + 417 + const grownEnough = winBounds[dim] <= slide[dim]; 418 + const newDim = grownEnough ? 419 + winBounds[dim] + offset 420 + : winBounds[dim]; 421 + 422 + const newBounds = {}; 423 + newBounds[coord] = parseInt(pos, 10); 424 + newBounds[dim] = parseInt(newDim, 10); 425 + 426 + // set new bounds 427 + win.setBounds(newBounds); 428 + 429 + }, timerInterval); 430 + }); 431 + }; 432 + 433 + })();
+138 -401
index.js
··· 14 14 Menu, 15 15 nativeTheme, 16 16 Notification, 17 - screen, 18 17 Tray 19 18 } = require('electron'); 20 19 21 20 const path = require('path'); 21 + const preloadPath = path.join(__dirname, 'preload.js'); 22 + 23 + const Store = require('electron-store'); 24 + 25 + const features = { 26 + peeks: require('./features/peeks/peeks'), 27 + slides: require('./features/slides/slides'), 28 + scripts: require('./features/scripts/scripts'), 29 + }; 22 30 23 31 const labels = { 24 32 app: { 33 + key: 'peek', 25 34 title: 'Peek' 26 35 }, 27 36 tray: { 28 - tooltip: 'Click to open Peek' 37 + tooltip: 'Click to open' 29 38 } 30 39 }; 31 - 32 - // load data 33 - let { data, schemas, set, watch } = require('./defaults'); 34 40 35 41 const ICON_RELATIVE_PATH = 'assets/icons/AppIcon.appiconset/Icon-App-20x20@2x.png'; 36 42 const ICON_PATH = path.join(__dirname, ICON_RELATIVE_PATH); ··· 68 74 }); 69 75 70 76 let _windows = []; 71 - let _peekWins = {}; 72 - let _slideWins = {}; 73 77 74 78 // main window 75 79 let _win = null; 80 + 76 81 // tray 77 82 let _tray = null; 78 83 ··· 84 89 }; 85 90 86 91 const createMainWindow = () => { 87 - console.log('createMainWindow'); 92 + console.log('createMainWindow, preloadPath', preloadPath); 88 93 // Create the browser window. 89 94 const mainWindow = new BrowserWindow({ 90 95 width: 800, 91 96 height: 600, 92 97 webPreferences: { 93 - preload: path.join(__dirname, 'preload.js') 98 + preload: preloadPath 94 99 } 95 100 }); 96 101 ··· 108 113 // Open the DevTools. 109 114 mainWindow.webContents.openDevTools() 110 115 116 + mainWindow.webContents.send('window', { 117 + path: path.join(__dirname), 118 + id: mainWindow.id, 119 + type: 'main', 120 + }); 121 + 122 + _windows.push(mainWindow); 123 + /* 124 + mainWindow.on('closed', () => { 125 + const idx = _windows.findIndex(mainWindow); 126 + //_windows. 127 + }); 128 + */ 129 + 111 130 return mainWindow; 112 131 }; 113 132 ··· 131 150 return _tray; 132 151 }; 133 152 134 - const execContentScript = (script, cb) => { 135 - const view = new BrowserView({ 136 - webPreferences: { 137 - preload: path.join(__dirname, 'preload.js'), 138 - // isolate content and do not persist it 139 - partition: Date.now() 140 - } 141 - }); 153 + const getData = () => { 154 + let rollup = { 155 + prefs: { 156 + schema: prefsSchema, 157 + data: appStore.get('prefs') 158 + }, 159 + features: [] 160 + }; 142 161 143 - view.webContents.send('window', { 144 - id: 'view', 145 - type: 'script', 146 - data: script 162 + Object.keys(features).forEach(k => { 163 + const feature = features[k]; 164 + rollup.features.push({ 165 + config: feature.config, 166 + labels: feature.labels, 167 + schemas: feature.schemas, 168 + data: feature.data 169 + }) 147 170 }); 148 171 149 - view.webContents.loadURL(script.address); 172 + return rollup; 173 + }; 174 + 175 + const updateData = newData => { 176 + console.log('updateData', newData); 150 177 151 - const str = ` 152 - const s = "${script.selector}"; 153 - const r = document.querySelector(s); 154 - const value = r ? r.textContent : null; 155 - value; 156 - `; 178 + if (newData.prefs) { 179 + appStore.set('prefs', newData.prefs); 180 + } 157 181 158 - view.webContents.on('dom-ready', async () => { 159 - try { 160 - const r = await view.webContents.executeJavaScript(str); 161 - cb(r); 162 - } catch(ex) { 163 - console.error('cs exec error', ex); 164 - cb(null); 182 + Object.keys(newData).forEach(k => { 183 + if (features[k]) { 184 + features[k].onChange(newData[k]); 165 185 } 166 186 }); 167 187 }; 168 188 169 - let _intervals = []; 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 + }; 170 204 171 - const initScripts = scripts => { 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 - 178 - // debounce me somehow so not shooting em all off 179 - // at once every time app starts 180 - scripts.forEach(script => { 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'); 205 + const initPrefs = store => { 206 + const defaults = { 207 + globalKeyCmd: 'CommandOrControl+Escape', 208 + }; 208 209 209 - new Notification({ title, body }).show(); 210 - */ 211 - } 212 - }); 213 - }, script.interval); 214 - }); 215 - }; 210 + let prefs = appStore.get('prefs'); 211 + if (!prefs) { 212 + store.set('prefs', defaults); 213 + prefs = store.get('prefs'); 214 + } 216 215 217 - const initGlobalShortcuts = prefs => { 216 + // register global activation shortcut 218 217 if (globalShortcut.isRegistered(prefs.globalKeyCmd)) { 219 218 globalShortcut.unregister(prefs.globalKeyCmd); 220 219 } 221 220 222 - // register global activation shortcut 223 221 const onGlobalKeyCmd = () => getMainWindow().show(); 224 222 225 223 const ret = globalShortcut.register(prefs.globalKeyCmd, onGlobalKeyCmd); ··· 229 227 } 230 228 }; 231 229 232 - const showPeek = (peek) => { 233 - const height = peek.height || 600; 234 - const width = peek.width || 800; 235 - 236 - let win = null; 230 + const initFeatures = (features) => { 237 231 238 - const key = 'peek' + peek.keyNum; 232 + // TODO: allow features to register 233 + // as app level prefs for enable/disable 239 234 240 - if (_peekWins[key]) { 241 - console.log('peek', peek.keyNum, 'using stored window'); 242 - win = _peekWins[key]; 243 - win.show(); 244 - } 245 - else { 246 - console.log('peek', peek.keyNum, 'creating new window'); 247 - win = new BrowserWindow({ 248 - height, 249 - width, 250 - center: true, 251 - skipTaskbar: true, 252 - autoHideMenuBar: true, 253 - titleBarStyle: 'hidden', 254 - webPreferences: { 255 - preload: path.join(__dirname, 'preload.js'), 256 - // isolate content and do not persist it 257 - //partition: Date.now() 258 - } 259 - }); 260 - } 235 + // inject into features 236 + // eventually get to less tight coupling 237 + const api = { 238 + preloadPath, 239 + }; 261 240 262 - const onGoAway = () => { 263 - /* 264 - if (peek.keepLive) { 265 - _peekWins[key] = win; 266 - win.hide(); 267 - } 268 - else { 269 - win.destroy(); 270 - } 271 - */ 272 - win.destroy(); 273 - } 274 - win.on('blur', onGoAway); 275 - win.on('close', onGoAway); 241 + const featureContainerPrefix = 'peekFeature'; 276 242 277 - /* 278 - const str = ` 279 - window.addEventListener('keyup', e => { 280 - if (e.key == 'Escape') { 281 - console.log('peek script esc'); 282 - } 283 - }); 284 - 1; 285 - `; 243 + Object.keys(features).forEach(k => { 244 + const feature = features[k]; 245 + const storeName = `${featureContainerPrefix}${feature.labels.featureType}`; 286 246 287 - win.webContents.on('dom-ready', async () => { 288 - try { 289 - const r = await win.webContents.executeJavaScript(str); 290 - console.log(r); 291 - } catch(ex) { 292 - console.error('cs exec error', ex); 293 - } 294 - }); 295 - */ 296 - 297 - win.webContents.send('window', { 298 - path: path.join(__dirname), 299 - id: win.id, 300 - type: 'peek', 301 - data: peek 302 - }); 303 - 304 - win.loadURL(peek.address); 305 - }; 306 - 307 - const initPeeks = (cmdPrefix, peeks) => { 308 - peeks.forEach((p, i) => { 309 - if (globalShortcut.isRegistered(cmdPrefix + `${i}`)) { 310 - globalShortcut.unregister(cmdPrefix + `${i}`) 311 - } 312 - 313 - const ret = globalShortcut.register(cmdPrefix + `${i}`, () => { 314 - showPeek(p); 247 + // have to make per feature stores for now, pfftt 248 + // maybe fine, better isolation 249 + const featureStore = new Store({ 250 + name: storeName, 251 + // TODO: figure out schema approach here 252 + //schema: fullSchema, 253 + watch: true 315 254 }); 316 255 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 - } 256 + featureStore.onDidAnyChange(newData => { 257 + initData(); 258 + //win.webContents.send('configchange', {}); 465 259 }); 466 260 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); 523 - }); 524 - 525 - if (!ret) { 526 - console.error('Unable to register slide'); 527 - } 528 - } 261 + feature.init(api, featureStore); 529 262 }); 530 263 }; 531 264 532 265 // initialized all bits which need updating if the data changes 533 266 // can be called repeatedly to refresh on changes 534 - const initData = data => { 535 - // initialize prefs 536 - const prefs = data.prefs; 537 - initGlobalShortcuts(prefs); 267 + const initData = () => { 268 + // initialize app prefs 269 + initPrefs(appStore); 538 270 539 - // initialize peeks 540 - if (data.peeks.length > 0) { 541 - initPeeks(prefs.peekKeyPrefix, data.peeks); 542 - } 271 + initFeatures(features); 543 272 273 + /* 544 274 // initialize slides 545 275 if (data.slides.length > 0) { 546 276 initSlides(prefs.slideKeyPrefix, data.slides); ··· 550 280 if (data.scripts.length > 0) { 551 281 initScripts(data.scripts); 552 282 } 283 + */ 553 284 }; 554 285 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 + 555 296 // app load 556 297 const onReady = () => { 557 298 console.log('onReady'); 299 + 558 300 // create main app window on app start 559 301 const win = getMainWindow(); 560 - 561 - win.webContents.send('window', { 562 - path: path.join(__dirname), 563 - id: win.id, 564 - type: 'main', 565 - }); 566 - 567 - initData(data); 568 302 569 303 // keep app out of dock and tab switcher 570 304 if (app.dock) { ··· 573 307 574 308 initTray(); 575 309 576 - watch(newData => { 577 - initData(newData); 310 + initData(); 311 + 312 + appStore.onDidAnyChange(newData => { 313 + initData(); 578 314 win.webContents.send('configchange', {}); 579 315 }); 580 316 }; ··· 582 318 app.whenReady().then(onReady); 583 319 584 320 // when renderer is ready, send over user data 585 - ipcMain.on('getconfig', () => { 586 - getMainWindow().webContents.send('config', { 587 - data, 588 - schemas 589 - }); 321 + ipcMain.on('getconfig', (ev, data) => { 322 + getMainWindow().webContents.send('config', getData()) 590 323 }); 591 324 592 325 // listen for updates ··· 594 327 // TODO: if any shortcuts changed, unregister the old ones 595 328 596 329 // write to datastore 597 - set(newData); 330 + updateData(newData); 598 331 }); 599 332 600 333 // ipc ESC handler 601 334 ipcMain.on('esc', (event, title) => { 602 335 console.log('esc'); 603 - const win = getMainWindow(); 336 + 337 + const fwin = BrowserWindow.getFocusedWindow(); 338 + 604 339 // 605 - if (!win.isDestroyed()) { 340 + if (!fwin.isDestroyed()) { 606 341 console.log('esc: killingit'); 607 - win.close(); 608 - win.destroy(); 609 - _win = null; 342 + fwin.close(); 343 + //win.destroy(); 344 + //_win = null; 610 345 } 611 346 // 612 347 /* ··· 628 363 _win.destroy(); 629 364 _win = null; 630 365 } 631 - // 366 + /* 632 367 if (_win.isVisible()) { 633 368 console.log('win is visible, hide it'); 634 369 //_win.hide(); 635 370 } 636 - else if (process.platform !== 'darwin') { 371 + */ 372 + 373 + if (process.platform !== 'darwin') { 637 374 onQuit(); 638 375 } 639 376 });
+37 -19
preload.js
··· 13 13 } 14 14 }); 15 15 16 + 16 17 // all window types close on escape 17 18 window.addEventListener('keyup', e => { 18 19 if (e.key == 'Escape') { ··· 20 21 } 21 22 }); 22 23 23 - const handleMainWindow = () => { 24 - let api = {}; 24 + let api = {}; 25 25 26 - api.onConfigChange = callback => { 27 - ipcRenderer.on('configchange', (ev, msg) => { 28 - callback(msg); 29 - }); 30 - }; 26 + api.onConfigChange = callback => { 27 + // noop if not an internal app file 28 + const isMain = window.location.protocol == 'file:'; 29 + if (!isMain) { 30 + return; 31 + } 31 32 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'); 33 + ipcRenderer.on('configchange', (ev, msg) => { 34 + callback(msg); 38 35 }); 36 + }; 39 37 40 - api.setConfig = cfg => { 41 - //console.log('preload: setConfig', cfg); 42 - ipcRenderer.send('setconfig', cfg); 43 - }; 38 + api.getConfig = new Promise((resolve, reject) => { 39 + // noop if not an internal app file 40 + const isMain = window.location.protocol == 'file:'; 41 + if (!isMain) { 42 + return; 43 + } 44 44 45 - contextBridge.exposeInMainWorld('app', api); 45 + // TODO: race potential 46 + ipcRenderer.once('config', (ev, msg) => { 47 + resolve(msg); 48 + }); 49 + ipcRenderer.send('getconfig', {isMain}); 50 + }); 46 51 47 - window.addEventListener('DOMContentLoaded', () => { 52 + api.setConfig = cfg => { 53 + // noop if not an internal app file 54 + const isMain = window.location.protocol == 'file:'; 55 + if (!isMain) { 56 + return; 57 + } 58 + 59 + ipcRenderer.send('setconfig', cfg); 60 + }; 61 + 62 + contextBridge.exposeInMainWorld('app', api); 63 + 64 + const handleMainWindow = () => { 65 + window.addEventListener('load', () => { 48 66 const replaceText = (selector, text) => { 49 67 const element = document.getElementById(selector) 50 68 if (element) element.innerText = text
+50 -44
renderer.js
··· 5 5 prefs: { 6 6 paneTitle: 'Preferences', 7 7 globalKeyCmd: 'App activation shortcut', 8 - peekKeyPrefix: 'Peek shortcut prefix', 9 - slideKeyPrefix: 'Slide shortcut prefix', 10 8 }, 9 + /* 11 10 peeks: { 12 11 paneTitle: 'Peeks', 13 12 type: 'Peek', ··· 24 23 addBtn: 'Add', 25 24 delBtn: 'Delete', 26 25 } 26 + */ 27 27 }; 28 28 29 29 // TODO: capture and internally navigate out of panes ··· 48 48 console.log('renderer: init'); 49 49 console.log('renderer: cfg', cfg); 50 50 51 - let { data, schemas } = cfg; 51 + let { prefs, features } = cfg; 52 + 52 53 const containerEl = document.querySelector('.houseofpane'); 53 54 54 55 // blow away panes if this is an update ··· 59 60 panes = []; 60 61 } 61 62 62 - // janky but hey it all is soooo 63 - const initPane = (type) => { 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; 64 79 const el = containerEl.querySelector('.' + type); 65 80 66 - const initPane = type == 'prefs' ? initValuesPane : initListPane; 67 - 68 - const allowNew = type == 'scripts' || false; 81 + const allowNew = feature.config.allowNew || false; 82 + const disabled = feature.config.disabled || []; 69 83 84 + /* 70 85 const disabled = { 71 - prefs: [], 72 86 scripts: ['previousValue'], 73 - peeks: ['keyNum'], 74 87 slides: ['screenEdge'], 75 88 }; 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 - }; 89 + */ 86 90 87 91 const onChange = newData => { 88 - data[type] = newData; 89 - updateToMain(data); 92 + const p = {}; 93 + p[type] = { items: newData }; 94 + updateToMain(p); 90 95 }; 91 96 92 - const pane = initPane( 97 + const pane = initFeaturePane( 93 98 el, 94 - labels[type], 95 - schemas[type], 96 - data[type], 97 - onChange, 98 - allowNew, 99 - disabled[type], 100 - labelMaker 99 + feature, 100 + onChange 101 101 ); 102 102 103 103 panes.push({ 104 104 el, 105 105 pane 106 106 }); 107 - }; 108 - 109 - ['prefs', 'peeks', 'slides', 'scripts'].forEach(initPane); 107 + }); 110 108 }; 111 109 112 110 const fillPaneFromSchema = (pane, labels, schema, data, onChange, disabled) => { ··· 135 133 } 136 134 137 135 params[k] = v; 136 + 138 137 const input = pane.addInput(params, k, opts); 139 138 // TODO: consider inline state management 140 139 input.on('change', ev => { ··· 179 178 return val; 180 179 }; 181 180 182 - const initValuesPane = (container, labels, schema, values, onChange, allowNew, disabled, labelMaker) => { 181 + const initValuesPane = (container, labels, schema, values, onChange) => { 183 182 const pane = new Tweakpane.Pane({ 184 183 container: container, 185 184 title: labels.paneTitle 186 185 }); 187 186 188 - fillPaneFromSchema(pane, labels, schema, values, onChange, disabled); 187 + fillPaneFromSchema(pane, labels, schema, values, onChange, []); 189 188 190 189 const update = (ev) => { 191 190 // TODO: this won't work forever ··· 201 200 return pane; 202 201 }; 203 202 204 - const initListPane = (container, labels, schema, items, onChange, allowNew, disabled, labelMaker) => { 203 + const initFeaturePane = (container, feature, onChange) => { 204 + const { config, labels, schemas, data } = feature; 205 + 205 206 const pane = new Tweakpane.Pane({ 206 207 container: container, 207 - title: labels.paneTitle 208 + title: labels.featureDisplay 208 209 }); 209 210 210 211 const update = (all) => { ··· 216 217 onChange(newData); 217 218 }; 218 219 219 - items.forEach(entry => { 220 + // add prefs 221 + 222 + // add items 223 + data.items.forEach(item => { 220 224 const folder = pane.addFolder({ 221 - title: labelMaker(entry), 225 + title: item.title, 222 226 expanded: false 223 227 }); 224 228 225 - fillPaneFromSchema(folder, labels, schema, entry, onChange, disabled); 229 + fillPaneFromSchema(folder, labels, schemas.item, item, onChange, config.disabled); 226 230 227 231 // TODO: implement 228 - folder.addButton({title: labels.testBtn}); 232 + //folder.addButton({title: labels.testBtn}); 229 233 230 - if (allowNew) { 234 + if (config.allowNew) { 231 235 const delBtn = folder.addButton({title: labels.delBtn}); 232 236 delBtn.on('click', () => { 233 237 pane.remove(folder); ··· 236 240 }); 237 241 } 238 242 239 - folder.on('change', () => update()); 243 + folder.on('change', () => update(!config.allowNew)); 240 244 }); 241 245 242 - if (allowNew) { 246 + /* 247 + if (config.allowNew) { 243 248 // add new item entry 244 249 const folder = pane.addFolder({ 245 250 title: labels.newFolder, ··· 256 261 update(true); 257 262 }); 258 263 } 264 + */ 259 265 260 266 return pane; 261 267 };
-5
scripts.js
··· 1 - // Manage content script execution 2 - 3 - const manager = { 4 - 5 - };