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:
@@ -0,0 +1,51 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles_CollectionListView = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto"
|
||||
},
|
||||
collectionList:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM
|
||||
},
|
||||
searchBar:
|
||||
{
|
||||
boxShadow: tokens.shadow2
|
||||
},
|
||||
emptySearch:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flexGrow: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: tokens.spacingVerticalS
|
||||
},
|
||||
empty:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: tokens.spacingVerticalS,
|
||||
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
|
||||
color: tokens.colorNeutralForeground2
|
||||
},
|
||||
msgBar:
|
||||
{
|
||||
flex: "none"
|
||||
},
|
||||
listView:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import CollectionView from "@/entrypoints/sidepanel/components/CollectionView";
|
||||
import GroupView from "@/entrypoints/sidepanel/components/GroupView";
|
||||
import { DndItem } from "@/entrypoints/sidepanel/hooks/useDndItem";
|
||||
import CloudIssueMessages from "@/entrypoints/sidepanel/layouts/collections/messages/CloudIssueMessages";
|
||||
import CtaMessage from "@/entrypoints/sidepanel/layouts/collections/messages/CtaMessage";
|
||||
import filterCollections, { CollectionFilterType } from "@/entrypoints/sidepanel/utils/filterCollections";
|
||||
import sortCollections from "@/entrypoints/sidepanel/utils/sortCollections";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { rectSortingStrategy, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { Body1, Button, Caption1, mergeClasses, Subtitle2 } from "@fluentui/react-components";
|
||||
import { ArrowUndo20Regular, SearchInfo24Regular, Sparkle48Regular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import TabView from "../../components/TabView";
|
||||
import CollectionContext from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import applyReorder from "../../utils/dnd/applyReorder";
|
||||
import { collisionDetector } from "../../utils/dnd/collisionDetector";
|
||||
import { useStyles_CollectionListView } from "./CollectionListView.styles";
|
||||
import SearchBar from "./SearchBar";
|
||||
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
|
||||
|
||||
export default function CollectionListView(): ReactElement
|
||||
{
|
||||
const { tilesView, updateCollections, collections } = useCollections();
|
||||
|
||||
const [sortMode, setSortMode] = useSettings("sortMode");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [colors, setColors] = useState<CollectionFilterType["colors"]>([]);
|
||||
|
||||
const [active, setActive] = useState<DndItem | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { delay: 100, tolerance: 0 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
|
||||
);
|
||||
|
||||
const resultList = useMemo(
|
||||
() => sortCollections(filterCollections(collections, { query, colors }), sortMode),
|
||||
[query, colors, sortMode, collections]
|
||||
);
|
||||
|
||||
const cls = useStyles_CollectionListView();
|
||||
|
||||
const resetFilter = useCallback(() =>
|
||||
{
|
||||
setQuery("");
|
||||
setColors([]);
|
||||
}, []);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent): void =>
|
||||
{
|
||||
setActive(event.active.data.current as DndItem);
|
||||
};
|
||||
|
||||
const handleDragEnd = (args: DragEndEvent): void =>
|
||||
{
|
||||
setActive(null);
|
||||
const result: CollectionItem[] | null = applyReorder(resultList, args);
|
||||
|
||||
if (result !== null)
|
||||
{
|
||||
updateCollections(result);
|
||||
if (sortMode !== "custom")
|
||||
setSortMode("custom");
|
||||
}
|
||||
};
|
||||
|
||||
if (sortMode === null || collections === null)
|
||||
return <></>;
|
||||
|
||||
if (collections.length < 1)
|
||||
return (
|
||||
<article className={ cls.empty }>
|
||||
<Sparkle48Regular />
|
||||
<Subtitle2 align="center">{ i18n.t("main.list.empty.title") }</Subtitle2>
|
||||
<Caption1 align="center">{ i18n.t("main.list.empty.message") }</Caption1>
|
||||
</article>
|
||||
);
|
||||
|
||||
return (
|
||||
<article className={ cls.root }>
|
||||
<SearchBar
|
||||
query={ query } onQueryChange={ setQuery }
|
||||
filter={ colors } onFilterChange={ setColors }
|
||||
sort={ sortMode } onSortChange={ setSortMode }
|
||||
onReset={ resetFilter } />
|
||||
|
||||
<CtaMessage className={ cls.msgBar } />
|
||||
<StorageCapacityIssueMessage className={ cls.msgBar } />
|
||||
<CloudIssueMessages className={ cls.msgBar } />
|
||||
|
||||
{ resultList.length < 1 ?
|
||||
<div className={ cls.emptySearch }>
|
||||
<SearchInfo24Regular />
|
||||
<Subtitle2>{ i18n.t("main.list.empty_search.title") }</Subtitle2>
|
||||
<Body1>{ i18n.t("main.list.empty_search.message") }</Body1>
|
||||
<Button appearance="subtle" icon={ <ArrowUndo20Regular /> } onClick={ resetFilter }>
|
||||
{ i18n.t("common.actions.reset_filters") }
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView) }>
|
||||
<DndContext
|
||||
sensors={ sensors }
|
||||
collisionDetection={ collisionDetector(!tilesView) }
|
||||
onDragStart={ handleDragStart }
|
||||
onDragEnd={ handleDragEnd }
|
||||
>
|
||||
<SortableContext
|
||||
items={ resultList.map((_, index) => index.toString()) }
|
||||
strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy }
|
||||
>
|
||||
{ resultList.map((collection, index) =>
|
||||
<CollectionView key={ index } collection={ collection } index={ index } />
|
||||
) }
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay dropAnimation={ null }>
|
||||
{ active &&
|
||||
<>
|
||||
{ active.item.type === "collection" &&
|
||||
<CollectionView collection={ active.item } index={ -1 } dragOverlay />
|
||||
}
|
||||
{ active.item.type === "group" &&
|
||||
<CollectionContext.Provider
|
||||
value={ {
|
||||
tabCount: 0,
|
||||
collectionIndex: active.indices[0],
|
||||
collection: resultList[active.indices[0]],
|
||||
hasPinnedGroup: true
|
||||
} }
|
||||
>
|
||||
<GroupView group={ active.item } indices={ [-1] } dragOverlay />
|
||||
</CollectionContext.Provider>
|
||||
}
|
||||
{ active.item.type === "tab" &&
|
||||
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</section>
|
||||
}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useGroupColors } from "@/hooks/useGroupColors";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { CollectionFilterType } from "../../utils/filterCollections";
|
||||
|
||||
export default function FilterCollectionsButton({ value, onChange }: FilterCollectionsButtonProps): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
const colorCls = useGroupColors();
|
||||
|
||||
const ColorFilterIcon = ic.bundleIcon(ic.Color20Filled, ic.Color20Regular);
|
||||
const ColorIcon = ic.bundleIcon(ic.Circle20Filled, ic.CircleHalfFill20Regular);
|
||||
const NoColorIcon = ic.bundleIcon(ic.CircleOffFilled, ic.CircleOffRegular);
|
||||
const AnyColorIcon = ic.bundleIcon(ic.PhotoFilter20Filled, ic.PhotoFilter20Regular);
|
||||
|
||||
return (
|
||||
<fui.Menu
|
||||
checkedValues={ !value || value.length < 1 ? { default: ["any"] } : { colors: value } }
|
||||
onCheckedValueChange={ (_, e) =>
|
||||
onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"])
|
||||
}
|
||||
>
|
||||
|
||||
<fui.Tooltip relationship="label" content={ i18n.t("main.list.searchbar.filter") }>
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle" icon={ <ColorFilterIcon /> } />
|
||||
</fui.MenuTrigger>
|
||||
</fui.Tooltip>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="default" value="any" icon={ <AnyColorIcon /> }>
|
||||
{ i18n.t("colors.any") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuDivider />
|
||||
<fui.MenuItemCheckbox name="colors" value="none" icon={ <NoColorIcon /> }>
|
||||
{ i18n.t("colors.none") }
|
||||
</fui.MenuItemCheckbox>
|
||||
|
||||
{ Object.keys(colorCls).map(i =>
|
||||
<fui.MenuItemCheckbox
|
||||
key={ i } name="colors" value={ i }
|
||||
icon={
|
||||
<ColorIcon
|
||||
className={ fui.mergeClasses(
|
||||
cls.colorIcon,
|
||||
colorCls[i as chrome.tabGroups.ColorEnum]
|
||||
) } />
|
||||
}
|
||||
>
|
||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||
</fui.MenuItemCheckbox>
|
||||
) }
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export type FilterCollectionsButtonProps =
|
||||
{
|
||||
value?: CollectionFilterType["colors"];
|
||||
onChange?: (value: CollectionFilterType["colors"]) => void;
|
||||
};
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
colorIcon:
|
||||
{
|
||||
color: "var(--border)"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Button, Input, makeStyles, tokens, Tooltip } from "@fluentui/react-components";
|
||||
import { ArrowUndo20Filled, ArrowUndo20Regular, bundleIcon, Search20Regular } from "@fluentui/react-icons";
|
||||
import { CollectionFilterType } from "../../utils/filterCollections";
|
||||
import { CollectionSortMode } from "../../utils/sortCollections";
|
||||
import FilterCollectionsButton from "./FilterCollectionsButton";
|
||||
import SortCollectionsButton from "./SortCollectionsButton";
|
||||
|
||||
export default function SearchBar(props: SearchBarProps): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
const ResetIcon = bundleIcon(ArrowUndo20Filled, ArrowUndo20Regular);
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ cls.root }
|
||||
appearance="filled-lighter"
|
||||
contentBefore={ <Search20Regular /> }
|
||||
placeholder={ i18n.t("main.list.searchbar.title") }
|
||||
value={ props.query } onChange={ (_, e) => props.onQueryChange?.(e.value) }
|
||||
contentAfter={
|
||||
<>
|
||||
{ (props.query || (props.filter && props.filter.length > 0)) &&
|
||||
<Tooltip relationship="label" content={ i18n.t("common.actions.reset_filters") }>
|
||||
<Button appearance="subtle" icon={ <ResetIcon /> } onClick={ props.onReset } />
|
||||
</Tooltip>
|
||||
}
|
||||
<FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } />
|
||||
<SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } />
|
||||
</>
|
||||
} />
|
||||
);
|
||||
}
|
||||
|
||||
export type SearchBarProps =
|
||||
{
|
||||
query?: string;
|
||||
onQueryChange?: (query: string) => void;
|
||||
filter?: CollectionFilterType["colors"];
|
||||
onFilterChange?: (filter: CollectionFilterType["colors"]) => void;
|
||||
sort?: CollectionSortMode;
|
||||
onSortChange?: (sort: CollectionSortMode) => void;
|
||||
onReset?: () => void;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
boxShadow: tokens.shadow2
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { CollectionSortMode } from "@/entrypoints/sidepanel/utils/sortCollections";
|
||||
import { Button, Menu, MenuItemRadio, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
|
||||
export default function SortCollectionsButton({ value, onChange }: SortCollectionsButtonProps): React.ReactElement
|
||||
{
|
||||
const ColorSortIcon = ic.bundleIcon(ic.ArrowSort20Filled, ic.ArrowSort20Regular);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
checkedValues={ { sort: value ? [value] : [] } }
|
||||
onCheckedValueChange={ (_, e) => onChange?.(e.checkedItems[0] as CollectionSortMode) }
|
||||
>
|
||||
<Tooltip relationship="label" content={ i18n.t("main.list.searchbar.sort.title") }>
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
<Button appearance="subtle" icon={ <ColorSortIcon /> } />
|
||||
</MenuTrigger>
|
||||
</Tooltip>
|
||||
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
{ Object.entries(sortIcons).map(([key, Icon]) =>
|
||||
<MenuItemRadio key={ key } name="sort" value={ key } icon={ <Icon /> }>
|
||||
{ i18n.t(`main.list.searchbar.sort.options.${key as CollectionSortMode}`) }
|
||||
</MenuItemRadio>
|
||||
) }
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export type SortCollectionsButtonProps =
|
||||
{
|
||||
value?: CollectionSortMode | null;
|
||||
onChange?: (value: CollectionSortMode) => void;
|
||||
};
|
||||
|
||||
const sortIcons: Record<CollectionSortMode, ic.FluentIcon> =
|
||||
{
|
||||
newest: ic.bundleIcon(ic.Sparkle20Filled, ic.Sparkle20Regular),
|
||||
oldest: ic.bundleIcon(ic.History20Filled, ic.History20Regular),
|
||||
ascending: ic.bundleIcon(ic.TextSortAscending20Filled, ic.TextSortAscending20Regular),
|
||||
descending: ic.bundleIcon(ic.TextSortDescending20Filled, ic.TextSortDescending20Regular),
|
||||
custom: ic.bundleIcon(ic.Star20Filled, ic.Star20Regular)
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import resolveConflict from "@/features/collectionStorage/utils/resolveConflict";
|
||||
import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
import { ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
|
||||
import { useCollections } from "../../../contexts/CollectionsProvider";
|
||||
|
||||
export default function CloudIssueMessages(props: MessageBarProps): React.ReactElement
|
||||
{
|
||||
const { cloudIssue, refreshCollections } = useCollections();
|
||||
|
||||
const overrideStorageWith = async (source: "local" | "sync") =>
|
||||
{
|
||||
await resolveConflict(source);
|
||||
await refreshCollections();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ cloudIssue === "parse_error" &&
|
||||
<MessageBar intent="error" layout="multiline" { ...props }>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ i18n.t("parse_error_message.title") }</MessageBarTitle>
|
||||
{ i18n.t("parse_error_message.message") }
|
||||
</MessageBarBody>
|
||||
<MessageBarActions>
|
||||
<Button icon={ <Wrench20Regular /> } onClick={ () => overrideStorageWith("local") }>
|
||||
{ i18n.t("parse_error_message.action") }
|
||||
</Button>
|
||||
</MessageBarActions>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
{ cloudIssue === "merge_conflict" &&
|
||||
<MessageBar intent="warning" layout="multiline" { ...props }>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ i18n.t("merge_conflict_message.title") }</MessageBarTitle>
|
||||
{ i18n.t("merge_conflict_message.message") }
|
||||
</MessageBarBody>
|
||||
<MessageBarActions>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }>
|
||||
{ i18n.t("merge_conflict_message.accept_local") }
|
||||
</Button>
|
||||
<Button icon={ <CloudArrowDown20Regular /> } onClick={ () => overrideStorageWith("sync") }>
|
||||
{ i18n.t("merge_conflict_message.accept_cloud") }
|
||||
</Button>
|
||||
</MessageBarActions>
|
||||
</MessageBar>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
|
||||
import { buyMeACoffeeLink, storeLink } from "@/data/links";
|
||||
import { useBmcStyles } from "@/hooks/useBmcStyles";
|
||||
import extLink from "@/utils/extLink";
|
||||
import { Button, Link, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
import { DismissRegular, HeartFilled } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export default function CtaMessage(props: MessageBarProps): ReactElement
|
||||
{
|
||||
const [counter, setCounter] = useState<number>(0);
|
||||
const bmcCls = useBmcStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
ctaCounter.getValue().then(c =>
|
||||
{
|
||||
if (c >= 0)
|
||||
{
|
||||
setCounter(c);
|
||||
ctaCounter.setValue(c + 1);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetCounter = async (counter: number) =>
|
||||
{
|
||||
await ctaCounter.setValue(counter);
|
||||
setCounter(counter);
|
||||
};
|
||||
|
||||
if (counter < 50)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<MessageBar layout="multiline" icon={ <HeartFilled color="red" /> } { ...props }>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ i18n.t("cta_message.title") }</MessageBarTitle>
|
||||
{ i18n.t("cta_message.message") } <Link { ...extLink(storeLink) }>{ i18n.t("cta_message.feedback") }</Link>
|
||||
</MessageBarBody>
|
||||
<MessageBarActions
|
||||
containerAction={
|
||||
<Button icon={ <DismissRegular /> } appearance="transparent" onClick={ () => resetCounter(0) } />
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="a" { ...extLink(buyMeACoffeeLink) }
|
||||
onClick={ () => resetCounter(-1) }
|
||||
appearance="primary"
|
||||
className={ bmcCls.button }
|
||||
icon={ <BuyMeACoffee20Regular /> }
|
||||
>
|
||||
{ i18n.t("common.cta.sponsor") }
|
||||
</Button>
|
||||
</MessageBarActions>
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
const ctaCounter = storage.defineItem<number>("local:ctaCounter", { fallback: 0 });
|
||||
@@ -0,0 +1,21 @@
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
|
||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): JSX.Element
|
||||
{
|
||||
const { usedStorageRatio } = useStorageInfo();
|
||||
|
||||
if (usedStorageRatio < 0.8)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<MessageBar intent="warning" layout="multiline" { ...props }>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>
|
||||
{ i18n.t("storage_full_message.title", [(usedStorageRatio * 100).toFixed(1)]) }
|
||||
</MessageBarTitle>
|
||||
{ i18n.t("storage_full_message.message") }
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import useSettings, { SettingsValue } from "@/hooks/useSettings";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import watchTabSelection from "@/utils/watchTabSelection";
|
||||
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export default function ActionButton(): ReactElement
|
||||
{
|
||||
const { addCollection } = useCollections();
|
||||
const [defaultAction] = useSettings("defaultSaveAction");
|
||||
const [selection, setSelection] = useState<"all" | "selected">("all");
|
||||
|
||||
const handleAction = async (primary: boolean) =>
|
||||
{
|
||||
const colection = await saveTabsToCollection(primary === (defaultAction === "set_aside"));
|
||||
addCollection(colection);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return watchTabSelection(setSelection);
|
||||
}, []);
|
||||
|
||||
if (defaultAction === null)
|
||||
return <div />;
|
||||
|
||||
const primaryActionKey: ActionsKey = `${defaultAction}.${selection}`;
|
||||
const PrimaryIcon = actionIcons[primaryActionKey];
|
||||
const secondaryActionKey: ActionsKey = `${defaultAction === "save" ? "set_aside" : "save"}.${selection}`;
|
||||
const SecondaryIcon = actionIcons[secondaryActionKey];
|
||||
|
||||
return (
|
||||
<Menu positioning="below-end">
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
{ (triggerProps: MenuButtonProps) => (
|
||||
<SplitButton
|
||||
appearance="primary"
|
||||
icon={ <PrimaryIcon /> }
|
||||
menuButton={ triggerProps }
|
||||
primaryActionButton={ { onClick: () => handleAction(true) } }
|
||||
>
|
||||
{ i18n.t(`actions.${primaryActionKey}`) }
|
||||
</SplitButton>
|
||||
) }
|
||||
</MenuTrigger>
|
||||
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
<MenuItem icon={ <SecondaryIcon /> } onClick={ () => handleAction(false) }>
|
||||
{ i18n.t(`actions.${secondaryActionKey}`) }
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const actionIcons: Record<ActionsKey, ic.FluentIcon> =
|
||||
{
|
||||
"save.all": ic.bundleIcon(ic.SaveArrowRight20Filled, ic.SaveArrowRight20Regular),
|
||||
"save.selected": ic.bundleIcon(ic.SaveCopy20Filled, ic.SaveCopy20Regular),
|
||||
"set_aside.all": ic.bundleIcon(ic.ArrowRight20Filled, ic.ArrowRight20Regular),
|
||||
"set_aside.selected": ic.bundleIcon(ic.CopyArrowRight20Filled, ic.CopyArrowRight20Regular)
|
||||
};
|
||||
|
||||
export type ActionsKey = `${SettingsValue<"defaultSaveAction">}.${"all" | "selected"}`;
|
||||
|
||||
export type ActionsValue =
|
||||
{
|
||||
label: string;
|
||||
icon: ic.FluentIcon;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import { Button, makeStyles, tokens, Tooltip } from "@fluentui/react-components";
|
||||
import { CollectionsAddRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import EditDialog from "../../components/EditDialog";
|
||||
import ActionButton from "./ActionButton";
|
||||
import MoreButton from "./MoreButton";
|
||||
|
||||
export default function Header(): ReactElement
|
||||
{
|
||||
const { addCollection } = useCollections();
|
||||
const dialog = useDialog();
|
||||
const cls = useStyles();
|
||||
|
||||
const handleCreateCollection = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
type="collection"
|
||||
onSave={ addCollection } />
|
||||
);
|
||||
|
||||
return (
|
||||
<header className={ cls.header }>
|
||||
<ActionButton />
|
||||
|
||||
<div className={ cls.headerSecondary }>
|
||||
<MoreButton />
|
||||
<Tooltip relationship="label" content={ i18n.t("main.header.create_collection") }>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={ <CollectionsAddRegular /> }
|
||||
onClick={ handleCreateCollection } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
header:
|
||||
{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
gap: tokens.spacingHorizontalS
|
||||
},
|
||||
headerSecondary:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalXS
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { BuyMeACoffee20Filled, BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
|
||||
import { buyMeACoffeeLink, githubLinks, storeLink } from "@/data/links";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import extLink from "@/utils/extLink";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export default function MoreButton(): ReactElement
|
||||
{
|
||||
const [tilesView, setTilesView] = useSettings("tilesView");
|
||||
|
||||
const SettingsIcon: ic.FluentIcon = ic.bundleIcon(ic.Settings20Filled, ic.Settings20Regular);
|
||||
const ViewIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular);
|
||||
const FeedbackIcon: ic.FluentIcon = ic.bundleIcon(ic.PersonFeedback20Filled, ic.PersonFeedback20Regular);
|
||||
const LearnIcon: ic.FluentIcon = ic.bundleIcon(ic.QuestionCircle20Filled, ic.QuestionCircle20Regular);
|
||||
const BmcIcon: ic.FluentIcon = ic.bundleIcon(BuyMeACoffee20Filled, BuyMeACoffee20Regular);
|
||||
|
||||
return (
|
||||
<fui.Menu
|
||||
hasIcons hasCheckmarks
|
||||
checkedValues={ { tilesView: tilesView ? ["true"] : [] } }
|
||||
onCheckedValueChange={ (_, e) => setTilesView(e.checkedItems.length > 0) }
|
||||
>
|
||||
<fui.Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle" icon={ <ic.MoreVerticalRegular /> } />
|
||||
</fui.MenuTrigger>
|
||||
</fui.Tooltip>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
|
||||
<fui.MenuItem icon={ <SettingsIcon /> } onClick={ () => browser.runtime.openOptionsPage() }>
|
||||
{ i18n.t("options_page.title") }
|
||||
</fui.MenuItem>
|
||||
<fui.MenuItemCheckbox name="tilesView" value="true" icon={ <ViewIcon /> }>
|
||||
{ i18n.t("main.header.menu.tiles_view") }
|
||||
</fui.MenuItemCheckbox>
|
||||
|
||||
<fui.MenuDivider />
|
||||
|
||||
<fui.MenuItemLink icon={ <BmcIcon /> } { ...extLink(buyMeACoffeeLink) }>
|
||||
{ i18n.t("common.cta.sponsor") }
|
||||
</fui.MenuItemLink>
|
||||
<fui.MenuItemLink icon={ <FeedbackIcon /> } { ...extLink(storeLink) } >
|
||||
{ i18n.t("common.cta.feedback") }
|
||||
</fui.MenuItemLink>
|
||||
<fui.MenuItemLink icon={ <LearnIcon /> } { ...extLink(githubLinks.release) } >
|
||||
{ i18n.t("main.header.menu.changelog") }
|
||||
</fui.MenuItemLink>
|
||||
|
||||
{ import.meta.env.DEV &&
|
||||
<fui.MenuGroup>
|
||||
<fui.MenuGroupHeader>Dev tools</fui.MenuGroupHeader>
|
||||
<fui.MenuItem
|
||||
icon={ <ic.ArrowClockwise20Regular /> }
|
||||
onClick={ () => document.location.reload() }
|
||||
>
|
||||
Reload page
|
||||
</fui.MenuItem>
|
||||
<fui.MenuItem
|
||||
icon={ <ic.Open20Regular /> }
|
||||
onClick={ () => browser.tabs.create({ url: browser.runtime.getURL("/sidepanel.html"), active: true }) }
|
||||
>
|
||||
Open in tab
|
||||
</fui.MenuItem>
|
||||
<fui.MenuItem
|
||||
icon={ <ic.Alert20Regular /> }
|
||||
onClick={ async () => await sendNotification({
|
||||
icon: "/notification_icons/cloud_error.png",
|
||||
message: "Notification message",
|
||||
title: "Notification title"
|
||||
}) }
|
||||
>
|
||||
Show test notification
|
||||
</fui.MenuItem>
|
||||
</fui.MenuGroup>
|
||||
}
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user