1
0
mirror of https://github.com/XFox111/TabsAsideExtension.git synced 2026-07-02 19:52:47 +03:00

Compare commits

..

32 Commits

Author SHA1 Message Date
xfox111 e6a69980c2 docs: privacy policy 2025-05-07 20:26:24 +03:00
xfox111 59b0547ec6 chore: version update 2025-05-07 20:01:17 +03:00
xfox111 eed5159a56 chore(loc): simplified chinese translation 2025-05-07 19:55:28 +03:00
xfox111 7effe309dd chore: minor updates 2025-05-07 19:40:03 +03:00
xfox111 6728a50056 chore(loc): spanish translation 2025-05-07 19:39:12 +03:00
xfox111 0cb036c69a chore(loc): italian translation 2025-05-07 19:18:48 +03:00
xfox111 4ef336da5b chore(loc): polish translation 2025-05-07 18:43:50 +03:00
xfox111 00492ad710 chore: tab list location logic improvement #117 2025-05-07 00:50:54 +03:00
xfox111 a706c3bc89 fix: when listLocation is "tab" or "pinned" add all tabs instead of selected #117 2025-05-07 00:28:48 +03:00
xfox111 4ef9e2651c fix: partial save notification appears when listLocation is "tab" or "pinned" 2025-05-07 00:27:16 +03:00
xfox111 bfb849fbdf fix: collection list refresh across multiple list views 2025-05-07 00:09:50 +03:00
xfox111 297a6aa95c feat: allow to create any group on firefox 2025-05-07 00:05:12 +03:00
xfox111 aa2ee02c79 chore: messenger refactoring 2025-05-07 00:04:19 +03:00
xfox111 8b77159abe hotfix: wrapped analytics into try/catch to prevent failing on firefox 2025-05-07 00:03:43 +03:00
xfox111 f5bf0db039 !feat: remove "pointer: coarse" media #117 2025-05-06 22:58:52 +03:00
xfox111 5d4a59153a feat: ga4 analytics #117 2025-05-05 19:25:25 +03:00
Maison da Silva b6be86aac9 locales pt_BR.yml
locales pt_BR.yml
2025-05-04 19:51:19 +03:00
Maison da Silva d872515b8b Update pt_BR.txt 2025-05-04 19:50:59 +03:00
Maison da Silva 8693e8d563 Create pt_BR.txt 2025-05-04 19:50:59 +03:00
xfox111 9c4121ea79 chore(ci): exclude locales from pr check 2025-05-04 19:50:29 +03:00
xfox111 f89d036ab8 feat: captureVisibleTab fallback for tab previews 2025-05-04 15:22:33 +03:00
xfox111 e59782973b feat: ability to disable cloud collection storage 2025-05-04 14:21:10 +03:00
xfox111 70ed16c286 chore: remove web-ext.config.js 2025-05-04 14:18:22 +03:00
xfox111 db78314a44 feat: remove context menu from "page" context #117 2025-05-04 13:17:01 +03:00
xfox111 a478352ca3 chore: cd pipeline refactoring 2025-05-04 11:15:04 +03:00
xfox111 2eba532901 chore: firefox extension id fix 2025-05-04 11:12:11 +03:00
xfox111 1e60b776c4 chore: npm scripts cleanup 2025-05-04 10:29:30 +03:00
xfox111 0c18a3de5a fix: firefox badge background color 2025-05-04 10:26:50 +03:00
xfox111 4e40742755 fix: partial save notification doesn't appear on firefox 2025-05-04 10:26:20 +03:00
xfox111 16023ac152 fix: clicking action button on firefox doesn't open sidebar 2025-05-04 10:25:31 +03:00
xfox111 39793a38c3 !feat: major 3.0 release candidate 2025-05-03 23:59:43 +03:00
xfox111 dbc8c7fd4d init: nuke the repo 2025-05-03 09:19:36 +03:00
44 changed files with 7154 additions and 10606 deletions
-2
View File
@@ -1,2 +0,0 @@
* @XFox111
locales/pt_BR.yml @maisondasilva @XFox111
+9 -3
View File
@@ -12,7 +12,9 @@ updates:
directory: "/" # Location of package manifests
target-branch: "next"
assignees:
- "XFox111"
- "xfox111"
reviewers:
- "xfox111"
schedule:
interval: monthly
rebase-strategy: disabled
@@ -22,7 +24,9 @@ updates:
directory: "/"
target-branch: "next"
assignees:
- "XFox111"
- "xfox111"
reviewers:
- "xfox111"
schedule:
interval: monthly
rebase-strategy: disabled
@@ -32,7 +36,9 @@ updates:
directory: "/"
target-branch: "next"
assignees:
- "XFox111"
- "xfox111"
reviewers:
- "xfox111"
schedule:
interval: monthly
rebase-strategy: disabled
+2 -9
View File
@@ -38,7 +38,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: node:24
container: node:20
strategy:
fail-fast: false
matrix:
@@ -51,14 +51,7 @@ jobs:
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }}
- name: Drop build artifacts (${{ matrix.target }})
@@ -75,7 +68,7 @@ jobs:
extension-root: ./.output/firefox-mv3
self-hosted: false
- run: yarn npm audit
- run: yarn audit
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
publish-github:
+2 -9
View File
@@ -30,7 +30,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: node:24
container: node:23
strategy:
fail-fast: false
matrix:
@@ -43,14 +43,7 @@ jobs:
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }}
- name: Drop artifacts (${{ matrix.target }})
@@ -67,4 +60,4 @@ jobs:
extension-root: ./.output/firefox-mv3
self-hosted: false
- run: yarn npm audit
- run: yarn audit
-1
View File
@@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.yarn/
.output
stats.html
stats-*.json
-117
View File
@@ -1,117 +0,0 @@
nodeLinker: node-modules
packageExtensions:
"@wxt-dev/module-react@*":
peerDependencies:
vite: "*"
"@fluentui/react-accordion@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-avatar@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-carousel@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-color-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-combobox@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-dialog@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-field@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-list@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-menu@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-nav@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-overflow@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-popover@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-swatch-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-table@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tabs@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tag-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-teaching-popover@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-toolbar@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tree@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-alert@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-checkbox@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-components@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-drawer@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-infobutton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-infolabel@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-input@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-persona@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-progress@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-radio@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-select@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-skeleton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-slider@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-spinbutton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-switch@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tags@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-textarea@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-search@*":
peerDependencies:
scheduler: "0.23.0"
-3
View File
@@ -39,19 +39,16 @@ body
/* Handle */
::-webkit-scrollbar-thumb
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Hover);
}
::-webkit-scrollbar-thumb:hover:active
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Pressed);
}
+1 -1
View File
@@ -21,4 +21,4 @@ export const storeLink: string =
? "https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/" :
chrome.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ?
"https://microsoftedge.microsoft.com/addons/detail/tabs-aside/kmnblllmalkiapkfknnlpobmjjdnlhnd" :
"https://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin";
"https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdbdjgin";
+2 -20
View File
@@ -12,7 +12,6 @@ import { settings } from "@/utils/settings";
import watchTabSelection from "@/utils/watchTabSelection";
import { Tabs, Windows } from "wxt/browser";
import { Unwatch } from "wxt/storage";
import { openCollection, openGroup } from "./sidepanel/utils/opener";
export default defineBackground(() =>
{
@@ -69,13 +68,6 @@ export default defineBackground(() =>
icon: graphicsCache[data.url]?.icon
};
});
onMessage("refreshCollections", () => { });
if (import.meta.env.FIREFOX)
{
onMessage("openCollection", ({ data }) => openCollection(data.collection, data.targetWindow));
onMessage("openGroup", ({ data }) => openGroup(data.group, data.newWindow));
}
setupTabCaputre();
async function setupTabCaputre(): Promise<void>
@@ -85,9 +77,6 @@ export default defineBackground(() =>
if (!tab.url || tab.status !== "complete" || !tab.active)
return;
if (graphicsCache[tab.url]?.capture || graphicsCache[tab.url]?.capture === null)
return;
try
{
// We use chrome here because polyfill throws uncatchable errors for some reason
@@ -103,14 +92,7 @@ export default defineBackground(() =>
};
}
}
catch
{
graphicsCache[tab.url] = {
capture: null!,
preview: graphicsCache[tab.url]?.preview,
icon: graphicsCache[tab.url]?.icon
};
}
catch { }
};
setInterval(() =>
@@ -264,7 +246,7 @@ export default defineBackground(() =>
for (const openWindow of openWindows)
{
if (openWindow.incognito || openWindow.type !== "normal")
if (openWindow.incognito)
continue;
const activeTabs: Tabs.Tab[] = openWindow.tabs!.filter(tab =>
@@ -27,9 +27,6 @@ export default function GeneralSection(): React.ReactElement
if (e.optionValue === "popup" && contextAction !== "open")
setContextAction("open");
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
browser.sidebarAction.close();
setListLocation(e.optionValue as ListLocationType);
};
+1 -4
View File
@@ -4,16 +4,13 @@ export default async function exportData(): Promise<void>
local: await browser.storage.local.get(null),
sync: await browser.storage.sync.get(null)
});
const blob: Blob = new Blob([data], { type: "application/json" });
const element: HTMLAnchorElement = document.createElement("a");
element.style.display = "none";
element.href = URL.createObjectURL(blob);
element.href = `data:application/json;charset=utf-8,${data}`;
element.setAttribute("download", "tabs-aside_data.json");
document.body.appendChild(element);
element.click();
URL.revokeObjectURL(element.href);
document.body.removeChild(element);
};
-5
View File
@@ -37,12 +37,7 @@ export default async function importData(): Promise<boolean | null>
await browser.storage.local.set(data.local);
if (data.sync)
{
if (import.meta.env.FIREFOX && data.sync.contextAction === "context")
data.sync.contextAction = "open";
await browser.storage.sync.set(data.sync);
}
}
catch (error)
{
@@ -27,7 +27,7 @@ export const useStyles_CollectionView = makeStyles({
},
verticalRoot:
{
maxHeight: "560px"
height: "560px"
},
empty:
{
@@ -74,8 +74,7 @@ export const useStyles_CollectionView = makeStyles({
{
gridAutoFlow: "row",
width: "100%",
paddingBottom: tokens.spacingVerticalS,
gridAutoRows: import.meta.env.FIREFOX ? "min-content" : undefined
paddingBottom: tokens.spacingVerticalS
},
dragOverlay:
{
@@ -33,7 +33,7 @@ export default function CollectionView({ collection, index: collectionIndex, dra
const colorCls = useGroupColors();
return (
<CollectionContext.Provider value={ { collection, tabCount, hasPinnedGroup } }>
<CollectionContext.Provider value={ { collection, collectionIndex, tabCount, hasPinnedGroup } }>
<div
ref={ setNodeRef } { ...nodeProps }
className={ mergeClasses(
@@ -50,30 +50,26 @@ export default function CollectionView({ collection, index: collectionIndex, dra
<CollectionHeader dragHandleProps={ activatorProps } dragHandleRef={ setActivatorNodeRef } />
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
<>
{ collection.items.length < 1 ?
<div className={ cls.empty }>
<CollectionsRegular fontSize={ 32 } />
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
</div>
:
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<SortableContext
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
>
{ collection.items.map((i, index) =>
i.type === "group" ?
<GroupView
key={ index } group={ i } indices={ [collectionIndex, index] } />
:
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
) }
</SortableContext>
</div>
}
</>
{ collection.items.length < 1 ?
<div className={ cls.empty }>
<CollectionsRegular fontSize={ 32 } />
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
</div>
:
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<SortableContext
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
>
{ collection.items.map((i, index) =>
i.type === "group" ?
<GroupView
key={ index } group={ i } indices={ [collectionIndex, index] } />
:
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
) }
</SortableContext>
</div>
}
</div >
</CollectionContext.Provider>
+72 -81
View File
@@ -24,13 +24,6 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
const cls = useStyles_EditDialog();
const colorCls = useGroupColors();
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
{
e.preventDefault();
handleSave();
};
const handleSave = () =>
{
@@ -44,7 +37,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
type: "collection",
timestamp: props.collection?.timestamp ?? Date.now(),
color: (color === "pinned") ? undefined : color!,
title: title ? title : undefined,
title,
items: props.collection?.items ?? []
});
else if (color === "pinned")
@@ -58,87 +51,85 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
type: "group",
pinned: false,
color: color!,
title: title ? title : undefined,
title,
items: props.group?.items ?? []
});
};
return (
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
<form onSubmit={ onSubmit }>
<fui.DialogBody>
<fui.DialogTitle>
{
props.type === "collection" ?
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
}
</fui.DialogTitle>
<fui.DialogBody>
<fui.DialogTitle>
{
props.type === "collection" ?
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
}
</fui.DialogTitle>
<fui.DialogContent>
<div className={ cls.content }>
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
<fui.Input
contentBefore={ <Rename20Regular /> }
disabled={ color === "pinned" }
placeholder={
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
}
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
onChange={ (_, e) => setTitle(e.value) } />
</fui.Field>
<fui.Field label="Color">
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton
checked={ color === "pinned" }
onClick={ () => setColor("pinned") }
icon={ <Pin20Filled /> }
shape="circular"
>
{ i18n.t("groups.pinned") }
</fui.ToggleButton>
}
{ props.type === "collection" &&
<fui.ToggleButton
checked={ color === undefined }
onClick={ () => setColor(undefined) }
icon={ <CircleOff20Regular /> }
shape="circular"
>
{ i18n.t("colors.none") }
</fui.ToggleButton>
}
{ Object.keys(colorCls).map(i =>
<fui.ToggleButton
checked={ color === i }
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
icon={ {
className: cls.colorButton_icon,
children: <Circle20Filled />
} }
key={ i }
shape="circular"
>
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
</fui.ToggleButton>
) }
</div>
</fui.Field>
</div>
</fui.DialogContent>
<fui.DialogContent>
<form className={ cls.content }>
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
<fui.Input
contentBefore={ <Rename20Regular /> }
disabled={ color === "pinned" }
placeholder={
props.type === "collection" ? getCollectionTitle(props.collection) : ""
}
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
onChange={ (_, e) => setTitle(e.value) } />
</fui.Field>
<fui.Field label="Color">
<div className={ cls.colorPicker }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton
checked={ color === "pinned" }
onClick={ () => setColor("pinned") }
icon={ <Pin20Filled /> }
shape="circular"
>
{ i18n.t("groups.pinned") }
</fui.ToggleButton>
}
{ props.type === "collection" &&
<fui.ToggleButton
checked={ color === undefined }
onClick={ () => setColor(undefined) }
icon={ <CircleOff20Regular /> }
shape="circular"
>
{ i18n.t("colors.none") }
</fui.ToggleButton>
}
{ Object.keys(colorCls).map(i =>
<fui.ToggleButton
checked={ color === i }
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
icon={ {
className: cls.colorButton_icon,
children: <Circle20Filled />
} }
key={ i }
shape="circular"
>
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
</fui.ToggleButton>
) }
</div>
</fui.Field>
</form>
</fui.DialogContent>
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
</form>
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" onClick={ handleSave }>{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
</fui.DialogSurface>
);
}
@@ -99,23 +99,12 @@ export const useStyles_GroupView = makeStyles({
display: "flex",
columnGap: tokens.spacingHorizontalS,
rowGap: tokens.spacingHorizontalSNudge,
height: "100%",
position: "relative"
height: "100%"
},
verticalList:
{
flexFlow: "column"
},
verticalListCollapsed:
{
maxHeight: "136px",
overflow: "clip"
},
horizontalListCollapsed:
{
maxWidth: "400px",
overflow: "clip"
},
listContainer:
{
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
@@ -88,13 +88,7 @@ export default function GroupView({ group, indices, dragOverlay }: GroupViewProp
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
</div>
:
<div
className={ mergeClasses(
cls.list,
!tilesView && cls.verticalList,
((active?.item.type === "group" && active?.indices[0] === indices[0]) || dragOverlay) && (tilesView ? cls.horizontalListCollapsed : cls.verticalListCollapsed)
) }
>
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<SortableContext
items={ group.items.map((_, index) => [...indices, index].join("/")) }
disabled={ disableSorting }
@@ -8,7 +8,6 @@ export const useStyles_TabView = makeStyles({
width: "160px",
height: "120px",
flexShrink: 0,
marginBottom: tokens.spacingVerticalSNudge,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
@@ -59,6 +58,8 @@ export const useStyles_TabView = makeStyles({
display: "grid",
gridTemplateColumns: "auto 1fr auto",
alignItems: "center",
gap: tokens.spacingHorizontalSNudge,
paddingLeft: tokens.spacingHorizontalS,
borderBottomLeftRadius: tokens.borderRadiusMedium,
borderBottomRightRadius: tokens.borderRadiusMedium,
@@ -71,9 +72,6 @@ export const useStyles_TabView = makeStyles({
icon:
{
cursor: "grab",
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalSNudge}`,
height: "32px",
boxSizing: "border-box",
"&:active":
{
+3 -6
View File
@@ -9,12 +9,10 @@ import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-c
import { Dismiss20Regular } from "@fluentui/react-icons";
import { MouseEventHandler, ReactElement } from "react";
import { useStyles_TabView } from "./TabView.styles";
import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext";
export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement
{
const { removeItem, graphics, tilesView } = useCollections();
const { collection } = useContext<CollectionContextType>(CollectionContext);
const {
setNodeRef, setActivatorNodeRef,
nodeProps, activatorProps, isBeingDragged
@@ -31,18 +29,16 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
args.preventDefault();
args.stopPropagation();
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("tabs.delete"),
content: i18n.t("common.delete_prompt"),
destructive: true,
confirmText: i18n.t("common.actions.delete"),
onConfirm: () => removeItem(...removeIndex)
onConfirm: () => removeItem(...indices)
});
else
removeItem(...removeIndex);
removeItem(...indices);
};
const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
@@ -83,6 +79,7 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
ref={ setActivatorNodeRef } { ...activatorProps }
src={ graphics[tab.url]?.icon ?? faviconPlaceholder }
onError={ e => e.currentTarget.src = faviconPlaceholder }
height={ 20 } width={ 20 }
className={ cls.icon } draggable={ false } />
<Tooltip relationship="description" content={ tab.title ?? tab.url }>
@@ -12,11 +12,10 @@ import saveTabsToCollection from "@/utils/saveTabsToCollection";
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
{
const [contextOpen, setContextOpen] = useState<boolean>(false);
const [listLocation] = useSettings("listLocation");
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { updateCollection } = useCollections();
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
const { tabCount, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
const AddIcon = bundleIcon(Add20Filled, Add20Regular);
@@ -26,7 +25,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
const newTabs: (TabItem | GroupItem)[] = isTab ?
(await saveTabsToCollection(false)).items :
await getSelectedTabs();
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collection.timestamp);
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collectionIndex);
};
const cls = useStyles();
@@ -54,7 +53,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
mergeClasses(
cls.toolbar,
"CollectionView__toolbar",
(alwaysShowToolbars === true || contextOpen) && cls.showToolbar
alwaysShowToolbars === true && cls.showToolbar
) }
>
{ tabCount < 1 ?
@@ -62,10 +61,10 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
</Button>
:
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
<OpenCollectionButton />
}
<CollectionMoreButton onAddSelected={ handleAddSelected } onOpenChange={ (_, e) => setContextOpen(e.open) } />
<CollectionMoreButton onAddSelected={ handleAddSelected } />
</div>
</div>
);
@@ -1,19 +1,19 @@
import { useDialog } from "@/contexts/DialogProvider";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings";
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import exportCollectionToBookmarks from "../../utils/exportCollectionToBookmarks";
import EditDialog from "../EditDialog";
export default function CollectionMoreButton({ onAddSelected, onOpenChange }: CollectionMoreButtonProps): React.ReactElement
export default function CollectionMoreButton({ onAddSelected }: CollectionMoreButtonProps): React.ReactElement
{
const [listLocation] = useSettings("listLocation");
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { removeItem, updateCollection } = useCollections();
const { tabCount, hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
const { tabCount, hasPinnedGroup, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const dialog = useDialog();
const [deletePrompt] = useSettings("deletePrompt");
@@ -33,10 +33,10 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
content: i18n.t("common.delete_prompt"),
destructive: true,
confirmText: i18n.t("common.actions.delete"),
onConfirm: () => removeItem(collection.timestamp)
onConfirm: () => removeItem(collectionIndex)
});
else
removeItem(collection.timestamp);
removeItem(collectionIndex);
};
const handleEdit = () =>
@@ -44,7 +44,7 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
<EditDialog
type="collection"
collection={ collection }
onSave={ item => updateCollection(item, collection.timestamp) } />
onSave={ item => updateCollection(item, collectionIndex) } />
);
const handleCreateGroup = () =>
@@ -52,11 +52,11 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
<EditDialog
type="group"
hidePinned={ hasPinnedGroup }
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collection.timestamp) } />
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collectionIndex) } />
);
return (
<Menu onOpenChange={ onOpenChange }>
<Menu>
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<MenuTrigger>
<Button appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
@@ -94,5 +94,4 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
export type CollectionMoreButtonProps =
{
onAddSelected?: () => void;
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
};
@@ -7,19 +7,18 @@ import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels";
import { sendMessage } from "@/utils/messaging";
import saveTabsToCollection from "@/utils/saveTabsToCollection";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import { ReactElement } from "react";
import { openGroup } from "../../utils/opener";
import saveTabsToCollection from "@/utils/saveTabsToCollection";
export default function GroupMoreMenu(): ReactElement
{
const [listLocation] = useSettings("listLocation");
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { group, indices } = useContext<GroupContextType>(GroupContext);
const { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
const { hasPinnedGroup } = useContext<CollectionContextType>(CollectionContext);
const [deletePrompt] = useSettings("deletePrompt");
const dialog = useDialog();
const { updateGroup, removeItem, ungroup } = useCollections();
@@ -34,18 +33,16 @@ export default function GroupMoreMenu(): ReactElement
const handleDelete = () =>
{
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("groups.menu.delete"),
content: i18n.t("common.delete_prompt"),
confirmText: i18n.t("common.actions.delete"),
destructive: true,
onConfirm: () => removeItem(...removeIndex)
onConfirm: () => removeItem(...indices)
});
else
removeItem(...removeIndex);
removeItem(...indices);
};
const handleEdit = () =>
@@ -54,23 +51,15 @@ export default function GroupMoreMenu(): ReactElement
type="group"
group={ group }
hidePinned={ hasPinnedGroup }
onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } />
onSave={ item => updateGroup(item, indices[0], indices[1]) } />
);
const openGroupInNewWindow = () =>
{
if (import.meta.env.FIREFOX && listLocation === "popup")
sendMessage("openGroup", { group, newWindow: true });
else
openGroup(group, true);
};
const handleAddSelected = async () =>
{
const newTabs: TabItem[] = isTab ?
(await saveTabsToCollection(false)).items.flatMap(i => i.type === "tab" ? i : i.items) :
await getSelectedTabs();
updateGroup({ ...group, items: [...group.items, ...newTabs] }, collection.timestamp, indices[1]);
updateGroup({ ...group, items: [...group.items, ...newTabs] }, indices[0], indices[1]);
};
return (
@@ -84,7 +73,7 @@ export default function GroupMoreMenu(): ReactElement
<MenuPopover>
<MenuList>
{ group.items.length > 0 &&
<MenuItem icon={ <NewWindowIcon /> } onClick={ openGroupInNewWindow }>
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
{ i18n.t("groups.menu.new_window") }
</MenuItem>
}
@@ -100,7 +89,7 @@ export default function GroupMoreMenu(): ReactElement
<MenuItem
className={ dangerCls.menuItem }
icon={ <UngroupIcon /> }
onClick={ () => ungroup(collection.timestamp, indices[1]) }
onClick={ () => ungroup(indices[0], indices[1]) }
>
{ i18n.t("groups.menu.ungroup") }
</MenuItem>
@@ -1,20 +1,18 @@
import { useDialog } from "@/contexts/DialogProvider";
import useSettings from "@/hooks/useSettings";
import browserLocaleKey from "@/utils/browserLocaleKey";
import { sendMessage } from "@/utils/messaging";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import { openCollection } from "../../utils/opener";
export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
export default function OpenCollectionButton(): React.ReactElement
{
const [defaultAction] = useSettings("defaultRestoreAction");
const [listLocation] = useSettings("listLocation");
const { removeItem } = useCollections();
const dialog = useDialog();
const { collection } = useContext<CollectionContextType>(CollectionContext);
const { collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const OpenIcon = ic.bundleIcon(ic.Open20Filled, ic.Open20Regular);
const RestoreIcon = ic.bundleIcon(ic.ArrowExportRtl20Filled, ic.ArrowExportRtl20Regular);
@@ -24,12 +22,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
const handleIncognito = async () =>
{
if (await browser.extension.isAllowedIncognitoAccess())
{
if (import.meta.env.FIREFOX && listLocation === "popup")
sendMessage("openCollection", { collection, targetWindow: "incognito" });
else
openCollection(collection, "incognito");
}
openCollection(collection, "incognito");
else
dialog.pushPrompt({
title: i18n.t("collections.incognito_check.title"),
@@ -52,18 +45,16 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
};
const handleOpen = (mode: "current" | "new") =>
import.meta.env.FIREFOX && listLocation === "popup" && mode === "new" ?
() => sendMessage("openCollection", { collection, targetWindow: "new" }) :
() => openCollection(collection, mode);
() => openCollection(collection, mode);
const handleRestore = async () =>
{
await openCollection(collection);
removeItem(collection.timestamp);
removeItem(collectionIndex);
};
return (
<Menu onOpenChange={ onOpenChange }>
<Menu>
<MenuTrigger disableButtonEnhancement>
{ (triggerProps: MenuButtonProps) => defaultAction === "restore" ?
<SplitButton
@@ -93,7 +84,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
{ i18n.t("collections.actions.restore") }
</MenuItem>
}
<MenuItem icon={ <NewWindowIcon /> } onClick={ handleOpen("new") }>
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => handleOpen("new") }>
{ i18n.t("collections.actions.new_window") }
</MenuItem>
<MenuItem icon={ <InPrivateIcon /> } onClick={ handleIncognito }>
@@ -104,8 +95,3 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
</Menu>
);
}
export type OpenCollectionButtonProps =
{
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
};
@@ -8,6 +8,7 @@ export default CollectionContext;
export type CollectionContextType =
{
collection: CollectionItem;
collectionIndex: number;
tabCount: number;
hasPinnedGroup: boolean;
};
@@ -50,14 +50,12 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
const removeItem = (...indices: number[]): void =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === indices[0]);
if (indices.length > 2)
(collections[collectionIndex].items[indices[1]] as GroupItem).items.splice(indices[2], 1);
(collections[indices[0]].items[indices[1]] as GroupItem).items.splice(indices[2], 1);
else if (indices.length > 1)
collections[collectionIndex].items.splice(indices[1], 1);
collections[indices[0]].items.splice(indices[1], 1);
else
collections.splice(collectionIndex, 1);
collections.splice(indices[0], 1);
updateStorage(collections);
};
@@ -67,23 +65,20 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
updateStorage(collectionList);
};
const updateCollection = (collection: CollectionItem, id: number): void =>
const updateCollection = (collection: CollectionItem, index: number): void =>
{
const index: number = collections.findIndex(i => i.timestamp === id);
collections[index] = collection;
updateStorage(collections);
};
const updateGroup = (group: GroupItem, collectionId: number, groupIndex: number): void =>
const updateGroup = (group: GroupItem, collectionIndex: number, groupIndex: number): void =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
collections[collectionIndex].items[groupIndex] = group;
updateStorage(collections);
};
const ungroup = (collectionId: number, groupIndex: number): void =>
const ungroup = (collectionIndex: number, groupIndex: number): void =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
const group = collections[collectionIndex].items[groupIndex] as GroupItem;
collections[collectionIndex].items.splice(groupIndex, 1, ...group.items);
updateStorage(collections);
@@ -113,9 +108,9 @@ export type CollectionsContextType =
addCollection: (collection: CollectionItem) => void;
updateCollections: (collections: CollectionItem[]) => void;
updateCollection: (collection: CollectionItem, id: number) => void;
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => void;
ungroup: (collectionId: number, groupIndex: number) => void;
updateCollection: (collection: CollectionItem, index: number) => void;
updateGroup: (group: GroupItem, collectionIndex: number, groupIndex: number) => void;
ungroup: (collectionIndex: number, groupIndex: number) => void;
removeItem: (...indices: number[]) => void;
};
@@ -46,10 +46,6 @@ export const useStyles_CollectionListView = makeStyles({
listView:
{
display: "grid",
"@media screen and (min-width: 360px)":
{
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
}
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
}
});
@@ -21,7 +21,6 @@ import { collisionDetector } from "../../utils/dnd/collisionDetector";
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
{
@@ -34,7 +33,7 @@ export default function CollectionListView(): ReactElement
const [active, setActive] = useState<DndItem | null>(null);
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }),
useSensor(MouseSensor, { activationConstraint: { delay: 100, tolerance: 0 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
);
@@ -111,7 +110,6 @@ export default function CollectionListView(): ReactElement
collisionDetection={ collisionDetector(!tilesView) }
onDragStart={ handleDragStart }
onDragEnd={ handleDragEnd }
modifiers={ [snapHandleToCursor] }
>
<SortableContext
items={ resultList.map((_, index) => index.toString()) }
@@ -123,25 +121,27 @@ export default function CollectionListView(): ReactElement
</SortableContext>
<DragOverlay dropAnimation={ null }>
{ active !== null ?
active.item.type === "collection" ?
<CollectionView collection={ active.item } index={ -1 } dragOverlay />
:
<CollectionContext.Provider
value={ {
tabCount: 0,
collection: resultList[active.indices[0]],
hasPinnedGroup: true
} }
>
{ active.item.type === "group" ?
{ 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 />
:
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
}
</CollectionContext.Provider>
:
<></>
</CollectionContext.Provider>
}
{ active.item.type === "tab" &&
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
}
</>
}
</DragOverlay>
</DndContext>
@@ -33,11 +33,9 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
if (activeItem.item.type === "collection")
{
// If we drag a collection, we should ignore other items, like tabs or groups
if (droppableItem.item.type !== "collection")
continue;
// Using distance between centers
value = distanceBetween(centerOfRectangle(rect), centerRect);
collisions.push({ id, data: { droppableContainer, value } });
continue;
@@ -46,20 +44,14 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
// Dragging a tab or a group over a collection
if (droppableItem.item.type === "collection")
{
// Ignoring collection, if the tab or the group is inside that collection
if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0])
continue;
// Ignoring collection if we're dragging a tab or a group that doesn't belong to the collection,
// but intersection ratio is less than 0.7
if (intersectionCoefficient < 0.7)
if (intersectionCoefficient < 0.7 && activeItem.item.type === "tab")
continue;
// If we're dragging a tab, that's inside a group that belongs to the collection,
// we substract the group's intersection from the collection's one
if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0])
{
const [collectionId, groupId] = activeItem.indices;
@@ -70,23 +62,16 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect));
}
// Otherwise, use intersection ratio
// At this point we're dragging either:
// - a group, that doesn't belong to the collection
// - a tab, that either belongs to the collection's group, or has intersection coefficient >= .7
else
{
value = 2 / intersectionRatio;
value = 1 / intersectionRatio;
}
}
// If we're dragging a tab or a group over another group's dropzone
else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone"))
{
// Ignore, if we're dragging a group
if (activeItem.item.type === "group")
continue;
// Ignore, if we're dragging a tab, that's inside the group
if (
activeItem.indices.length === 3 &&
activeItem.indices[0] === droppableItem.indices[0] &&
@@ -94,15 +79,11 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
)
continue;
// Ignore, if coefficient is less than .5
// (at this point we're dragging a tab, that's outside of the group's dropzone)
if (intersectionCoefficient < 0.5)
continue;
// Use intersection between the tab and the group's dropzone
value = 1 / intersectionRatio;
}
// We're dragging a group or a tab over its sibling
else if (activeItem.indices.length === droppableItem.indices.length)
{
if (activeItem.indices[0] !== droppableItem.indices[0])
@@ -111,22 +92,9 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1])
continue;
// Ignore pinned groups
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
continue;
const collectionRect: ClientRect | undefined = droppableRects.get(activeItem.indices[0].toString());
if (!collectionRect)
continue;
const collectionIntersectionRatio: number = getIntersectionRatio(collectionRect, collisionRect);
const collectionIntersectionCoefficient: number = collectionIntersectionRatio / getMaxIntersectionRatio(collectionRect, collisionRect);
// Ignore if we are outside of the home collection
if (collectionIntersectionCoefficient < 0.7)
continue;
if (activeItem.item.type === "tab" && droppableItem.item.type === "tab")
{
value = distanceBetween(centerOfRectangle(rect), centerRect);
@@ -1,34 +0,0 @@
import { Modifier } from "@dnd-kit/core";
import { Coordinates, getEventCoordinates } from "@dnd-kit/utilities";
import { DndItem } from "../../hooks/useDndItem";
export const snapHandleToCursor: Modifier = ({
activatorEvent,
draggingNodeRect,
transform,
active
}) =>
{
if (draggingNodeRect && activatorEvent)
{
const activeItem: DndItem | undefined = active?.data.current as DndItem;
const activatorCoordinates: Coordinates | null = getEventCoordinates(activatorEvent);
if (!activatorCoordinates)
return transform;
const initX: number = activatorCoordinates.x - draggingNodeRect.left;
const initY: number = activatorCoordinates.y - draggingNodeRect.top;
const offsetX: number = activeItem?.item.type === "group" ? 24 : draggingNodeRect.height / 2;
const offsetY: number = activeItem?.item.type === "group" ? 20 : draggingNodeRect.height / 2;
return {
...transform,
x: transform.x + initX - offsetX,
y: transform.y + initY - offsetY
};
}
return transform;
};
@@ -1,10 +1,8 @@
import { CollectionItem } from "@/models/CollectionModels";
export function getCollectionTitle(collection?: CollectionItem, useTimestamp?: boolean): string
export function getCollectionTitle(collection?: CollectionItem): string
{
if (collection?.title !== undefined && useTimestamp !== true)
return collection.title;
return new Date(collection?.timestamp ?? Date.now())
.toLocaleDateString(browser.i18n.getUILanguage(), { year: "numeric", month: "short", day: "numeric" });
return collection?.title
|| new Date(collection?.timestamp ?? Date.now())
.toLocaleDateString(browser.i18n.getUILanguage(), { year: "numeric", month: "short", day: "numeric" });
}
+2 -18
View File
@@ -1,24 +1,8 @@
import { TabItem } from "@/models/CollectionModels";
import sendNotification from "@/utils/sendNotification";
import { Tabs } from "wxt/browser";
export default async function getSelectedTabs(): Promise<TabItem[]>
{
let tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
const tabCount: number = tabs.length;
tabs = tabs.filter(i =>
i.url
&& new URL(i.url).protocol !== "about:"
&& new URL(i.url).hostname !== "newtab"
);
if (tabs.length < tabCount)
await sendNotification({
title: i18n.t("notifications.partial_save.title"),
message: i18n.t("notifications.partial_save.message"),
icon: "/notification_icons/save_warning.png"
});
return tabs.map(i => ({ type: "tab", url: i.url!, title: i.title }));
const tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
return tabs.filter(i => i.url).map(i => ({ type: "tab", url: i.url!, title: i.title }));
}
+8 -8
View File
@@ -55,17 +55,19 @@ export async function openGroup(group: GroupItem, newWindow: boolean = false): P
async function createGroup(group: GroupItem, windowId: number, discard?: boolean): Promise<void>
{
discard ??= await settings.dismissOnLoad.getValue();
const tabs: Tabs.Tab[] = await Promise.all(group.items.map(async i =>
await createTab(i.url, windowId, discard, group.pinned)
const tabIds: number[] = await Promise.all(group.items.map(async i =>
(await createTab(i.url, windowId, discard, group.pinned)).id!
));
// "Pinned" group is technically not a group, so not much else to do here
if (group.pinned === true)
// and Firefox doesn't even support tab groups
if (group.pinned === true || import.meta.env.FIREFOX)
return;
const groupId: number = await chrome.tabs.group({
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!),
createProperties: { windowId }
tabIds, createProperties: {
windowId
}
});
await chrome.tabGroups.update(groupId, {
@@ -77,14 +79,12 @@ async function createGroup(group: GroupItem, windowId: number, discard?: boolean
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Windows.CreateCreateDataType): Promise<void>
{
const currentWindow: Windows.Window = windowProps ?
await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }) :
await browser.windows.create({ url: "about:blank", focused: true, ...windowProps }) :
await browser.windows.getCurrent();
const windowId: number = currentWindow.id!;
await handle(windowId);
await browser.windows.update(windowId, { focused: true });
if (windowProps)
// Close "about:blank" tab
await browser.tabs.remove(currentWindow.tabs![0].id!);
@@ -14,7 +14,7 @@ export default async function getCollectionsFromCloud(): Promise<CollectionItem[
const chunks: Record<string, string> =
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "Base64" });
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "StorageBinaryString" });
return parseCollections(data);
}
@@ -14,7 +14,7 @@ export default async function saveCollectionsToCloud(collections: CollectionItem
return;
}
const data: string = compress(serializeCollections(collections), { outputEncoding: "Base64" });
const data: string = compress(serializeCollections(collections), { outputEncoding: "StorageBinaryString" });
const chunks: string[] = splitIntoChunks(data);
if (chunks.length > collectionStorage.maxChunkCount)
@@ -1,4 +1,3 @@
import { sendMessage } from "@/utils/messaging";
import { collectionStorage } from "./collectionStorage";
import saveCollectionsToCloud from "./saveCollectionsToCloud";
@@ -15,6 +14,6 @@ export default async function setCloudStorage(enable: boolean): Promise<void>
{
await collectionStorage.disableCloud.setValue(true);
await saveCollectionsToCloud([], 0);
await sendMessage("refreshCollections", undefined);
browser.runtime.reload();
}
}
+2 -2
View File
@@ -161,7 +161,7 @@ collections:
menu:
delete: "Delete collection"
add_selected: "Add selected tabs"
add_all: "Add all tabs"
add_all: "Add selected tabs"
add_group: "Add empty group"
export_bookmarks: "Export to bookmarks"
edit: "Edit collection"
@@ -174,7 +174,7 @@ groups:
menu:
new_window: "Open in new window"
add_selected: "Add selected tabs"
add_all: "Add all tabs"
add_all: "Add selected tabs"
edit: "Edit group"
ungroup: "Ungroup"
delete: "Delete group"
+1 -1
View File
@@ -84,7 +84,7 @@ options_page:
icon_action:
title: "Po kliknięciu ikony rozszerzenia:"
options:
action: "Zapisz karty (domyślna akcja)"
action: "Wykonaj domyślną akcję"
context: "Pokaż menu kontekstowe"
open: "Otwórz listę kolekcji"
change_shortcuts: "Zmień skróty klawiszowe"
+5 -5
View File
@@ -84,7 +84,7 @@ options_page:
icon_action:
title: "При нажатии на иконку расширения:"
options:
action: "Сохранить вкладки (действие по умолчанию)"
action: "Выполнить действие по умолчанию"
context: "Показать контекстное меню"
open: "Открыть список коллекций"
change_shortcuts: "Изменить горячие клавиши"
@@ -149,13 +149,13 @@ collections:
title: "Требуется разрешение"
message:
edge:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме InPrivate"
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме InPrivate"
p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\""
firefox:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в приватном окне"
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в приватном окне"
p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\""
chrome:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме инкогнито"
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме инкогнито"
p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\""
action: "Настройки"
menu:
@@ -246,6 +246,6 @@ parse_error_message:
merge_conflict_message:
title: "В локальном и облачном хранилищах есть конфликтующие изменения."
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако либо принять изменения из облака."
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако, либо принять изменения из облака."
accept_local: "Заменить локальной"
accept_cloud: "Принять облачные изменения"
+2 -2
View File
@@ -84,7 +84,7 @@ options_page:
icon_action:
title: "При натисканні на іконку розширення:"
options:
action: "Зберегти вкладки (дія за замовчуванням)"
action: "Виконати дію за замовчуванням"
context: "Показати контекстне меню"
open: "Відкрити список колекцій"
change_shortcuts: "Змінити гарячі клавіші"
@@ -246,6 +246,6 @@ parse_error_message:
merge_conflict_message:
title: "В локальному і облачному хранилищах є конфліктуючі зміни."
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі або прийняти зміни з хмари."
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі, або прийняти зміни з хмари."
accept_local: "Заменить локальною"
accept_cloud: "Прийняти облачні зміни"
+20 -20
View File
@@ -1,47 +1,47 @@
{
"name": "tabs-aside",
"private": true,
"version": "3.0.0",
"version": "3.0.0-rc3",
"type": "module",
"scripts": {
"dev": "wxt",
"build": "yarn lint && wxt build --mv3",
"zip": "yarn lint && wxt zip --mv3",
"build": "wxt build --mv3",
"zip": "wxt zip --mv3",
"lint": "tsc --noEmit && eslint . -c eslint.config.js",
"prepare": "wxt prepare",
"postinstall": "yarn prepare"
"prebuild": "yarn lint",
"prezip": "yarn lint",
"postinstall": "wxt prepare"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fluentui/react-components": "^9.68.1",
"@fluentui/react-icons": "^2.0.307",
"@webext-core/messaging": "^2.3.0",
"@fluentui/react-components": "^9.63.0",
"@fluentui/react-icons": "^2.0.298",
"@webext-core/messaging": "^2.2.0",
"@wxt-dev/analytics": "^0.4.1",
"@wxt-dev/i18n": "^0.2.4",
"@wxt-dev/i18n": "^0.2.3",
"lzutf8": "^0.6.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "18.3.1"
},
"devDependencies": {
"@eslint/css": "^0.10.0",
"@eslint/js": "^9.32.0",
"@eslint/json": "^0.13.1",
"@stylistic/eslint-plugin": "^5.2.2",
"@eslint/css": "^0.7.0",
"@eslint/js": "^9.26.0",
"@eslint/json": "^0.12.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@types/scheduler": "0.23.0",
"@wxt-dev/module-react": "^1.1.3",
"eslint": "^9.32.0",
"eslint": "^9.26.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"globals": "^16.0.0",
"scheduler": "0.23.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^7.0.6",
"typescript-eslint": "^8.31.1",
"vite": "^6.3.4",
"wxt": "~0.19.29"
},
"packageManager": "yarn@4.9.2"
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+5 -12
View File
@@ -1,33 +1,26 @@
import { trackError } from "@/features/analytics";
import { CollectionItem, GraphicsStorage, GroupItem } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger, ExtensionSendMessageArgs, GetDataType, GetReturnType } from "@webext-core/messaging";
import { GraphicsStorage } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger } from "@webext-core/messaging";
type ProtocolMap =
{
addThumbnail(data: { url: string; thumbnail: string; }): void;
getGraphicsCache(): GraphicsStorage;
refreshCollections(): void;
openCollection(data: { collection: CollectionItem; targetWindow: "new" | "incognito"; }): void;
openGroup(data: { group: GroupItem; newWindow: boolean; }): void;
};
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
{
const { onMessage, sendMessage, removeAllListeners }: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>(config);
const { onMessage, sendMessage, removeAllListeners } = defineExtensionMessaging<ProtocolMap>(config);
return {
onMessage,
removeAllListeners,
async sendMessage<TType extends keyof ProtocolMap>(
type: TType,
data: GetDataType<ProtocolMap[TType]>,
...args: ExtensionSendMessageArgs
): Promise<GetReturnType<ProtocolMap[TType]>>
sendMessage: async (type, data, args): Promise<any> =>
{
try
{
return await sendMessage(type, data, ...args);
return await sendMessage(type, data, args);
}
catch (ex)
{
+12 -10
View File
@@ -23,13 +23,7 @@ export default async function saveTabsToCollection(closeTabs: boolean): Promise<
const [collection, tabsToClose] = await createCollectionFromTabs(tabs);
if (closeTabs)
{
await browser.tabs.create({
active: true,
windowId: tabs[0].windowId
});
await browser.tabs.remove(tabsToClose.map(i => i.id!));
}
track(closeTabs ? "set_aside" : "save");
@@ -83,12 +77,20 @@ async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionIt
}
}
if (import.meta.env.FIREFOX)
{
for (; tabIndex < tabs.length; tabIndex++)
collection.items.push({ type: "tab", url: tabs[tabIndex].url!, title: tabs[tabIndex].title });
return [collection, tabs];
}
// Special case, if all tabs are in the same group, create a collection with the group title
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
if (tabs[0].groupId && tabs[0].groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE &&
tabs.every(i => i.groupId === tabs[0].groupId)
)
{
const group = await chrome.tabGroups.get(tabs[0].groupId);
const group = await browser.tabGroups!.get(tabs[0].groupId);
collection.title = group.title;
collection.color = group.color;
@@ -105,7 +107,7 @@ async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionIt
{
const tab = tabs[tabIndex];
if (!tab.groupId || tab.groupId === -1)
if (!tab.groupId || tab.groupId === chrome.tabGroups.TAB_GROUP_ID_NONE)
{
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
activeGroup = null;
@@ -115,7 +117,7 @@ async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionIt
if (!activeGroup || activeGroup !== tab.groupId)
{
activeGroup = tab.groupId;
const group = await chrome.tabGroups.get(activeGroup);
const group = await browser.tabGroups!.get(activeGroup);
collection.items.push({
type: "group",
+7 -11
View File
@@ -3,12 +3,6 @@ import { ConfigEnv, defineConfig, UserManifest } from "wxt";
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module", "@wxt-dev/analytics/module"],
vite: () => ({
build:
{
chunkSizeWarningLimit: 1000
}
}),
imports: {
dirsScanOptions:
{
@@ -24,8 +18,7 @@ export default defineConfig({
description: "__MSG_manifest_description__",
homepage_url: "https://github.com/xfox111/TabsAsideExtension/",
action:
{
action: {
default_title: "__MSG_manifest_name__"
},
@@ -37,8 +30,7 @@ export default defineConfig({
"tabs",
"notifications",
"contextMenus",
"bookmarks",
"tabGroups"
"bookmarks"
],
commands:
@@ -80,13 +72,17 @@ export default defineConfig({
gecko:
{
id: "tabsaside@xfox111.net",
strict_min_version: "139.0"
strict_min_version: "109.0"
}
};
// @ts-expect-error author key in Firefox has a different format
manifest.author = "__MSG_manifest_author__";
}
else
{
manifest.permissions!.push("tabGroups");
}
return manifest;
}
+6901 -10028
View File
File diff suppressed because it is too large Load Diff