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

!feat: major 3.0 release candidate

This commit is contained in:
2025-05-03 23:59:43 +03:00
parent dbc8c7fd4d
commit 39793a38c3
143 changed files with 14277 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
const browserLocaleKey: "firefox" | "edge" | "chrome" =
import.meta.env.FIREFOX ?
"firefox" :
import.meta.env.EDGE ?
"edge" :
"chrome";
export default browserLocaleKey;
+7
View File
@@ -0,0 +1,7 @@
const extLink = (href: string) => ({
href,
target: "_blank",
rel: "noopener"
});
export default extLink;
+13
View File
@@ -0,0 +1,13 @@
/**
* Creates a logger function for a specific component.
* The logger prepends a standardized prefix to all log messages,
* indicating the component name for easier debugging.
*
* @param component - The name of the component to include in the log prefix.
* @returns A logging function that accepts any number of arguments and logs them
* to the console with the component-specific prefix.
*/
export default function getLogger(component: string): (...data: any[]) => void
{
return (...data: any[]): void => console.log(`[TabsAside.${component}]`, ...data);
}
+26
View File
@@ -0,0 +1,26 @@
import { GraphicsStorage } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessenger } from "@webext-core/messaging";
type ProtocolMap =
{
addThumbnail(data: { url: string; thumbnail: string; }): void;
getGraphicsCache(): GraphicsStorage;
refreshCollections(): void;
};
const protocol: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>();
export const onMessage = protocol.onMessage;
export const sendMessage: ExtensionMessenger<ProtocolMap>["sendMessage"] = async (...args) =>
{
try
{
return await protocol.sendMessage(...args);
}
catch (ex)
{
console.error(ex);
return undefined!;
}
};
+134
View File
@@ -0,0 +1,134 @@
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
import { Tabs } from "wxt/browser";
import { settings } from "./settings";
import sendNotification from "./sendNotification";
export default async function saveTabsToCollection(closeTabs: boolean): Promise<CollectionItem>
{
let tabs: Tabs.Tab[] = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
if (tabs.length < 2)
{
const ignorePinned: boolean = await settings.ignorePinned.getValue();
tabs = await browser.tabs.query({
currentWindow: true,
pinned: ignorePinned ? false : undefined
});
}
const [collection, tabsToClose] = await createCollectionFromTabs(tabs);
if (closeTabs)
await browser.tabs.remove(tabsToClose.map(i => i.id!));
return collection;
}
async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionItem, Tabs.Tab[]]>
{
if (tabs.length < 1)
return [{ type: "collection", timestamp: Date.now(), items: [] }, []];
const tabCount: number = tabs.length;
tabs = tabs.filter(i =>
i.url &&
!i.url.startsWith(browser.runtime.getURL("/")) &&
new URL(i.url).protocol !== "about:" &&
new URL(i.url).hostname !== "newtab"
);
const collection: CollectionItem = {
type: "collection",
timestamp: Date.now(),
items: []
};
let tabIndex: number = 0;
if (tabs[tabIndex].pinned)
{
collection.items.push({ type: "group", pinned: true, items: [] });
for (; tabIndex < tabs.length; tabIndex++)
{
if (!tabs[tabIndex].pinned)
break;
(collection.items[0] as GroupItem).items.push({
type: "tab",
url: tabs[tabIndex].url!,
title: tabs[tabIndex].title
});
}
}
if (import.meta.env.FIREFOX)
{
for (; tabIndex < tabs.length; tabIndex++)
collection.items.push({ type: "tab", url: tabs[tabIndex].url!, title: tabs[tabIndex].title });
return [collection, tabs];
}
// Special case, if all tabs are in the same group, create a collection with the group title
if (tabs[0].groupId && tabs[0].groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE &&
tabs.every(i => i.groupId === tabs[0].groupId)
)
{
const group = await browser.tabGroups!.get(tabs[0].groupId);
collection.title = group.title;
collection.color = group.color;
tabs.forEach(i =>
collection.items.push({ type: "tab", url: i.url!, title: i.title })
);
return [collection, tabs];
}
let activeGroup: number | null = null;
for (; tabIndex < tabs.length; tabIndex++)
{
const tab = tabs[tabIndex];
if (!tab.groupId || tab.groupId === chrome.tabGroups.TAB_GROUP_ID_NONE)
{
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
activeGroup = null;
continue;
}
if (!activeGroup || activeGroup !== tab.groupId)
{
activeGroup = tab.groupId;
const group = await browser.tabGroups!.get(activeGroup);
collection.items.push({
type: "group",
color: group.color,
title: group.title,
items: []
});
}
(collection.items[collection.items.length - 1] as GroupItem).items.push({
type: "tab",
url: tab.url!,
title: tab.title
});
}
if (tabs.length < tabCount)
await sendNotification({
title: i18n.t("notifications.partial_save.title"),
message: i18n.t("notifications.partial_save.message"),
icon: "/notification_icons/save_warning.png"
});
return [collection, tabs];
}
+26
View File
@@ -0,0 +1,26 @@
import { PublicPath } from "wxt/browser";
export default async function sendNotification(props: NotificationProps): Promise<void>
{
try
{
await browser.notifications.create({
type: "basic",
title: props.title,
message: props.message,
iconUrl: browser.runtime.getURL(props.icon)
});
}
catch (ex)
{
console.error("Error while showing cloud error notification (probably because of user restrictions)");
console.error(ex);
}
}
export type NotificationProps =
{
title: string;
message: string;
icon: PublicPath;
};
+99
View File
@@ -0,0 +1,99 @@
import { CollectionSortMode } from "@/entrypoints/sidepanel/utils/sortCollections";
export const settings = {
defaultRestoreAction: storage.defineItem<"open" | "restore">(
"sync:defaultRestoreAction",
{
fallback: "open",
version: 1
}
),
defaultSaveAction: storage.defineItem<"save" | "set_aside">(
"sync:defaultSaveAction",
{
fallback: "set_aside",
version: 1
}
),
dismissOnLoad: storage.defineItem<boolean>(
"sync:dismissOnLoad",
{
fallback: false,
version: 1
}
),
deletePrompt: storage.defineItem<boolean>(
"sync:deletePrompt",
{
fallback: true,
version: 1
}
),
tilesView: storage.defineItem<boolean>(
"sync:tilesView",
{
fallback: true,
version: 1
}
),
sortMode: storage.defineItem<CollectionSortMode>(
"sync:sortMode",
{
fallback: "custom",
version: 1
}
),
ignorePinned: storage.defineItem<boolean>(
"sync:ignorePinned",
{
fallback: true,
version: 1
}
),
alwaysShowToolbars: storage.defineItem<boolean>(
"sync:alwaysShowToolbars",
{
fallback: false,
version: 1
}
),
showBadge: storage.defineItem<boolean>(
"sync:showBadge",
{
fallback: true,
version: 1
}
),
contextAction: storage.defineItem<"action" | "context" | "open">(
"sync:contextAction",
{
fallback: "open",
version: 1
}
),
listLocation: storage.defineItem<"sidebar" | "popup" | "tab" | "pinned">(
"sync:listLocation",
{
fallback: "sidebar",
version: 1
}
),
notifyOnSave: storage.defineItem<boolean>(
"sync:notifyOnSave",
{
fallback: true,
version: 1
}
)
};
+20
View File
@@ -0,0 +1,20 @@
import { Unwatch } from "wxt/storage";
export default function watchTabSelection(onChange: TabSelectChangeHandler): Unwatch
{
const handleTabSelection = async () =>
{
const highlightedTabs = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
onChange(highlightedTabs.length > 1 ? "selected" : "all");
};
handleTabSelection();
browser.tabs.onHighlighted.addListener(handleTabSelection);
return () => browser.tabs.onHighlighted.removeListener(handleTabSelection);
}
export type TabSelectChangeHandler = (selection: "all" | "selected") => void;