mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-07-02 19:52:47 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41123bd8db | |||
| 9fbc152a91 |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: e.g. Sometimes clicking on the extension icon doesn't open the side panel
|
||||
placeholder: e.g. Sometimes when generating a password not all character sets are included
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -105,5 +105,3 @@ body:
|
||||
required: true
|
||||
- label: The provided reproduction is a minimal reproducible example of the bug.
|
||||
required: true
|
||||
- label: This issue was written in English.
|
||||
required: true
|
||||
|
||||
@@ -60,5 +60,3 @@ body:
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
- label: This issue was written in English.
|
||||
required: true
|
||||
|
||||
@@ -52,10 +52,7 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
@@ -65,7 +62,4 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
devcontainers:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
with:
|
||||
name: chrome
|
||||
|
||||
- uses: wdzeng/chrome-extension@v2.0.1
|
||||
- uses: wdzeng/chrome-extension@v1.3.0
|
||||
with:
|
||||
extension-id: ${{ secrets.CHROME_EXT_ID }}
|
||||
zip-path: tabs-aside-*-chrome.zip
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
with:
|
||||
name: chrome
|
||||
|
||||
- uses: wdzeng/edge-addon@v2.1.1
|
||||
- uses: wdzeng/edge-addon@v2.1.0
|
||||
with:
|
||||
product-id: ${{ secrets.EDGE_PRODUCT_ID }}
|
||||
zip-path: tabs-aside-*-chrome.zip
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
with:
|
||||
name: firefox
|
||||
|
||||
- uses: wdzeng/firefox-addon@v1.2.1
|
||||
- uses: wdzeng/firefox-addon@v1.2.0
|
||||
with:
|
||||
addon-guid: ${{ secrets.FIREFOX_EXT_UUID }}
|
||||
xpi-path: tabs-aside-*-firefox.zip
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
extver=`jq -r ".version" package.json`
|
||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: dev-build-deploy/release-me@v1.0.0
|
||||
- uses: dev-build-deploy/release-me@v0.18.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: v
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Eugene Fox
|
||||
Copyright (c) 2025 Eugene Fox
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -97,4 +97,4 @@ If you are interested in fixing issues and contributing directly to the code bas
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
|
||||
> ©2026 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE)
|
||||
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE)
|
||||
|
||||
@@ -42,10 +42,8 @@ export const useOptionsStyles = makeStyles({
|
||||
alignItems: "flex-start",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
},
|
||||
img:
|
||||
messageBar:
|
||||
{
|
||||
height: "100px",
|
||||
flexGrow: 1,
|
||||
alignSelf: "flex-end"
|
||||
flexShrink: 0
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
|
||||
import { bskyLink, buyMeACoffeeLink, githubLinks, storeLink, websiteLink } from "@/data/links";
|
||||
import { useBmcStyles } from "@/hooks/useBmcStyles";
|
||||
import extLink from "@/utils/extLink";
|
||||
import { Body1, Button, Caption1, Image, Link, Subtitle1, Text } from "@fluentui/react-components";
|
||||
import { Body1, Button, Caption1, Link, Subtitle1, Text } from "@fluentui/react-components";
|
||||
import { PersonFeedback20Regular } from "@fluentui/react-icons";
|
||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import Package from "@/package.json";
|
||||
@@ -19,6 +19,25 @@ export default function AboutSection(): React.ReactElement
|
||||
<sup><Caption1> v{ Package.version }</Caption1></sup>
|
||||
</Text>
|
||||
|
||||
<Body1 as="p">
|
||||
{ i18n.t("options_page.about.developed_by") } (<Link { ...extLink(bskyLink) }>@xfox111.net</Link>)<br />
|
||||
{ i18n.t("options_page.about.licensed_under") } <Link { ...extLink(githubLinks.license) }>{ i18n.t("options_page.about.mit_license") }</Link>
|
||||
</Body1>
|
||||
|
||||
<Body1 as="p">
|
||||
{ i18n.t("options_page.about.translation_cta.text") }<br />
|
||||
<Link { ...extLink(githubLinks.translationGuide) }>
|
||||
{ i18n.t("options_page.about.translation_cta.button") }
|
||||
</Link>
|
||||
</Body1>
|
||||
|
||||
<Body1 as="p">
|
||||
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
|
||||
</Body1>
|
||||
|
||||
<div className={ cls.horizontalButtons }>
|
||||
<Button
|
||||
as="a" { ...extLink(storeLink) }
|
||||
@@ -35,27 +54,6 @@ export default function AboutSection(): React.ReactElement
|
||||
{ i18n.t("common.cta.sponsor") }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Body1 as="p">
|
||||
{ i18n.t("options_page.about.translation_cta.text") }<br />
|
||||
<Link { ...extLink(githubLinks.translationGuide) }>
|
||||
{ i18n.t("options_page.about.translation_cta.button") }
|
||||
</Link>
|
||||
</Body1>
|
||||
|
||||
<Body1 as="p">
|
||||
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
|
||||
</Body1>
|
||||
|
||||
<Body1 as="p">
|
||||
{ i18n.t("options_page.about.developed_by") } (<Link { ...extLink(bskyLink) }>@xfox111.net</Link>)<br />
|
||||
{ i18n.t("options_page.about.licensed_under") } <Link { ...extLink(githubLinks.license) }>{ i18n.t("options_page.about.mit_license") }</Link>
|
||||
</Body1>
|
||||
|
||||
<Image className={ cls.img } src="/fox.svg" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components";
|
||||
import { Button, Divider, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Subtitle2, Switch } from "@fluentui/react-components";
|
||||
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import exportData from "../utils/exportData";
|
||||
import importData from "../utils/importData";
|
||||
import BookmarksSection from "@/features/netscapeBookmarks/layouts/BookmarksSection";
|
||||
|
||||
export default function StorageSection(): React.ReactElement
|
||||
{
|
||||
@@ -78,6 +79,59 @@ export default function StorageSection(): React.ReactElement
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle2>{ i18n.t("options_page.storage.manage_title") }</Subtitle2>
|
||||
|
||||
{ isCloudDisabled === false &&
|
||||
<Field
|
||||
label={ i18n.t("options_page.storage.capacity.title") }
|
||||
hint={ i18n.t("options_page.storage.capacity.description", [(bytesInUse / 1024).toFixed(1), storageQuota / 1024]) }
|
||||
validationState={ usedStorageRatio >= 0.8 ? "error" : undefined }
|
||||
>
|
||||
<ProgressBar value={ usedStorageRatio } thickness="large" />
|
||||
</Field>
|
||||
}
|
||||
|
||||
<div className={ cls.horizontalButtons }>
|
||||
{ isCloudDisabled === true &&
|
||||
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
|
||||
{ i18n.t("options_page.storage.enable") }
|
||||
</Button>
|
||||
}
|
||||
|
||||
{ isCloudDisabled === false &&
|
||||
<div className={ cls.horizontalButtons }>
|
||||
<Button
|
||||
appearance="subtle" className={ dangerCls.buttonSubtle }
|
||||
onClick={ handleDisableCloud }
|
||||
>
|
||||
{ i18n.t("options_page.storage.disable") }
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={ cls.horizontalButtons }>
|
||||
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
|
||||
{ i18n.t("options_page.storage.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
|
||||
{ i18n.t("options_page.storage.import") }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ importResult !== null &&
|
||||
<MessageBar intent={ importResult ? "success" : "error" } className={ cls.messageBar }>
|
||||
<MessageBarBody>
|
||||
{ importResult === true ?
|
||||
i18n.t("options_page.storage.import_results.success") :
|
||||
i18n.t("options_page.storage.import_results.error")
|
||||
}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<Divider />
|
||||
<Subtitle2>{ i18n.t("options_page.storage.thumbnails_title") }</Subtitle2>
|
||||
<div className={ cls.group }>
|
||||
<Switch
|
||||
checked={ isThumbnailCaptureEnabled ?? true }
|
||||
@@ -101,52 +155,8 @@ export default function StorageSection(): React.ReactElement
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ isCloudDisabled === false &&
|
||||
<Field
|
||||
label={ i18n.t("options_page.storage.capacity.title") }
|
||||
hint={ i18n.t("options_page.storage.capacity.description", [(bytesInUse / 1024).toFixed(1), storageQuota / 1024]) }
|
||||
validationState={ usedStorageRatio >= 0.8 ? "error" : undefined }
|
||||
>
|
||||
<ProgressBar value={ usedStorageRatio } thickness="large" />
|
||||
</Field>
|
||||
}
|
||||
|
||||
{ isCloudDisabled === true &&
|
||||
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
|
||||
{ i18n.t("options_page.storage.enable") }
|
||||
</Button>
|
||||
}
|
||||
|
||||
<div className={ cls.horizontalButtons }>
|
||||
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
|
||||
{ i18n.t("options_page.storage.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
|
||||
{ i18n.t("options_page.storage.import") }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ importResult !== null &&
|
||||
<MessageBar intent={ importResult ? "success" : "error" }>
|
||||
<MessageBarBody>
|
||||
{ importResult === true ?
|
||||
i18n.t("options_page.storage.import_results.success") :
|
||||
i18n.t("options_page.storage.import_results.error")
|
||||
}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
{ isCloudDisabled === false &&
|
||||
<div className={ cls.horizontalButtons }>
|
||||
<Button
|
||||
appearance="subtle" className={ dangerCls.buttonSubtle }
|
||||
onClick={ handleDisableCloud }
|
||||
>
|
||||
{ i18n.t("options_page.storage.disable") }
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
<Divider />
|
||||
<BookmarksSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,11 +19,6 @@ export const useStyles_CollectionView = makeStyles({
|
||||
"&:hover":
|
||||
{
|
||||
boxShadow: tokens.shadow4
|
||||
},
|
||||
|
||||
"&:not(:focus-within) .compact":
|
||||
{
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
color:
|
||||
|
||||
@@ -12,12 +12,7 @@ import { useStyles_CollectionView } from "./CollectionView.styles";
|
||||
import GroupView from "./GroupView";
|
||||
import TabView from "./TabView";
|
||||
|
||||
export default function CollectionView({
|
||||
collection,
|
||||
index: collectionIndex,
|
||||
dragOverlay,
|
||||
compact
|
||||
}: CollectionViewProps): ReactElement
|
||||
export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement
|
||||
{
|
||||
const { tilesView } = useCollections();
|
||||
const {
|
||||
@@ -58,12 +53,12 @@ export default function CollectionView({
|
||||
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
|
||||
<>
|
||||
{ collection.items.length < 1 ?
|
||||
<div className={ mergeClasses(cls.empty, compact === true && "compact") }>
|
||||
<div className={ cls.empty }>
|
||||
<CollectionsRegular fontSize={ 32 } />
|
||||
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
|
||||
</div>
|
||||
:
|
||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList, compact === true && "compact") }>
|
||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
||||
<SortableContext
|
||||
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
|
||||
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
|
||||
@@ -71,12 +66,9 @@ export default function CollectionView({
|
||||
{ collection.items.map((i, index) =>
|
||||
i.type === "group" ?
|
||||
<GroupView
|
||||
key={ index } group={ i } indices={ [collectionIndex, index] }
|
||||
collectionId={ collection.timestamp } />
|
||||
key={ index } group={ i } indices={ [collectionIndex, index] } />
|
||||
:
|
||||
<TabView
|
||||
key={ index } tab={ i } indices={ [collectionIndex, index] }
|
||||
collectionId={ collection.timestamp } />
|
||||
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
@@ -93,5 +85,4 @@ export type CollectionViewProps =
|
||||
collection: CollectionItem;
|
||||
index: number;
|
||||
dragOverlay?: boolean;
|
||||
compact?: boolean | null;
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
||||
onChange={ (_, e) => setTitle(e.value) } />
|
||||
</fui.Field>
|
||||
<fui.Field label={ i18n.t("dialogs.edit.color") }>
|
||||
<fui.Field label="Color">
|
||||
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
|
||||
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
||||
<fui.ToggleButton
|
||||
|
||||
@@ -14,7 +14,7 @@ import GroupMoreMenu from "./collections/GroupMoreMenu";
|
||||
import { useStyles_GroupView } from "./GroupView.styles";
|
||||
import TabView from "./TabView";
|
||||
|
||||
export default function GroupView({ group, indices, dragOverlay, collectionId }: GroupViewProps): ReactElement
|
||||
export default function GroupView({ group, indices, dragOverlay }: GroupViewProps): ReactElement
|
||||
{
|
||||
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
|
||||
const { tilesView } = useCollections();
|
||||
@@ -101,9 +101,7 @@ export default function GroupView({ group, indices, dragOverlay, collectionId }:
|
||||
strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy }
|
||||
>
|
||||
{ group.items.map((i, index) =>
|
||||
<TabView
|
||||
key={ index } tab={ i } indices={ [...indices, index] }
|
||||
collectionId={ collectionId } />
|
||||
<TabView key={ index } tab={ i } indices={ [...indices, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
@@ -119,5 +117,4 @@ export type GroupViewProps =
|
||||
group: GroupItem;
|
||||
indices: number[];
|
||||
dragOverlay?: boolean;
|
||||
collectionId: number;
|
||||
};
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { track } from "@/features/analytics";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { Button, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger, Field, Input, makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export default function TabEditDialog({ tab, onSave }: TabEditDialogProps): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
const [title, setTitle] = useState(tab.title ?? "");
|
||||
const [url, setUrl] = useState(tab.url);
|
||||
const isValid = useMemo(() => url.trim().length > 0, [url]);
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
track("item_edited", { type: "tab" });
|
||||
onSave({
|
||||
...tab,
|
||||
title: title.trim().length > 0 ? title : undefined,
|
||||
url: url.trim()
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogSurface>
|
||||
<form onSubmit={ onSubmit }>
|
||||
<DialogBody>
|
||||
<DialogTitle>{ i18n.t("dialogs.edit.title.edit_tab") }</DialogTitle>
|
||||
<DialogContent className={ cls.content }>
|
||||
<Input
|
||||
value={ title } onChange={ (_, e) => setTitle(e.value) }
|
||||
placeholder={ i18n.t("dialogs.edit.collection_title") } />
|
||||
<Field validationMessage={ isValid ? undefined : i18n.t("dialogs.edit.url_error") }>
|
||||
<Input
|
||||
value={ url } onChange={ (_, e) => setUrl(e.value) }
|
||||
placeholder="URL" />
|
||||
</Field>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button disabled={ !isValid } appearance="primary" as="button" type="submit">
|
||||
{ i18n.t("common.actions.save") }
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="subtle">{ i18n.t("common.actions.cancel") }</Button>
|
||||
</DialogTrigger>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</form>
|
||||
</DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalMNudge
|
||||
}
|
||||
});
|
||||
|
||||
export type TabEditDialogProps =
|
||||
{
|
||||
tab: TabItem;
|
||||
onSave: (updatedTab: TabItem) => void;
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||
import { bundleIcon, Delete20Filled, Delete20Regular, Edit20Filled, Edit20Regular, MoreHorizontal20Regular } from "@fluentui/react-icons";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
export default function TabMoreButton({ onEdit, onDelete, ...props }: TabMoreButtonProps): React.ReactElement
|
||||
{
|
||||
const EditIcon = bundleIcon(Edit20Filled, Edit20Regular);
|
||||
const DeleteIcon = bundleIcon(Delete20Filled, Delete20Regular);
|
||||
const dangerCls = useDangerStyles();
|
||||
|
||||
const onClick = (ev: React.MouseEvent): void =>
|
||||
{
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
<Button
|
||||
appearance="subtle" icon={ <MoreHorizontal20Regular /> }
|
||||
onClick={ onClick }
|
||||
{ ...props } />
|
||||
</MenuTrigger>
|
||||
</Tooltip>
|
||||
|
||||
<MenuPopover onClick={ ev => ev.stopPropagation() }>
|
||||
<MenuList>
|
||||
<MenuItem icon={ <EditIcon /> } onClick={ onEdit }>
|
||||
{ i18n.t("dialogs.edit.title.edit_tab") }
|
||||
</MenuItem>
|
||||
<MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ onDelete }>
|
||||
{ i18n.t("tabs.delete") }
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export type TabMoreButtonProps =
|
||||
ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
{
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
};
|
||||
@@ -4,17 +4,16 @@ 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 { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
|
||||
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";
|
||||
import TabMoreButton from "./TabMoreButton";
|
||||
import TabEditDialog from "./TabEditDialog";
|
||||
|
||||
export default function TabView({ tab, indices, dragOverlay, collectionId }: TabViewProps): ReactElement
|
||||
export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement
|
||||
{
|
||||
const { removeItem, graphics, tilesView, collections, updateCollection } = useCollections();
|
||||
const { removeItem, graphics, tilesView } = useCollections();
|
||||
const { collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const {
|
||||
setNodeRef, setActivatorNodeRef,
|
||||
@@ -27,8 +26,11 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
|
||||
const cls = useStyles_TabView();
|
||||
|
||||
const handleDelete = (): void =>
|
||||
const handleDelete: MouseEventHandler<HTMLButtonElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
args.stopPropagation();
|
||||
|
||||
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
|
||||
|
||||
if (deletePrompt)
|
||||
@@ -43,26 +45,6 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
removeItem(...removeIndex);
|
||||
};
|
||||
|
||||
const handleEdit = (): void =>
|
||||
{
|
||||
if (collectionId < 0)
|
||||
return;
|
||||
|
||||
const updateTab = async (updatedTab: TabItem): Promise<void> =>
|
||||
{
|
||||
const collection: CollectionItem = collections!.find(i => i.timestamp === collectionId)!;
|
||||
|
||||
if (indices.length > 2)
|
||||
(collection.items[indices[1]] as GroupItem).items[indices[2]] = updatedTab;
|
||||
else
|
||||
collection.items[indices[1]] = updatedTab;
|
||||
|
||||
await updateCollection(collection, collection.timestamp);
|
||||
};
|
||||
|
||||
dialog.pushCustom(<TabEditDialog tab={ tab } onSave={ updateTab } />);
|
||||
};
|
||||
|
||||
const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
@@ -109,10 +91,12 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
</Caption1>
|
||||
</Tooltip>
|
||||
|
||||
<TabMoreButton
|
||||
<Tooltip relationship="label" content={ i18n.t("tabs.delete") }>
|
||||
<Button
|
||||
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
|
||||
onEdit={ handleEdit }
|
||||
onDelete={ handleDelete } />
|
||||
appearance="subtle" icon={ <Dismiss20Regular /> }
|
||||
onClick={ handleDelete } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -123,5 +107,4 @@ export type TabViewProps =
|
||||
tab: TabItem;
|
||||
indices: number[];
|
||||
dragOverlay?: boolean;
|
||||
collectionId: number;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionT
|
||||
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, EyeOff16Regular } from "@fluentui/react-icons";
|
||||
import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons";
|
||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import CollectionMoreButton from "./CollectionMoreButton";
|
||||
@@ -23,7 +23,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync(true);
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateCollection({
|
||||
@@ -45,12 +45,9 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
content={ getCollectionTitle(collection) }
|
||||
positioning="above-start"
|
||||
>
|
||||
<div className={ cls.titleContainer }>
|
||||
{ collection.hidden && <EyeOff16Regular /> }
|
||||
<Subtitle2 truncate wrap={ false } className={ cls.titleText }>
|
||||
{ getCollectionTitle(collection) }
|
||||
</Subtitle2>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Caption1>
|
||||
@@ -115,11 +112,5 @@ const useStyles = makeStyles({
|
||||
showToolbar:
|
||||
{
|
||||
display: "flex"
|
||||
},
|
||||
titleContainer:
|
||||
{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalS
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,8 +22,6 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
|
||||
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 ShowIcon = ic.bundleIcon(ic.Eye20Filled, ic.Eye20Regular);
|
||||
const HideIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular);
|
||||
|
||||
const dangerCls = useDangerStyles();
|
||||
|
||||
@@ -41,11 +39,6 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
|
||||
removeItem(collection.timestamp);
|
||||
};
|
||||
|
||||
const toggleHidden = () =>
|
||||
{
|
||||
updateCollection({ ...collection, hidden: !collection.hidden }, collection.timestamp);
|
||||
};
|
||||
|
||||
const handleEdit = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
@@ -89,9 +82,6 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
|
||||
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
|
||||
{ i18n.t("collections.menu.edit") }
|
||||
</MenuItem>
|
||||
<MenuItem icon={ collection.hidden ? <ShowIcon /> : <HideIcon /> } onClick={ toggleHidden }>
|
||||
{ collection.hidden ? i18n.t("collections.menu.unhide") : i18n.t("collections.menu.hide") }
|
||||
</MenuItem>
|
||||
<MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ handleDelete }>
|
||||
{ i18n.t("collections.menu.delete") }
|
||||
</MenuItem>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function GroupMoreMenu(): ReactElement
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync(true);
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateGroup({
|
||||
|
||||
@@ -51,9 +51,5 @@ export const useStyles_CollectionListView = makeStyles({
|
||||
{
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
|
||||
}
|
||||
},
|
||||
compactList:
|
||||
{
|
||||
alignItems: "baseline"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,10 +18,10 @@ import CollectionContext from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import applyReorder from "../../utils/dnd/applyReorder";
|
||||
import { collisionDetector } from "../../utils/dnd/collisionDetector";
|
||||
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
|
||||
import { useStyles_CollectionListView } from "./CollectionListView.styles";
|
||||
import SearchBar from "./SearchBar";
|
||||
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
|
||||
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
|
||||
|
||||
export default function CollectionListView(): ReactElement
|
||||
{
|
||||
@@ -30,19 +30,17 @@ export default function CollectionListView(): ReactElement
|
||||
const [sortMode, setSortMode] = useSettings("sortMode");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [colors, setColors] = useState<CollectionFilterType["colors"]>([]);
|
||||
const [showHidden, setShowHidden] = useState<boolean>(false);
|
||||
const [compactView] = useSettings("compactView");
|
||||
|
||||
const [active, setActive] = useState<DndItem | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { delay: 150, tolerance: 20 } }),
|
||||
useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
|
||||
);
|
||||
|
||||
const resultList = useMemo(
|
||||
() => sortCollections(filterCollections(collections, { query, colors, showHidden }), sortMode),
|
||||
[query, colors, sortMode, collections, showHidden]
|
||||
() => sortCollections(filterCollections(collections, { query, colors }), sortMode),
|
||||
[query, colors, sortMode, collections]
|
||||
);
|
||||
|
||||
const cls = useStyles_CollectionListView();
|
||||
@@ -51,13 +49,6 @@ export default function CollectionListView(): ReactElement
|
||||
{
|
||||
setQuery("");
|
||||
setColors([]);
|
||||
setShowHidden(false);
|
||||
}, []);
|
||||
|
||||
const updateFilter = useCallback((newColors: CollectionFilterType["colors"], newShowHidden: boolean) =>
|
||||
{
|
||||
setColors(newColors);
|
||||
setShowHidden(newShowHidden);
|
||||
}, []);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent): void =>
|
||||
@@ -96,9 +87,8 @@ export default function CollectionListView(): ReactElement
|
||||
<article className={ cls.root }>
|
||||
<SearchBar
|
||||
query={ query } onQueryChange={ setQuery }
|
||||
filter={ colors } onFilterChange={ updateFilter }
|
||||
filter={ colors } onFilterChange={ setColors }
|
||||
sort={ sortMode } onSortChange={ setSortMode }
|
||||
showHidden={ showHidden }
|
||||
onReset={ resetFilter } />
|
||||
|
||||
<CtaMessage className={ cls.msgBar } />
|
||||
@@ -115,7 +105,7 @@ export default function CollectionListView(): ReactElement
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView, !!(!tilesView && compactView) && cls.compactList) }>
|
||||
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView) }>
|
||||
<DndContext
|
||||
sensors={ sensors }
|
||||
collisionDetection={ collisionDetector(!tilesView) }
|
||||
@@ -128,7 +118,7 @@ export default function CollectionListView(): ReactElement
|
||||
strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy }
|
||||
>
|
||||
{ resultList.map((collection, index) =>
|
||||
<CollectionView key={ index } collection={ collection } index={ index } compact={ compactView } />
|
||||
<CollectionView key={ index } collection={ collection } index={ index } />
|
||||
) }
|
||||
</SortableContext>
|
||||
|
||||
@@ -145,9 +135,9 @@ export default function CollectionListView(): ReactElement
|
||||
} }
|
||||
>
|
||||
{ active.item.type === "group" ?
|
||||
<GroupView group={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay />
|
||||
<GroupView group={ active.item } indices={ [-1] } dragOverlay />
|
||||
:
|
||||
<TabView tab={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay />
|
||||
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
|
||||
}
|
||||
</CollectionContext.Provider>
|
||||
:
|
||||
|
||||
@@ -3,48 +3,32 @@ 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, showHidden }: FilterCollectionsButtonProps): React.ReactElement
|
||||
export default function FilterCollectionsButton({ value, onChange }: FilterCollectionsButtonProps): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
const colorCls = useGroupColors();
|
||||
|
||||
const FilterIcon = ic.bundleIcon(ic.Filter20Filled, ic.Filter20Regular);
|
||||
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);
|
||||
const HiddenIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular);
|
||||
|
||||
const values: Record<string, string[]> = useMemo(() => ({
|
||||
default: !value || value.length < 1 ? ["any"] : [],
|
||||
colors: value || [],
|
||||
hidden: showHidden ? ["show"] : []
|
||||
}), [value, showHidden]);
|
||||
|
||||
const onCheckedValueChange = useCallback((_: fui.MenuCheckedValueChangeEvent, e: fui.MenuCheckedValueChangeData) =>
|
||||
{
|
||||
if (e.name === "hidden")
|
||||
onChange?.(value ?? [], e.checkedItems.includes("show"));
|
||||
else
|
||||
onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"], showHidden ?? false);
|
||||
}, [onChange, showHidden, value]);
|
||||
|
||||
return (
|
||||
<fui.Menu
|
||||
checkedValues={ values }
|
||||
onCheckedValueChange={ onCheckedValueChange }
|
||||
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={ <FilterIcon /> } />
|
||||
<fui.Button appearance="subtle" icon={ <ColorFilterIcon /> } />
|
||||
</fui.MenuTrigger>
|
||||
</fui.Tooltip>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="hidden" value="show" icon={ <HiddenIcon /> }>
|
||||
{ i18n.t("main.list.searchbar.show_hidden") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="default" value="any" icon={ <AnyColorIcon /> }>
|
||||
{ i18n.t("colors.any") }
|
||||
</fui.MenuItemCheckbox>
|
||||
@@ -76,8 +60,7 @@ export default function FilterCollectionsButton({ value, onChange, showHidden }:
|
||||
export type FilterCollectionsButtonProps =
|
||||
{
|
||||
value?: CollectionFilterType["colors"];
|
||||
showHidden?: boolean;
|
||||
onChange?: (value: CollectionFilterType["colors"], showHidden: boolean) => void;
|
||||
onChange?: (value: CollectionFilterType["colors"]) => void;
|
||||
};
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function SearchBar(props: SearchBarProps): React.ReactElement
|
||||
<Button appearance="subtle" icon={ <ResetIcon /> } onClick={ props.onReset } />
|
||||
</Tooltip>
|
||||
}
|
||||
<FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } showHidden={ props.showHidden } />
|
||||
<FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } />
|
||||
<SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } />
|
||||
</>
|
||||
} />
|
||||
@@ -37,9 +37,8 @@ export type SearchBarProps =
|
||||
query?: string;
|
||||
onQueryChange?: (query: string) => void;
|
||||
filter?: CollectionFilterType["colors"];
|
||||
onFilterChange?: (filter: CollectionFilterType["colors"], showHidden: boolean) => void;
|
||||
onFilterChange?: (filter: CollectionFilterType["colors"]) => void;
|
||||
sort?: CollectionSortMode;
|
||||
showHidden?: boolean;
|
||||
onSortChange?: (sort: CollectionSortMode) => void;
|
||||
onReset?: () => void;
|
||||
};
|
||||
|
||||
@@ -11,32 +11,18 @@ import { ReactElement } from "react";
|
||||
export default function MoreButton(): ReactElement
|
||||
{
|
||||
const [tilesView, setTilesView] = useSettings("tilesView");
|
||||
const [compactView, setCompactView] = useSettings("compactView");
|
||||
|
||||
const SettingsIcon: ic.FluentIcon = ic.bundleIcon(ic.Settings20Filled, ic.Settings20Regular);
|
||||
const GridIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular);
|
||||
const CompactIcon: ic.FluentIcon = ic.bundleIcon(ic.ArrowMinimizeVerticalFilled, ic.ArrowMinimizeVerticalRegular);
|
||||
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);
|
||||
|
||||
const checkedValues = useMemo(() => ({
|
||||
view: [
|
||||
tilesView ? "tiles" : "",
|
||||
compactView ? "compact" : ""
|
||||
]
|
||||
}), [tilesView, compactView]);
|
||||
|
||||
const onCheckedValueChange = (_: unknown, e: fui.MenuCheckedValueChangeData) =>
|
||||
{
|
||||
setTilesView(e.checkedItems.includes("tiles"));
|
||||
setCompactView(e.checkedItems.includes("compact"));
|
||||
};
|
||||
|
||||
return (
|
||||
<fui.Menu
|
||||
hasIcons hasCheckmarks
|
||||
checkedValues={ checkedValues } onCheckedValueChange={ onCheckedValueChange }
|
||||
checkedValues={ { tilesView: tilesView ? ["true"] : [] } }
|
||||
onCheckedValueChange={ (_, e) => setTilesView(e.checkedItems.length > 0) }
|
||||
>
|
||||
<fui.Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
@@ -50,12 +36,9 @@ export default function MoreButton(): ReactElement
|
||||
<fui.MenuItem icon={ <SettingsIcon /> } onClick={ () => browser.runtime.openOptionsPage() }>
|
||||
{ i18n.t("options_page.title") }
|
||||
</fui.MenuItem>
|
||||
<fui.MenuItemCheckbox name="view" value="tiles" icon={ <GridIcon /> }>
|
||||
<fui.MenuItemCheckbox name="tilesView" value="true" icon={ <ViewIcon /> }>
|
||||
{ i18n.t("main.header.menu.tiles_view") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="view" value="compact" icon={ <CompactIcon /> }>
|
||||
{ i18n.t("main.header.menu.compact_view") }
|
||||
</fui.MenuItemCheckbox>
|
||||
|
||||
<fui.MenuDivider />
|
||||
|
||||
|
||||
@@ -9,16 +9,13 @@ export default function filterCollections(
|
||||
if (!collections || collections.length < 1)
|
||||
return [];
|
||||
|
||||
if (!filter.query && filter.colors.length < 1 && filter.showHidden)
|
||||
if (!filter.query && filter.colors.length < 1)
|
||||
return collections;
|
||||
|
||||
const query: string = filter.query.toLocaleLowerCase();
|
||||
|
||||
return collections.filter(collection =>
|
||||
{
|
||||
if (filter.showHidden === false && collection.hidden === true)
|
||||
return false;
|
||||
|
||||
let querySatisfied: boolean = query.length < 1 ||
|
||||
getCollectionTitle(collection).toLocaleLowerCase().includes(query);
|
||||
let colorSatisfied: boolean = filter.colors.length < 1 ||
|
||||
@@ -65,5 +62,4 @@ export type CollectionFilterType =
|
||||
{
|
||||
query: string;
|
||||
colors: (`${Browser.tabGroups.Color}` | "none")[];
|
||||
showHidden: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { Body1, Button, makeStyles, MessageBar, MessageBarBody, Subtitle2, tokens } from "@fluentui/react-components";
|
||||
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
||||
import importBookmarks from "../utils/importBookmarks";
|
||||
import exportBookmarks from "../utils/exportBookmarks";
|
||||
|
||||
export default function BookmarksSection(): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
const dialog = useDialog();
|
||||
|
||||
const [importResult, setImportResult] = useState<number | null>(null);
|
||||
|
||||
const handleImport = (): void =>
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("features.netscape_bookmarks.import_dialog.title"),
|
||||
confirmText: i18n.t("options_page.storage.import_prompt.proceed"),
|
||||
onConfirm: () => importBookmarks().then(setImportResult),
|
||||
content: (
|
||||
<Body1 as="p">
|
||||
{ i18n.t("features.netscape_bookmarks.import_dialog.content") }
|
||||
</Body1>
|
||||
)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cls.root }>
|
||||
<Subtitle2>{ i18n.t("features.netscape_bookmarks.title") }</Subtitle2>
|
||||
|
||||
{ importResult !== null &&
|
||||
<MessageBar intent={ importResult >= 0 ? "success" : "error" } layout="multiline">
|
||||
<MessageBarBody>
|
||||
{ importResult >= 0 ?
|
||||
i18n.t("features.netscape_bookmarks.import_result.success", [importResult]) :
|
||||
i18n.t("features.netscape_bookmarks.import_result.error")
|
||||
}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<div className={ cls.buttons }>
|
||||
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportBookmarks }>
|
||||
{ i18n.t("features.netscape_bookmarks.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
|
||||
{ i18n.t("features.netscape_bookmarks.import") }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalMNudge
|
||||
},
|
||||
buttons:
|
||||
{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { CollectionItem, GraphicsStorage, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
|
||||
|
||||
export default function convertBookmarks(bookmarks: Bookmark[]): [CollectionItem[], GraphicsStorage, number]
|
||||
{
|
||||
let count: number = 0;
|
||||
const graphics: GraphicsStorage = {};
|
||||
const items: CollectionItem[] = [];
|
||||
const untitled: CollectionItem = {
|
||||
items: [],
|
||||
timestamp: Date.now(),
|
||||
type: "collection"
|
||||
};
|
||||
|
||||
for (const bookmark of bookmarks)
|
||||
{
|
||||
if (bookmark.type === "bookmark")
|
||||
{
|
||||
untitled.items.push(getTab(bookmark, graphics));
|
||||
count++;
|
||||
}
|
||||
else if (bookmark.type === "folder")
|
||||
{
|
||||
const collection: CollectionItem = getCollection(bookmark, graphics);
|
||||
items.push(collection);
|
||||
count += collection.items.reduce((acc, item) =>
|
||||
{
|
||||
if (item.type === "tab")
|
||||
return acc + 1;
|
||||
else if (item.type === "group")
|
||||
return acc + item.items.length;
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (untitled.items.length > 0)
|
||||
items.unshift(untitled);
|
||||
|
||||
return [items, graphics, count];
|
||||
}
|
||||
|
||||
function getTab(bookmark: Bookmark, graphics: GraphicsStorage): TabItem
|
||||
{
|
||||
if (bookmark.icon)
|
||||
graphics[bookmark.url!] = {
|
||||
icon: bookmark.icon
|
||||
};
|
||||
|
||||
return {
|
||||
type: "tab",
|
||||
url: bookmark.url!,
|
||||
title: bookmark.title || bookmark.url!
|
||||
};
|
||||
}
|
||||
|
||||
function getCollection(bookmark: Bookmark, graphics: GraphicsStorage): CollectionItem
|
||||
{
|
||||
const collection: CollectionItem = {
|
||||
items: [],
|
||||
title: bookmark.title,
|
||||
timestamp: Date.now(),
|
||||
type: "collection"
|
||||
};
|
||||
|
||||
if (bookmark.children)
|
||||
for (const child of bookmark.children)
|
||||
{
|
||||
if (child.type === "bookmark")
|
||||
collection.items.push(getTab(child, graphics));
|
||||
else if (child.type === "folder" && child.children)
|
||||
collection.items.push(getGroup(child, graphics));
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
function getGroup(bookmark: Bookmark, graphics: GraphicsStorage): GroupItem
|
||||
{
|
||||
const group: GroupItem = {
|
||||
items: [],
|
||||
title: bookmark.title,
|
||||
pinned: false,
|
||||
type: "group",
|
||||
color: getRandomColor()
|
||||
};
|
||||
|
||||
if (bookmark.children)
|
||||
for (const child of bookmark.children)
|
||||
{
|
||||
if (child.type === "bookmark")
|
||||
group.items.push(getTab(child, graphics));
|
||||
else if (child.type === "folder")
|
||||
group.items.push(...getGroup(child, graphics).items);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
function getRandomColor(): "blue" | "cyan" | "green" | "grey" | "orange" | "pink" | "purple" | "red" | "yellow"
|
||||
{
|
||||
const colors = ["blue", "cyan", "green", "grey", "orange", "pink", "purple", "red", "yellow"] as const;
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { getCollections } from "@/features/collectionStorage";
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
|
||||
export default async function exportBookmarks(): Promise<void>
|
||||
{
|
||||
const [collections] = await getCollections();
|
||||
const lines: string[] = [
|
||||
"<!DOCTYPE NETSCAPE-Bookmark-file-1>",
|
||||
"<!-- This is an automatically generated file.",
|
||||
" It will be read and overwritten.",
|
||||
" DO NOT EDIT! -->",
|
||||
"<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">",
|
||||
"<TITLE>Bookmarks</TITLE>",
|
||||
"<H1>Bookmarks</H1>",
|
||||
"<DL><p>"
|
||||
];
|
||||
|
||||
for (const collection of collections)
|
||||
lines.push(...createFolder(collection));
|
||||
|
||||
lines.push("</DL><p>");
|
||||
|
||||
const data: string = lines.join("\n");
|
||||
|
||||
const blob: Blob = new Blob([data], { type: "text/html" });
|
||||
|
||||
const element: HTMLAnchorElement = document.createElement("a");
|
||||
element.style.display = "none";
|
||||
element.href = URL.createObjectURL(blob);
|
||||
element.setAttribute("download", "collections.html");
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
|
||||
URL.revokeObjectURL(element.href);
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
function createFolder(item: CollectionItem | GroupItem): string[]
|
||||
{
|
||||
const lines: string[] = [];
|
||||
const title: string = item.type === "collection" ?
|
||||
(item.title ?? getCollectionTitle(item)) :
|
||||
(item.pinned ? i18n.t("groups.pinned") : (item.title ?? ""));
|
||||
|
||||
lines.push(`<DT><H3>${sanitizeString(title)}</H3>`);
|
||||
lines.push("<DL><p>");
|
||||
|
||||
for (const subItem of item.items)
|
||||
{
|
||||
if (subItem.type === "tab")
|
||||
lines.push(`<DT><A HREF="${encodeURI(subItem.url).replace(/"/g, "%22")}">${sanitizeString(subItem.title || subItem.url)}</A>`);
|
||||
else if (subItem.type === "group")
|
||||
lines.push(...createFolder(subItem));
|
||||
}
|
||||
|
||||
lines.push("</DL><p>");
|
||||
return lines;
|
||||
}
|
||||
|
||||
function sanitizeString(str: string): string
|
||||
{
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getCollections, saveCollections } from "@/features/collectionStorage";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import parse from "node-bookmarks-parser";
|
||||
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
|
||||
import convertBookmarks from "./convertBookmarks";
|
||||
|
||||
export default async function importBookmarks(): Promise<number | null>
|
||||
{
|
||||
const element: HTMLInputElement = document.createElement("input");
|
||||
element.style.display = "none";
|
||||
element.hidden = true;
|
||||
element.type = "file";
|
||||
element.accept = ".html";
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
|
||||
await new Promise(resolve =>
|
||||
{
|
||||
const listener = () =>
|
||||
{
|
||||
element.removeEventListener("input", listener);
|
||||
resolve(null);
|
||||
};
|
||||
element.addEventListener("input", listener);
|
||||
});
|
||||
|
||||
if (!element.files || element.files.length < 1)
|
||||
return null;
|
||||
|
||||
const file: File = element.files[0];
|
||||
const content: string = await file.text();
|
||||
|
||||
document.body.removeChild(element);
|
||||
|
||||
try
|
||||
{
|
||||
const bookmarks: Bookmark[] = parse(content);
|
||||
const [data, graphics, tabCount] = convertBookmarks(bookmarks);
|
||||
const [collections, cloudIssues] = await getCollections();
|
||||
|
||||
await saveCollections([...data, ...collections], cloudIssues === null, graphics);
|
||||
sendMessage("refreshCollections", undefined);
|
||||
|
||||
return tabCount;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error("Failed to parse bookmarks file", error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default function useSettingsReviewDialog(dialog: DialogContextType): Prom
|
||||
if (needsReview.length > 0)
|
||||
dialog.pushCustom(
|
||||
<SettingsReviewDialog />,
|
||||
"alert",
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
settingsForReview.removeValue();
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function useWelcomeDialog(dialog: DialogContextType): Promise<voi
|
||||
if (showWelcome || import.meta.env.DEV)
|
||||
dialog.pushCustom(
|
||||
<WelcomeDialog />,
|
||||
"alert",
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
showWelcomeDialog.removeValue();
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "See the full list of what we collect"
|
||||
p3_link: "here"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Browser bookmarks"
|
||||
export: "Export collections as bookmarks"
|
||||
import: "Import from bookmarks file"
|
||||
import_dialog:
|
||||
title: "Import bookmarks"
|
||||
content: "Import bookmarks from a Netscape-format bookmarks file exported from your browser."
|
||||
import_result:
|
||||
success: "Successfully imported $1 bookmarks."
|
||||
error: "Failed to import bookmarks. Please ensure the file is a valid bookmarks file."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "New collection created"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Open tabs and remove the collection"
|
||||
storage:
|
||||
title: "Storage"
|
||||
manage_title: "Storage management"
|
||||
thumbnails_title: "Thumbnails & icons"
|
||||
capacity:
|
||||
title: "Cloud storage capacity"
|
||||
description: "$1 of $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Add empty group"
|
||||
export_bookmarks: "Export to bookmarks"
|
||||
edit: "Edit collection"
|
||||
hide: "Hide collection"
|
||||
unhide: "Unhide collection"
|
||||
|
||||
groups:
|
||||
title: "Group"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Edit collection"
|
||||
edit_group: "Edit group"
|
||||
edit_tab: "Edit tab"
|
||||
new_group: "New group"
|
||||
new_collection: "New collection"
|
||||
collection_title: "Title"
|
||||
color: "Color"
|
||||
url_error: "URL is required"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Create new collection"
|
||||
menu:
|
||||
tiles_view: "Tiles view"
|
||||
compact_view: "Compact view"
|
||||
changelog: "What's new?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Search"
|
||||
filter: "Filter"
|
||||
show_hidden: "Show hidden"
|
||||
sort:
|
||||
title: "Sort"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Ver la lista completa de lo que recopilamos"
|
||||
p3_link: "aquí"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Marcadores del navegador"
|
||||
export: "Exportar colecciones como marcadores"
|
||||
import: "Importar desde archivo de marcadores"
|
||||
import_dialog:
|
||||
title: "Importar marcadores"
|
||||
content: "Importa marcadores desde un archivo de marcadores en formato Netscape exportado desde tu navegador."
|
||||
import_result:
|
||||
success: "Se importaron correctamente $1 marcadores."
|
||||
error: "No se pudieron importar los marcadores. Asegúrate de que el archivo sea un archivo de marcadores válido."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Nueva colección creada"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Abrir pestañas y eliminar la colección"
|
||||
storage:
|
||||
title: "Almacenamiento"
|
||||
manage_title: "Administrar almacenamiento"
|
||||
thumbnails_title: "Miniaturas e íconos"
|
||||
capacity:
|
||||
title: "Capacidad de almacenamiento en la nube"
|
||||
description: "$1 de $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Agregar grupo vacío"
|
||||
export_bookmarks: "Exportar a marcadores"
|
||||
edit: "Editar colección"
|
||||
hide: "Ocultar colección"
|
||||
unhide: "Mostrar colección"
|
||||
|
||||
groups:
|
||||
title: "Grupo"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Editar colección"
|
||||
edit_group: "Editar grupo"
|
||||
edit_tab: "Editar pestaña"
|
||||
new_group: "Nuevo grupo"
|
||||
new_collection: "Nueva colección"
|
||||
collection_title: "Título"
|
||||
color: "Color"
|
||||
url_error: "La URL es obligatoria"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Crear nueva colección"
|
||||
menu:
|
||||
tiles_view: "Vista de mosaicos"
|
||||
compact_view: "Vista compacta"
|
||||
changelog: "¿Qué hay de nuevo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Buscar"
|
||||
filter: "Filtrar"
|
||||
show_hidden: "Mostrar ocultas"
|
||||
sort:
|
||||
title: "Ordenar"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
|
||||
p3_link: "qui"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Segnalibri del browser"
|
||||
export: "Esporta collezioni come segnalibri"
|
||||
import: "Importa da file di segnalibri"
|
||||
import_dialog:
|
||||
title: "Importa segnalibri"
|
||||
content: "Importa segnalibri da un file di segnalibri in formato Netscape esportato dal tuo browser."
|
||||
import_result:
|
||||
success: "Importati con successo $1 segnalibri."
|
||||
error: "Impossibile importare i segnalibri. Assicurati che il file sia un file di segnalibri valido."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Nuova collezione creata"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Apri le schede e rimuovi la collezione"
|
||||
storage:
|
||||
title: "Archiviazione"
|
||||
manage_title: "Gestisci archiviazione"
|
||||
thumbnails_title: "Miniature e icone"
|
||||
capacity:
|
||||
title: "Capacità di archiviazione cloud"
|
||||
description: "$1 di $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Aggiungi gruppo vuoto"
|
||||
export_bookmarks: "Esporta nei segnalibri"
|
||||
edit: "Modifica collezione"
|
||||
hide: "Nascondi collezione"
|
||||
unhide: "Mostra collezione"
|
||||
|
||||
groups:
|
||||
title: "Gruppo"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Modifica collezione"
|
||||
edit_group: "Modifica gruppo"
|
||||
edit_tab: "Modifica scheda"
|
||||
new_group: "Nuovo gruppo"
|
||||
new_collection: "Nuova collezione"
|
||||
collection_title: "Titolo"
|
||||
color: "Colore"
|
||||
url_error: "L'URL è obbligatorio"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Crea nuova collezione"
|
||||
menu:
|
||||
tiles_view: "Vista a riquadri"
|
||||
compact_view: "Vista compatta"
|
||||
changelog: "Cosa c'è di nuovo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Cerca"
|
||||
filter: "Filtra"
|
||||
show_hidden: "Mostra nascoste"
|
||||
sort:
|
||||
title: "Ordina"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Pełną listę zbieranych danych można zobaczyć"
|
||||
p3_link: "tutaj"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Import/eksport zakładek"
|
||||
export: "Eksportuj kolekcje jako plik zakładek"
|
||||
import: "Importuj z pliku zakładek"
|
||||
import_dialog:
|
||||
title: "Import zakładek"
|
||||
content: "Importuj zakładki z pliku zakładek w formacie Netscape wyeksportowanego z przeglądarki."
|
||||
import_result:
|
||||
success: "Zakładki zostały pomyślnie zaimportowane ($1)"
|
||||
error: "Nie udało się zaimportować zakładek. Upewnij się, że plik jest poprawnym plikiem zakładek."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Utworzono nową kolekcję"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Otwórz karty i usuń kolekcję"
|
||||
storage:
|
||||
title: "Magazyn"
|
||||
manage_title: "Zarządzaj magazynem"
|
||||
thumbnails_title: "Podglądy i ikony"
|
||||
capacity:
|
||||
title: "Magazyn w chmurze"
|
||||
description: "$1 z $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Dodaj pustą grupę"
|
||||
export_bookmarks: "Eksportuj do zakładek"
|
||||
edit: "Edytuj kolekcję"
|
||||
hide: "Ukryj kolekcję"
|
||||
unhide: "Pokaż kolekcję"
|
||||
|
||||
groups:
|
||||
title: "Grupa"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Edytuj kolekcję"
|
||||
edit_group: "Edytuj grupę"
|
||||
edit_tab: "Edytuj zakładkę"
|
||||
new_group: "Nowa grupa"
|
||||
new_collection: "Nowa kolekcja"
|
||||
collection_title: "Nazwij"
|
||||
color: "Kolor"
|
||||
url_error: "URL jest wymagany"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Utwórz nową kolekcję"
|
||||
menu:
|
||||
tiles_view: "Kafelki"
|
||||
compact_view: "Widok kompaktowy"
|
||||
changelog: "Co nowego?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Szukaj"
|
||||
filter: "Filtr"
|
||||
show_hidden: "Pokaż ukryte"
|
||||
sort:
|
||||
title: "Sortowanie"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Veja a lista completa do que coletamos"
|
||||
p3_link: "aqui"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Favoritos do navegador"
|
||||
export: "Exportar coleções como favoritos"
|
||||
import: "Importar de arquivo de favoritos"
|
||||
import_dialog:
|
||||
title: "Importar favoritos"
|
||||
content: "Importe favoritos de um arquivo de favoritos no formato Netscape exportado do seu navegador."
|
||||
import_result:
|
||||
success: "Importados com sucesso $1 favoritos."
|
||||
error: "Falha ao importar favoritos. Por favor, certifique-se de que o arquivo é um arquivo de favoritos válido."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Nova coleção criada"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Abrir abas e remover a coleção"
|
||||
storage:
|
||||
title: "Armazenamento"
|
||||
manage_title: "Gerenciar armazenamento"
|
||||
thumbnails_title: "Miniaturas e ícones"
|
||||
capacity:
|
||||
title: "Capacidade de armazenamento na nuvem"
|
||||
description: "$1 de $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Adicionar grupo vazio"
|
||||
export_bookmarks: "Exportar para favoritos"
|
||||
edit: "Editar coleção"
|
||||
hide: "Ocultar coleção"
|
||||
unhide: "Mostrar coleção"
|
||||
|
||||
groups:
|
||||
title: "Grupo"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Editar coleção"
|
||||
edit_group: "Editar grupo"
|
||||
edit_tab: "Editar aba"
|
||||
new_group: "Novo grupo"
|
||||
new_collection: "Nova coleção"
|
||||
collection_title: "Título"
|
||||
color: "Cor"
|
||||
url_error: "A URL é obrigatória"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Criar nova coleção"
|
||||
menu:
|
||||
tiles_view: "Visualização em blocos"
|
||||
compact_view: "Visualização compacta"
|
||||
changelog: "O que há de novo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Pesquisar"
|
||||
filter: "Filtrar"
|
||||
show_hidden: "Mostrar ocultas"
|
||||
sort:
|
||||
title: "Ordenar"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Полный список собираемых данных можно посмотреть"
|
||||
p3_link: "здесь"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Импорт/экспорт закладок"
|
||||
export: "Экспортировать коллекции как файл закладок"
|
||||
import: "Импорт из файла закладок"
|
||||
import_dialog:
|
||||
title: "Импорт закладок"
|
||||
content: "Импортируйте закладки из файла закладок в формате Netscape, экспортированного из вашего браузера."
|
||||
import_result:
|
||||
success: "Закладки успешно импортированы ($1 шт.)"
|
||||
error: "Не удалось импортировать закладки. Пожалуйста, убедитесь, что файл является допустимым файлом закладок."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Создана новая коллекция"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Открыть вкладки и удалить коллекцию"
|
||||
storage:
|
||||
title: "Хранилище"
|
||||
manage_title: "Управление хранилищем"
|
||||
thumbnails_title: "Превью и иконки"
|
||||
capacity:
|
||||
title: "Объём облачного хранилища"
|
||||
description: "$1 из $2 КиБ"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Добавить пустую группу"
|
||||
export_bookmarks: "Экспортировать в закладки"
|
||||
edit: "Редактировать коллекцию"
|
||||
hide: "Скрыть коллекцию"
|
||||
unhide: "Показать коллекцию"
|
||||
|
||||
groups:
|
||||
title: "Группа"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Редактировать коллекцию"
|
||||
edit_group: "Редактировать группу"
|
||||
edit_tab: "Редактировать вкладку"
|
||||
new_group: "Новая группа"
|
||||
new_collection: "Новая коллекция"
|
||||
collection_title: "Название"
|
||||
color: "Цвет"
|
||||
url_error: "URL является обязательным"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Создать новую коллекцию"
|
||||
menu:
|
||||
tiles_view: "Плитки"
|
||||
compact_view: "Компактный вид"
|
||||
changelog: "Что нового?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Поиск"
|
||||
filter: "Фильтр"
|
||||
show_hidden: "Показать скрытые"
|
||||
sort:
|
||||
title: "Сортировка"
|
||||
options:
|
||||
|
||||
+16
-9
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "Повний список зібраних даних можна подивитися"
|
||||
p3_link: "тут"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "Імпорт/експорт закладок"
|
||||
export: "Експортувати колекції як файл закладок"
|
||||
import: "Імпорт із файлу закладок"
|
||||
import_dialog:
|
||||
title: "Імпорт закладок"
|
||||
content: "Імпортуйте закладки з файлу закладок у форматі Netscape, експортованого з вашого браузера."
|
||||
import_result:
|
||||
success: "Закладки успішно імпортовані ($1 шт.)"
|
||||
error: "Не вдалося імпортувати закладки. Будь ласка, переконайтеся, що файл є коректним файлом закладок."
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "Створено нову колекцію"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "Відкрити вкладки та видалити колекцію"
|
||||
storage:
|
||||
title: "Сховище"
|
||||
manage_title: "Керування сховищем"
|
||||
thumbnails_title: "Прев'ю та іконки"
|
||||
capacity:
|
||||
title: "Хмарне сховище"
|
||||
description: "$1 з $2 КіБ"
|
||||
@@ -132,13 +145,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
|
||||
action: "Вимкнути та перезавантажити розширення"
|
||||
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
|
||||
thumbnail_capture: "Зберігати прев'ю і іконки для збережених вкладок"
|
||||
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
|
||||
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
|
||||
clear_thumbnails:
|
||||
action: "Видалити збережені іконки"
|
||||
title: "Видалити превью і іконки?"
|
||||
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
|
||||
title: "Видалити прев'ю і іконки?"
|
||||
prompt: "Ця дія видалить всі прев'ю і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
|
||||
about:
|
||||
title: "О розширенні"
|
||||
developed_by: "Розробник: Євген Лис"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "Додати порожню групу"
|
||||
export_bookmarks: "Експортувати в закладки"
|
||||
edit: "Редагувати колекцію"
|
||||
hide: "Приховати колекцію"
|
||||
unhide: "Показати колекцію"
|
||||
|
||||
groups:
|
||||
title: "Група"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Редагувати колекцію"
|
||||
edit_group: "Редагувати групу"
|
||||
edit_tab: "Редагувати вкладку"
|
||||
new_group: "Нова група"
|
||||
new_collection: "Нова колекція"
|
||||
collection_title: "Назва"
|
||||
color: "Колір"
|
||||
url_error: "URL є обов'язковим"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Створити нову колекцію"
|
||||
menu:
|
||||
tiles_view: "Плитки"
|
||||
compact_view: "Компактний вид"
|
||||
changelog: "Що нового?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Пошук"
|
||||
filter: "Фільтр"
|
||||
show_hidden: "Показати приховані"
|
||||
sort:
|
||||
title: "Сортування"
|
||||
options:
|
||||
|
||||
+13
-6
@@ -46,6 +46,17 @@ features:
|
||||
p3_text: "请参阅我们收集内容的"
|
||||
p3_link: "完整列表"
|
||||
|
||||
netscape_bookmarks:
|
||||
title: "浏览器书签"
|
||||
export: "将收藏导出为书签"
|
||||
import: "从书签文件导入"
|
||||
import_dialog:
|
||||
title: "导入书签"
|
||||
content: "从您的浏览器导出的 Netscape 格式书签文件中导入书签。"
|
||||
import_result:
|
||||
success: "成功导入 $1 个书签。"
|
||||
error: "导入书签失败。请确保该文件是有效的书签文件。"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "已创建新收藏"
|
||||
@@ -114,6 +125,8 @@ options_page:
|
||||
restore: "打开标签页并删除收藏"
|
||||
storage:
|
||||
title: "存储"
|
||||
manage_title: "存储管理"
|
||||
thumbnails_title: "缩略图和图标"
|
||||
capacity:
|
||||
title: "云存储容量"
|
||||
description: "$1 / $2 KiB"
|
||||
@@ -184,8 +197,6 @@ collections:
|
||||
add_group: "添加空分组"
|
||||
export_bookmarks: "导出到书签"
|
||||
edit: "编辑收藏"
|
||||
hide: "隐藏收藏"
|
||||
unhide: "显示收藏"
|
||||
|
||||
groups:
|
||||
title: "分组"
|
||||
@@ -221,25 +232,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "编辑收藏"
|
||||
edit_group: "编辑分组"
|
||||
edit_tab: "编辑标签页"
|
||||
new_group: "新分组"
|
||||
new_collection: "新收藏"
|
||||
collection_title: "标题"
|
||||
color: "颜色"
|
||||
url_error: "需要 URL"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "创建新收藏"
|
||||
menu:
|
||||
tiles_view: "平铺视图"
|
||||
compact_view: "紧凑视图"
|
||||
changelog: "更新内容"
|
||||
list:
|
||||
searchbar:
|
||||
title: "搜索"
|
||||
filter: "筛选"
|
||||
show_hidden: "显示隐藏项"
|
||||
sort:
|
||||
title: "排序"
|
||||
options:
|
||||
|
||||
@@ -30,7 +30,6 @@ export type CollectionItem =
|
||||
title?: string;
|
||||
color?: `${Browser.tabGroups.Color}`;
|
||||
items: (TabItem | GroupItem)[];
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type GraphicsStorage = Record<string, GraphicsItem>;
|
||||
|
||||
Generated
+1770
-1532
File diff suppressed because it is too large
Load Diff
+19
-30
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabs-aside",
|
||||
"private": true,
|
||||
"version": "3.3.2",
|
||||
"version": "3.2.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
@@ -16,39 +16,28 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fluentui/react-components": "^9.74.1",
|
||||
"@fluentui/react-icons": "^2.0.328",
|
||||
"@webext-core/messaging": "^3.0.1",
|
||||
"@wxt-dev/analytics": "^0.5.4",
|
||||
"@wxt-dev/i18n": "^0.2.5",
|
||||
"@fluentui/react-components": "^9.72.8",
|
||||
"@fluentui/react-icons": "^2.0.316",
|
||||
"@webext-core/messaging": "^2.3.0",
|
||||
"@wxt-dev/analytics": "^0.5.1",
|
||||
"@wxt-dev/i18n": "^0.2.4",
|
||||
"lzutf8": "^0.6.3",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7"
|
||||
"node-bookmarks-parser": "^2.0.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^1.3.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@eslint/json": "^2.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/react": "^19.2.16",
|
||||
"@eslint/css": "^0.14.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@eslint/json": "^0.14.0",
|
||||
"@stylistic/eslint-plugin": "^5.6.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@wxt-dev/module-react": "^1.2.2",
|
||||
"eslint": "^9.39.4",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"wxt": "^0.20.26"
|
||||
},
|
||||
"overrides": {
|
||||
"node-notifier": {
|
||||
"uuid": "^11.1.1"
|
||||
},
|
||||
"web-ext-run": {
|
||||
"tmp": "^0.2.6"
|
||||
}
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild": true,
|
||||
"spawn-sync": true
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"wxt": "^0.20.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_5" data-name="Layer 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 1000">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2,
|
||||
.cls-3 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-1,
|
||||
.cls-4 {
|
||||
fill: #ff7545;
|
||||
}
|
||||
|
||||
.cls-2,
|
||||
.cls-6 {
|
||||
fill: #242424;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
stroke-width: 12px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.cls-6,
|
||||
.cls-4 {
|
||||
stroke: #242424;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.cls-6,
|
||||
.cls-4 {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 8px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.laptop {
|
||||
fill: #424242;
|
||||
stroke: #424242;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.laptop {
|
||||
fill: #d6d6d6;
|
||||
stroke: #d6d6d6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-1"
|
||||
d="M1656,996.17c-62.9,0-124.32-15.87-177.6-45.9-48.31-27.23-88.43-65.09-116.63-109.96,42.24,16.15,87.02,24.34,133.29,24.34,191.24,0,346.83-142.61,346.83-317.91,0-49.29-17.77-79.71-40.27-118.22-.25-.44-.51-.87-.77-1.31,56.26,25.15,103.09,59.13,135.92,98.71,38.75,46.72,58.4,100.56,58.4,160,0,82.81-35.24,160.68-99.22,219.27-64.08,58.67-149.29,90.99-239.96,90.99Z" />
|
||||
<path class="cls-2"
|
||||
d="M1810.27,435.77c21.37,10.21,41.26,21.71,59.35,34.32,24.99,17.43,46.59,37.03,64.2,58.27,38.17,46.02,57.52,99.03,57.52,157.56,0,41.28-8.83,81.34-26.25,119.04-16.85,36.47-40.98,69.24-71.73,97.4-30.8,28.2-66.67,50.34-106.62,65.82-41.4,16.04-85.39,24.17-130.75,24.17-62.25,0-123.01-15.7-175.72-45.4-44.28-24.95-81.59-58.94-108.99-99.08,39.45,13.69,80.98,20.62,123.77,20.62,193.35,0,350.66-144.33,350.66-321.74,0-46.31-15.25-76.1-35.44-110.97M1791.69,419.07c25.27,43.77,46.37,74.72,46.37,127.67,0,173.46-153.57,314.08-343,314.08-50.83,0-99.07-10.14-142.46-28.3,57.52,99.6,171.79,167.48,303.4,167.48,189.44,0,343-140.62,343-314.08,0-126.92-88.98-217.31-207.31-266.85h0Z" />
|
||||
</g>
|
||||
<path class="cls-4"
|
||||
d="M1850.7,210.11c40.63,49.7,33.28,122.93-16.42,163.56-49.7,40.63-174.15,75.16-214.78,25.46-40.63-49.7,17.94-164.81,67.64-205.44,49.7-40.63,122.93-33.28,163.56,16.42Z" />
|
||||
<g>
|
||||
<path class="cls-1"
|
||||
d="M1141.23,996c-107.8,0-211.68-30.24-292.51-85.15-34.04-23.13-63.19-50-86.62-79.87-30.95-39.44-50.89-82.52-59.27-128.07,2.74-4.13,13.24-18.52,34.99-33.02,23.72-15.81,66.05-35,133.04-36.62,3.24-.07,6.3-.11,9.33-.11,43.77,0,86.56,14.88,130.79,45.49,38.84,26.88,73.68,62.33,104.42,93.61,24.59,25.02,47.83,48.66,69.78,64.67,62.27,45.4,122.66,67.87,162.35,78.74,26.53,7.27,47.34,10.45,59.7,11.84-35.66,20.71-74.9,37.05-116.83,48.62-47.77,13.19-97.96,19.88-149.17,19.88Z" />
|
||||
<path class="cls-2"
|
||||
d="M880.19,637.16c42.93,0,84.97,14.65,128.51,44.78,38.53,26.66,73.23,61.97,103.85,93.12,24.71,25.14,48.06,48.89,70.28,65.1,62.76,45.75,123.63,68.41,163.65,79.36,19.52,5.35,36,8.51,48.36,10.37-32.55,17.79-67.94,32.01-105.51,42.38-47.43,13.09-97.26,19.73-148.11,19.73-54.46,0-107.6-7.59-157.95-22.55-48.57-14.43-93.08-35.26-132.31-61.91-33.7-22.89-62.54-49.48-85.72-79.03-30.18-38.46-49.75-80.41-58.19-124.72,8.21-11.64,51.24-63.8,163.88-66.52,3.21-.07,6.24-.11,9.25-.11M880.19,629.16c-3.2,0-6.34.04-9.43.11-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36,23.74,30.26,53.31,57.47,87.52,80.71,78.67,53.44,181.82,85.84,294.76,85.84,105.42,0,202.3-28.24,278.72-75.46,0,0-28.21-.92-71.36-12.74-43.16-11.81-101.26-34.52-161.05-78.11-76.68-55.9-167.65-204.53-307.35-204.53h0Z" />
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3"
|
||||
d="M760.28,828.65c-29.91-38.81-49.23-81.08-57.46-125.74,2.74-4.13,13.24-18.52,34.99-33.02,23.38-15.59,64.87-34.46,130.24-36.54l51.71,133.53-159.48,61.77Z" />
|
||||
<path class="cls-2"
|
||||
d="M865.35,637.45l49.24,127.14-152.95,59.24c-28.13-37.18-46.48-77.52-54.58-120.04,8.07-11.44,49.8-62.07,158.29-66.35M870.76,629.27c-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36l165.99-64.29-54.18-139.89h0Z" />
|
||||
</g>
|
||||
<rect class="cls-5 laptop" x="1219.11" y="766.83" width="270.32" height="9.95"
|
||||
transform="translate(304.74 -380.67) rotate(18)" />
|
||||
<rect class="cls-5 laptop" x="1059.2" y="596.18" width="270.32" height="9.95"
|
||||
transform="translate(1479.19 -705.28) rotate(75.58)" />
|
||||
<path class="cls-6"
|
||||
d="M1666.04,416.09c1.06-29.87-22.29-54.95-52.17-56.01-2.32-.08-4.6,0-6.85.2.75,15,4.9,28.4,13.47,38.89,10.4,12.71,26.28,19.9,44.99,22.9.29-1.96.48-3.95.55-5.98Z" />
|
||||
<path class="cls-4"
|
||||
d="M1851.96,176.25c-29.01-25.87-78.84-33.24-78.84-33.24,0,0,37.28-38.45,83.99-62.26,46.65-23.78,102.73-32.92,102.73-32.92,0,0-26.34,46.29-43.76,93.12-19.06,51.23-22.35,98.62-22.35,98.62,0,0-13.99-38.55-41.77-63.33Z" />
|
||||
<ellipse class="cls-2" cx="1700.37" cy="301.32" rx="10.19" ry="17.93"
|
||||
transform="translate(271.38 1270.89) rotate(-44.21)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,13 +1,13 @@
|
||||
import { settings } from "./settings";
|
||||
|
||||
export async function getTabsToSaveAsync(forceSelected: boolean = false): Promise<[Browser.tabs.Tab[], number]>
|
||||
export async function getTabsToSaveAsync(): Promise<[Browser.tabs.Tab[], number]>
|
||||
{
|
||||
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
highlighted: true
|
||||
});
|
||||
|
||||
if (!forceSelected && tabs.length < 2)
|
||||
if (tabs.length < 2)
|
||||
{
|
||||
const ignorePinned: boolean = await settings.ignorePinned.getValue();
|
||||
tabs = await browser.tabs.query({
|
||||
|
||||
@@ -103,13 +103,5 @@ export const settings = {
|
||||
fallback: true,
|
||||
version: 1
|
||||
}
|
||||
),
|
||||
|
||||
compactView: storage.defineItem<boolean>(
|
||||
"sync:compactView",
|
||||
{
|
||||
fallback: false,
|
||||
version: 1
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
@@ -87,6 +87,7 @@ export default defineConfig({
|
||||
id: "tabsaside@xfox111.net",
|
||||
strict_min_version: "139.0",
|
||||
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection_permissions: {
|
||||
required: ["browsingActivity"],
|
||||
optional: ["technicalAndInteraction"]
|
||||
|
||||
Reference in New Issue
Block a user