1
0
mirror of https://github.com/XFox111/TabsAsideExtension.git synced 2026-04-22 07:58:01 +03:00

Major 3.0 (#118)

Co-authored-by: Maison da Silva <maisonmdsgreen@hotmail.com>
This commit is contained in:
2025-07-30 15:02:26 +03:00
committed by GitHub
parent d6996031b6
commit 2bd9337e63
200 changed files with 19452 additions and 3339 deletions
@@ -0,0 +1,38 @@
import { makeStyles, tokens } from "@fluentui/react-components";
export const useOptionsStyles = makeStyles({
main:
{
display: "grid",
gridTemplateRows: "auto 1fr",
height: "100%"
},
tabList:
{
flexWrap: "wrap"
},
article:
{
display: "flex",
flexFlow: "column",
gap: tokens.spacingVerticalMNudge,
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
overflowY: "auto"
},
section:
{
display: "flex",
flexFlow: "column",
alignItems: "flex-start"
},
buttonFix:
{
minHeight: "32px"
},
horizontalButtons:
{
display: "flex",
flexWrap: "wrap",
gap: tokens.spacingHorizontalS
}
});
+24
View File
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tabs aside | Settings</title>
<meta name="manifest.open_in_tab" content="false" />
<style type="text/css">
body
{
height: 500px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,58 @@
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, Link, Subtitle1, Text } from "@fluentui/react-components";
import { PersonFeedback20Regular } from "@fluentui/react-icons";
import { useOptionsStyles } from "../hooks/useOptionsStyles";
import Package from "@/package.json";
export default function AboutSection(): React.ReactElement
{
const cls = useOptionsStyles();
const bmcCls = useBmcStyles();
return (
<>
<Text as="p">
<Subtitle1>{ i18n.t("manifest.name") }</Subtitle1>
<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>
</Body1>
<div className={ cls.horizontalButtons }>
<Button
as="a" { ...extLink(storeLink) }
appearance="primary"
icon={ <PersonFeedback20Regular /> }
>
{ i18n.t("common.cta.feedback") }
</Button>
<Button
as="a" { ...extLink(buyMeACoffeeLink) }
appearance="primary" className={ bmcCls.button }
icon={ <BuyMeACoffee20Regular /> }
>
{ i18n.t("common.cta.sponsor") }
</Button>
</div>
</>
);
}
@@ -0,0 +1,55 @@
import useSettings, { SettingsValue } from "@/hooks/useSettings";
import { Dropdown, Field, Option } from "@fluentui/react-components";
export default function ActionsSection(): React.ReactElement
{
const [saveAction, setSaveAction] = useSettings("defaultSaveAction");
const [restoreAction, setRestoreAction] = useSettings("defaultRestoreAction");
return (
<>
<Field label={ i18n.t("options_page.actions.options.save_actions.title") }>
<Dropdown
value={ saveAction ? saveActionOptions[saveAction] : "" }
selectedOptions={ [saveAction ?? ""] }
onOptionSelect={ (_, e) => setSaveAction(e.optionValue as SaveActionType) }
>
{ Object.entries(saveActionOptions).map(([value, label]) =>
<Option key={ value } value={ value }>
{ label }
</Option>
) }
</Dropdown>
</Field>
<Field label={ i18n.t("options_page.actions.options.restore_actions.title") }>
<Dropdown
value={ restoreAction ? restoreActionOptions[restoreAction] : "" }
selectedOptions={ [restoreAction ?? ""] }
onOptionSelect={ (_, e) => setRestoreAction(e.optionValue as RestoreActionType) }
>
{ Object.entries(restoreActionOptions).map(([value, label]) =>
<Option key={ value } value={ value }>
{ label }
</Option>
) }
</Dropdown>
</Field>
</>
);
}
type SaveActionType = SettingsValue<"defaultSaveAction">;
type RestoreActionType = SettingsValue<"defaultRestoreAction">;
const restoreActionOptions: Record<RestoreActionType, string> =
{
"open": i18n.t("options_page.actions.options.restore_actions.options.open"),
"restore": i18n.t("options_page.actions.options.restore_actions.options.restore")
};
const saveActionOptions: Record<SaveActionType, string> =
{
"set_aside": i18n.t("options_page.actions.options.save_actions.options.set_aside"),
"save": i18n.t("options_page.actions.options.save_actions.options.save")
};
@@ -0,0 +1,121 @@
import useSettings, { SettingsValue } from "@/hooks/useSettings";
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
import { KeyCommand20Regular } from "@fluentui/react-icons";
import { useOptionsStyles } from "../hooks/useOptionsStyles";
export default function GeneralSection(): React.ReactElement
{
const [alwaysShowToolbars, setAlwaysShowToolbars] = useSettings("alwaysShowToolbars");
const [ignorePinned, setIgnorePinned] = useSettings("ignorePinned");
const [deletePrompt, setDeletePrompt] = useSettings("deletePrompt");
const [showBadge, setShowBadge] = useSettings("showBadge");
const [notifyOnSave, setNotifyOnSave] = useSettings("notifyOnSave");
const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad");
const [listLocation, setListLocation] = useSettings("listLocation");
const [contextAction, setContextAction] = useSettings("contextAction");
const cls = useOptionsStyles();
const openShortcutsPage = (): Promise<any> =>
browser.tabs.create({
url: "chrome://extensions/shortcuts",
active: true
});
const handleListLocationChange = (_: any, e: OptionOnSelectData): void =>
{
if (e.optionValue === "popup" && contextAction !== "open")
setContextAction("open");
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
browser.sidebarAction.close();
setListLocation(e.optionValue as ListLocationType);
};
return (
<>
<section className={ cls.section }>
<Checkbox
label={ i18n.t("options_page.general.options.always_show_toolbars") }
checked={ alwaysShowToolbars ?? false }
onChange={ (_, e) => setAlwaysShowToolbars(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.include_pinned") }
checked={ !ignorePinned }
onChange={ (_, e) => setIgnorePinned(!e.checked) } />
<Checkbox
label={ i18n.t("options_page.general.options.show_delete_prompt") }
checked={ deletePrompt ?? false }
onChange={ (_, e) => setDeletePrompt(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.show_badge") }
checked={ showBadge ?? false }
onChange={ (_, e) => setShowBadge(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.show_notification") }
checked={ notifyOnSave ?? false }
onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.unload_tabs") }
checked={ dismissOnLoad ?? false }
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
</section>
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
<Dropdown
value={ listLocation ? listLocationOptions[listLocation] : "" }
selectedOptions={ [listLocation ?? ""] }
onOptionSelect={ handleListLocationChange }
>
{ Object.entries(listLocationOptions).map(([key, value]) =>
<Option key={ key } value={ key }>
{ value }
</Option>
) }
</Dropdown>
</Field>
<Field label={ i18n.t("options_page.general.options.icon_action.title") }>
<Dropdown
value={ contextAction ? contextActionOptions[contextAction] : "" }
selectedOptions={ [contextAction ?? ""] }
onOptionSelect={ (_, e) => setContextAction(e.optionValue as ContextActionType) }
disabled={ listLocation === "popup" }
>
{ Object.entries(contextActionOptions).map(([key, value]) =>
key === "context" && import.meta.env.FIREFOX
? <></> :
<Option key={ key } value={ key }>
{ value }
</Option>
) }
</Dropdown>
</Field>
{ !import.meta.env.FIREFOX &&
<Button icon={ <KeyCommand20Regular /> } onClick={ openShortcutsPage } className={ cls.buttonFix }>
{ i18n.t("options_page.general.options.change_shortcuts") }
</Button>
}
</>
);
}
type ListLocationType = SettingsValue<"listLocation">;
type ContextActionType = SettingsValue<"contextAction">;
const listLocationOptions: Record<ListLocationType, string> =
{
"sidebar": i18n.t("options_page.general.options.list_locations.options.sidebar"),
"popup": i18n.t("options_page.general.options.list_locations.options.popup"),
"tab": i18n.t("options_page.general.options.list_locations.options.tab"),
"pinned": i18n.t("options_page.general.options.list_locations.options.pinned")
};
const contextActionOptions: Record<ContextActionType, string> =
{
"action": i18n.t("options_page.general.options.icon_action.options.action"),
"context": i18n.t("options_page.general.options.icon_action.options.context"),
"open": i18n.t("options_page.general.options.icon_action.options.open")
};
@@ -0,0 +1,102 @@
import { useDialog } from "@/contexts/DialogProvider";
import { cloudDisabled, setCloudStorage } from "@/features/collectionStorage";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useStorageInfo from "@/hooks/useStorageInfo";
import { Button, Field, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
import { useOptionsStyles } from "../hooks/useOptionsStyles";
import exportData from "../utils/exportData";
import importData from "../utils/importData";
export default function StorageSection(): React.ReactElement
{
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
const [importResult, setImportResult] = useState<boolean | null>(null);
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
const dialog = useDialog();
const cls = useOptionsStyles();
const dangerCls = useDangerStyles();
useEffect(() =>
{
cloudDisabled.getValue().then(setCloudDisabled);
return cloudDisabled.watch(setCloudDisabled);
}, []);
const handleImport = (): void =>
dialog.pushPrompt({
title: i18n.t("options_page.storage.import_prompt.title"),
confirmText: i18n.t("options_page.storage.import_prompt.proceed"),
onConfirm: () => importData().then(setImportResult),
content: (
<MessageBar intent="warning">
<MessageBarBody>
<MessageBarTitle>{ i18n.t("options_page.storage.import_prompt.warning_title") }</MessageBarTitle>
{ i18n.t("options_page.storage.import_prompt.warning_text") }
</MessageBarBody>
</MessageBar>
)
});
const handleDisableCloud = (): void =>
dialog.pushPrompt({
title: i18n.t("options_page.storage.disable"),
content: i18n.t("options_page.storage.disable_prompt.text"),
confirmText: i18n.t("options_page.storage.disable_prompt.action"),
destructive: true,
onConfirm: () => setCloudStorage(false)
});
return (
<>
{ 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>
}
</>
);
}
+47
View File
@@ -0,0 +1,47 @@
import App from "@/App.tsx";
import "@/assets/global.css";
import { Tab, TabList } from "@fluentui/react-components";
import ReactDOM from "react-dom/client";
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
import AboutSection from "./layouts/AboutSection.tsx";
import ActionsSection from "./layouts/ActionsSection.tsx";
import GeneralSection from "./layouts/GeneralSection.tsx";
import StorageSection from "./layouts/StorageSection.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<App>
<OptionsPage />
</App>
);
analytics.page("options_page");
function OptionsPage(): React.ReactElement
{
const [selection, setSelection] = useState<SelectionType>("general");
const cls = useOptionsStyles();
return (
<main className={ cls.main }>
<TabList
className={ cls.tabList }
selectedValue={ selection }
onTabSelect={ (_, data) => setSelection(data.value as SelectionType) }
>
<Tab value="general">{ i18n.t("options_page.general.title") }</Tab>
<Tab value="actions">{ i18n.t("options_page.actions.title") }</Tab>
<Tab value="storage">{ i18n.t("options_page.storage.title") }</Tab>
<Tab value="about">{ i18n.t("options_page.about.title") }</Tab>
</TabList>
<article className={ cls.article }>
{ selection === "general" && <GeneralSection /> }
{ selection === "actions" && <ActionsSection /> }
{ selection === "storage" && <StorageSection /> }
{ selection === "about" && <AboutSection /> }
</article>
</main>
);
}
type SelectionType = "general" | "actions" | "storage" | "about";
+19
View File
@@ -0,0 +1,19 @@
export default async function exportData(): Promise<void>
{
const data: string = JSON.stringify({
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.setAttribute("download", "tabs-aside_data.json");
document.body.appendChild(element);
element.click();
URL.revokeObjectURL(element.href);
document.body.removeChild(element);
};
+56
View File
@@ -0,0 +1,56 @@
import { sendMessage } from "@/utils/messaging";
export default async function importData(): Promise<boolean | null>
{
const element: HTMLInputElement = document.createElement("input");
element.style.display = "none";
element.hidden = true;
element.type = "file";
element.accept = ".json";
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 data: any = JSON.parse(content);
if (data.local)
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)
{
console.error("Failed to parse JSON", error);
return false;
}
sendMessage("refreshCollections", undefined);
return true;
}