mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-04-22 07:58:01 +03:00
Major 3.0 (#118)
Co-authored-by: Maison da Silva <maisonmdsgreen@hotmail.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { DragEndEvent } from "@dnd-kit/core";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import { DndItem } from "../../hooks/useDndItem";
|
||||
|
||||
export default function applyReorder(collections: CollectionItem[], { over, active }: DragEndEvent): null | CollectionItem[]
|
||||
{
|
||||
if (!over || active.id === over.id)
|
||||
return null;
|
||||
|
||||
const activeItem: DndItem = active.data.current as DndItem;
|
||||
const overItem: DndItem = over.data.current as DndItem;
|
||||
|
||||
console.log("DragEnd", `active: ${active.id} ${activeItem.item.type}`, `over: ${over.id} ${overItem.item.type}`);
|
||||
|
||||
let newList: CollectionItem[] = [
|
||||
...collections.map(collection => ({
|
||||
...collection,
|
||||
items: collection.items.map<TabItem | GroupItem>(item =>
|
||||
item.type === "group" ?
|
||||
{ ...item, items: item.items.map(tab => ({ ...tab })) } :
|
||||
{ ...item }
|
||||
)
|
||||
}))
|
||||
];
|
||||
|
||||
if (activeItem.item.type === "collection")
|
||||
{
|
||||
newList = arrayMove(
|
||||
newList,
|
||||
activeItem.indices[0],
|
||||
overItem.indices[0]
|
||||
);
|
||||
|
||||
return newList;
|
||||
}
|
||||
|
||||
const sourceItem: GroupItem | CollectionItem = activeItem.indices.length > 2 ?
|
||||
(newList[activeItem.indices[0]].items[activeItem.indices[1]] as GroupItem) :
|
||||
newList[activeItem.indices[0]];
|
||||
|
||||
if ((over.id as string).endsWith("_dropzone") || overItem.item.type === "collection")
|
||||
{
|
||||
const destItem: GroupItem | CollectionItem = overItem.indices.length > 1 ?
|
||||
(newList[overItem.indices[0]].items[overItem.indices[1]] as GroupItem) :
|
||||
newList[overItem.indices[0]];
|
||||
|
||||
destItem.items.push(activeItem.item as any);
|
||||
sourceItem.items.splice(activeItem.indices[activeItem.indices.length - 1], 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceItem.items = arrayMove(
|
||||
sourceItem.items,
|
||||
activeItem.indices[activeItem.indices.length - 1],
|
||||
overItem.indices[overItem.indices.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
return newList;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ClientRect, Collision, CollisionDescriptor, CollisionDetection } from "@dnd-kit/core";
|
||||
import { DndItem } from "../../hooks/useDndItem";
|
||||
import { centerOfRectangle, distanceBetween, getIntersectionRatio, getMaxIntersectionRatio, getRectSideCoordinates, sortCollisionsAsc } from "./dndUtils";
|
||||
|
||||
export function collisionDetector(vertical?: boolean): CollisionDetection
|
||||
{
|
||||
return (args): Collision[] =>
|
||||
{
|
||||
const { collisionRect, droppableContainers, droppableRects, active, pointerCoordinates } = args;
|
||||
const activeItem = active.data.current as DndItem;
|
||||
|
||||
if (!pointerCoordinates)
|
||||
return [];
|
||||
|
||||
const collisions: CollisionDescriptor[] = [];
|
||||
const centerRect = centerOfRectangle(
|
||||
collisionRect,
|
||||
collisionRect.left,
|
||||
collisionRect.top
|
||||
);
|
||||
|
||||
for (const droppableContainer of droppableContainers)
|
||||
{
|
||||
const { id, data } = droppableContainer;
|
||||
const rect = droppableRects.get(id);
|
||||
|
||||
const droppableItem: DndItem = data.current as DndItem;
|
||||
|
||||
if (!rect)
|
||||
continue;
|
||||
|
||||
let value: number = 0;
|
||||
|
||||
if (activeItem.item.type === "collection")
|
||||
{
|
||||
// If we drag a collection, we should ignore other items, like tabs or groups
|
||||
if (droppableItem.item.type !== "collection")
|
||||
continue;
|
||||
|
||||
// Using distance between centers
|
||||
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
||||
collisions.push({ id, data: { droppableContainer, value } });
|
||||
continue;
|
||||
}
|
||||
|
||||
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
|
||||
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
|
||||
|
||||
// Dragging a tab or a group over a collection
|
||||
if (droppableItem.item.type === "collection")
|
||||
{
|
||||
// Ignoring collection, if the tab or the group is inside that collection
|
||||
if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0])
|
||||
continue;
|
||||
|
||||
// Ignoring collection if we're dragging a tab or a group that doesn't belong to the collection,
|
||||
// but intersection ratio is less than 0.7
|
||||
if (intersectionCoefficient < 0.7)
|
||||
continue;
|
||||
|
||||
// If we're dragging a tab, that's inside a group that belongs to the collection,
|
||||
// we substract the group's intersection from the collection's one
|
||||
if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0])
|
||||
{
|
||||
const [collectionId, groupId] = activeItem.indices;
|
||||
const groupRect: ClientRect | undefined = droppableRects.get(`${collectionId}/${groupId}`);
|
||||
|
||||
if (!groupRect)
|
||||
continue;
|
||||
|
||||
value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect));
|
||||
}
|
||||
// Otherwise, use intersection ratio
|
||||
// At this point we're dragging either:
|
||||
// - a group, that doesn't belong to the collection
|
||||
// - a tab, that either belongs to the collection's group, or has intersection coefficient >= .7
|
||||
else
|
||||
{
|
||||
value = 2 / intersectionRatio;
|
||||
}
|
||||
}
|
||||
// If we're dragging a tab or a group over another group's dropzone
|
||||
else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone"))
|
||||
{
|
||||
// Ignore, if we're dragging a group
|
||||
if (activeItem.item.type === "group")
|
||||
continue;
|
||||
|
||||
// Ignore, if we're dragging a tab, that's inside the group
|
||||
if (
|
||||
activeItem.indices.length === 3 &&
|
||||
activeItem.indices[0] === droppableItem.indices[0] &&
|
||||
activeItem.indices[1] === droppableItem.indices[1]
|
||||
)
|
||||
continue;
|
||||
|
||||
// Ignore, if coefficient is less than .5
|
||||
// (at this point we're dragging a tab, that's outside of the group's dropzone)
|
||||
if (intersectionCoefficient < 0.5)
|
||||
continue;
|
||||
|
||||
// Use intersection between the tab and the group's dropzone
|
||||
value = 1 / intersectionRatio;
|
||||
}
|
||||
// We're dragging a group or a tab over its sibling
|
||||
else if (activeItem.indices.length === droppableItem.indices.length)
|
||||
{
|
||||
if (activeItem.indices[0] !== droppableItem.indices[0])
|
||||
continue;
|
||||
|
||||
if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1])
|
||||
continue;
|
||||
|
||||
// Ignore pinned groups
|
||||
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
|
||||
continue;
|
||||
|
||||
const collectionRect: ClientRect | undefined = droppableRects.get(activeItem.indices[0].toString());
|
||||
|
||||
if (!collectionRect)
|
||||
continue;
|
||||
|
||||
const collectionIntersectionRatio: number = getIntersectionRatio(collectionRect, collisionRect);
|
||||
const collectionIntersectionCoefficient: number = collectionIntersectionRatio / getMaxIntersectionRatio(collectionRect, collisionRect);
|
||||
|
||||
// Ignore if we are outside of the home collection
|
||||
if (collectionIntersectionCoefficient < 0.7)
|
||||
continue;
|
||||
|
||||
if (activeItem.item.type === "tab" && droppableItem.item.type === "tab")
|
||||
{
|
||||
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
const activeIndex: number = activeItem.indices[activeItem.indices.length - 1];
|
||||
const droppableIndex: number = droppableItem.indices[droppableItem.indices.length - 1];
|
||||
const before: boolean = activeIndex < droppableIndex;
|
||||
|
||||
value = distanceBetween(
|
||||
getRectSideCoordinates(rect, before, vertical),
|
||||
getRectSideCoordinates(collisionRect, before, vertical)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ((value > 0 && value < Number.POSITIVE_INFINITY) || active.id === id)
|
||||
collisions.push({ id, data: { droppableContainer, value } });
|
||||
};
|
||||
|
||||
return collisions.sort(sortCollisionsAsc);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { ClientRect, CollisionDescriptor } from "@dnd-kit/core";
|
||||
import { Coordinates } from "@dnd-kit/utilities";
|
||||
|
||||
export function getRectSideCoordinates(rect: ClientRect, before: boolean, vertical?: boolean)
|
||||
{
|
||||
if (before)
|
||||
return vertical ? bottomsideOfRect(rect) : rightsideOfRect(rect);
|
||||
|
||||
return vertical ? topsideOfRect(rect) : leftsideOfRect(rect);
|
||||
}
|
||||
|
||||
export function getMaxIntersectionRatio(entry: ClientRect, target: ClientRect): number
|
||||
{
|
||||
const entrySize = entry.width * entry.height;
|
||||
const targetSize = target.width * target.height;
|
||||
|
||||
return Math.min(targetSize / entrySize, entrySize / targetSize);
|
||||
}
|
||||
|
||||
function topsideOfRect(rect: ClientRect): Coordinates
|
||||
{
|
||||
const { left, top } = rect;
|
||||
|
||||
return {
|
||||
x: left + rect.width * 0.5,
|
||||
y: top
|
||||
};
|
||||
}
|
||||
|
||||
function bottomsideOfRect(rect: ClientRect): Coordinates
|
||||
{
|
||||
const { left, bottom } = rect;
|
||||
return {
|
||||
x: left + rect.width * 0.5,
|
||||
y: bottom
|
||||
};
|
||||
}
|
||||
|
||||
function rightsideOfRect(rect: ClientRect): Coordinates
|
||||
{
|
||||
const { right, top } = rect;
|
||||
return {
|
||||
x: right,
|
||||
y: top + rect.height * 0.5
|
||||
};
|
||||
}
|
||||
|
||||
function leftsideOfRect(rect: ClientRect): Coordinates
|
||||
{
|
||||
const { left, top } = rect;
|
||||
return {
|
||||
x: left,
|
||||
y: top + rect.height * 0.5
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021, Claudéric Demers
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export function distanceBetween(p1: Coordinates, p2: Coordinates)
|
||||
{
|
||||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||
}
|
||||
|
||||
export function sortCollisionsAsc(
|
||||
{ data: { value: a } }: CollisionDescriptor,
|
||||
{ data: { value: b } }: CollisionDescriptor
|
||||
)
|
||||
{
|
||||
return a - b;
|
||||
}
|
||||
|
||||
export function getIntersectionRatio(entry: ClientRect, target: ClientRect): number
|
||||
{
|
||||
const top = Math.max(target.top, entry.top);
|
||||
const left = Math.max(target.left, entry.left);
|
||||
const right = Math.min(target.left + target.width, entry.left + entry.width);
|
||||
const bottom = Math.min(target.top + target.height, entry.top + entry.height);
|
||||
const width = right - left;
|
||||
const height = bottom - top;
|
||||
|
||||
if (left < right && top < bottom)
|
||||
{
|
||||
const targetArea = target.width * target.height;
|
||||
const entryArea = entry.width * entry.height;
|
||||
const intersectionArea = width * height;
|
||||
const intersectionRatio =
|
||||
intersectionArea / (targetArea + entryArea - intersectionArea);
|
||||
|
||||
return Number(intersectionRatio.toFixed(4));
|
||||
}
|
||||
|
||||
// Rectangles do not overlap, or overlap has an area of zero (edge/corner overlap)
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function centerOfRectangle(
|
||||
rect: ClientRect,
|
||||
left = rect.left,
|
||||
top = rect.top
|
||||
): Coordinates
|
||||
{
|
||||
return {
|
||||
x: left + rect.width * 0.5,
|
||||
y: top + rect.height * 0.5
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Modifier } from "@dnd-kit/core";
|
||||
import { Coordinates, getEventCoordinates } from "@dnd-kit/utilities";
|
||||
import { DndItem } from "../../hooks/useDndItem";
|
||||
|
||||
export const snapHandleToCursor: Modifier = ({
|
||||
activatorEvent,
|
||||
draggingNodeRect,
|
||||
transform,
|
||||
active
|
||||
}) =>
|
||||
{
|
||||
if (draggingNodeRect && activatorEvent)
|
||||
{
|
||||
const activeItem: DndItem | undefined = active?.data.current as DndItem;
|
||||
const activatorCoordinates: Coordinates | null = getEventCoordinates(activatorEvent);
|
||||
|
||||
if (!activatorCoordinates)
|
||||
return transform;
|
||||
|
||||
const initX: number = activatorCoordinates.x - draggingNodeRect.left;
|
||||
const initY: number = activatorCoordinates.y - draggingNodeRect.top;
|
||||
|
||||
const offsetX: number = activeItem?.item.type === "group" ? 24 : draggingNodeRect.height / 2;
|
||||
const offsetY: number = activeItem?.item.type === "group" ? 20 : draggingNodeRect.height / 2;
|
||||
|
||||
return {
|
||||
...transform,
|
||||
x: transform.x + initX - offsetX,
|
||||
y: transform.y + initY - offsetY
|
||||
};
|
||||
}
|
||||
|
||||
return transform;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Bookmarks } from "wxt/browser";
|
||||
import { getCollectionTitle } from "./getCollectionTitle";
|
||||
|
||||
export default async function exportCollectionToBookmarks(collection: CollectionItem)
|
||||
{
|
||||
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||
title: getCollectionTitle(collection)
|
||||
});
|
||||
|
||||
for (let i = 0; i < collection.items.length; i++)
|
||||
{
|
||||
const item = collection.items[i];
|
||||
|
||||
if (item.type === "tab")
|
||||
{
|
||||
await createTabBookmark(item, rootFolder.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
const groupFolder = await browser.bookmarks.create({
|
||||
parentId: rootFolder.id,
|
||||
title: item.pinned
|
||||
? `📌 ${i18n.t("groups.pinned")}` :
|
||||
(item.title?.trim() || `${i18n.t("groups.title")} ${i}`)
|
||||
});
|
||||
|
||||
for (const tab of item.items)
|
||||
await createTabBookmark(tab, groupFolder.id);
|
||||
}
|
||||
}
|
||||
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.bookmark_saved.title"),
|
||||
message: i18n.t("notifications.bookmark_saved.message"),
|
||||
icon: "/notification_icons/bookmark_add.png"
|
||||
});
|
||||
}
|
||||
|
||||
async function createTabBookmark(tab: TabItem, parentId: string): Promise<void>
|
||||
{
|
||||
await browser.bookmarks.create({
|
||||
parentId,
|
||||
title: tab.title?.trim() || tab.url,
|
||||
url: tab.url
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
|
||||
export default function filterCollections(
|
||||
collections: CollectionItem[] | null,
|
||||
filter: CollectionFilterType
|
||||
): CollectionItem[]
|
||||
{
|
||||
if (!collections || collections.length < 1)
|
||||
return [];
|
||||
|
||||
if (!filter.query && filter.colors.length < 1)
|
||||
return collections;
|
||||
|
||||
const query: string = filter.query.toLocaleLowerCase();
|
||||
|
||||
return collections.filter(collection =>
|
||||
{
|
||||
let querySatisfied: boolean = query.length < 1 ||
|
||||
getCollectionTitle(collection).toLocaleLowerCase().includes(query);
|
||||
let colorSatisfied: boolean = filter.colors.length < 1 ||
|
||||
filter.colors.includes(collection.color ?? "none");
|
||||
|
||||
if (querySatisfied && colorSatisfied)
|
||||
return true;
|
||||
|
||||
function probeTab(tab: TabItem, query: string): boolean
|
||||
{
|
||||
return tab.title?.toLocaleLowerCase().includes(query) || tab.url.toLocaleLowerCase().includes(query);
|
||||
}
|
||||
|
||||
for (const item of collection.items)
|
||||
{
|
||||
if (item.type === "tab" && !querySatisfied)
|
||||
{
|
||||
querySatisfied = probeTab(item, query);
|
||||
}
|
||||
else if (item.type === "group")
|
||||
{
|
||||
if (item.pinned !== true)
|
||||
{
|
||||
if (!querySatisfied)
|
||||
querySatisfied = (item.title?.toLocaleLowerCase() ?? "").includes(query);
|
||||
|
||||
if (!colorSatisfied)
|
||||
colorSatisfied = filter.colors.includes(item.color);
|
||||
}
|
||||
|
||||
if (!querySatisfied)
|
||||
querySatisfied = item.items.some(i => probeTab(i, query));
|
||||
}
|
||||
|
||||
if (querySatisfied && colorSatisfied)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export type CollectionFilterType =
|
||||
{
|
||||
query: string;
|
||||
colors: (chrome.tabGroups.ColorEnum | "none")[];
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
|
||||
export function getCollectionTitle(collection?: CollectionItem, useTimestamp?: boolean): string
|
||||
{
|
||||
if (collection?.title !== undefined && useTimestamp !== true)
|
||||
return collection.title;
|
||||
|
||||
return new Date(collection?.timestamp ?? Date.now())
|
||||
.toLocaleDateString(browser.i18n.getUILanguage(), { year: "numeric", month: "short", day: "numeric" });
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Tabs } from "wxt/browser";
|
||||
|
||||
export default async function getSelectedTabs(): Promise<TabItem[]>
|
||||
{
|
||||
let tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
|
||||
const tabCount: number = tabs.length;
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
i.url
|
||||
&& new URL(i.url).protocol !== "about:"
|
||||
&& new URL(i.url).hostname !== "newtab"
|
||||
);
|
||||
|
||||
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 tabs.map(i => ({ type: "tab", url: i.url!, title: i.title }));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
|
||||
export default function mergePinnedGroups(collection: CollectionItem): void
|
||||
{
|
||||
const pinnedItems: TabItem[] = [];
|
||||
const otherItems: CollectionItem["items"] = [];
|
||||
let pinExists: boolean = false;
|
||||
|
||||
collection.items.forEach(item =>
|
||||
{
|
||||
if (item.type === "group" && item.pinned === true)
|
||||
{
|
||||
pinExists = true;
|
||||
pinnedItems.push(...item.items);
|
||||
}
|
||||
else
|
||||
otherItems.push(item);
|
||||
});
|
||||
|
||||
if (pinnedItems.length > 0 || pinExists)
|
||||
collection.items = [
|
||||
{ type: "group", pinned: true, items: pinnedItems },
|
||||
...otherItems
|
||||
];
|
||||
else
|
||||
collection.items = otherItems;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { Tabs, Windows } from "wxt/browser";
|
||||
|
||||
export async function openCollection(collection: CollectionItem, targetWindow?: "current" | "new" | "incognito"): Promise<void>
|
||||
{
|
||||
if (targetWindow === "incognito" && !(await browser.extension.isAllowedIncognitoAccess()))
|
||||
throw new Error("The extension doesn't have incognito permission");
|
||||
|
||||
const discard: boolean = await settings.dismissOnLoad.getValue();
|
||||
|
||||
await manageWindow(
|
||||
async windowId =>
|
||||
{
|
||||
if (collection.items.some(i => i.type === "group"))
|
||||
// Open tabs as regular, open groups as groups
|
||||
await Promise.all(collection.items.map(async i =>
|
||||
{
|
||||
if (i.type === "tab")
|
||||
await createTab(i.url, windowId, discard);
|
||||
else
|
||||
await createGroup(i, windowId, discard);
|
||||
}));
|
||||
|
||||
else if (collection.color)
|
||||
// Open collection as one big group
|
||||
await createGroup({
|
||||
type: "group",
|
||||
color: collection.color,
|
||||
title: getCollectionTitle(collection),
|
||||
items: collection.items as TabItem[]
|
||||
}, windowId);
|
||||
|
||||
else
|
||||
// Open collection tabs as is
|
||||
await Promise.all(collection.items.map(async i =>
|
||||
await createTab((i as TabItem).url, windowId, discard)
|
||||
));
|
||||
},
|
||||
(!targetWindow || targetWindow === "current") ?
|
||||
undefined :
|
||||
{ incognito: targetWindow === "incognito" }
|
||||
);
|
||||
}
|
||||
|
||||
export async function openGroup(group: GroupItem, newWindow: boolean = false): Promise<void>
|
||||
{
|
||||
await manageWindow(
|
||||
windowId => createGroup(group, windowId),
|
||||
newWindow ? {} : undefined
|
||||
);
|
||||
}
|
||||
|
||||
async function createGroup(group: GroupItem, windowId: number, discard?: boolean): Promise<void>
|
||||
{
|
||||
discard ??= await settings.dismissOnLoad.getValue();
|
||||
const tabs: Tabs.Tab[] = await Promise.all(group.items.map(async i =>
|
||||
await createTab(i.url, windowId, discard, group.pinned)
|
||||
));
|
||||
|
||||
// "Pinned" group is technically not a group, so not much else to do here
|
||||
if (group.pinned === true)
|
||||
return;
|
||||
|
||||
const groupId: number = await chrome.tabs.group({
|
||||
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!),
|
||||
createProperties: { windowId }
|
||||
});
|
||||
|
||||
await chrome.tabGroups.update(groupId, {
|
||||
title: group.title,
|
||||
color: group.color
|
||||
});
|
||||
}
|
||||
|
||||
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Windows.CreateCreateDataType): Promise<void>
|
||||
{
|
||||
const currentWindow: Windows.Window = windowProps ?
|
||||
await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }) :
|
||||
await browser.windows.getCurrent();
|
||||
const windowId: number = currentWindow.id!;
|
||||
|
||||
await handle(windowId);
|
||||
|
||||
await browser.windows.update(windowId, { focused: true });
|
||||
|
||||
if (windowProps)
|
||||
// Close "about:blank" tab
|
||||
await browser.tabs.remove(currentWindow.tabs![0].id!);
|
||||
}
|
||||
|
||||
async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<Tabs.Tab>
|
||||
{
|
||||
const tab = await browser.tabs.create({ url, windowId: windowId, active: false, pinned });
|
||||
|
||||
if (discard)
|
||||
discardOnLoad(tab.id!);
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
function discardOnLoad(tabId: number): void
|
||||
{
|
||||
const handleTabUpdated = (id: number, _: any, tab: Tabs.Tab) =>
|
||||
{
|
||||
if (id !== tabId || !tab.url)
|
||||
return;
|
||||
|
||||
browser.tabs.onUpdated.removeListener(handleTabUpdated);
|
||||
|
||||
if (!tab.active)
|
||||
browser.tabs.discard(tabId);
|
||||
};
|
||||
|
||||
browser.tabs.onUpdated.addListener(handleTabUpdated);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
|
||||
export default function sortCollections(
|
||||
collections: CollectionItem[],
|
||||
mode?: CollectionSortMode | null
|
||||
): CollectionItem[]
|
||||
{
|
||||
return sorters[mode ?? "custom"]([...collections]);
|
||||
}
|
||||
|
||||
export type CollectionSortMode = "ascending" | "descending" | "newest" | "oldest" | "custom";
|
||||
|
||||
const sorters: Record<CollectionSortMode, CollectionSorter> =
|
||||
{
|
||||
ascending: i => i.sort((a, b) => getCollectionTitle(a).localeCompare(getCollectionTitle(b))),
|
||||
descending: i => i.sort((a, b) => getCollectionTitle(b).localeCompare(getCollectionTitle(a))),
|
||||
newest: i => i.sort((a, b) => b.timestamp - a.timestamp),
|
||||
oldest: i => i.sort((a, b) => a.timestamp - b.timestamp),
|
||||
custom: i => i
|
||||
};
|
||||
|
||||
type CollectionSorter = (collections: CollectionItem[]) => CollectionItem[];
|
||||
Reference in New Issue
Block a user