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:
+252
-129
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user