1
0
mirror of https://github.com/XFox111/TabsAsideExtension.git synced 2026-04-22 07:58:01 +03:00

Major 2.0 (#51)

- Added cloud storage for sets
- WeblySleek UI font replaced with Segoe fonts
- Reworked icons (#55)
- Removed changelog auto-opening on install
- 'Contributors' option replaced with 'Changelog' one
- Added icons to menu items
- Updated UI with Fluent UI compliance
- Added tooltip on how to use selective activation (#57)
- Added Italian language (#52) by @blackcat-917
- Added Brazilian Portuguese language (#53) by @amandafilizola
- Updated Firefox CI/CD config
- Updated README: rearranged badges and added demo GIF
- Updated documentation

Co-authored-by: Michael Gordeev <michael@xfox111.net>
Co-authored-by: blackcat-917 <53786619+blackcat-917@users.noreply.github.com>
Co-authored-by: Amanda de Paiva Filizola <amandapaivafilizola@gmail.com>
Co-authored-by: Amine A <15179425+AmineI@users.noreply.github.com>
Co-authored-by: Dustin Jiang <56217843+Dustin-Jiang@users.noreply.github.com>
This commit is contained in:
Michael Gordeev
2021-03-03 17:06:30 +03:00
committed by GitHub
parent 4559b25739
commit 8da01f34e1
55 changed files with 1019 additions and 347 deletions
+252 -129
View File
@@ -2,21 +2,23 @@
//We can't populate it later, as selected tabs get deselected on a click inside a tab.
var tabsToSave = [];
var syncEnabled = true; //This variable controls whether to use the chrome sync storage, along with its size limitations, or the much larger chrome local storage. The option is currently not exposed to the users.
var collectionStorage = syncEnabled ? chrome.storage.sync : chrome.storage.local;
//Get the tabs to save, either all the window or the selected tabs only, and pass them through a callback.
function GetTabsToSave(callback)
function GetTabsToSave (callback)
{
chrome.tabs.query({ currentWindow: true }, (windowTabs) =>
{
var highlightedTabs = windowTabs.filter(item => item.highlighted);
//If there are more than one selected tab in the window, we set only those aside.
// Otherwise, all the window's tabs get saved.
return callback((highlightedTabs.length > 1 ? highlightedTabs : windowTabs));
});
chrome.tabs.query({ currentWindow: true }, (windowTabs) =>
{
var highlightedTabs = windowTabs.filter(item => item.highlighted);
//If there are more than one selected tab in the window, we set only those aside.
// Otherwise, all the window's tabs get saved.
return callback((highlightedTabs.length > 1 ? highlightedTabs : windowTabs));
});
}
function TogglePane(tab)
function TogglePane (tab)
{
if (tab.url.startsWith("http")
&& !tab.url.includes("chrome.google.com")
@@ -59,7 +61,7 @@ function TogglePane(tab)
}
}
function ProcessCommand(command)
function ProcessCommand (command)
{
GetTabsToSave((returnedTabs) =>
{
@@ -98,17 +100,180 @@ chrome.browserAction.onClicked.addListener((tab) =>
});
});
var collections = JSON.parse(localStorage.getItem("sets")) || [];
collections = { };
thumbnails = { };
/**
* Updates the thumbnail storage, then the collection synced storage. Calls the onSuccess callback when successful, and onFailure when failed.
* @param {Object} collectionsToUpdate An object containing one or more collections to be updated. By default, updates the whole "collections" item.
* @param {function} onSuccess A function without arguments, that will be called after the collections and thumbnails values are obtained, if collections are successfully updated
* @param {function} onFailure A function that will be called with the error, after the collections and thumbnails values are obtained, if collections are not successfully updated
*/
function UpdateStorages (collectionsToUpdate = collections, onSuccess = () => null, onFailure = (error) => { throw error.message; })
{
//The collections storage is updated after the thumbnail storage, so that the thumbnails are ready when the collections are updated.
chrome.storage.local.set({ "thumbnails": thumbnails },
() => collectionStorage.set(CompressCollectionsStorage(collectionsToUpdate),
() =>
{
if (!chrome.runtime.lastError)
onSuccess();
else
onFailure(chrome.runtime.lastError);
})
);
//When the collection storage is updated, a listener set up below reacts and updates the collections global variable, so we do not need to update that variable here
}
/**
* Use a compression mechanism to compress collections in an object, one by one.
* @param {Object} collectionsToCompress object of collections to compress
* @returns {Object} Object of compressed stringified collections.
*/
function CompressCollectionsStorage (collectionsToCompress)
{
var compressedStorage = { };
for (var [key, value] of Object.entries(collectionsToCompress)) //For each collection in the uncompressed collectionsToCompress
{
var cloneWithoutTimestamp = Object.assign({ }, value, { timestamp: null });
compressedStorage[key] = LZUTF8.compress(JSON.stringify(cloneWithoutTimestamp), { outputEncoding: "StorageBinaryString" });
}
return compressedStorage;
}
/**
* Load and decompresses the thumbnails and collections global variables from the storage, updates the theme and eventually sends the collections and thumbnails to a callback
* @param {function} callback A function that will be called after the collections and thumbnails values are obtained.
* These collections and thumbnails are sent as a data={"collections":collections,"thumbnails":thumbnails} argument to the callback.
*/
function LoadStorages (callback = () => null)
{
chrome.storage.local.get("thumbnails", values =>
{
thumbnails = values?.thumbnails ?? { };
collectionStorage.get(null, values =>
{
collections = DecompressCollectionsStorage(values);
UpdateBadgeCounter();
callback({ "collections": collections, "thumbnails": thumbnails });
});
});
}
/**
* Use a decompression mechanism to decompress stringified collections in an object, one by one.
* Ignores non collections items (items with a key not starting with "set_"
* @param {Object} compressedCollections object of stringified collections to decompress
* @returns {Object} Object of decompressed and parsed collections.
*/
function DecompressCollectionsStorage (compressedCollections)
{
var decompressedStorage = { };
for (var [key, value] of Object.entries(compressedCollections))
{
if (!key.startsWith("set_"))
continue;
decompressedStorage[key] = JSON.parse(LZUTF8.decompress(value, { inputEncoding: "StorageBinaryString" }));
decompressedStorage[key].timestamp = parseInt(key.substr(4));
}
return decompressedStorage;
}
/**
* Merges a provided collections array with older pre v2 collections,
* saving the result into the new post v2 storage, or into bookmarks on failure.
* Allows to preserve backward compatibility with the localStorage method of storing collections in pre v2 versions.
* @param {Object} collections The current collections object
*/
function MergePreV2Collections ()
{
if (localStorage.getItem("sets"))
{
console.log("Found pre-v2 data");
var old_collections = JSON.parse(localStorage.getItem("sets"));
//Migrate thumbnails and icons to follow the new format .
old_collections.forEach(collection =>
{
for (var i = 0; i < collection.links.length; i++)
thumbnails[collection.links[i]] =
{
"pageCapture": collection.thumbnails[i],
"iconUrl": collection.icons[i]
};
delete collection.thumbnails;
delete collection.icons;
UpdateStorages({ ["set_" + collection.timestamp]: collection },
() => null,
() =>
{
SaveCollectionAsBookmarks(collection);
alert(chrome.i18n.getMessage("olderDataMigrationFailed"));
});
});
localStorage.removeItem("sets");
}
}
function SaveCollectionAsBookmarks (collection)
{
//The id 1 is the browser's bookmark bar
chrome.bookmarks.create({
"parentId": "1",
"title": "TabsAside " + (collection.name ?? new Date(collection.timestamp).toISOString())
},
(collectionFolder) =>
{
for (var i = 0; i < collection.links.length; i++)
chrome.bookmarks.create(
{
"parentId": collectionFolder.id,
"title": collection.titles[i],
"url": collection.links[i]
});
});
}
LoadStorages(MergePreV2Collections);
chrome.storage.onChanged.addListener((changes, namespace) =>
{
if (namespace == "sync")
for (key in changes)
if (key.startsWith("set_"))
{
if (changes[key].newValue)
{
collections[key] = DecompressCollectionsStorage({ [key]: changes[key].newValue })[key];
}
else
delete collections[key];
UpdateBadgeCounter();
chrome.runtime.sendMessage(
{
command: "reloadCollections",
collections: collections,
thumbnails: thumbnails
});
}
});
var shortcuts;
chrome.commands.getAll((commands) => shortcuts = commands);
chrome.commands.onCommand.addListener(ProcessCommand);
chrome.contextMenus.onClicked.addListener((info) => ProcessCommand(info.menuItemId));
chrome.runtime.onInstalled.addListener((reason) =>
chrome.runtime.onInstalled.addListener((updateInfo) =>
{
//chrome.tabs.create({ url: "https://github.com/XFox111/TabsAsideExtension/releases/latest" });
// Adding context menu options
if (updateInfo.reason == "update" && updateInfo.previousVersion != chrome.runtime.getManifest()["version"])
chrome.storage.local.set({ "showUpdateBadge": true });
// Adding context menu options, must be done on extension install and update, and probably chrome update as well.
chrome.contextMenus.create(
{
id: "toggle-pane",
@@ -134,26 +299,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) =>
chrome.tabs.create({ url: message.url });
break;
case "loadData":
sendResponse(collections);
break;
LoadStorages(sendResponse); //Sends the collections as a response
return true; //Required to indicate the answer will be sent asynchronously https://developer.chrome.com/extensions/messaging
case "saveTabs":
SaveCollection();
break;
case "restoreTabs":
RestoreCollection(message.collectionIndex, message.removeCollection);
RestoreCollection(message.collectionKey, message.removeCollection);
sendResponse();
break;
case "deleteTabs":
DeleteCollection(message.collectionIndex);
DeleteCollection(message.collectionKey);
sendResponse();
break;
case "removeTab":
RemoveTab(message.collectionIndex, message.tabIndex);
RemoveTab(message.collectionKey, message.tabIndex);
sendResponse();
break;
case "renameCollection":
collections[message.collectionIndex].name = message.newName;
localStorage.setItem("sets", JSON.stringify(collections));
collections[message.collectionKey].name = message.newName;
UpdateStorages({ [message.collectionKey]: collections[message.collectionKey] });
break;
case "togglePane":
chrome.tabs.query(
@@ -162,7 +327,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) =>
currentWindow: true
},
(tabs) => TogglePane(tabs[0])
)
);
break;
case "getShortcuts":
sendResponse(shortcuts);
@@ -171,38 +336,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) =>
});
// This function updates the extension's toolbar icon
function UpdateTheme()
function UpdateBadgeCounter ()
{
var collectionsLength = Object.keys(collections).length;
// Updating badge counter
chrome.browserAction.setBadgeText({ text: collections.length < 1 ? "" : collections.length.toString() });
chrome.browserAction.setBadgeText({ text: collectionsLength < 1 ? "" : collectionsLength.toString() });
if (chrome.theme) // Firefox sets theme automatically
return;
var theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
var iconStatus = collections.length ? "full" : "empty";
var basePath = "icons/" + theme + "/" + iconStatus + "/";
chrome.browserAction.setIcon(
{
path:
{
"128": basePath + "128.png",
"48": basePath + "48.png",
"32": basePath + "32.png",
"16": basePath + "16.png"
}
});
}
UpdateTheme();
chrome.windows.onFocusChanged.addListener(UpdateTheme);
chrome.tabs.onUpdated.addListener(UpdateTheme);
chrome.tabs.onActivated.addListener(UpdateTheme);
// Set current tabs aside
function SaveCollection()
function SaveCollection ()
{
var tabs = tabsToSave.filter(i => i.url != chrome.runtime.getURL("TabsAside.html") && !i.pinned && !i.url.includes("//newtab") && !i.url.includes("about:blank") && !i.url.includes("about:home"));
@@ -216,46 +359,41 @@ function SaveCollection()
{
timestamp: Date.now(),
tabsCount: tabs.length,
titles: tabs.map(tab => tab.title ?? ""),
links: tabs.map(tab => tab.url ?? ""),
icons: tabs.map(tab => tab.favIconUrl ?? ""),
thumbnails: tabs.map(tab => thumbnails.find(i => i.tabId == tab.id)?.url ?? "")
titles: tabs.map(tab => (tab.title ?? "").substr(0, 100)),
links: tabs.map(tab => tab.url ?? "")
};
var rawData;
if (localStorage.getItem("sets") === null)
rawData = [collection];
else
tabs.forEach(tab => //For each tab to save, Add relevant thumbnails in the thumbnails object
{
rawData = JSON.parse(localStorage.getItem("sets"));
rawData.unshift(collection);
}
localStorage.setItem("sets", JSON.stringify(rawData));
collections = JSON.parse(localStorage.getItem("sets"));
var newTabId;
chrome.tabs.create({}, (tab) =>
{
newTabId = tab.id;
chrome.tabs.remove(tabsToSave.filter(i => !i.pinned && i.id != newTabId).map(tab => tab.id));
thumbnails[tab.url] =
{
"pageCapture": sessionCaptures[tab.url] ?? thumbnails[tab.url]?.pageCapture,
"iconUrl": tab.favIconUrl
};
});
UpdateTheme();
UpdateStorages({["set_" + collection.timestamp]: collection}, () =>
chrome.tabs.create({}, (tab) =>
{
var newTabId = tab.id;
chrome.tabs.remove(tabsToSave.filter(i => !i.pinned && i.id != newTabId).map(tab => tab.id));
}),
() => alert(chrome.i18n.getMessage("errorSavingTabs"))
);
}
function DeleteCollection(collectionIndex)
function DeleteCollection (collectionKey)
{
collections = collections.filter(i => i != collections[collectionIndex]);
localStorage.setItem("sets", JSON.stringify(collections));
UpdateTheme();
var deletedUrls = collections[collectionKey].links;
delete collections[collectionKey];
ForEachUnusedUrl(deletedUrls, (url) => delete thumbnails[url]); //We delete the thumbnails that are not used in any other collection.
UpdateStorages({});//Updates the thumbnails storage only, by providing an empty "collectionsToUpdate" object.
collectionStorage.remove(collectionKey);//Remove the collection from the collectionstorage
}
function RestoreCollection(collectionIndex, removeCollection)
function RestoreCollection (collectionKey, removeCollection)
{
collections[collectionIndex].links.forEach(i =>
collections[collectionKey].links.forEach(i =>
{
chrome.tabs.create(
{
@@ -264,14 +402,16 @@ function RestoreCollection(collectionIndex, removeCollection)
},
(createdTab) =>
{
chrome.storage.sync.get({ "loadOnRestore" : true }, values =>
chrome.storage.sync.get({"loadOnRestore": true}, values =>
{
if (!(values?.loadOnRestore))
chrome.tabs.onUpdated.addListener(function DiscardTab(updatedTabId, changeInfo, updatedTab)
chrome.tabs.onUpdated.addListener(function DiscardTab (updatedTabId, changeInfo, updatedTab)
{
if (updatedTabId === createdTab.id) {
if (updatedTabId === createdTab.id)
{
chrome.tabs.onUpdated.removeListener(DiscardTab);
if (!updatedTab.active) {
if (!updatedTab.active)
{
chrome.tabs.discard(updatedTabId);
}
}
@@ -281,56 +421,48 @@ function RestoreCollection(collectionIndex, removeCollection)
});
//We added new tabs by restoring a collection, so we refresh the array of tabs ready to be saved.
GetTabsToSave((returnedTabs) =>
tabsToSave = returnedTabs)
GetTabsToSave((returnedTabs) =>
tabsToSave = returnedTabs);
if (!removeCollection)
return;
collections = collections.filter(i => i != collections[collectionIndex]);
localStorage.setItem("sets", JSON.stringify(collections));
UpdateTheme();
DeleteCollection(collectionKey);
}
function RemoveTab(collectionIndex, tabIndex)
function RemoveTab (collectionKey, tabIndex)
{
var set = collections[collectionIndex];
var set = collections[collectionKey];
if (--set.tabsCount < 1)
{
collections = collections.filter(i => i != set);
localStorage.setItem("sets", JSON.stringify(collections));
UpdateTheme();
DeleteCollection(collectionKey);
return;
}
var titles = [];
var links = [];
var icons = [];
var urlToRemove = set.links[tabIndex];
set.titles.splice(tabIndex, 1);
set.links.splice(tabIndex, 1);
for (var i = set.links.length - 1; i >= 0; i--)
{
if (i == tabIndex)
continue;
titles.unshift(set.titles[i]);
links.unshift(set.links[i]);
icons.unshift(set.icons[i]);
}
set.titles = titles;
set.links = links;
set.icons = icons;
localStorage.setItem("sets", JSON.stringify(collections));
UpdateTheme();
ForEachUnusedUrl([urlToRemove], (url) => delete thumbnails[url]);
UpdateStorages({[collectionKey]: set});
}
var thumbnails = [];
/**
* Execute a callback for each url in urlsToFilter that is not in any collection urls
* @param {Array} urlsToFilter array of urls to check
* @param {function} callback callback to execute on an url
*/
function ForEachUnusedUrl (urlsToFilter, callback)
{
for (var i = 0; i < urlsToFilter.length; i++) // If the url of the tab n°i is not present among all the collections, we call the callback on it
if (!Object.values(collections).some(collection => collection.links.some(link => link == urlsToFilter[i])))
callback(urlsToFilter[i]);
}
function AppendThumbnail(tabId, tab)
// page Captures are not always used in a collection, so we keep them in a specific variable until a collection is saved.
var sessionCaptures = { };
function AppendThumbnail (tab)
{
if (!tab.active || !tab.url.startsWith("http"))
return;
@@ -340,31 +472,22 @@ function AppendThumbnail(tabId, tab)
format: "jpeg",
quality: 1
},
(dataUrl) =>
(image) =>
{
if (!dataUrl)
if (!image)
{
console.log("Failed to retrieve thumbnail");
return;
}
console.log("Thumbnail retrieved");
var item = thumbnails.find(i => i.tabId == tabId);
if (item)
item.url = dataUrl;
else
thumbnails.unshift(
{
tabId: tabId,
url: dataUrl
}
);
sessionCaptures[tab.url] = image;
}
);
}
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) =>
{
if (changeInfo.status === "complete")
AppendThumbnail(tabId, tab)
if (changeInfo.status == "complete")
AppendThumbnail(tab);
});