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 #12 from autonome/groups

Groups

authored by

Dietrich Ayala and committed by
GitHub
b068cc6b a31cb607

+748 -28
+93 -23
README.md
··· 9 9 10 10 <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"> 11 11 12 + ## Design 13 + 14 + Many user tasks on the web are either transient, chained or persistent, data oriented, or some mix of those. The document-oriented web does not meet those needs. Major browser vendors can't meet those needs well, for many reasons. 15 + 16 + * transient 17 + * chained 18 + * persistent 19 + * data oriented 20 + 21 + About this space: 22 + * Embrace the app-ness aspect of the web platform, less about the document-ness 23 + * Javascript is ok here 24 + * Decouple html+js+css from http+dns+ssl - not entirely, but that trust+security model is not a required starting point 25 + 12 26 ## Features 13 27 14 - You can use Peek in three ways, with more coming: 28 + You can use Peek in a few ways, with more coming: 15 29 16 30 * Peeks - Keyboard activated modal chromeless web pages 17 31 * Slides - Keyboard or gesture activated modal chromeless web pages which slide in from any screen edges 18 32 * Scripts - Scripts periodically executed against a web page in the background which extract data and notify on changes 33 + 34 + In progress: 35 + * Commands 36 + * Groups 19 37 20 38 ### Peeks 21 39 ··· 93 111 94 112 ## Roadmap 95 113 96 - Next 114 + Core moduluarization 97 115 * ✅ Modularize feature types, eyeing the extensibility model 98 - * Settings Cleanup 99 - * ✅ move settings window to features/settings 100 - * App cleanup 101 - * ✅ main window vs settings 102 - * ✅ change settings shortcut from global+esc to app+comma 103 - * Features cleanup 104 - * make features enable/disable-able 105 - * enable/disable individual frozen items - slides, peeks 106 - * enable/disable individual non-frozen items - scripts 107 - * re-enable label previews, eg "Peek {key} - {address}" 108 - * Window lifecycle 109 - * ✅modularize window open/close + hidden/visible 110 - * ✅update settings, peeks, slides, scripts 111 - * hide/show window vs create fresh 112 - * modularize window close/hide/show across all windows 113 - * update slides / animation 114 - * isolate web loading code, have features load content that way instead of raw BrowserWindow 115 - * ✅ Basic command bar to open pages 116 - * Basic history store 116 + * ✅ move settings window to features/settings 117 + 118 + App cleanup 119 + * ✅ main window vs settings 120 + * ✅ change settings shortcut from global+esc to opt+comma 121 + 122 + Window lifecycle 123 + * ✅modularize window open/close + hidden/visible 124 + * ✅update settings, peeks, slides, scripts 125 + * hide/show window vs create fresh 126 + * modularize window close/hide/show across all windows 127 + * move animation to main, and update slides impl to use it 128 + * window settings persistence 129 + 130 + Core/Basic 131 + * ✅ basic command bar to open pages 132 + * schema migration 133 + * basic history storage 134 + * app updates 135 + * don't blow away and re-init everything on any change 136 + * fix setting layout wrapping issue 137 + * enable/disable individual slides, peeks 138 + * enable/disable individual scripts 139 + 140 + -> mvp 141 + 142 + ------- 143 + 144 + Features cleanup 145 + * enable/disable whole features 117 146 * move feature list and enablement to storage 118 - * configurable default feature to open on app open 147 + * re-enable label previews, eg "Peek {key} - {address}" 148 + * configurable default feature to load on app open 149 + 150 + Web Platform 151 + * need a web loader that's not full BrowserWindow? 152 + * sandboxing 153 + * blocklist 119 154 120 155 After that 121 156 * Extension model? ··· 158 193 159 194 ## History 160 195 196 + In working on Firefox and related things at Mozilla from 2006 - 2019, there were a few specific initiatives which best aligned with my needs as a user on the web: 197 + 198 + * The Awesomebar: infinite history + personalized local search index 199 + * Ubiquity: Natural language commands + chaining 200 + * Jetpack: The Mozilla Labs version - web-platfrom-centric extensibility 201 + * Panorama: née TabCandy, web pages as groups instead of tabs in windows 202 + 203 + A few others which were in the right direction but didn't achieve their optimal form: 204 + 205 + * Greasemonkey 206 + * Microsummaries 207 + * Contacts extension 208 + 209 + The first version of the Peek application has some bits of each of these, and the original Peek browser extension. 210 + 211 + ### Peek browser extension 212 + 161 213 Peek was a browser extension that let you quickly peek at your favorite web pages without breaking your flow - loading pages mapped to keyboard shortcuts into a modal window with no controls, closable via the `Escape` key. 162 214 163 - However, as browser extension APIs become increasingly limited, it was not possible to create a decent user experience and I abandoned it. You can access the extension in this repo [in the extension directory](/autonome/peek/extension/). 215 + However, as browser extension APIs became increasingly limited, it was not possible to create a decent user experience and I abandoned it. You can access the extension in this repo [in the extension directory](/autonome/peek/extension/). 164 216 165 217 The only way to create the ideal user experience for a web user agent that *Does What I Want* is to make it a browser-ish application, and that's what Peek is now. 166 218 219 + 220 + 221 + ## Testcase: Authoring Flows 222 + 223 + * author web content 224 + * pull in bits from the web 225 + * share preview for feedback 226 + * publish (or at least get output) 227 + 228 + writing the recap of the web track at ipfs thing 2023 229 + 230 + - make a new markdown doc 231 + - sections titled for each video title 232 + - each video's embode code in each section 233 + - navigate around the document for review and updates 234 + - need to easily preview rendered content 235 + - share preview link 236 + - publish somewhere
+208
features/groups/groups.js
··· 1 + // groups/groups.js 2 + (async () => { 3 + 4 + console.log('groups/groups'); 5 + 6 + const labels = { 7 + featureType: 'groups', 8 + featureDisplay: 'Groups', 9 + prefs: { 10 + shortcutKey: 'Groups shortcut', 11 + } 12 + }; 13 + 14 + const { 15 + BrowserWindow, 16 + globalShortcut, 17 + } = require('electron'); 18 + 19 + const path = require('path'); 20 + 21 + let _store = null; 22 + let _data = {}; 23 + 24 + const prefsSchema = { 25 + "$schema": "https://json-schema.org/draft/2020-12/schema", 26 + "$id": "peek.groups.prefs.schema.json", 27 + "title": "Groups preferences", 28 + "description": "Peek app Groups user preferences", 29 + "type": "object", 30 + "properties": { 31 + "shortcutKey": { 32 + "description": "Global OS hotkey to open command panel", 33 + "type": "string", 34 + "default": "Option+Space" 35 + }, 36 + }, 37 + "required": [ "shortcutKey"] 38 + }; 39 + 40 + /* 41 + const itemSchema = { 42 + "$schema": "https://json-schema.org/draft/2020-12/schema", 43 + "$id": "peek.groups.entry.schema.json", 44 + "title": "Peek - command entry", 45 + "description": "Peek command entry", 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/entry" } 98 + }; 99 + */ 100 + 101 + const schemas = { 102 + prefs: prefsSchema, 103 + //item: itemSchema, 104 + //items: listSchema 105 + }; 106 + 107 + const _defaults = { 108 + prefs: { 109 + shortcutKey: 'Option+g' 110 + }, 111 + }; 112 + 113 + const openGroupsWindow = (api) => { 114 + const height = 600; 115 + const width = 800; 116 + 117 + const params = { 118 + type: labels.featureType, 119 + file: 'features/groups/home.html', 120 + height, 121 + width 122 + }; 123 + 124 + _api.openWindow(params); 125 + }; 126 + 127 + const initStore = (store, data) => { 128 + const sp = store.get('prefs'); 129 + if (!sp) { 130 + store.set('prefs', data.prefs); 131 + } 132 + }; 133 + 134 + const initShortcut = (api, prefs) => { 135 + const shortcut = prefs.shortcutKey; 136 + 137 + if (globalShortcut.isRegistered(shortcut)) { 138 + globalShortcut.unregister(shortcut); 139 + } 140 + 141 + const ret = globalShortcut.register(shortcut, () => { 142 + openGroupsWindow(api); 143 + }); 144 + 145 + if (!ret) { 146 + console.error('Unable to register shortcut', shortcut); 147 + } 148 + }; 149 + 150 + const init = (api, store) => { 151 + _store = store; 152 + _api = api; 153 + 154 + initStore(_store, _defaults); 155 + 156 + 157 + _data = { 158 + get prefs() { return _store.get('prefs'); }, 159 + //get items() { return _store.get('items'); }, 160 + }; 161 + 162 + initShortcut(api, _data.prefs); 163 + }; 164 + 165 + const onChange = (changed, old) => { 166 + console.log(labels.featureType, 'onChange', changed); 167 + 168 + // TODO only update store if changed 169 + // and re-init 170 + if (changed.prefs) { 171 + console.log('groups: updating prefs', changed.prefs); 172 + _store.set('prefs', changed.prefs); 173 + } 174 + 175 + if (changed.items) { 176 + _store.set('items', changed.items); 177 + } 178 + }; 179 + 180 + const onMessage = msg => { 181 + console.log('groups:onMessage', msg) 182 + if (msg.command == 'openWindow') { 183 + _api.openWindow(msg); 184 + } 185 + }; 186 + 187 + // ui config 188 + const config = { 189 + // allow user to create new items 190 + allowNew: false, 191 + // fields that are view only 192 + disabled: ['keyNum'], 193 + }; 194 + 195 + module.exports = { 196 + init: init, 197 + config, 198 + labels, 199 + schemas, 200 + data: { 201 + get prefs() { return _store.get('prefs'); }, 202 + }, 203 + onChange, 204 + onMessage: onMessage 205 + }; 206 + 207 + 208 + })();
+93
features/groups/home.css
··· 1 + .controls { 2 + } 3 + 4 + .controls.tabsview { 5 + display: none; 6 + } 7 + 8 + html { 9 + background: #f5f7f8; 10 + font-family: 'Roboto', sans-serif; 11 + -webkit-font-smoothing: antialiased; 12 + padding: 20px 0; 13 + } 14 + 15 + .cards { 16 + width: 90%; 17 + max-width: 1240px; 18 + margin: 0 auto; 19 + 20 + display: grid; 21 + 22 + grid-template-columns: 1fr; 23 + grid-template-rows: auto; 24 + grid-gap: 20px; 25 + } 26 + 27 + @media only screen and (min-width: 500px) { 28 + .cards { 29 + grid-template-columns: 1fr 1fr; 30 + } 31 + } 32 + 33 + @media only screen and (min-width: 850px) { 34 + .cards { 35 + grid-template-columns: 1fr 1fr 1fr 1fr; 36 + } 37 + } 38 + 39 + /* card */ 40 + 41 + .card { 42 + min-height: 100%; 43 + background: white; 44 + box-shadow: 0 2px 5px rgba(0,0,0,0.1); 45 + display: flex; 46 + flex-direction: column; 47 + text-decoration: none; 48 + color: #444; 49 + position: relative; 50 + top: 0; 51 + transition: all .1s ease-in; 52 + } 53 + 54 + .card:hover { 55 + top: -2px; 56 + box-shadow: 0 4px 5px rgba(0,0,0,0.2); 57 + } 58 + 59 + .card article { 60 + padding: 20px; 61 + display: flex; 62 + 63 + flex: 1; 64 + justify-content: space-between; 65 + flex-direction: column; 66 + 67 + } 68 + .card .thumb { 69 + padding-bottom: 60%; 70 + background-size: cover; 71 + background-position: center center; 72 + } 73 + 74 + .card p { 75 + flex: 1; /* make p grow to fill available space*/ 76 + line-height: 1.4; 77 + } 78 + 79 + /* typography */ 80 + h1 { 81 + font-size: 20px; 82 + margin: 0; 83 + color: #333; 84 + } 85 + 86 + .card span { 87 + font-size: 12px; 88 + font-weight: bold; 89 + color: #999; 90 + text-transform: uppercase; 91 + letter-spacing: .05em; 92 + margin: 2em 0 0 0; 93 + }
+34
features/groups/home.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 6 + 7 + <title>ESCAPE</title> 8 + 9 + <script src="home.js"></script> 10 + 11 + <link rel="stylesheet" type="text/css" href="home.css"> 12 + 13 + </head> 14 + <body> 15 + 16 + <div class="controls tabsview"> 17 + <button class="newgroup">[+] new group</button> 18 + </div> 19 + 20 + <div class="cards"> 21 + </div> 22 + 23 + <template class="tpl-card"> 24 + <div class="card"> 25 + <div class="thumb" style="background-image: url();"></div> 26 + <article> 27 + <h1></h1> 28 + <span></span> 29 + </article> 30 + </div> 31 + </template> 32 + 33 + </body> 34 + </html>
+274
features/groups/home.js
··· 1 + /* 2 + 3 + groups 4 + * there's always a default group 5 + 6 + managing storage 7 + * scaling to lots of pages/groups 8 + * collect all on startup 9 + * manage cache 10 + * what can change? 11 + * new page in group 12 + * page changes between groups 13 + * page closed 14 + * undo close page to a group that no longer exists (use current group) 15 + 16 + relevant page events 17 + * new page 18 + * add to current group 19 + * activate (and switch active group) 20 + * close page 21 + 22 + */ 23 + 24 + (async () => { 25 + 26 + // TODO: make extensible 27 + const VIEW_GROUPS = 1; 28 + const VIEW_TABS = 2; 29 + 30 + // keys for extension-level data 31 + const EXT_DATA_CONFIG_KEY = 'config'; 32 + 33 + // keys for per-tab data 34 + const TAB_DATA_GROUP_KEY = 'groupId'; 35 + 36 + // default strings 37 + // TODO: move to i18n 38 + const DEFAULT_GROUP_TITLE = 'Default Group'; 39 + const DEFAULT_NEW_GROUP_TITLE = 'New Group'; 40 + 41 + // Handle ESC 42 + document.onkeydown = function(evt) { 43 + evt = evt || window.event; 44 + var isEscape = evt.key == 'Escape'; 45 + if (isEscape && currentView == VIEW_TABS) { 46 + showGroups(); 47 + } 48 + }; 49 + 50 + var currentView = null; 51 + 52 + var config = null; 53 + 54 + const init = () => { 55 + // Initialize storage - this loads groups, everything else 56 + initStorage(); 57 + 58 + // Data loaded, start building UI 59 + populateUI(); 60 + 61 + // New group click handler 62 + document.querySelector('.newgroup').addEventListener('click', function() { 63 + newGroup(); 64 + }); 65 + 66 + // Listen for things that'll change state 67 + initEventListeners(); 68 + 69 + // Populate groups with their pages 70 + showCards(); 71 + }; 72 + 73 + const initStorage = () => { 74 + let data = localStorage.getItem(EXT_DATA_CONFIG_KEY); 75 + 76 + console.log('initStorage', data); 77 + 78 + // Not first run! 79 + if (data && data.config) { 80 + config = data.config; 81 + } 82 + else { 83 + // First run! 84 + 85 + config = {}; 86 + 87 + // Set up group storage 88 + config.groups = {}; 89 + 90 + // Create default group 91 + const id = newGroup(DEFAULT_GROUP_TITLE); 92 + 93 + // Storage default group id as last active group 94 + config.lastGroupId = id; 95 + 96 + // save on first run 97 + updateStorage(EXT_DATA_CONFIG_KEY, config); 98 + } 99 + 100 + return config; 101 + } 102 + 103 + // Save changes to config to persistent storage 104 + // TODO: Temporary hack. Replace with proper evented solution. 105 + function updateStorage(key, data) { 106 + localStorage.setItem(key, data); 107 + } 108 + 109 + function initEventListeners() { 110 + /* 111 + // add new tabs in this window to current group 112 + browser.tabs.onCreated.addListener(tab => { 113 + addCardToGroup(tab.id, config.lastGroupId); 114 + }); 115 + 116 + // remove detached tabs from their group 117 + browser.tabs.onDetached.addListener(tab => { 118 + removeCardFromGroup(tab.id, config.lastGroupId); 119 + }); 120 + 121 + // remove removed tabs from their group 122 + browser.tabs.onRemoved.addListener(tab => { 123 + removeCardFromGroup(tab.id, config.lastGroupId); 124 + }); 125 + */ 126 + } 127 + 128 + function addCardToGroup(pageId, groupId) { 129 + const index = config.groups[groupId].pages.indexOf(pageId); 130 + if (index == -1) { 131 + config.groups[groupId].pages.push(pageId); 132 + } 133 + } 134 + 135 + function removeCardFromGroup(pageId, groupId) { 136 + var index = config.groups[groupId].tabs.indexOf(pageId); 137 + if (index > -1) { 138 + config.groups[groupId].tabs.splice(index, 1); 139 + } 140 + } 141 + 142 + function getGroupById(id) { 143 + return config.groups[id]; 144 + } 145 + 146 + function activateGroup(groupId) { 147 + var group = getGroupById(groupId); 148 + var lastGroup = getGroupById(config.lastGroupId); 149 + if (group.tabs.length === 0) { 150 + // New group? 151 + browser.tabs.create({}).then(tab => { 152 + addCardToGroup(tab.id, groupId); 153 + browser.tabshideshow.show(group.tabs); 154 + browser.tabshideshow.hide(lastGroup.tabs); 155 + setLastActiveGroupId(groupId); 156 + }); 157 + } 158 + else { 159 + browser.tabshideshow.show(group.tabs); 160 + browser.tabshideshow.hide(lastGroup.tabs); 161 + setLastActiveGroupId(groupId); 162 + } 163 + } 164 + 165 + function getLastActiveGroupId() { 166 + return config.lastGroupId; 167 + } 168 + 169 + function setLastActiveGroupId(id) { 170 + config.lastGroupId = id; 171 + } 172 + 173 + function initializeGroupData() { 174 + return new Promise(function(resolve, reject) { 175 + // Clear out old tab ids 176 + for (let id in config.groups) { 177 + config.groups[id].tabs = []; 178 + } 179 + 180 + browser.tabs.query({currentWindow: true}).then(tabs => { 181 + tabs.forEach(tab => { 182 + browser.sessions.getCardValue(tab.id, TAB_DATA_GROUP_KEY).then(groupId => { 183 + if (groupId && config.groups[groupId]) { 184 + config.groups[groupId].tabs.push(tab.id); 185 + } 186 + else { 187 + // This should only happen on first run. 188 + // Add all default tabs to default group. 189 + var groupId = getLastActiveGroupId(); 190 + addCardToGroup(tab.id, groupId); 191 + } 192 + }); 193 + }); 194 + resolve(); 195 + }); 196 + }); 197 + } 198 + 199 + function newGroup(title) { 200 + var id = window.crypto.getRandomValues(new Uint32Array(1))[0]; 201 + var group = { 202 + id: id, 203 + title: title || 'New Group', 204 + tabs: [] 205 + }; 206 + config.groups[id] = group; 207 + showGroups(); 208 + updateStorage(); 209 + return id; 210 + } 211 + 212 + function showGroups() { 213 + clearCards(); 214 + for (let id in config.groups) { 215 + var group = config.groups[id]; 216 + var card = addCard(); 217 + card.querySelector('h1').innerText = group.title; 218 + card.dataset.id = group.id; 219 + card.addEventListener('click', e => { 220 + var groupId = parseInt(card.dataset.id); 221 + if (groupId != config.lastGroupId) { 222 + activateGroup(groupId); 223 + } 224 + }); 225 + card.classList.add('group'); 226 + } 227 + currentView = VIEW_GROUPS; 228 + document.querySelector('.controls').classList.add('groupsview'); 229 + document.querySelector('.controls').classList.remove('tabsview'); 230 + } 231 + 232 + function showCards() { 233 + var group = getGroupById(config.lastGroupId); 234 + clearCards(); 235 + 236 + group.pages.forEach(page => { 237 + var card = addCard(); 238 + card.querySelector('h1').innerText = page.title; 239 + card.dataset.id = page.id; 240 + card.addEventListener('click', e => { 241 + var pageId = parseInt(card.dataset.id); 242 + browser.pages.update(pageId, { active: true }); 243 + }); 244 + }); 245 + 246 + currentView = VIEW_PAGES; 247 + document.querySelector('.controls').classList.add('pagesview'); 248 + document.querySelector('.controls').classList.remove('groupsview'); 249 + } 250 + 251 + function clearCards() { 252 + var container = document.querySelector('.cards'); 253 + Array.prototype.slice.call(container.children).forEach(child => { 254 + child.parentNode.removeChild(child); 255 + }); 256 + } 257 + 258 + function addCard() { 259 + var container = document.querySelector('.cards'); 260 + 261 + var cardTpl = document.querySelector('.tpl-card') 262 + var cardClone = document.importNode(cardTpl.content, true); 263 + container.appendChild(cardClone); 264 + var card = container.lastElementChild; 265 + card.classList.add('card'); 266 + 267 + return card; 268 + } 269 + 270 + // Kick out the jams 271 + document.addEventListener('DOMContentLoaded', init); 272 + 273 + 274 + })();
+2 -1
features/peeks/peeks.js
··· 135 135 type: labels.featureType, 136 136 address: item.address, 137 137 height, 138 - width 138 + width, 139 + persistKey: item.keepLive ? 'peek:' + item.keyNum : undefined 139 140 }; 140 141 141 142 _api.openWindow(params);
+44 -4
index.js
··· 66 66 slides: require('./features/slides/slides'), 67 67 peeks: require('./features/peeks/peeks'), 68 68 scripts: require('./features/scripts/scripts'), 69 + groups: require('./features/groups/groups'), 69 70 }; 70 71 71 72 const labels = { ··· 155 156 // inject into features 156 157 // eventually get to less tight coupling 157 158 const api = { 159 + debug: DEBUG, 158 160 preloadPath, 159 161 openWindow 160 162 }; ··· 257 259 console.log('esc'); 258 260 259 261 const fwin = BrowserWindow.getFocusedWindow(); 260 - 261 - if (!fwin.isDestroyed()) { 262 + const entry = windowCache.byId(fwin.id); 263 + if (entry) { 264 + BrowserWindow.fromId(entry.id).hide(); 265 + } 266 + else if (!fwin.isDestroyed()) { 262 267 fwin.close(); 263 268 } 264 269 }); 265 270 271 + const windowCache = { 272 + cache: [], 273 + add: entry => windowCache.cache.push(entry), 274 + byId: id => windowCache.cache.find(w => w.id == id), 275 + byKey: key => windowCache.cache.find(w => w.key == key) 276 + }; 266 277 267 278 const openWindow = (params) => { 268 - console.log('creating new window', params); 279 + if (params.persistKey) { 280 + console.log('openWindow(): key', params.persistKey) 281 + const entry = windowCache.byKey(params.persistKey); 282 + if (entry != undefined) { 283 + const win = BrowserWindow.fromId(entry.id); 284 + if (win) { 285 + console.log('openWindow(): opening persistent window for', params.persistKey) 286 + win.show(); 287 + return; 288 + } 289 + } 290 + } 291 + 292 + console.log('openWindow(): creating new window', params); 269 293 270 294 const height = params.height || 600; 271 295 const width = params.width || 800; ··· 286 310 } 287 311 }); 288 312 313 + // if persisting window, cache caller's key and window id 314 + if (params.persistKey) { 315 + windowCache.add({ 316 + id: win.id, 317 + key: params.persistKey 318 + }); 319 + } 320 + 289 321 // TODO: make configurable 290 322 const onGoAway = () => { 291 - win.destroy(); 323 + if (params.persistKey) { 324 + win.hide(); 325 + } 326 + else { 327 + win.destroy(); 328 + } 292 329 } 293 330 win.on('blur', onGoAway); 294 331 win.on('close', onGoAway); ··· 342 379 343 380 const onQuit = () => { 344 381 console.log('onquit'); 382 + 345 383 // Unregister all shortcuts on app close 346 384 globalShortcut.unregisterAll(); 385 + 386 + // Close all persisent windows 347 387 348 388 app.quit(); 349 389 };