diff --git a/unclose/background.js b/unclose/background.js new file mode 100644 index 0000000..37f0b05 --- /dev/null +++ b/unclose/background.js @@ -0,0 +1 @@ +importScripts("storage.js", "init.js", "bg.js"); diff --git a/unclose/bg.js b/unclose/bg.js index f2a8d3e..f93d31f 100644 --- a/unclose/bg.js +++ b/unclose/bg.js @@ -1,33 +1,98 @@ -// Replace HTML tags < > -function quote(s) { - var s1 = s.replace("<", "<"); - var s2 = s1.replace(">", ">"); - return s2; +// Serialize all storage writes to prevent counter races on rapid tab events. +var _writeQueue = Promise.resolve(); +function enqueueWrite(asyncFn) { + _writeQueue = _writeQueue.then(asyncFn).catch((err) => { + console.error('[unclose] storage write failed:', err); + }); + return _writeQueue; } -chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { - localStorage["TabList-"+tabId] = tab.url; - localStorage["TabIndex-"+tabId] = tab.index; - if (tab.favIconUrl) - localStorage["TabFavicon-"+tabId] = tab.favIconUrl; - if(tab.title != null) - localStorage["TabTitle-"+tabId] = quote(tab.title); - else - localStorage["TabTitle-"+tabId] = tab.url; +async function restoreTab(tabId) { + await ensureInitialized(); + + var state = await storageGet({ + actualCount: 0, + ["TabList-" + tabId]: null, + ["TabIndex-" + tabId]: null + }); + var url = state["TabList-" + tabId]; + var index = parseInt(state["TabIndex-" + tabId], 10); + if (!url) + return false; + + var createProperties = {"url": url}; + if (!isNaN(index)) + createProperties.index = index; + + await chrome.tabs.create(createProperties); + await clear(tabId); + await storageSet({ + actualCount: Math.max((parseInt(state.actualCount, 10) || 0) - 1, 0) + }); + await setBadgeText(); + return true; +} + +chrome.tabs.onUpdated.addListener(async function(tabId, changeInfo, tab) { + await enqueueWrite(async () => { + await ensureInitialized(); + var tabKey = "TabList-" + tabId; + var currentUrl = tab.url || await storageGet(tabKey); + var updates = {}; + if (tab.url) + updates[tabKey] = tab.url; + updates["TabIndex-" + tabId] = tab.index; + if (tab.favIconUrl) + updates["TabFavicon-" + tabId] = tab.favIconUrl; + if (tab.title != null) + updates["TabTitle-" + tabId] = tab.title; + else if (currentUrl) + updates["TabTitle-" + tabId] = currentUrl; + await storageSet(updates); + }); }); -chrome.tabs.onRemoved.addListener(function(tabId, info) { - // Should we record this tab? - var url = localStorage["TabList-"+tabId]; - var re = /^(http:|https:|ftp:|file:)/; - if (url && re.test(url)) { - var digital = new Date(); - - localStorage["ClosedTab-"+localStorage["closeCount"]] = tabId; - localStorage["ClosedTabTime-"+localStorage["closeCount"]] = digital.getTime(); - localStorage["closeCount"] ++; - localStorage["actualCount"] ++; - setBadgeText(); - } - else clear(tabId); +chrome.tabs.onRemoved.addListener(async function(tabId, info) { + await enqueueWrite(async () => { + await ensureInitialized(); + // Should we record this tab? + var tabKey = "TabList-" + tabId; + var state = await storageGet({ + closeCount: 0, + actualCount: 0, + [tabKey]: null + }); + var url = state[tabKey]; + var re = /^(http:|https:|ftp:|file:)/; + if (url && re.test(url)) { + var closeCount = parseInt(state.closeCount, 10) || 0; + var actualCount = (parseInt(state.actualCount, 10) || 0) + 1; + var updates = { + closeCount: closeCount + 1, + actualCount: actualCount + }; + updates["ClosedTab-" + closeCount] = tabId; + updates["ClosedTabTime-" + closeCount] = new Date().getTime(); + await storageSet(updates); + await setBadgeText(); + } + else + await clear(tabId); + }); +}); + +chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { + if (!message || message.method !== "restoreTab") + return false; + + enqueueWrite(async function() { + return restoreTab(message.tabId); + }).then(function(restored) { + sendResponse({ok: restored}); + }).catch(function(error) { + console.error('[unclose] restore failed:', error); + sendResponse({ok: false}); + }); + + return true; }); diff --git a/unclose/init.js b/unclose/init.js index 0deb60b..8016c03 100644 --- a/unclose/init.js +++ b/unclose/init.js @@ -1,28 +1,68 @@ -function clear(tabId) { - delete localStorage["TabList-"+tabId]; - delete localStorage["TabIndex-"+tabId]; - delete localStorage["TabTitle-"+tabId]; - delete localStorage["TabFavicon-"+tabId]; +var _initializationPromise = null; + +async function clear(tabId) { + return storageRemove(getTabKeys(tabId)); } -function setBadgeText() { - var n = localStorage["actualCount"]; - if (parseInt(n) > 0) - chrome.browserAction.setBadgeText({text:n}); +async function setBadgeText() { + var n = parseInt(await storageGet("actualCount"), 10) || 0; + if (n > 0) + await chrome.action.setBadgeText({text: String(n)}); else - chrome.browserAction.setBadgeText({text:""}); + await chrome.action.setBadgeText({text:""}); } -function initialize() { - localStorage["closeCount"] = 0; - localStorage["actualCount"] = 0; - setBadgeText(); +async function initialize() { + await storageSet({ + closeCount: 0, + actualCount: 0 + }); + _initializationPromise = Promise.resolve(); + await setBadgeText(); } -function init() +async function ensureInitialized() { - // Deep clean: clear localStorage otherwise we get old history from previous sessions - localStorage.clear(); + if (_initializationPromise) + return _initializationPromise; + + _initializationPromise = (async function() { + var state = await storageGet({ + closeCount: null, + actualCount: null + }); + var updates = {}; + + if (state.closeCount === null) + updates.closeCount = 0; + if (state.actualCount === null) + updates.actualCount = 0; - initialize(); + if (Object.keys(updates).length > 0) + await storageSet(updates); + })().catch(function(error) { + _initializationPromise = null; + throw error; + }); + + return _initializationPromise; } + +async function init() +{ + // Deep clean: clear storage otherwise we get old history from previous sessions + await storageClear(); + _initializationPromise = null; + await initialize(); +} + +chrome.runtime.onInstalled.addListener(async function(details) { + if (details.reason === 'install') await init(); + else await ensureInitialized(); +}); + +chrome.runtime.onStartup.addListener(async function() { + // chrome.storage.session is cleared automatically on browser restart; + // just reset the counters and badge. + await initialize(); +}); diff --git a/unclose/manifest.json b/unclose/manifest.json index 1e4a4a5..ed8119c 100644 --- a/unclose/manifest.json +++ b/unclose/manifest.json @@ -3,18 +3,22 @@ "version": "4.0", "author": "MK & Wei Hu & Xerios", "description": "Undo your closed tabs", - "background_page": "background.html", + "manifest_version": 3, + "background": { + "service_worker": "background.js" + }, "icons": { "48": "icon.png", - "128": "icon-128.png" - }, + "128": "icon-128.png" + }, "options_page": "popup.html", - "browser_action": { + "action": { "default_icon": "icon.png", "popup": "popup.html", "default_title": "Undo closed tabs" }, "permissions": [ + "storage", "tabs" ] } diff --git a/unclose/popup.html b/unclose/popup.html index 108e37f..8687335 100644 --- a/unclose/popup.html +++ b/unclose/popup.html @@ -1,7 +1,7 @@ - - - - - - -
- - - - - -
- - - - -
+ + + + + + + +
+ + + + + +
+ + + + +
diff --git a/unclose/popup.js b/unclose/popup.js index 7e2c6d0..515ae77 100644 --- a/unclose/popup.js +++ b/unclose/popup.js @@ -8,33 +8,51 @@ function createLink(id, url) { return link; } -function loadText() +function appendTime(parent, label) { - // Don't popup if there's nothing to show - var n = localStorage["actualCount"]; - if (parseInt(n) == 0) + if (!label) + return; + + var textdiv = document.createElement('span'); + var bold = document.createElement('b'); + bold.textContent = label; + textdiv.appendChild(bold); + parent.appendChild(textdiv); +} + +async function loadText() +{ + await ensureInitialized(); + + var state = await storageGetAll(); + var n = parseInt(state.actualCount, 10) || 0; + if (n == 0) { window.close(); + return; + } + + while (pageNo > 0 && n <= pageNo * nItems) + pageNo--; var tabId, tabUrl, tabTime; - - content = document.getElementById("contentDiv"); + var content = document.getElementById("contentDiv"); // Clear while (content.hasChildNodes()) content.removeChild(content.firstChild); // Drop the first (pageNo*nItems) valid items - for (j = 0, i = localStorage["closeCount"] - 1; i>=0 && j=0 && j=0 && j'; - else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = ''+ minutesDifference + ' min'; - else if (hoursDifference < 1) timeTextz = ''+ minutesDifference + ' min'; - else if (hoursDifference < 4) timeTextz= '' + hoursDifference + 'hr ' + minutesDifference + 'm'; - else if (hoursDifference < 24) timeTextz='' + hoursDifference + ' hr'; - textdiv2.innerHTML=timeTextz; - text_link.appendChild(textdiv2); + if ( hoursDifference < 1 && minutesDifference < 1) timeTextz = secondsDifference + ' sec'; + else if (hoursDifference < 1 && minutesDifference < 10) timeTextz = minutesDifference + ' min'; + else if (hoursDifference < 1) timeTextz = minutesDifference + ' min'; + else if (hoursDifference < 4) timeTextz = hoursDifference + 'hr ' + minutesDifference + 'm'; + else if (hoursDifference < 24) timeTextz = hoursDifference + ' hr'; + if (timeTextz) + appendTime(text_link, timeTextz); content.appendChild(text_link); j++; @@ -82,25 +99,25 @@ function loadText() if (pageNo > 0) document.getElementById("prev").style.visibility="visible"; else document.getElementById("prev").style.visibility="hidden"; - if (localStorage["actualCount"] > (pageNo+1) * nItems) + if (n > (pageNo+1) * nItems) document.getElementById("next").style.visibility="visible"; else document.getElementById("next").style.visibility="hidden"; } function loadFavicon() { var imgs = document.images; - for (i=0; i (pageNo+1) * nItems) +async function next() { + if ((parseInt(await storageGet("actualCount"), 10) || 0) > (pageNo+1) * nItems) pageNo++; loadContent(); } @@ -112,29 +129,43 @@ function prev() { } // Show |url| in a new tab. -function showUrl(tabId) { - var url = localStorage["TabList-"+tabId]; - var index = parseInt(localStorage["TabIndex-"+tabId]); - chrome.tabs.create({"url": url, "index": index}); - clear(tabId); - localStorage["actualCount"] --; - setBadgeText(); - loadContent(); +async function showUrl(tabId) { + var response = await chrome.runtime.sendMessage({ + method: "restoreTab", + tabId: tabId + }); + + if (response && response.ok) + await loadContent(); } -function reset() +async function reset() { // Shallow clean: only forgets history about closed tabs - for(i = localStorage["closeCount"]-1; i >= 0; i--) + var state = await storageGetAll(); + var keys = []; + + for(var i = (parseInt(state.closeCount, 10) || 0) - 1; i >= 0; i--) { - tabId = localStorage["ClosedTab-"+i]; - delete localStorage["ClosedTab-"+i]; - delete localStorage["ClosedTabTime-"+i]; - clear(tabId); + var tabId = state["ClosedTab-"+i]; + keys.push("ClosedTab-"+i, "ClosedTabTime-"+i); + if (tabId != null) + keys = keys.concat(getTabKeys(tabId)); } - initialize(); + await storageRemove(keys); + await initialize(); pageNo = 0; window.close(); } + +document.addEventListener("DOMContentLoaded", function() { + document.body.addEventListener("selectstart", function(event) { + event.preventDefault(); + }); + document.getElementById("prev").addEventListener("click", prev); + document.getElementById("next").addEventListener("click", next); + document.getElementById("clear").addEventListener("click", reset); + loadContent(); +}); diff --git a/unclose/storage.js b/unclose/storage.js new file mode 100644 index 0000000..b3ad93c --- /dev/null +++ b/unclose/storage.js @@ -0,0 +1,31 @@ +function getTabKeys(tabId) { + return [ + "TabList-" + tabId, + "TabIndex-" + tabId, + "TabTitle-" + tabId, + "TabFavicon-" + tabId + ]; +} + +async function storageGet(keyOrKeys) { + var data = await chrome.storage.session.get(keyOrKeys); + if (typeof keyOrKeys == "string") + return data[keyOrKeys]; + return data; +} + +async function storageGetAll() { + return chrome.storage.session.get(null); +} + +async function storageSet(items) { + return chrome.storage.session.set(items); +} + +async function storageRemove(keys) { + return chrome.storage.session.remove(keys); +} + +async function storageClear() { + return chrome.storage.session.clear(); +}