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,102 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles_CollectionView = makeStyles({
|
||||
root:
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
|
||||
borderRadius: tokens.borderRadiusLarge,
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
|
||||
"--border": tokens.colorNeutralForeground1,
|
||||
|
||||
"&:hover .CollectionView__toolbar, &:focus-within .CollectionView__toolbar":
|
||||
{
|
||||
display: "flex"
|
||||
},
|
||||
|
||||
"&:hover":
|
||||
{
|
||||
boxShadow: tokens.shadow4
|
||||
}
|
||||
},
|
||||
color:
|
||||
{
|
||||
border: `${tokens.strokeWidthThick} solid var(--border)`
|
||||
},
|
||||
verticalRoot:
|
||||
{
|
||||
maxHeight: "560px"
|
||||
},
|
||||
empty:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flexGrow: 1,
|
||||
margin: `${tokens.spacingVerticalNone} ${tokens.spacingHorizontalSNudge}`,
|
||||
marginBottom: tokens.spacingVerticalSNudge,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: tokens.spacingVerticalS,
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalL}`,
|
||||
color: tokens.colorNeutralForeground3,
|
||||
height: "144px"
|
||||
},
|
||||
emptyText:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingVerticalXS
|
||||
},
|
||||
emptyCaption:
|
||||
{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
columnGap: tokens.spacingHorizontalXS
|
||||
},
|
||||
list:
|
||||
{
|
||||
display: "grid",
|
||||
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
|
||||
columnGap: tokens.spacingHorizontalS,
|
||||
rowGap: tokens.spacingHorizontalSNudge,
|
||||
overflowX: "auto",
|
||||
alignItems: "flex-end",
|
||||
alignSelf: "flex-start",
|
||||
maxWidth: "100%",
|
||||
gridAutoFlow: "column"
|
||||
},
|
||||
verticalList:
|
||||
{
|
||||
gridAutoFlow: "row",
|
||||
width: "100%",
|
||||
paddingBottom: tokens.spacingVerticalS,
|
||||
gridAutoRows: import.meta.env.FIREFOX ? "min-content" : undefined
|
||||
},
|
||||
dragOverlay:
|
||||
{
|
||||
cursor: "grabbing !important",
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: `${tokens.shadow16} !important`,
|
||||
"& > div":
|
||||
{
|
||||
pointerEvents: "none"
|
||||
}
|
||||
},
|
||||
sorting:
|
||||
{
|
||||
pointerEvents: "none"
|
||||
},
|
||||
dragging:
|
||||
{
|
||||
visibility: "hidden"
|
||||
},
|
||||
draggingOver:
|
||||
{
|
||||
backgroundColor: tokens.colorBrandBackground2
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import CollectionHeader from "@/entrypoints/sidepanel/components/collections/CollectionHeader";
|
||||
import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem";
|
||||
import { useGroupColors } from "@/hooks/useGroupColors";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { horizontalListSortingStrategy, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { Body1Strong, mergeClasses } from "@fluentui/react-components";
|
||||
import { CollectionsRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import CollectionContext from "../contexts/CollectionContext";
|
||||
import { useCollections } from "../contexts/CollectionsProvider";
|
||||
import { useStyles_CollectionView } from "./CollectionView.styles";
|
||||
import GroupView from "./GroupView";
|
||||
import TabView from "./TabView";
|
||||
|
||||
export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement
|
||||
{
|
||||
const { tilesView } = useCollections();
|
||||
const {
|
||||
setNodeRef,
|
||||
nodeProps,
|
||||
setActivatorNodeRef,
|
||||
activatorProps,
|
||||
activeItem, isCurrentlySorting, isBeingDragged, isActiveOverThis: isOver
|
||||
} = useDndItem({ id: collectionIndex.toString(), data: { indices: [collectionIndex], item: collection } });
|
||||
|
||||
const isActiveOverThis: boolean = isOver && activeItem?.item.type !== "collection";
|
||||
|
||||
const tabCount: number = useMemo(() => collection.items.flatMap(i => i.type === "group" ? i.items : i).length, [collection.items]);
|
||||
const hasPinnedGroup: boolean = useMemo(() => collection.items.length > 0 &&
|
||||
(collection.items[0].type === "group" && collection.items[0].pinned === true), [collection.items]);
|
||||
|
||||
const cls = useStyles_CollectionView();
|
||||
const colorCls = useGroupColors();
|
||||
|
||||
return (
|
||||
<CollectionContext.Provider value={ { collection, tabCount, hasPinnedGroup } }>
|
||||
<div
|
||||
ref={ setNodeRef } { ...nodeProps }
|
||||
className={ mergeClasses(
|
||||
cls.root,
|
||||
collection.color && colorCls[collection.color],
|
||||
collection.color && cls.color,
|
||||
!tilesView && cls.verticalRoot,
|
||||
dragOverlay && cls.dragOverlay,
|
||||
isBeingDragged && cls.dragging,
|
||||
isCurrentlySorting && cls.sorting,
|
||||
isActiveOverThis && cls.draggingOver
|
||||
) }
|
||||
>
|
||||
|
||||
<CollectionHeader dragHandleProps={ activatorProps } dragHandleRef={ setActivatorNodeRef } />
|
||||
|
||||
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
|
||||
<>
|
||||
{ collection.items.length < 1 ?
|
||||
<div className={ cls.empty }>
|
||||
<CollectionsRegular fontSize={ 32 } />
|
||||
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
|
||||
</div>
|
||||
:
|
||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
||||
<SortableContext
|
||||
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
|
||||
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
|
||||
>
|
||||
{ collection.items.map((i, index) =>
|
||||
i.type === "group" ?
|
||||
<GroupView
|
||||
key={ index } group={ i } indices={ [collectionIndex, index] } />
|
||||
:
|
||||
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div >
|
||||
</CollectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export type CollectionViewProps =
|
||||
{
|
||||
collection: CollectionItem;
|
||||
index: number;
|
||||
dragOverlay?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles_EditDialog = makeStyles({
|
||||
surface:
|
||||
{
|
||||
"--border": tokens.colorTransparentStroke,
|
||||
...shorthands.borderWidth(tokens.strokeWidthThick),
|
||||
...shorthands.borderColor("var(--border)")
|
||||
},
|
||||
content:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalS
|
||||
},
|
||||
colorPicker:
|
||||
{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
rowGap: tokens.spacingVerticalS,
|
||||
columnGap: tokens.spacingVerticalS
|
||||
},
|
||||
colorButton:
|
||||
{
|
||||
"&[aria-pressed=true]":
|
||||
{
|
||||
color: "var(--text) !important",
|
||||
backgroundColor: "var(--border) !important",
|
||||
|
||||
"& .fui-Button__icon":
|
||||
{
|
||||
color: "var(--text)"
|
||||
}
|
||||
}
|
||||
},
|
||||
colorButton_icon:
|
||||
{
|
||||
color: "var(--border)"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { track } from "@/features/analytics";
|
||||
import { useGroupColors } from "@/hooks/useGroupColors";
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { Circle20Filled, CircleOff20Regular, Pin20Filled, Rename20Regular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles_EditDialog } from "./EditDialog.styles";
|
||||
|
||||
export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
{
|
||||
const [title, setTitle] = useState<string>(
|
||||
(props.type === "collection"
|
||||
? props.collection?.title :
|
||||
(props.group?.pinned !== true ? props.group?.title : ""))
|
||||
?? ""
|
||||
);
|
||||
|
||||
const [color, setColor] = useState<chrome.tabGroups.ColorEnum | undefined | "pinned">(
|
||||
props.type === "collection"
|
||||
? props.collection?.color :
|
||||
props.group?.pinned === true ? "pinned" : (props.group?.color ?? "blue")
|
||||
);
|
||||
|
||||
const cls = useStyles_EditDialog();
|
||||
const colorCls = useGroupColors();
|
||||
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
};
|
||||
|
||||
const handleSave = () =>
|
||||
{
|
||||
if (props.type === "collection" ? props.collection !== null : props.group !== null)
|
||||
track("item_edited", { type: props.type });
|
||||
else
|
||||
track("item_created", { type: props.type });
|
||||
|
||||
if (props.type === "collection")
|
||||
props.onSave({
|
||||
type: "collection",
|
||||
timestamp: props.collection?.timestamp ?? Date.now(),
|
||||
color: (color === "pinned") ? undefined : color!,
|
||||
title: title ? title : undefined,
|
||||
items: props.collection?.items ?? []
|
||||
});
|
||||
else if (color === "pinned")
|
||||
props.onSave({
|
||||
type: "group",
|
||||
pinned: true,
|
||||
items: props.group?.items ?? []
|
||||
});
|
||||
else
|
||||
props.onSave({
|
||||
type: "group",
|
||||
pinned: false,
|
||||
color: color!,
|
||||
title: title ? title : undefined,
|
||||
items: props.group?.items ?? []
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
|
||||
<form onSubmit={ onSubmit }>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogTitle>
|
||||
{
|
||||
props.type === "collection" ?
|
||||
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
|
||||
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
|
||||
}
|
||||
</fui.DialogTitle>
|
||||
|
||||
<fui.DialogContent>
|
||||
<div className={ cls.content }>
|
||||
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
|
||||
<fui.Input
|
||||
contentBefore={ <Rename20Regular /> }
|
||||
disabled={ color === "pinned" }
|
||||
placeholder={
|
||||
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
|
||||
}
|
||||
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
||||
onChange={ (_, e) => setTitle(e.value) } />
|
||||
</fui.Field>
|
||||
<fui.Field label="Color">
|
||||
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
|
||||
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === "pinned" }
|
||||
onClick={ () => setColor("pinned") }
|
||||
icon={ <Pin20Filled /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("groups.pinned") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ props.type === "collection" &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === undefined }
|
||||
onClick={ () => setColor(undefined) }
|
||||
icon={ <CircleOff20Regular /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("colors.none") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ Object.keys(colorCls).map(i =>
|
||||
<fui.ToggleButton
|
||||
checked={ color === i }
|
||||
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
|
||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
|
||||
icon={ {
|
||||
className: cls.colorButton_icon,
|
||||
children: <Circle20Filled />
|
||||
} }
|
||||
key={ i }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||
</fui.ToggleButton>
|
||||
) }
|
||||
</div>
|
||||
</fui.Field>
|
||||
</div>
|
||||
</fui.DialogContent>
|
||||
|
||||
<fui.DialogActions>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</form>
|
||||
</fui.DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
export type GroupEditDialogProps =
|
||||
{
|
||||
type: "collection";
|
||||
collection?: CollectionItem;
|
||||
onSave: (item: CollectionItem) => void;
|
||||
} |
|
||||
{
|
||||
type: "group";
|
||||
hidePinned?: boolean;
|
||||
group?: GroupItem;
|
||||
onSave: (item: GroupItem) => void;
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles_GroupView = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignSelf: "normal",
|
||||
|
||||
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
|
||||
paddingBottom: tokens.spacingVerticalNone,
|
||||
borderRadius: tokens.borderRadiusLarge,
|
||||
|
||||
"&:hover .GroupView-toolbar, &:focus-within .GroupView-toolbar":
|
||||
{
|
||||
visibility: "visible"
|
||||
},
|
||||
|
||||
"&:hover":
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover
|
||||
}
|
||||
},
|
||||
header:
|
||||
{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
|
||||
borderBottom: `${tokens.strokeWidthThick} solid var(--border)`,
|
||||
borderBottomLeftRadius: tokens.borderRadiusLarge
|
||||
},
|
||||
verticalHeader:
|
||||
{
|
||||
borderBottomLeftRadius: tokens.borderRadiusNone
|
||||
},
|
||||
title:
|
||||
{
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
alignItems: "center",
|
||||
minHeight: "12px",
|
||||
minWidth: "24px",
|
||||
gap: tokens.spacingHorizontalXS,
|
||||
width: "max-content",
|
||||
maxWidth: "160px",
|
||||
|
||||
padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
|
||||
paddingBottom: tokens.spacingVerticalXS,
|
||||
marginBottom: "-2px",
|
||||
|
||||
border: `${tokens.strokeWidthThick} solid var(--border)`,
|
||||
borderRadius: `${tokens.borderRadiusLarge} ${tokens.borderRadiusLarge} ${tokens.borderRadiusNone} ${tokens.borderRadiusLarge}`,
|
||||
borderBottom: "none",
|
||||
backgroundColor: "var(--border)",
|
||||
color: "var(--text)"
|
||||
},
|
||||
verticalTitle:
|
||||
{
|
||||
borderBottomLeftRadius: tokens.borderRadiusNone
|
||||
},
|
||||
pinned:
|
||||
{
|
||||
backgroundColor: "transparent"
|
||||
},
|
||||
toolbar:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
visibility: "hidden"
|
||||
},
|
||||
showToolbar:
|
||||
{
|
||||
visibility: "visible"
|
||||
},
|
||||
openAllLink:
|
||||
{
|
||||
whiteSpace: "nowrap"
|
||||
},
|
||||
empty:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: tokens.colorNeutralForeground3,
|
||||
minWidth: "160px",
|
||||
height: "120px",
|
||||
marginBottom: tokens.spacingVerticalSNudge
|
||||
},
|
||||
verticalEmpty:
|
||||
{
|
||||
height: "auto",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`
|
||||
},
|
||||
list:
|
||||
{
|
||||
display: "flex",
|
||||
columnGap: tokens.spacingHorizontalS,
|
||||
rowGap: tokens.spacingHorizontalSNudge,
|
||||
height: "100%",
|
||||
position: "relative"
|
||||
},
|
||||
verticalList:
|
||||
{
|
||||
flexFlow: "column"
|
||||
},
|
||||
verticalListCollapsed:
|
||||
{
|
||||
maxHeight: "136px",
|
||||
overflow: "clip"
|
||||
},
|
||||
horizontalListCollapsed:
|
||||
{
|
||||
maxWidth: "400px",
|
||||
overflow: "clip"
|
||||
},
|
||||
listContainer:
|
||||
{
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
|
||||
paddingBottom: tokens.spacingVerticalNone,
|
||||
height: "100%"
|
||||
},
|
||||
verticalListContainer:
|
||||
{
|
||||
borderLeft: `${tokens.strokeWidthThick} solid var(--border)`,
|
||||
padding: tokens.spacingVerticalSNudge,
|
||||
marginBottom: tokens.spacingVerticalSNudge,
|
||||
borderTopLeftRadius: tokens.borderRadiusNone,
|
||||
borderBottomLeftRadius: tokens.borderRadiusNone,
|
||||
borderTop: "none"
|
||||
},
|
||||
pinnedColor:
|
||||
{
|
||||
"--border": tokens.colorNeutralStrokeAccessible,
|
||||
"--text": tokens.colorNeutralForeground1
|
||||
},
|
||||
dragOverlay:
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||
transform: "scale(1.05)",
|
||||
cursor: "grabbing !important",
|
||||
boxShadow: `${tokens.shadow16} !important`,
|
||||
"& > div":
|
||||
{
|
||||
pointerEvents: "none"
|
||||
}
|
||||
},
|
||||
dragging:
|
||||
{
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import GroupContext from "@/entrypoints/sidepanel/contexts/GroupContext";
|
||||
import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem";
|
||||
import { openGroup } from "@/entrypoints/sidepanel/utils/opener";
|
||||
import { useGroupColors } from "@/hooks/useGroupColors";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { GroupItem } from "@/models/CollectionModels";
|
||||
import { horizontalListSortingStrategy, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { Caption1Strong, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
|
||||
import { Pin16Filled, WebAssetRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useCollections } from "../contexts/CollectionsProvider";
|
||||
import GroupDropZone from "./collections/GroupDropZone";
|
||||
import GroupMoreMenu from "./collections/GroupMoreMenu";
|
||||
import { useStyles_GroupView } from "./GroupView.styles";
|
||||
import TabView from "./TabView";
|
||||
|
||||
export default function GroupView({ group, indices, dragOverlay }: GroupViewProps): ReactElement
|
||||
{
|
||||
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
|
||||
const { tilesView } = useCollections();
|
||||
|
||||
const groupId: string = useMemo(() => indices.join("/"), [indices]);
|
||||
|
||||
const {
|
||||
setNodeRef, nodeProps,
|
||||
setActivatorNodeRef, activatorProps,
|
||||
activeItem: active, isBeingDragged
|
||||
} = useDndItem({ id: groupId, data: { indices, item: group }, disabled: group.pinned });
|
||||
|
||||
const disableDropZone: boolean = useMemo(
|
||||
() => active !== null &&
|
||||
(active.item.type !== "tab" || (active.indices[0] === indices[0] && active.indices[1] === indices[1])),
|
||||
[active, indices]);
|
||||
const disableSorting: boolean = useMemo(
|
||||
() => active !== null && (active.item.type !== "tab" || active.indices[0] !== indices[0]),
|
||||
[active, indices]);
|
||||
|
||||
const cls = useStyles_GroupView();
|
||||
const colorCls = useGroupColors();
|
||||
|
||||
return (
|
||||
<GroupContext.Provider value={ { group, indices } }>
|
||||
<div
|
||||
ref={ setNodeRef } { ...nodeProps }
|
||||
className={ mergeClasses(
|
||||
cls.root,
|
||||
group.pinned === true ? cls.pinnedColor : colorCls[group.color],
|
||||
isBeingDragged && cls.dragging,
|
||||
dragOverlay && cls.dragOverlay
|
||||
) }
|
||||
>
|
||||
<div className={ mergeClasses(cls.header, !tilesView && cls.verticalHeader) }>
|
||||
|
||||
<div
|
||||
ref={ setActivatorNodeRef } { ...activatorProps }
|
||||
className={ mergeClasses(cls.title, group.pinned && cls.pinned, !tilesView && cls.verticalTitle) }
|
||||
>
|
||||
{ group.pinned === true ?
|
||||
<>
|
||||
<Pin16Filled />
|
||||
<Caption1Strong truncate wrap={ false }>{ i18n.t("groups.pinned") }</Caption1Strong>
|
||||
</>
|
||||
:
|
||||
<Tooltip relationship="description" content={ group.title ?? "" }>
|
||||
<Caption1Strong truncate wrap={ false }>{ group.title }</Caption1Strong>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={ mergeClasses(cls.toolbar, "GroupView-toolbar", alwaysShowToolbars === true && cls.showToolbar) }>
|
||||
{ group.items.length > 0 &&
|
||||
<Link className={ cls.openAllLink } onClick={ () => openGroup(group, false) }>
|
||||
{ i18n.t("groups.open") }
|
||||
</Link>
|
||||
}
|
||||
|
||||
<GroupMoreMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GroupDropZone
|
||||
disabled={ disableDropZone }
|
||||
className={ mergeClasses(cls.listContainer, !tilesView && cls.verticalListContainer) }
|
||||
>
|
||||
{ group.items.length < 1 ?
|
||||
<div className={ mergeClasses(cls.empty, !tilesView && cls.verticalEmpty) }>
|
||||
<WebAssetRegular fontSize={ 32 } />
|
||||
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
|
||||
</div>
|
||||
:
|
||||
<div
|
||||
className={ mergeClasses(
|
||||
cls.list,
|
||||
!tilesView && cls.verticalList,
|
||||
((active?.item.type === "group" && active?.indices[0] === indices[0]) || dragOverlay) && (tilesView ? cls.horizontalListCollapsed : cls.verticalListCollapsed)
|
||||
) }
|
||||
>
|
||||
<SortableContext
|
||||
items={ group.items.map((_, index) => [...indices, index].join("/")) }
|
||||
disabled={ disableSorting }
|
||||
strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy }
|
||||
>
|
||||
{ group.items.map((i, index) =>
|
||||
<TabView key={ index } tab={ i } indices={ [...indices, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
}
|
||||
</GroupDropZone>
|
||||
</div>
|
||||
</GroupContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export type GroupViewProps =
|
||||
{
|
||||
group: GroupItem;
|
||||
indices: number[];
|
||||
dragOverlay?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles_TabView = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "grid",
|
||||
position: "relative",
|
||||
|
||||
width: "160px",
|
||||
height: "120px",
|
||||
flexShrink: 0,
|
||||
marginBottom: tokens.spacingVerticalSNudge,
|
||||
|
||||
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
|
||||
cursor: "pointer",
|
||||
textDecoration: "none !important",
|
||||
userSelect: "none",
|
||||
|
||||
"&:hover button, &:focus-within button":
|
||||
{
|
||||
display: "inline-flex"
|
||||
},
|
||||
|
||||
"&:hover":
|
||||
{
|
||||
boxShadow: tokens.shadow4
|
||||
},
|
||||
|
||||
"&:focus-visible":
|
||||
{
|
||||
outline: `2px solid ${tokens.colorStrokeFocus2}`
|
||||
}
|
||||
},
|
||||
listView:
|
||||
{
|
||||
width: "100%",
|
||||
height: "min-content",
|
||||
marginBottom: tokens.spacingVerticalNone
|
||||
},
|
||||
image:
|
||||
{
|
||||
zIndex: 0,
|
||||
position: "absolute",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
objectFit: "cover"
|
||||
},
|
||||
header:
|
||||
{
|
||||
zIndex: 1,
|
||||
alignSelf: "end",
|
||||
minHeight: "32px",
|
||||
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 1fr auto",
|
||||
alignItems: "center",
|
||||
|
||||
borderBottomLeftRadius: tokens.borderRadiusMedium,
|
||||
borderBottomRightRadius: tokens.borderRadiusMedium,
|
||||
|
||||
backgroundColor: tokens.colorSubtleBackgroundLightAlphaHover,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
"-webkit-backdrop-filer": "blur(4px)",
|
||||
backdropFilter: "blur(4px)"
|
||||
},
|
||||
icon:
|
||||
{
|
||||
cursor: "grab",
|
||||
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalSNudge}`,
|
||||
height: "32px",
|
||||
boxSizing: "border-box",
|
||||
|
||||
"&:active":
|
||||
{
|
||||
cursor: "grabbing"
|
||||
}
|
||||
},
|
||||
title:
|
||||
{
|
||||
overflowX: "hidden",
|
||||
justifySelf: "start",
|
||||
maxWidth: "100%"
|
||||
},
|
||||
deleteButton:
|
||||
{
|
||||
display: "none"
|
||||
},
|
||||
showDeleteButton:
|
||||
{
|
||||
display: "inline-flex"
|
||||
},
|
||||
dragOverlay:
|
||||
{
|
||||
cursor: "grabbing !important",
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: `${tokens.shadow16} !important`,
|
||||
"& > div":
|
||||
{
|
||||
pointerEvents: "none"
|
||||
}
|
||||
},
|
||||
dragging:
|
||||
{
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import faviconPlaceholder from "@/assets/FaviconPlaceholder.svg";
|
||||
import pagePlaceholder from "@/assets/PagePlaceholder.svg";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
|
||||
import { Dismiss20Regular } from "@fluentui/react-icons";
|
||||
import { MouseEventHandler, ReactElement } from "react";
|
||||
import { useStyles_TabView } from "./TabView.styles";
|
||||
import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext";
|
||||
|
||||
export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement
|
||||
{
|
||||
const { removeItem, graphics, tilesView } = useCollections();
|
||||
const { collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const {
|
||||
setNodeRef, setActivatorNodeRef,
|
||||
nodeProps, activatorProps, isBeingDragged
|
||||
} = useDndItem({ id: indices.join("/"), data: { indices, item: tab } });
|
||||
const dialog = useDialog();
|
||||
|
||||
const [deletePrompt] = useSettings("deletePrompt");
|
||||
const [showToolbar] = useSettings("alwaysShowToolbars");
|
||||
|
||||
const cls = useStyles_TabView();
|
||||
|
||||
const handleDelete: MouseEventHandler<HTMLButtonElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
args.stopPropagation();
|
||||
|
||||
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
|
||||
|
||||
if (deletePrompt)
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("tabs.delete"),
|
||||
content: i18n.t("common.delete_prompt"),
|
||||
destructive: true,
|
||||
confirmText: i18n.t("common.actions.delete"),
|
||||
onConfirm: () => removeItem(...removeIndex)
|
||||
});
|
||||
else
|
||||
removeItem(...removeIndex);
|
||||
};
|
||||
|
||||
const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
browser.tabs.create({ url: tab.url, active: true });
|
||||
};
|
||||
|
||||
const handleAuxClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
|
||||
if (args.button === 1)
|
||||
browser.tabs.create({ url: tab.url, active: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ setNodeRef } { ...nodeProps }
|
||||
href={ tab.url }
|
||||
onClick={ handleClick } onAuxClick={ handleAuxClick }
|
||||
className={ mergeClasses(
|
||||
cls.root,
|
||||
!tilesView && cls.listView,
|
||||
isBeingDragged && cls.dragging,
|
||||
dragOverlay && cls.dragOverlay
|
||||
) }
|
||||
>
|
||||
{ tilesView &&
|
||||
<img
|
||||
src={ graphics[tab.url]?.preview ?? graphics[tab.url]?.capture ?? pagePlaceholder }
|
||||
onError={ e => e.currentTarget.src = pagePlaceholder }
|
||||
className={ cls.image } draggable={ false } />
|
||||
}
|
||||
|
||||
<div className={ cls.header }>
|
||||
<img
|
||||
ref={ setActivatorNodeRef } { ...activatorProps }
|
||||
src={ graphics[tab.url]?.icon ?? faviconPlaceholder }
|
||||
onError={ e => e.currentTarget.src = faviconPlaceholder }
|
||||
className={ cls.icon } draggable={ false } />
|
||||
|
||||
<Tooltip relationship="description" content={ tab.title ?? tab.url }>
|
||||
<Caption1 truncate wrap={ false } className={ cls.title }>
|
||||
{ tab.title ?? tab.url }
|
||||
</Caption1>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip relationship="label" content={ i18n.t("tabs.delete") }>
|
||||
<Button
|
||||
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
|
||||
appearance="subtle" icon={ <Dismiss20Regular /> }
|
||||
onClick={ handleDelete } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export type TabViewProps =
|
||||
{
|
||||
tab: TabItem;
|
||||
indices: number[];
|
||||
dragOverlay?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { GroupItem, 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";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
|
||||
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
|
||||
{
|
||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { updateCollection } = useCollections();
|
||||
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
|
||||
|
||||
const AddIcon = bundleIcon(Add20Filled, Add20Regular);
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const newTabs: (TabItem | GroupItem)[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items :
|
||||
await getSelectedTabs();
|
||||
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collection.timestamp);
|
||||
};
|
||||
|
||||
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 || contextOpen) && cls.showToolbar
|
||||
) }
|
||||
>
|
||||
{ tabCount < 1 ?
|
||||
<Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }>
|
||||
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
||||
</Button>
|
||||
:
|
||||
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||
}
|
||||
|
||||
<CollectionMoreButton onAddSelected={ handleAddSelected } onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||
</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"
|
||||
},
|
||||
showToolbar:
|
||||
{
|
||||
display: "flex"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, 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, onOpenChange }: CollectionMoreButtonProps): React.ReactElement
|
||||
{
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { removeItem, updateCollection } = useCollections();
|
||||
const { tabCount, hasPinnedGroup, collection } = 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 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(collection.timestamp)
|
||||
});
|
||||
else
|
||||
removeItem(collection.timestamp);
|
||||
};
|
||||
|
||||
const handleEdit = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
type="collection"
|
||||
collection={ collection }
|
||||
onSave={ item => updateCollection(item, collection.timestamp) } />
|
||||
);
|
||||
|
||||
const handleCreateGroup = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
type="group"
|
||||
hidePinned={ hasPinnedGroup }
|
||||
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collection.timestamp) } />
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu onOpenChange={ onOpenChange }>
|
||||
<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?.() }>
|
||||
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
||||
</MenuItem>
|
||||
}
|
||||
<MenuItem icon={ <GroupIcon /> } onClick={ handleCreateGroup }>
|
||||
{ i18n.t("collections.menu.add_group") }
|
||||
</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;
|
||||
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => 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,115 @@
|
||||
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 { sendMessage } from "@/utils/messaging";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
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 [listLocation] = useSettings("listLocation");
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { group, indices } = useContext<GroupContextType>(GroupContext);
|
||||
const { hasPinnedGroup, collection } = 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 = () =>
|
||||
{
|
||||
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
|
||||
|
||||
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(...removeIndex)
|
||||
});
|
||||
else
|
||||
removeItem(...removeIndex);
|
||||
};
|
||||
|
||||
const handleEdit = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
type="group"
|
||||
group={ group }
|
||||
hidePinned={ hasPinnedGroup }
|
||||
onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } />
|
||||
);
|
||||
|
||||
const openGroupInNewWindow = () =>
|
||||
{
|
||||
if (import.meta.env.FIREFOX && listLocation === "popup")
|
||||
sendMessage("openGroup", { group, newWindow: true });
|
||||
else
|
||||
openGroup(group, true);
|
||||
};
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const newTabs: TabItem[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items.flatMap(i => i.type === "tab" ? i : i.items) :
|
||||
await getSelectedTabs();
|
||||
updateGroup({ ...group, items: [...group.items, ...newTabs] }, collection.timestamp, 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={ openGroupInNewWindow }>
|
||||
{ i18n.t("groups.menu.new_window") }
|
||||
</MenuItem>
|
||||
}
|
||||
|
||||
<MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }>
|
||||
{ isTab ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") }
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
|
||||
{ i18n.t("groups.menu.edit") }
|
||||
</MenuItem>
|
||||
{ group.items.length > 0 &&
|
||||
<MenuItem
|
||||
className={ dangerCls.menuItem }
|
||||
icon={ <UngroupIcon /> }
|
||||
onClick={ () => ungroup(collection.timestamp, 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,111 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import browserLocaleKey from "@/utils/browserLocaleKey";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, 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({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
|
||||
{
|
||||
const [defaultAction] = useSettings("defaultRestoreAction");
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const { removeItem } = useCollections();
|
||||
const dialog = useDialog();
|
||||
const { collection } = 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())
|
||||
{
|
||||
if (import.meta.env.FIREFOX && listLocation === "popup")
|
||||
sendMessage("openCollection", { collection, targetWindow: "incognito" });
|
||||
else
|
||||
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") =>
|
||||
import.meta.env.FIREFOX && listLocation === "popup" && mode === "new" ?
|
||||
() => sendMessage("openCollection", { collection, targetWindow: "new" }) :
|
||||
() => openCollection(collection, mode);
|
||||
|
||||
const handleRestore = async () =>
|
||||
{
|
||||
await openCollection(collection);
|
||||
removeItem(collection.timestamp);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu onOpenChange={ onOpenChange }>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export type OpenCollectionButtonProps =
|
||||
{
|
||||
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
|
||||
};
|
||||
Reference in New Issue
Block a user