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();
+}