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
@@ -0,0 +1,109 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels";
import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components";
import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import CollectionMoreButton from "./CollectionMoreButton";
import OpenCollectionButton from "./OpenCollectionButton";
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
{
const { updateCollection } = useCollections();
const { tabCount, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
const AddIcon = bundleIcon(Add20Filled, Add20Regular);
const handleAddSelected = async () =>
{
const newTabs: TabItem[] = await getSelectedTabs();
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collectionIndex);
};
const cls = useStyles();
return (
<div className={ cls.header }>
<div className={ cls.title } ref={ dragHandleRef } { ...dragHandleProps }>
<Tooltip
relationship="description"
content={ getCollectionTitle(collection) }
positioning="above-start"
>
<Subtitle2 truncate wrap={ false } className={ cls.titleText }>
{ getCollectionTitle(collection) }
</Subtitle2>
</Tooltip>
<Caption1>
{ i18n.t("collections.tabs_count", [tabCount]) }
</Caption1>
</div>
<div
className={
mergeClasses(
cls.toolbar,
"CollectionView__toolbar",
alwaysShowToolbars === true && cls.showToolbar
) }
>
{ tabCount < 1 ?
<Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }>
{ i18n.t("collections.menu.add_selected") }
</Button>
:
<OpenCollectionButton />
}
<CollectionMoreButton onAddSelected={ handleAddSelected } />
</div>
</div>
);
}
export type CollectionHeaderProps =
{
dragHandleRef?: React.LegacyRef<HTMLDivElement>;
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
};
const useStyles = makeStyles({
header:
{
color: "var(--border)",
display: "grid",
gridTemplateColumns: "1fr auto",
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
paddingBottom: tokens.spacingVerticalS
},
title:
{
display: "flex",
flexFlow: "column",
alignItems: "flex-start",
overflow: "hidden"
},
titleText:
{
maxWidth: "100%"
},
toolbar:
{
display: "none",
gap: tokens.spacingHorizontalS,
alignItems: "flex-start",
"@media (pointer: coarse)":
{
display: "flex"
}
},
showToolbar:
{
display: "flex"
}
});
@@ -0,0 +1,114 @@
import { useDialog } from "@/contexts/DialogProvider";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings";
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import exportCollectionToBookmarks from "../../utils/exportCollectionToBookmarks";
import EditDialog from "../EditDialog";
export default function CollectionMoreButton({ onAddSelected }: CollectionMoreButtonProps): React.ReactElement
{
const { removeItem, updateCollection } = useCollections();
const { tabCount, hasPinnedGroup, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const dialog = useDialog();
const [deletePrompt] = useSettings("deletePrompt");
const AddIcon = ic.bundleIcon(ic.Add20Filled, ic.Add20Regular);
const GroupIcon = ic.bundleIcon(ic.GroupList20Filled, ic.GroupList20Regular);
const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular);
const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular);
const PinnedIcon = ic.bundleIcon(ic.Pin20Filled, ic.Pin20Regular);
const BookmarkIcon = ic.bundleIcon(ic.BookmarkAdd20Filled, ic.BookmarkAdd20Regular);
const dangerCls = useDangerStyles();
const handleDelete = () =>
{
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("collections.menu.delete"),
content: i18n.t("common.delete_prompt"),
destructive: true,
confirmText: i18n.t("common.actions.delete"),
onConfirm: () => removeItem(collectionIndex)
});
else
removeItem(collectionIndex);
};
const handleEdit = () =>
dialog.pushCustom(
<EditDialog
type="collection"
collection={ collection }
onSave={ item => updateCollection(item, collectionIndex) } />
);
const handleCreateGroup = () =>
dialog.pushCustom(
<EditDialog
type="group"
hidePinned={ hasPinnedGroup }
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collectionIndex) } />
);
const handleAddPinnedGroup = () =>
{
updateCollection({
...collection,
items: [
{ type: "group", pinned: true, items: [] },
...collection.items
]
}, collectionIndex);
};
return (
<Menu>
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<MenuTrigger>
<Button appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
</MenuTrigger>
</Tooltip>
<MenuPopover>
<MenuList>
{ tabCount > 0 &&
<MenuItem icon={ <AddIcon /> } onClick={ () => onAddSelected?.() }>
{ i18n.t("collections.menu.add_selected") }
</MenuItem>
}
{ !import.meta.env.FIREFOX &&
<MenuItem icon={ <GroupIcon /> } onClick={ handleCreateGroup }>
{ i18n.t("collections.menu.add_group") }
</MenuItem>
}
{ (import.meta.env.FIREFOX && !hasPinnedGroup) &&
<MenuItem icon={ <PinnedIcon /> } onClick={ handleAddPinnedGroup }>
{ i18n.t("collections.menu.add_pinned") }
</MenuItem>
}
{ tabCount > 0 &&
<MenuItem icon={ <BookmarkIcon /> } onClick={ () => exportCollectionToBookmarks(collection) }>
{ i18n.t("collections.menu.export_bookmarks") }
</MenuItem>
}
<MenuDivider />
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
{ i18n.t("collections.menu.edit") }
</MenuItem>
<MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ handleDelete }>
{ i18n.t("collections.menu.delete") }
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
}
export type CollectionMoreButtonProps =
{
onAddSelected?: () => void;
};
@@ -0,0 +1,45 @@
import { useDroppable } from "@dnd-kit/core";
import { makeStyles, mergeClasses, tokens } from "@fluentui/react-components";
import GroupContext, { GroupContextType } from "../../contexts/GroupContext";
export default function GroupDropZone({ disabled, ...props }: DropZoneProps): React.ReactElement
{
const { group, indices } = useContext<GroupContextType>(GroupContext);
const id: string = indices.join("/") + "_dropzone";
const { isOver, setNodeRef, active } = useDroppable({ id, data: { indices, item: group }, disabled });
const isDragging = !disabled && active !== null;
const cls = useStyles();
return (
<div
ref={ isDragging ? setNodeRef : undefined } { ...props }
className={ mergeClasses(cls.root, isDragging && cls.dragging, isOver && cls.over, props.className) }
>
{ props.children }
</div>
);
}
export type DropZoneProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
& {
disabled?: boolean;
};
const useStyles = makeStyles({
root:
{
borderRadius: tokens.borderRadiusLarge,
borderTopRightRadius: tokens.borderRadiusNone,
border: `${tokens.strokeWidthThin} solid transparent`
},
over:
{
backgroundColor: tokens.colorBrandBackground2,
border: `${tokens.strokeWidthThin} solid ${tokens.colorBrandStroke1}`
},
dragging:
{
border: `${tokens.strokeWidthThin} dashed ${tokens.colorNeutralStroke1}`
}
});
@@ -0,0 +1,101 @@
import { useDialog } from "@/contexts/DialogProvider";
import EditDialog from "@/entrypoints/sidepanel/components/EditDialog";
import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext";
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import { ReactElement } from "react";
import { openGroup } from "../../utils/opener";
export default function GroupMoreMenu(): ReactElement
{
const { group, indices } = useContext<GroupContextType>(GroupContext);
const { hasPinnedGroup } = useContext<CollectionContextType>(CollectionContext);
const [deletePrompt] = useSettings("deletePrompt");
const dialog = useDialog();
const { updateGroup, removeItem, ungroup } = useCollections();
const dangerCls = useDangerStyles();
const AddIcon = ic.bundleIcon(ic.Add20Filled, ic.Add20Regular);
const UngroupIcon = ic.bundleIcon(ic.FullScreenMaximize20Filled, ic.FullScreenMaximize20Regular);
const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular);
const NewWindowIcon = ic.bundleIcon(ic.WindowNew20Filled, ic.WindowNew20Regular);
const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular);
const handleDelete = () =>
{
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("groups.menu.delete"),
content: i18n.t("common.delete_prompt"),
confirmText: i18n.t("common.actions.delete"),
destructive: true,
onConfirm: () => removeItem(...indices)
});
else
removeItem(...indices);
};
const handleEdit = () =>
dialog.pushCustom(
<EditDialog
type="group"
group={ group }
hidePinned={ hasPinnedGroup }
onSave={ item => updateGroup(item, indices[0], indices[1]) } />
);
const handleAddSelected = async () =>
{
const newTabs: TabItem[] = await getSelectedTabs();
updateGroup({ ...group, items: [...group.items, ...newTabs] }, indices[0], indices[1]);
};
return (
<Menu>
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<MenuTrigger>
<Button size="small" appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
</MenuTrigger>
</Tooltip>
<MenuPopover>
<MenuList>
{ group.items.length > 0 &&
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
{ i18n.t("groups.menu.new_window") }
</MenuItem>
}
<MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }>
{ i18n.t("groups.menu.add_selected") }
</MenuItem>
{ (!import.meta.env.FIREFOX || group.pinned !== true) &&
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
{ i18n.t("groups.menu.edit") }
</MenuItem>
}
{ group.items.length > 0 &&
<MenuItem
className={ dangerCls.menuItem }
icon={ <UngroupIcon /> }
onClick={ () => ungroup(indices[0], indices[1]) }
>
{ i18n.t("groups.menu.ungroup") }
</MenuItem>
}
<MenuItem className={ dangerCls.menuItem } icon={ <DeleteIcon /> } onClick={ handleDelete }>
{ i18n.t("groups.menu.delete") }
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
}
@@ -0,0 +1,97 @@
import { useDialog } from "@/contexts/DialogProvider";
import useSettings from "@/hooks/useSettings";
import browserLocaleKey from "@/utils/browserLocaleKey";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import { openCollection } from "../../utils/opener";
export default function OpenCollectionButton(): React.ReactElement
{
const [defaultAction] = useSettings("defaultRestoreAction");
const { removeItem } = useCollections();
const dialog = useDialog();
const { collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const OpenIcon = ic.bundleIcon(ic.Open20Filled, ic.Open20Regular);
const RestoreIcon = ic.bundleIcon(ic.ArrowExportRtl20Filled, ic.ArrowExportRtl20Regular);
const NewWindowIcon = ic.bundleIcon(ic.WindowNew20Filled, ic.WindowNew20Regular);
const InPrivateIcon = ic.bundleIcon(ic.TabInPrivate20Filled, ic.TabInPrivate20Regular);
const handleIncognito = async () =>
{
if (await browser.extension.isAllowedIncognitoAccess())
openCollection(collection, "incognito");
else
dialog.pushPrompt({
title: i18n.t("collections.incognito_check.title"),
content: (
<>
{ i18n.t(`collections.incognito_check.message.${browserLocaleKey}.p1`) }
<br />
<br />
{ i18n.t(`collections.incognito_check.message.${browserLocaleKey}.p2`) }
</>
),
confirmText: i18n.t("collections.incognito_check.action"),
onConfirm: async () => import.meta.env.FIREFOX ?
await browser.runtime.openOptionsPage() :
await browser.tabs.create({
url: `chrome://extensions/?id=${browser.runtime.id}`,
active: true
})
});
};
const handleOpen = (mode: "current" | "new") =>
() => openCollection(collection, mode);
const handleRestore = async () =>
{
await openCollection(collection);
removeItem(collectionIndex);
};
return (
<Menu>
<MenuTrigger disableButtonEnhancement>
{ (triggerProps: MenuButtonProps) => defaultAction === "restore" ?
<SplitButton
appearance="subtle" icon={ <RestoreIcon /> } menuButton={ triggerProps }
primaryActionButton={ { onClick: handleRestore } }
>
{ i18n.t("collections.actions.restore") }
</SplitButton>
:
<SplitButton
appearance="subtle" icon={ <OpenIcon /> } menuButton={ triggerProps }
primaryActionButton={ { onClick: handleOpen("current") } }
>
{ i18n.t("collections.actions.open") }
</SplitButton>
}
</MenuTrigger>
<MenuPopover>
<MenuList>
{ defaultAction === "restore" ?
<MenuItem icon={ <OpenIcon /> } onClick={ handleOpen("current") }>
{ i18n.t("collections.actions.open") }
</MenuItem>
:
<MenuItem icon={ <RestoreIcon /> } onClick={ handleRestore }>
{ i18n.t("collections.actions.restore") }
</MenuItem>
}
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => handleOpen("new") }>
{ i18n.t("collections.actions.new_window") }
</MenuItem>
<MenuItem icon={ <InPrivateIcon /> } onClick={ handleIncognito }>
{ i18n.t(`collections.actions.incognito.${browserLocaleKey}`) }
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
}