Skip to content
Merged
1 change: 1 addition & 0 deletions unclose/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
importScripts("storage.js", "init.js", "bg.js");
121 changes: 93 additions & 28 deletions unclose/bg.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,98 @@
// Replace HTML tags < >
function quote(s) {
var s1 = s.replace("<", "&lt;");
var s2 = s1.replace(">", "&gt;");
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);
Comment on lines +36 to +40
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;
});
76 changes: 58 additions & 18 deletions unclose/init.js
Original file line number Diff line number Diff line change
@@ -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();
});
12 changes: 8 additions & 4 deletions unclose/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
52 changes: 27 additions & 25 deletions unclose/popup.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
<style type="text/css">
#contentDiv {
width: 450px;
line-height: 95%;
Expand All @@ -25,12 +25,13 @@
#contentDiv div:hover {
background:#E4E7FF;
}
#contentDiv div a{
display:inline-block;
width:330px;
margin: 3px 0 3px 0;
overflow:hidden;
}
#contentDiv div a{
display:inline-block;
width:330px;
margin: 3px 0 3px 0;
padding-left: 1px;
overflow:hidden;
}
#contentDiv div span,b{
padding: 3px 0 0 0;
vertical-align:top;
Expand Down Expand Up @@ -64,24 +65,25 @@
-moz-user-select: none;
}

</style>
<script src="popup.js"> </script>
<script src="init.js"> </script>

</head>
<body onload="loadContent()" onselectstart="return false;">
<div id="contentDiv"> </div>
<table class="table" cellspacing="0" cellpadding="0" border="0" frame="void">
<tr>
<td>
<button id="prev" onclick="prev()">&lt;</button>
<button id="next" onclick="next()">&gt;</button>
</td>
<td style="text-align: right">
<button onclick="reset()">Clear</button>
</td>
</tr>
</table>
</style>
<script src="storage.js"> </script>
<script src="init.js"> </script>
<script src="popup.js"> </script>

</head>
<body>
<div id="contentDiv"> </div>
<table class="table" cellspacing="0" cellpadding="0" border="0" frame="void">
<tr>
<td>
<button id="prev">&lt;</button>
<button id="next">&gt;</button>
</td>
<td style="text-align: right">
<button id="clear">Clear</button>
</td>
</tr>
</table>

</body>
</html>
Loading