diff --git a/app/BTAppNode.js b/app/BTAppNode.js index e4a9cbd..c50e222 100644 --- a/app/BTAppNode.js +++ b/app/BTAppNode.js @@ -253,6 +253,17 @@ class BTAppNode extends BTNode { }); } + storeFavicon() { + // store favicon in browser local storage + try { + const host = this.URL.split(/[?#]/)[0]; // strip off any query or hash + localFileManager.set(host, this.faviconUrl); + } + catch (e) { + console.warn(`Error storing favicon: ${e}`); + } + } + async populateFavicon() { // add favicon icon either from local storage or goog if (this.isTopic() || !this.URL) return; @@ -527,7 +538,7 @@ class BTAppNode extends BTNode { if (n.hasOpenChildren()) { const openTabIds = n.childIds.flatMap( c => AllNodes[c].tabId ? [AllNodes[c].tabId] :[]); - window.postMessage({'function': 'groupAll', 'groupName': n.displayTag, + window.postMessage({'function': 'moveOpenTabsToTG', 'groupName': n.displayTag, 'tabIds': openTabIds, 'windowId': n.windowId}); } }); @@ -936,19 +947,20 @@ class BTLinkNode extends BTAppNode { const Handlers = { "launchApp": launchApp, // Kick the whole thing off "loadBookmarks": loadBookmarks, - "tabActivated": tabActivated, // User nav to Existing tab - "tabGrouped": tabGrouped, //!!!reconcile with tabJOinedTG - "tabJoinedTG" : tabJoinedTG, // a tab was dragged into a TG + "tabActivated": tabActivated, // User nav to Existing tab + "tabJoinedTG" : tabJoinedTG, // a tab was dragged or moved into a TG "tabLeftTG" : tabLeftTG, // a tab was dragged out of a TG "tabNavigated": tabNavigated, // User navigated a tab to a new url "tabOpened" : tabOpened, // New tab opened "tabMoved" : tabMoved, // user moved a tab - "tabPositioned": tabPositioned // tab moved by extension + "tabPositioned": tabPositioned, // tab moved by extension "tabClosed" : tabClosed, // tab closed - "storeTabs": storeTabs, // popup store operation - page, window or session - "importSession": importSession, //!!!reconcile with storeTabs + "saveTabs": saveTabs, // popup save operation - page, tg, window or session "tabGroupCreated": tabGroupCreated, "tabGroupUpdated": tabGroupUpdated, + //"tabGrouped": tabGrouped, // merged with tabJOinedTG + //"storeTabs": storeTabs, // replaced by saveTabs. popup store operation - page, window or session + //"importSession": importSession, // erplaced by saveTabs. }; // Set handler for extension messaging diff --git a/app/bt.js b/app/bt.js index c860735..d4bf779 100644 --- a/app/bt.js +++ b/app/bt.js @@ -738,6 +738,68 @@ function tabClosed(data) { } +function saveTabs(data) { + // iterate thru array of tabsData and save each to BT + // data is of the form: {'function': 'saveTabs', 'saveType':Tab|TG|Window|Session, 'tabs': [], 'note': msg.note, 'close': msg.close} + // tabs: [{'tabId': t.id, 'groupId': t.groupId, 'windowId': t.windowId, 'url': t.url, 'topic': topic, 'title': msg.title, favIconUrl: t.favIconUrl}] + // topic is potentially topic-dn:window##:TGName:TODO + console.log('saveTabs: ', data); + const note = data.note; + const close = data.close; + + // Iterate tabs and create btappNodes as needed + const changedTopicNodes = new Set(); + data.tabs.reverse().forEach(tab=> { + const existingNode = BTAppNode.findFromTab(tab.tabId); + if (existingNode) { + if (note) { + existingNode.text = note; + existingNode.redisplay(); + } + return; // already saved, ignore other than making any note update + } + const [topicDN, keyword] = BTNode.processTopicString(tab.topic || "📝 Scratch"); + const topicNode = BTAppNode.findOrCreateFromTopicDN(topicDN); + changedTopicNodes.add(topicNode); + const title = cleanTitle(tab.title); // get rid of unprintable characters etc + // create and populate node + const node = new BTAppNode(`[[${tab.url}][${title}]]`, topicNode.id, note || "", topicNode.level + 1); + node.tabId = tab.tabId; node.windowId = tab.windowId; + topicNode.windowId = tab.windowId; + if (tab.groupId > 0) { // groupid = -1 if not set + node.tabGroupId = tab.groupId; + topicNode.tabGroupId = tab.groupId; + } + node.faviconUrl = tab.favIconUrl; node.tabIndex = tab.tabIndex; + if (keyword) node.keyword = keyword; + if (note) node.text = note; + + // handle display aspects of single node + $("table.treetable").treetable("loadBranch", topicNode.getTTNode(), node.HTML()); + if (close) node.closeTab(); else setNodeOpen(node); // save and close popup operation + node.storeFavicon(); node.populateFavicon(); + MRUTopicPerWindow[node.windowId] = topicDN; // track mru topic per window for popup population + }); + + // update subtree of each changed topic node + changedTopicNodes.forEach(node => { + if (['TG', 'Session'].includes(data.saveType)) node.childIds.reverse(); // reverse to get back to order of insertion when whole tg added + node.redisplay(); + node.groupAndPosition(); + }); + + // update topic list, sync extension, reset ui and save changes. + BTAppNode.generateTags(); + let lastTopic = Array.from(changedTopicNodes).pop(); + window.postMessage({'function': 'localStore', + 'data': { 'tags': Tags, 'mruTopics': MRUTopicPerWindow, 'currentTag': lastTopic, 'currentText': note}}); + window.postMessage({'function' : 'brainZoom', 'tabId' : data.tabs[0].tabId}); + + initializeUI(); + saveBT(); +} + +/* // StoreTabs and importSession replaced by more generic saveTabs above function storeTabs(data) { // put tab(s) under storage w given topic. tabsData is a list, could be one or all tabs in window // NB topicString may be topic, topic:hierarchy:nodes etc and may have a terminating :TODO @@ -833,6 +895,78 @@ function storeTabs(data) { } } + +function importSession(msg) { + // handler for session sent via msg {winID: {tabs[], tgs:{tabs[]}}} + + const wins = msg.windows; + const dateString = getDateString().replace(':', '∷'); // 12:15 => :15 is a sub topic + const sessionName = "Session Import (" + dateString + ")"; + const parentName = msg.topic || "📝 Scratch"; + const parent = BTAppNode.findOrCreateFromTopicDN(parentName); + const sessionNode = new BTAppNode(sessionName, parent.id, "", parent.level + 1); + const closeP = msg.close; // close tabs after saving? + const newTabNodes = []; + + Object.values(wins).forEach(win => { + const wNode = new BTAppNode(win.windowName, sessionNode.id, "", sessionNode.level + 1); + wNode.windowId = win.windowId; + win.tabs.reverse().forEach(tab => { + // Find or create node for tab + let tabNode = BTAppNode.findFromTab(tab.id); + if (!tabNode) { + tabNode = new BTAppNode(`[[${tab.url}][${tab.title}]]`, wNode.id, "", wNode.level + 1); + tabNode.createDisplayNode(); + } + newTabNodes.push(tabNode); + tabNode.windowId = tab.windowId; + tabNode.tabId = tab.id; + tabNode.tabIndex = tab.tabIndex; + tabNode.faviconUrl = tab.faviconUrl; + setNodeOpen(tabNode); + if (closeP) tabNode.closeTab(); + }); + Object.values(win.tabGroups).reverse().forEach(tg => { + if (BTAppNode.findFromGroup(tg.id)) return; // already managed + const tgNode = new BTAppNode(tg.title, wNode.id, "", wNode.level + 1); + tgNode.tabGroupId = tg.id; + tg.tabs.reverse().forEach(tab => { + let tabNode = BTAppNode.findFromTab(tab.id); + if (!tabNode) { + tabNode = new BTAppNode(`[[${tab.url}][${tab.title}]]`, tgNode.id, "", tgNode.level + 1); + tabNode.createDisplayNode(); + } + newTabNodes.push(tabNode); + tabNode.windowId = win.windowId; + tabNode.tabGroupId = tg.id; + tabNode.tabId = tab.id; + tabNode.tabIndex = tab.tabIndex; + tabNode.faviconUrl = tab.faviconUrl; + setNodeOpen(tabNode); + if (closeP) tabNode.closeTab(); + }); + tabGroupUpdated({'tabGroupId': tg.id, 'tabGroupColor': tg.color, + 'tabGroupName': tg.title, 'tabGroupCollapsed': tg.collapsed}); + }); + wNode.createTabGroup(); // since win is topic, create Tabgroup + }); + + // save the hostname<->favicon mapping locally for Favicon feature. + try { + newTabNodes.forEach(n => { + const u = n.URL.split(/[?#]/)[0]; // remove any params + n.faviconUrl && localFileManager.set(u, n.faviconUrl); + }); + } + catch (e) { + console.warn('URL/Favicon storage error: ', e); + }; + + processImport(sessionNode.id); +} +*/ + + function tabPositioned(data, highlight = false) { // handle tab move, currently as a result of an earlier groupAndPosition - see StoreTabs and tabOpened @@ -923,6 +1057,7 @@ function tabActivated(data) { window.postMessage({'function': 'localStore', 'data': {...m1, ...m2}}); } +/* merged with tabJoinedTG function tabGrouped(data) { // tab added to group at index const node = BTAppNode.findFromTab(data.tabId); @@ -932,6 +1067,7 @@ function tabGrouped(data) { if (node?.parentId) AllNodes[node.parentId].tabGroupId = data.tgId; } +*/ function tabGroupCreated(data) { // TG created update associated topic color as appropriate @@ -1063,6 +1199,7 @@ function positionInTopic(topicNode, tabNode, index, indices, winId) { } function cleanTitle(text) { + if (!text) return ""; // NOTE: Regex is from https://stackoverflow.com/a/11598864 const clean_non_printable_chars_re = /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g; // clean page title text of things that can screw up BT. Currently [] and non printable chars @@ -1637,75 +1774,6 @@ function loadBookmarks(msg) { processImport(importNode.id); // see above } -function importSession(msg) { - // handler for session sent via msg {winID: {tabs[], tgs:{tabs[]}}} - - const wins = msg.windows; - const dateString = getDateString().replace(':', '∷'); // 12:15 => :15 is a sub topic - const sessionName = "Session Import (" + dateString + ")"; - const parentName = msg.topic || "📝 Scratch"; - const parent = BTAppNode.findOrCreateFromTopicDN(parentName); - const sessionNode = new BTAppNode(sessionName, parent.id, "", parent.level + 1); - const closeP = msg.close; // close tabs after saving? - const newTabNodes = []; - - Object.values(wins).forEach(win => { - const wNode = new BTAppNode(win.windowName, sessionNode.id, "", sessionNode.level + 1); - wNode.windowId = win.windowId; - win.tabs.reverse().forEach(tab => { - // Find or create node for tab - let tabNode = BTAppNode.findFromTab(tab.id); - if (!tabNode) { - tabNode = new BTAppNode(`[[${tab.url}][${tab.title}]]`, wNode.id, "", wNode.level + 1); - tabNode.createDisplayNode(); - } - newTabNodes.push(tabNode); - tabNode.windowId = tab.windowId; - tabNode.tabId = tab.id; - tabNode.tabIndex = tab.tabIndex; - tabNode.faviconUrl = tab.faviconUrl; - setNodeOpen(tabNode); - if (closeP) tabNode.closeTab(); - }); - Object.values(win.tabGroups).reverse().forEach(tg => { - if (BTAppNode.findFromGroup(tg.id)) return; // already managed - const tgNode = new BTAppNode(tg.title, wNode.id, "", wNode.level + 1); - tgNode.tabGroupId = tg.id; - tg.tabs.reverse().forEach(tab => { - let tabNode = BTAppNode.findFromTab(tab.id); - if (!tabNode) { - tabNode = new BTAppNode(`[[${tab.url}][${tab.title}]]`, tgNode.id, "", tgNode.level + 1); - tabNode.createDisplayNode(); - } - newTabNodes.push(tabNode); - tabNode.windowId = win.windowId; - tabNode.tabGroupId = tg.id; - tabNode.tabId = tab.id; - tabNode.tabIndex = tab.tabIndex; - tabNode.faviconUrl = tab.faviconUrl; - setNodeOpen(tabNode); - if (closeP) tabNode.closeTab(); - }); - tabGroupUpdated({'tabGroupId': tg.id, 'tabGroupColor': tg.color, - 'tabGroupName': tg.title, 'tabGroupCollapsed': tg.collapsed}); - }); - wNode.createTabGroup(); // since win is topic, create Tabgroup - }); - - // save the hostname<->favicon mapping locally for Favicon feature. - try { - newTabNodes.forEach(n => { - const u = n.URL.split(/[?#]/)[0]; // remove any params - n.faviconUrl && localFileManager.set(u, n.faviconUrl); - }); - } - catch (e) { - console.warn('URL/Favicon storage error: ', e); - }; - - processImport(sessionNode.id); -} - function loadBookmarkNode(node, parent) { // load a new node from bookmark export format as child of parent BTNode and recurse on children diff --git a/extension/background.js b/extension/background.js index 86dc14c..4c39328 100644 --- a/extension/background.js +++ b/extension/background.js @@ -20,6 +20,12 @@ var LocalTest = false; // control code path during un var InitialInstall = false; // should we serve up the welcome page var UpdateInstall = false; // or the release notes page +function btSendMessage(tabId, msg) { + // send message to BT window/tab. Wrapper to facilitate debugging messaging + console.log(`Sending to BT: ${JSON.stringify(msg)}`); + chrome.tabs.sendMessage(tabId, msg); +} + async function getBTTabWin() { // read from local storage let p = await chrome.storage.local.get(['BTTab', 'BTWin']); @@ -86,9 +92,10 @@ const Handlers = { "closeTab": closeTab, "moveTab": moveTab, "ungroup": ungroup, - "groupAll": groupAll, + "moveOpenTabsToTG": moveOpenTabsToTG, "updateGroup": updateGroup, - "importSession": importSession + "saveTabs": saveTabs, +// "importSession": importSession }; chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { @@ -116,7 +123,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { (msg.function == 'getBookmarks') ? getBookmarks() : exportBookmarks(); } else { // send back denial - chrome.tabs.sendMessage(sender.tab.id, {'function': 'loadBookmarks', + btSendMessage(sender.tab.id, {'function': 'loadBookmarks', 'result': 'denied'}); } }); @@ -149,10 +156,9 @@ chrome.tabs.onMoved.addListener(async (tabId, otherInfo) => { const tab = await chrome.tabs.get(tabId); if (!tab || tab.status == 'loading') return; const indicies = await tabIndices(); - const prevIndex = otherInfo.fromIndex; console.log('moved event:', otherInfo, tab); - chrome.tabs.sendMessage( - BTTab, {'function': 'tabPositioned', 'tabId': tabId, 'groupId': tab.groupId, + btSendMessage( + BTTab, {'function': 'tabMoved', 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, 'windowId': tab.windowId, 'tabIndices': indicies, 'tab': tab}); setTimeout(function() {setBadge(tabId);}, 200); }); @@ -161,18 +167,19 @@ chrome.tabs.onRemoved.addListener(async (tabId, otherInfo) => { // listen for tabs being closed and let BT know const [BTTab, BTWin] = await getBTTabWin(); if (!tabId || !BTTab) return; // closed? - chrome.tabs.sendMessage(BTTab, {'function': 'tabClosed', 'tabId': tabId}); + btSendMessage(BTTab, {'function': 'tabClosed', 'tabId': tabId}); if (tabId == BTTab) setTimeout(() => suspendExtension(), 100); }); chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { // listen for tabs navigating to and from BT URLs or being moved to/from TGs const [BTTab, BTWin] = await getBTTabWin(); - if (!tabId || !BTTab || (tabId == BTTab)) return; // not set up yet or don't care - console.log(`TabUpdated ${tabId}, [${JSON.stringify(changeInfo)}], [${JSON.stringify(tab)}]`); + if (PauseTabEventsDuringTGMove) return; // ignore tab events for a few seconds after TG is dragged creating a new window + if (!tabId || !BTTab || (tabId == BTTab)) return; // not set up yet or don't care + if (changeInfo.status == 'complete') { // tab navigated to/from url - chrome.tabs.sendMessage( + btSendMessage( BTTab, {'function': 'tabNavigated', 'tabId': tabId, 'groupId': tab.groupId, 'tabURL': tab.url, 'windowId': tab.windowId}); setTimeout(function() {setBadge(tabId);}, 200); @@ -181,8 +188,9 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { if (changeInfo.groupId && (tab.status == 'complete') && tab.url) { // tab moved to/from TG, wait til loaded so url etc is filled in const indices = await tabIndices(); - chrome.tabs.sendMessage( - BTTab, {'function': 'tabPositioned', 'tabId': tabId, 'groupId': tab.groupId, + btSendMessage( + BTTab, {'function': (tab.groupId > 0) ? 'tabJoinedTG' : 'tabLeftTG', + 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, 'windowId': tab.windowId, 'tabIndices': indices, 'tab': tab}); setTimeout(function() {setBadge(tabId);}, 200); @@ -191,12 +199,13 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { chrome.tabs.onActivated.addListener(async (info) => { // Let app know there's a new top tab + if (PauseTabEventsDuringTGMove) return; // ignore TG events for a few seconds after a new window is created const [BTTab, BTWin] = await getBTTabWin(); if (!info.tabId || !BTTab) return; - console.log(`tabs.onActiviated fired, info: [${info}]`); - chrome.tabs.get(info.tabId, tab => { + console.log(`tabs.onActiviated fired, info: [${JSON.stringify(info)}]`); + chrome.tabs.get(info.tabId, tab => { if (!tab) return; - chrome.tabs.sendMessage(BTTab, {'function': 'tabActivated', 'tabId': info.tabId, + btSendMessage(BTTab, {'function': 'tabActivated', 'tabId': info.tabId, 'windowId': tab.windowId, 'groupId': tab.groupId}); setTimeout(function() {setBadge(info.tabId);}, 250); }); @@ -206,24 +215,41 @@ chrome.tabGroups.onCreated.addListener(async (tg) => { // listen for TG creation and let app know color etc const [BTTab, BTWin] = await getBTTabWin(); if (!BTTab) return; // not set up yet or don't care - chrome.tabs.sendMessage(BTTab, {'function': 'tabGroupCreated', 'tabGroupId': tg.id, + btSendMessage(BTTab, {'function': 'tabGroupCreated', 'tabGroupId': tg.id, 'tabGroupColor': tg.color}); }); +var NewWindowID = 0; +var PauseTabEventsDuringTGMove = false; +chrome.windows.onCreated.addListener(async (win) => { + // When a TG is dragged out of its window a new one is created followed by a cascade of tab events leaving and rejoining the TG. + // Its too complex to track all these events so we just ignore TG events for a few seconds after a new window is created. + console.log('window created:', win); + NewWindowID = win.id; + setTimeout(() => NewWindowID = 0, 1000); +}); chrome.tabGroups.onUpdated.addListener(async (tg) => { // listen for TG updates and let app know color etc + console.log('tabGroup updated:', tg, NewWindowID ); + if (NewWindowID && (NewWindowID == tg.windowId)) { + // ignore TG updates in btSendMessage for a few seconds cos tabs get disconnected and reconnected + console.log('Pausing TG events for 5 seconds'); + PauseTabEventsDuringTGMove = true; + setTimeout(() => PauseTabEventsDuringTGMove = false, 5000); + } const [BTTab, BTWin] = await getBTTabWin(); if (!BTTab) return; // not set up yet or don't care - chrome.tabs.sendMessage(BTTab, {'function': 'tabGroupUpdated', 'tabGroupId': tg.id, + btSendMessage(BTTab, {'function': 'tabGroupUpdated', 'tabGroupId': tg.id, 'tabGroupColor': tg.color, 'tabGroupName': tg.title, - 'tabGroupCollapsed': tg.collapsed}); + 'tabGroupCollapsed': tg.collapsed, + 'tabGroupWindowId': tg.windowId}); }); chrome.tabGroups.onRemoved.addListener(async (tg) => { // listen for TG deletion const [BTTab, BTWin] = await getBTTabWin(); if (!BTTab) return; // not set up yet or don't care - chrome.tabs.sendMessage(BTTab, {'function': 'tabGroupRemoved', 'tabGroupId': tg.id}); + btSendMessage(BTTab, {'function': 'tabGroupRemoved', 'tabGroupId': tg.id}); }); chrome.windows.onFocusChanged.addListener(async (windowId) => { @@ -236,7 +262,7 @@ chrome.windows.onFocusChanged.addListener(async (windowId) => { chrome.tabs.query({'active': true, 'windowId': windowId},tabs => { check(); if (!tabs.length) return; - chrome.tabs.sendMessage(BTTab, {'function': 'tabActivated', 'tabId': tabs[0].id}); + btSendMessage(BTTab, {'function': 'tabActivated', 'tabId': tabs[0].id}); setTimeout(function() {setBadge(tabs[0].id);}, 200); }); }); @@ -283,7 +309,7 @@ async function tabIndices() { async function tabOpened(winId, tabId, nodeId, index, tgId = 0) { const [BTTab, BTWin] = await getBTTabWin(); check(); - chrome.tabs.sendMessage(BTTab, + btSendMessage(BTTab, {'function': 'tabOpened', 'nodeId': nodeId, 'tabIndex': index, 'tabId': tabId, 'windowId': winId, 'tabGroupId': tgId}); setTimeout(function() {setBadge(tabId);}, 250); @@ -328,7 +354,7 @@ async function initializeExtension(msg, sender) { let allTGs = await getOpenTabGroups(); // send over gdrive app info - chrome.tabs.sendMessage( + btSendMessage( BTTab, {'function': 'launchApp', 'client_id': Keys.CLIENT_ID, 'api_key': Keys.API_KEY, 'fb_key': Keys.FB_KEY, @@ -421,6 +447,7 @@ function openTabs(msg, sender) { tabOpened(win.id, win.tabs[0].id, first.nodeId, win.tabs[0].index); openTabsInWin(rest, win.id); }); + else if (!defaultWinId) openTabsInWin(msg.tabs); // open in current win else // else check window exists & iterate on all adding to current window chrome.windows.get(defaultWinId, (w) => { @@ -437,7 +464,7 @@ function openTabs(msg, sender) { function openTabGroups(msg, sender) { // open tabs in specified or new tab group, potentially in new window - const tabGroups = msg.tabGroups; // [{tg, win, [{id, url}]},..] + const tabGroups = msg.tabGroups; // [{tg, win, tgname[{id, url}]},..] const newWinNeeded = msg.newWin; function openTabsInTg(winId, tgid, tabInfo) { @@ -456,8 +483,9 @@ function openTabGroups(msg, sender) { } tabGroups.forEach(tg => { - // handle a {windowId, tabGroupId, 'tabGroupTabs': [{nodeId, url}]} instance + // handle a {windowId, tabGroupId, groupName, 'tabGroupTabs': [{nodeId, url}]} instance const[first, ...rest] = tg.tabGroupTabs; + const groupName = tg.groupName || ''; if (newWinNeeded) // need to create window for first tab chrome.windows.create({'url': first.url}, win => { @@ -467,6 +495,7 @@ function openTabGroups(msg, sender) { check(); tabOpened(win.id, win.tabs[0].id, first.nodeId, win.tabs[0].index, tgid); + chrome.tabGroups.update(tgid, {'title' : groupName}); openTabsInTg(win.id, tgid, rest); }); }); @@ -483,6 +512,7 @@ function openTabGroups(msg, sender) { check(); tabOpened(tab.windowId, tab.id, first.nodeId, tab.index, tgid); + chrome.tabGroups.update(tgid, {'title' : groupName}); openTabsInTg(tab.windowId, tgid, rest); }); }); @@ -519,8 +549,8 @@ function groupAndPositionTabs(msg, sender) { const theTabs = Array.isArray(tabs) ? tabs : [tabs]; // single tab? theTabs.forEach(t => { const nodeInfo = tabInfo.find(ti => ti.tabId == t.id); - chrome.tabs.sendMessage( - sender.tab.id, {'function': 'tabMoved', 'tabId': t.id, + btSendMessage( + sender.tab.id, {'function': 'tabPositioned', 'tabId': t.id, 'nodeId': nodeInfo.nodeId, 'tabGroupId': groupId, 'windowId': t.windowId, 'tabIndex': t.index}); }); @@ -533,19 +563,25 @@ function ungroup(msg, sender) { chrome.tabs.ungroup(msg.tabIds, () => check()); } -function groupAll(msg, sender) { - // add tabs to new group, either cos pref's changed or grouped tab was opened - chrome.tabs.group({'createProperties': {'windowId': msg.windowId}, 'tabIds': msg.tabIds}, tg => { +function moveOpenTabsToTG(msg, sender) { + // add tabs to new group, cos pref's changed + // send back tabGrouped msg per tab + + chrome.tabs.group({'createProperties': {'windowId': msg.windowId}, 'tabIds': msg.tabIds}, async (tgId) => { check(); - chrome.tabGroups.update(tg, {'title' : msg.groupName}, tgid => { - console.log('tabgroupd updated:', tgid); + chrome.tabGroups.update(tgId, {'title' : msg.groupName}, tg => { + console.log('tabgroup updated:', tg); }); + const indices = await tabIndices(); msg.tabIds.forEach(tid => { chrome.tabs.get(tid, tab => { check(); - chrome.tabs.sendMessage( + btSendMessage( sender.tab.id, - {'function': 'tabGrouped', 'tgId': tg, 'tabId': tid, 'tabIndex': tab.index}); + {'tabId': tid, 'groupId': tgId, + 'tabIndex': tab.index, 'windowId': tab.windowId, 'tabIndicies': indices, + 'tab': tab}); + // was {'function': 'tabGrouped', 'tgId': tgId, 'tabId': tid, 'tabIndex': tab.index}); }); }); }); @@ -656,7 +692,7 @@ function getBookmarks() { const [BTTab, BTWin] = await getBTTabWin(); itemTree[0].title = "Imported Bookmarks"; chrome.storage.local.set({'bookmarks': itemTree[0]}, function() { - chrome.tabs.sendMessage(BTTab, {'function': 'loadBookmarks', + btSendMessage(BTTab, {'function': 'loadBookmarks', 'result': 'success'}); }); }); @@ -688,6 +724,7 @@ function exportBookmarks() { }); } +/* async function importSession(msg, sender) { // return hierarchy of all windows/tgs/tabs to enable a topic tree to be created in Mgr @@ -717,6 +754,55 @@ async function importSession(msg, sender) { win.tabs.push(t); } }); - chrome.tabs.sendMessage(BTTab, {'function': 'importSession', 'windows': allWins, 'topic': msg.topic, 'close': msg.close}); + btSendMessage(BTTab, {'function': 'importSession', 'windows': allWins, 'topic': msg.topic, 'close': msg.close}); console.log('allWins = ', allWins); } +*/ + +async function saveTabs(msg, sender) { + // handle save for popup. msg.type Could be Tab, TG, Window or Session. + // msg: {'close': 'topic', 'note', 'title', 'currentWindowId' } + // Create array of appropriate tab data and send to BT window + + const currentTabs = await chrome.tabs.query({'active': true, 'windowId': msg.currentWindowId}); + const currentTab = currentTabs[0]; + const saveType = msg.type; + const [BTTab, BTWin] = await getBTTabWin(); + const allTabs = await getOpenTabs(); // array of tabs + const allTGs = await getOpenTabGroups(); // array of tgs + + // Create a hash of TGIds to TG names + const tgNames = {}; + allTGs.forEach(tg => tgNames[tg.id] = tg.title); + + // Loop thru tabs, decide based on msg.type if it should be saved and if so add to array to send to BTTab + const tabsToSave = []; + allTabs.forEach(t => { + if (t.id == BTTab || t.pinned) return; + const tab = {'tabId': t.id, 'groupId': t.groupId, 'windowId': t.windowId, 'url': t.url, + 'favIconUrl': t.faviconUrl, 'tabIndex': t.tabIndex, 'title': t.title}; + const tgName = tgNames[t.groupId] || ''; + const [topic, todo] = msg.topic.split(':'); // topic might have trailing :TODO or :DONE. split it off + if (saveType == 'Tab' && t.id == currentTab.id) { + tab['topic'] = topic+(todo ? ':'+todo : ''); + tab['title'] = msg.title; // original or popup-edited tab title + tabsToSave.push(tab); + } + if (saveType == 'TG' && t.groupId == currentTab.groupId) { + tab['topic'] = (topic||"📝 Scratch")+':'+tgName+(todo ? ':'+todo : ''); + tabsToSave.push(tab); + } + if (saveType == 'Window' && t.windowId == currentTab.windowId) { + tab['topic'] = (tgName ? topic+':'+tgName : topic)+(todo ? ':'+todo : ''); + tabsToSave.push(tab); + } + if (saveType == 'Session') { + tab['topic'] = (topic||"📝 Scratch")+':Window'+t.windowId + (tgName ? ':'+tgName : '')+(todo ? ':'+todo : ''); + tabsToSave.push(tab); + } + }); + // Send save msg to BT. NB reverse tabs array cos push operations above reversed the displayed tab order in the browser + if (tabsToSave.length) btSendMessage(BTTab, {'function': 'saveTabs', 'saveType':saveType, 'tabs': tabsToSave, 'note': msg.note, 'close': msg.close}); + btSendMessage(BTTab, {'function': 'tabActivated', 'tabId': currentTab.id }); // ensure BT selects the current tab + +} diff --git a/extension/popup.html b/extension/popup.html index 4e4d439..f24d0d7 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -43,9 +43,13 @@

Multiple Pages

- - - + + + + + + + @@ -56,8 +60,8 @@

Multiple Pages


-

Assign a topic to the page:

-

Assign a topic to the pages:

+

Assign a parent topic:

+

Assign a topic:

Select from below, type for autocomplete, or Enter (for Scratch)
diff --git a/extension/popup.js b/extension/popup.js index a6ebe31..d4cee60 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -151,11 +151,12 @@ SaveAndCloseBtn.addEventListener('click', () => saveCB(true)); // Logic for saveTab/saveAllTabs toggle const SavePage = document.getElementById("savePage"); -const SaveWindow = document.getElementById("saveAllPages"); +const SaveWindow = document.getElementById("saveWindow"); +const SaveTG = document.getElementById("saveTG"); const SaveSession = document.getElementById("saveAllSession"); SavePage.addEventListener('change', e => { - if (SavePage.checked) {SaveWindow.checked = false; SaveSession.checked = false;} + if (SavePage.checked) {SaveWindow.checked = false; SaveSession.checked = false; SaveTG.checked = false;} else SaveWindow.checked = true; updateForAll(); }); @@ -164,8 +165,13 @@ SaveWindow.addEventListener('change', e => { else SavePage.checked = true; updateForAll(); }); +SaveTG.addEventListener('change', e => { + if (SaveTG.checked) {SavePage.checked = false; SaveSession.checked = false;} + else SavePage.checked = true; + updateForAll(); +}); SaveSession.addEventListener('change', e => { - if (SaveSession.checked) {SavePage.checked = false; SaveWindow.checked = false;} + if (SaveSession.checked) {SavePage.checked = false; SaveWindow.checked = false; SaveTG.checked = false;} else SavePage.checked = true; updateForAll(); }); @@ -173,7 +179,7 @@ function updateForAll(all) { // handle AllPages toggle const onePageElements = document.getElementsByClassName("onePage"); const allPageElements = document.getElementsByClassName("allPages"); - if (SaveWindow.checked || SaveSession.checked) { + if (SaveWindow.checked || SaveSession.checked || SaveTG.checked) { Array.from(onePageElements).forEach(e => e.style.display = "none"); Array.from(allPageElements).forEach(e => e.style.display = "block"); } else { @@ -188,8 +194,12 @@ function popupOpen(tab) { const messageElt = document.getElementById('message'); const saverDiv = document.getElementById("saver"); const titleH2 = document.getElementById('title'); + const saveTG = document.getElementById('saveTGSpan'); + const saveWindow = document.getElementById('saveWindowSpan'); saverDiv.style.display = 'block'; messageElt.style.display = 'none'; + if (tab.groupId > 0) {saveWindow.style.display = 'none';} + else {saveTG.style.display = 'none';} // Pull data from local storage, prepopulate and open saver chrome.storage.local.get( @@ -242,16 +252,15 @@ function cardCompleted() { } async function saveCB(close) { - // save topic card for page, optionally close tab - // Call out to BT app which handles everything + // Call out to background to do the save const title = TopicCard.title(); const note = TopicCard.note(); - const url = CurrentTab.url; const newTopic = OldTopic || TopicSelector.topic(); - const allTabs = SaveWindow.checked; - const wholeSession = SaveSession.checked; - const tabsToStore = allTabs ? Tabs : new Array(CurrentTab); + const saveType = SavePage.checked ? 'Tab' : (SaveTG.checked ? 'TG' : (SaveWindow.checked ? 'Window' : 'Session')); + await chrome.runtime.sendMessage({'from': 'popup', 'function': 'saveTabs', 'type': saveType, 'currentWindowId': CurrentTab.windowId, + 'close': close, 'topic': newTopic, 'note': note, 'title': title}); + /* if (wholeSession) { // send message to background to save whole session await chrome.runtime.sendMessage( @@ -272,11 +281,11 @@ async function saveCB(close) { }); message.tabsData = tabsData; await chrome.tabs.sendMessage(BTTab, message); - - if (!close) // if tab isn't closing animate the brain - await chrome.runtime.sendMessage( - {'from': 'popup', 'function': 'brainZoom', 'tabId': CurrentTab.id}); + } + */ + if (!close) // if tab isn't closing animate the brain + await chrome.runtime.sendMessage({'from': 'popup', 'function': 'brainZoom', 'tabId': CurrentTab.id}); await chrome.storage.local.set({'saveAndClose': close}); window.close(); }