mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-04-22 07:58:01 +03:00
!feat: major 3.0 release candidate
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { collectionStorage } from "./utils/collectionStorage";
|
||||
export * from "./utils/getCollections";
|
||||
|
||||
export { default as getCollections } from "./utils/getCollections";
|
||||
export { default as resoveConflict } from "./utils/resolveConflict";
|
||||
export { default as saveCollections } from "./utils/saveCollections";
|
||||
|
||||
export const collectionCount = collectionStorage.count;
|
||||
export const graphics = collectionStorage.graphics;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
|
||||
export const collectionStorage =
|
||||
{
|
||||
chunkCount: storage.defineItem<number>("sync:chunkCount", { fallback: 0 }),
|
||||
syncLastUpdated: storage.defineItem<number>("sync:lastUpdated", { fallback: 0 }),
|
||||
localLastUpdated: storage.defineItem<number>("local:lastUpdated", { fallback: 0 }),
|
||||
localCollections: storage.defineItem<CollectionItem[]>("local:collections", { fallback: [] }),
|
||||
count: storage.defineItem<number>("local:count", { fallback: 0 }),
|
||||
graphics: storage.defineItem<GraphicsStorage>("local:graphics", { fallback: {} }),
|
||||
maxChunkCount: 12
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default function getChunkKeys(start: number = 0, end: number = collectionStorage.maxChunkCount): string[]
|
||||
{
|
||||
return Array.from({ length: end - start }, (_, i) => "c" + (i + start));
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getCollectionsFromCloud from "./getCollectionsFromCloud";
|
||||
import getCollectionsFromLocal from "./getCollectionsFromLocal";
|
||||
import saveCollectionsToLocal from "./saveCollectionsToLocal";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
|
||||
const logger = getLogger("getCollections");
|
||||
|
||||
export default async function getCollections(): Promise<[CollectionItem[], CloudStorageIssueType | null]>
|
||||
{
|
||||
const lastUpdatedLocal: number = await collectionStorage.localLastUpdated.getValue();
|
||||
const lastUpdatedSync: number = await collectionStorage.syncLastUpdated.getValue();
|
||||
|
||||
if (lastUpdatedLocal === lastUpdatedSync)
|
||||
return [await getCollectionsFromLocal(), null];
|
||||
|
||||
if (lastUpdatedLocal > lastUpdatedSync)
|
||||
return [await getCollectionsFromLocal(), "merge_conflict"];
|
||||
|
||||
try
|
||||
{
|
||||
const collections: CollectionItem[] = await getCollectionsFromCloud();
|
||||
await saveCollectionsToLocal(collections, lastUpdatedSync);
|
||||
|
||||
return [collections, null];
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to get cloud storage");
|
||||
console.error(ex);
|
||||
return [await getCollectionsFromLocal(), "parse_error"];
|
||||
}
|
||||
}
|
||||
|
||||
export type CloudStorageIssueType = "parse_error" | "merge_conflict";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { decompress } from "lzutf8";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getChunkKeys from "./getChunkKeys";
|
||||
import parseCollections from "./parseCollections";
|
||||
|
||||
export default async function getCollectionsFromCloud(): Promise<CollectionItem[]>
|
||||
{
|
||||
const chunkCount: number = await collectionStorage.chunkCount.getValue();
|
||||
|
||||
if (chunkCount < 1)
|
||||
return [];
|
||||
|
||||
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: "StorageBinaryString" });
|
||||
|
||||
return parseCollections(data);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default async function getCollectionsFromLocal(): Promise<CollectionItem[]>
|
||||
{
|
||||
return await collectionStorage.localCollections.getValue();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
|
||||
export default function parseCollections(data: string): CollectionItem[]
|
||||
{
|
||||
if (!data)
|
||||
return [];
|
||||
|
||||
const collections: CollectionItem[] = [];
|
||||
const lines: string[] = data.split("\n");
|
||||
|
||||
for (const line of lines)
|
||||
{
|
||||
if (line.startsWith("c"))
|
||||
{
|
||||
const collection: CollectionItem = parseCollection(line);
|
||||
collections.push(collection);
|
||||
}
|
||||
else if (line.startsWith("\tg"))
|
||||
{
|
||||
const group: GroupItem = parseGroup(line);
|
||||
collections[collections.length - 1].items.push(group);
|
||||
}
|
||||
else if (line.startsWith("\t\tt"))
|
||||
{
|
||||
const tab: TabItem = parseTab(line);
|
||||
|
||||
const collectionIndex: number = collections.length - 1;
|
||||
const groupIndex: number = collections[collectionIndex].items.length - 1;
|
||||
|
||||
(collections[collectionIndex].items[groupIndex] as GroupItem).items.push(tab);
|
||||
}
|
||||
else if (line.startsWith("\tt"))
|
||||
{
|
||||
const tab: TabItem = parseTab(line);
|
||||
collections[collections.length - 1].items.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
function parseCollection(data: string): CollectionItem
|
||||
{
|
||||
return {
|
||||
type: "collection",
|
||||
timestamp: parseInt(data.match(/(?<=^c)\d+/)!.toString()),
|
||||
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
title: data.match(/(?<=^c[\da-z/]*\|).*/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
function parseGroup(data: string): GroupItem
|
||||
{
|
||||
const isPinned: boolean = data.match(/^\tg\/p$/) !== null;
|
||||
|
||||
if (isPinned)
|
||||
return {
|
||||
type: "group",
|
||||
pinned: true,
|
||||
items: []
|
||||
};
|
||||
|
||||
return {
|
||||
type: "group",
|
||||
pinned: false,
|
||||
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
title: data.match(/(?<=^\tg\/[a-z]+\|).*$/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
function parseTab(data: string): TabItem
|
||||
{
|
||||
return {
|
||||
type: "tab",
|
||||
url: data.match(/(?<=^(\t){1,2}t\|).*(?=\|)/)!.toString(),
|
||||
title: data.match(/(?<=^(\t){1,2}t\|.*\|).*$/)?.toString()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getCollectionsFromCloud from "./getCollectionsFromCloud";
|
||||
import getCollectionsFromLocal from "./getCollectionsFromLocal";
|
||||
import saveCollectionsToCloud from "./saveCollectionsToCloud";
|
||||
import saveCollectionsToLocal from "./saveCollectionsToLocal";
|
||||
|
||||
const logger = getLogger("resolveConflict");
|
||||
|
||||
export default function resolveConflict(acceptSource: "local" | "sync"): Promise<void>
|
||||
{
|
||||
if (acceptSource === "local")
|
||||
return replaceCloudWithLocal();
|
||||
|
||||
return replaceLocalWithCloud();
|
||||
}
|
||||
|
||||
async function replaceCloudWithLocal(): Promise<void>
|
||||
{
|
||||
const collections: CollectionItem[] = await getCollectionsFromLocal();
|
||||
const lastUpdated: number = await collectionStorage.localLastUpdated.getValue();
|
||||
|
||||
await saveCollectionsToCloud(collections, lastUpdated);
|
||||
}
|
||||
|
||||
async function replaceLocalWithCloud(): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
const collections: CollectionItem[] = await getCollectionsFromCloud();
|
||||
const lastUpdated: number = await collectionStorage.syncLastUpdated.getValue();
|
||||
|
||||
await saveCollectionsToLocal(collections, lastUpdated);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to get cloud storage");
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import saveCollectionsToCloud from "./saveCollectionsToCloud";
|
||||
import saveCollectionsToLocal from "./saveCollectionsToLocal";
|
||||
import updateGraphics from "./updateGraphics";
|
||||
|
||||
const logger = getLogger("saveCollections");
|
||||
|
||||
export default async function saveCollections(
|
||||
collections: CollectionItem[],
|
||||
updateCloud: boolean = true,
|
||||
graphicsCache?: GraphicsStorage
|
||||
): Promise<void>
|
||||
{
|
||||
const timestamp: number = Date.now();
|
||||
await saveCollectionsToLocal(collections, timestamp);
|
||||
|
||||
if (updateCloud)
|
||||
try
|
||||
{
|
||||
await saveCollectionsToCloud(collections, timestamp);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to save cloud storage");
|
||||
console.error(ex);
|
||||
|
||||
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_quota_exceeded.title"),
|
||||
message: i18n.t("notifications.error_quota_exceeded.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
else
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_storage_full.title"),
|
||||
message: i18n.t("notifications.error_storage_full.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
}
|
||||
|
||||
await updateGraphics(collections, graphicsCache);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { compress } from "lzutf8";
|
||||
import { WxtStorageItem } from "wxt/storage";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getChunkKeys from "./getChunkKeys";
|
||||
import serializeCollections from "./serializeCollections";
|
||||
|
||||
export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void>
|
||||
{
|
||||
if (!collections || collections.length < 1)
|
||||
{
|
||||
await collectionStorage.chunkCount.setValue(0);
|
||||
await browser.storage.sync.remove(getChunkKeys());
|
||||
return;
|
||||
}
|
||||
|
||||
const data: string = compress(serializeCollections(collections), { outputEncoding: "StorageBinaryString" });
|
||||
const chunks: string[] = splitIntoChunks(data);
|
||||
|
||||
if (chunks.length > collectionStorage.maxChunkCount)
|
||||
throw new Error("Data is too large to be stored in sync storage.");
|
||||
|
||||
// Since there's a limit for cloud write operations, we need to write all chunks in one go.
|
||||
const newRecords: Record<string, string | number> =
|
||||
{
|
||||
[getStorageKey(collectionStorage.chunkCount)]: chunks.length,
|
||||
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
|
||||
};
|
||||
|
||||
for (let i = 0; i < chunks.length; i++)
|
||||
newRecords[`c${i}`] = chunks[i];
|
||||
|
||||
await browser.storage.sync.set(newRecords);
|
||||
|
||||
if (chunks.length < collectionStorage.maxChunkCount)
|
||||
await browser.storage.sync.remove(getChunkKeys(chunks.length));
|
||||
}
|
||||
|
||||
function splitIntoChunks(data: string): string[]
|
||||
{
|
||||
// QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason).
|
||||
const chunkKey: string = getChunkKeys(collectionStorage.maxChunkCount - 1)[0];
|
||||
const chunkSize = (chrome.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2;
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += chunkSize)
|
||||
chunks.push(data.slice(i, i + chunkSize));
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function getStorageKey(storageItem: WxtStorageItem<any, any>): string
|
||||
{
|
||||
return storageItem.key.split(":")[1];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default async function saveCollectionsToLocal(collections: CollectionItem[], timestamp: number): Promise<void>
|
||||
{
|
||||
await collectionStorage.localCollections.setValue(collections);
|
||||
await collectionStorage.count.setValue(collections.length);
|
||||
await collectionStorage.localLastUpdated.setValue(timestamp);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
|
||||
export default function serializeCollections(collections: CollectionItem[]): string
|
||||
{
|
||||
let data: string = "";
|
||||
|
||||
for (const collection of collections)
|
||||
{
|
||||
data += getCollectionString(collection);
|
||||
|
||||
for (const item of collection.items)
|
||||
{
|
||||
if (item.type === "group")
|
||||
{
|
||||
data += getGroupString(item);
|
||||
|
||||
for (const tab of item.items)
|
||||
data += `\t${getTabString(tab)}`;
|
||||
}
|
||||
else if (item.type === "tab")
|
||||
data += getTabString(item);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getCollectionString(collection: CollectionItem): string
|
||||
{
|
||||
let data: string = `c${collection.timestamp}`;
|
||||
|
||||
if (collection.color)
|
||||
data += `/${collection.color}`;
|
||||
|
||||
if (collection.title)
|
||||
data += `|${collection.title}`;
|
||||
|
||||
data += "\n";
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getGroupString(group: GroupItem): string
|
||||
{
|
||||
let data: string = "\tg";
|
||||
|
||||
if (group.pinned === true)
|
||||
data += "/p";
|
||||
else
|
||||
{
|
||||
data += `/${group.color}`;
|
||||
|
||||
if (group.title)
|
||||
data += `|${group.title}`;
|
||||
}
|
||||
|
||||
data += "\n";
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getTabString(tab: TabItem): string
|
||||
{
|
||||
let data: string = "\tt";
|
||||
|
||||
data += `|${tab.url}|`;
|
||||
|
||||
if (tab.title)
|
||||
data += tab.title;
|
||||
|
||||
data += "\n";
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CollectionItem, GraphicsItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default async function updateGraphics(
|
||||
collections: CollectionItem[],
|
||||
graphicsCache?: GraphicsStorage
|
||||
): Promise<void>
|
||||
{
|
||||
const localGraphics: GraphicsStorage = await collectionStorage.graphics.getValue();
|
||||
const tempGraphics: GraphicsStorage = graphicsCache || await sendMessage("getGraphicsCache", undefined);
|
||||
|
||||
function getGraphics(url: string): GraphicsItem | null
|
||||
{
|
||||
const preview = tempGraphics[url]?.preview ?? localGraphics[url]?.preview;
|
||||
const icon = tempGraphics[url]?.icon ?? localGraphics[url]?.icon;
|
||||
|
||||
const graphics: GraphicsItem = {};
|
||||
|
||||
if (preview)
|
||||
graphics.preview = preview;
|
||||
if (icon)
|
||||
graphics.icon = icon;
|
||||
|
||||
return preview || icon ? graphics : null;
|
||||
}
|
||||
|
||||
const newGraphics: GraphicsStorage = {};
|
||||
|
||||
for (const collection of collections)
|
||||
for (const item of collection.items)
|
||||
{
|
||||
if (item.type === "group")
|
||||
for (const tab of item.items)
|
||||
{
|
||||
const graphics = getGraphics(tab.url);
|
||||
|
||||
if (graphics)
|
||||
newGraphics[tab.url] = graphics;
|
||||
}
|
||||
else
|
||||
{
|
||||
const graphics = getGraphics(item.url);
|
||||
|
||||
if (graphics)
|
||||
newGraphics[item.url] = graphics;
|
||||
}
|
||||
}
|
||||
|
||||
await collectionStorage.graphics.setValue(newGraphics);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import migrateLocalStorage from "../utils/migrateLocalStorage";
|
||||
|
||||
export default function useLocalMigration(): void
|
||||
{
|
||||
useEffect(() =>
|
||||
{
|
||||
if (globalThis.localStorage?.getItem("sets"))
|
||||
migrateLocalStorage().then(() => document.location.reload());
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as useLocalMigration } from "./hooks/useLocalMigration";
|
||||
export { default as migrateStorage } from "./utils/migrateStorage";
|
||||
@@ -0,0 +1,15 @@
|
||||
export type LegacyCollection =
|
||||
{
|
||||
timestamp: number;
|
||||
tabsCount: number;
|
||||
titles: string[];
|
||||
links: string[];
|
||||
icons?: string[];
|
||||
thumbnails?: string[];
|
||||
};
|
||||
|
||||
export type LegacyGraphics =
|
||||
{
|
||||
pageCapture?: string;
|
||||
iconUrl?: string;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CollectionItem, GraphicsStorage, TabItem } from "@/models/CollectionModels";
|
||||
import { LegacyCollection } from "../models/LegacyModels";
|
||||
|
||||
export default function migrateCollections(legacyCollections: LegacyCollection[]): [CollectionItem[], GraphicsStorage]
|
||||
{
|
||||
const collections: CollectionItem[] = [];
|
||||
const graphics: GraphicsStorage = {};
|
||||
|
||||
for (let i = 0; i < legacyCollections.length; i++)
|
||||
{
|
||||
const legacyCollection: LegacyCollection = legacyCollections[i];
|
||||
const items: TabItem[] = legacyCollection.links.map((url, index) =>
|
||||
{
|
||||
const title: string | undefined = legacyCollection.titles[index];
|
||||
const icon: string | undefined = legacyCollection.icons?.[index];
|
||||
const preview: string | undefined = legacyCollection.thumbnails?.[index];
|
||||
|
||||
if (!graphics[url])
|
||||
graphics[url] = { icon, preview };
|
||||
else
|
||||
graphics[url] = { icon: graphics[url].icon ?? icon, preview: graphics[url].preview ?? preview };
|
||||
|
||||
return {
|
||||
type: "tab",
|
||||
url,
|
||||
title
|
||||
};
|
||||
});
|
||||
|
||||
collections.push({
|
||||
type: "collection",
|
||||
timestamp: legacyCollection.timestamp,
|
||||
items
|
||||
});
|
||||
}
|
||||
|
||||
return [collections, graphics];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getCollections } from "@/features/collectionStorage";
|
||||
import saveCollections from "@/features/collectionStorage/utils/saveCollections";
|
||||
import { LegacyCollection } from "../models/LegacyModels";
|
||||
import migrateCollections from "./migrateCollections";
|
||||
|
||||
export default async function migrateLocalStorage(): Promise<void>
|
||||
{
|
||||
// Retrieve v1 collections
|
||||
const legacyCollections: LegacyCollection[] = JSON.parse(globalThis.localStorage?.getItem("sets") || "[]");
|
||||
|
||||
// Nuke localStorage
|
||||
globalThis.localStorage?.clear();
|
||||
|
||||
// Migrate collections
|
||||
const [resultCollections, resultGraphics] = migrateCollections(legacyCollections);
|
||||
const [collections] = await getCollections();
|
||||
await saveCollections([...collections, ...resultCollections], true, resultGraphics);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { saveCollections } from "@/features/collectionStorage";
|
||||
import { GraphicsStorage } from "@/models/CollectionModels";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { decompress } from "lzutf8";
|
||||
import { LegacyCollection, LegacyGraphics } from "../models/LegacyModels";
|
||||
import migrateCollections from "./migrateCollections";
|
||||
|
||||
export default async function migrateStorage(): Promise<void>
|
||||
{
|
||||
// Retrieve settings
|
||||
const loadOnRestore: boolean | null = await storage.getItem<boolean>("sync:loadOnRestore");
|
||||
const setAsideOnClick: boolean | null = await storage.getItem<boolean>("sync:setAsideOnClick");
|
||||
const showDeleteDialog: boolean | null = await storage.getItem<boolean>("sync:showDeleteDialog");
|
||||
const listView: boolean | null = await storage.getItem<boolean>("sync:listview");
|
||||
|
||||
// Retrieve v2 collections
|
||||
const legacyCollections: LegacyCollection[] = [];
|
||||
Object.entries(await browser.storage.sync.get(null)).forEach(([key, value]) =>
|
||||
{
|
||||
if (key.startsWith("set_"))
|
||||
legacyCollections.push({
|
||||
...JSON.parse(decompress(value, { inputEncoding: "StorageBinaryString" })),
|
||||
timestamp: parseInt(key.substring(4))
|
||||
});
|
||||
});
|
||||
|
||||
// Retrieve v2 graphics
|
||||
const v2Graphics: Record<string, LegacyGraphics> = await storage.getItem("local:thumbnails") ?? {};
|
||||
|
||||
// Nuke everything
|
||||
await browser.storage.local.clear();
|
||||
await browser.storage.sync.clear();
|
||||
|
||||
// Migrate collections & graphics
|
||||
const [collections] = migrateCollections(legacyCollections);
|
||||
const graphics: GraphicsStorage = {};
|
||||
|
||||
for (const [key, record] of Object.entries(v2Graphics))
|
||||
{
|
||||
if (!graphics[key])
|
||||
graphics[key] = { icon: record.iconUrl, preview: record.pageCapture };
|
||||
else
|
||||
{
|
||||
graphics[key].icon ??= record.iconUrl;
|
||||
graphics[key].preview ??= record.pageCapture;
|
||||
}
|
||||
}
|
||||
|
||||
await saveCollections(collections, true, graphics);
|
||||
|
||||
// Migrate settings
|
||||
if (loadOnRestore !== null)
|
||||
settings.dismissOnLoad.setValue(!loadOnRestore);
|
||||
if (setAsideOnClick !== null)
|
||||
settings.contextAction.setValue(setAsideOnClick ? "action" : "open");
|
||||
if (showDeleteDialog !== null)
|
||||
settings.deletePrompt.setValue(showDeleteDialog);
|
||||
if (listView !== null)
|
||||
settings.tilesView.setValue(!listView);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useTheme } from "@/contexts/ThemeProvider";
|
||||
import { v3blogPost } from "@/data/links";
|
||||
import extLink from "@/utils/extLink";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
|
||||
export default function WelcomeDialog(): React.ReactElement
|
||||
{
|
||||
const { isDark } = useTheme();
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<fui.DialogSurface>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogContent className={ cls.root }>
|
||||
<img alt="" src={ browser.runtime.getURL(isDark ? "/promo/dark.webp" : "/promo/light.webp") } />
|
||||
|
||||
<fui.Title2>{ i18n.t("features.v3welcome.title") }</fui.Title2>
|
||||
|
||||
<fui.Body1 as="p">
|
||||
{ i18n.t("features.v3welcome.text1") }
|
||||
</fui.Body1>
|
||||
<fui.Body1 as="p">
|
||||
{ i18n.t("features.v3welcome.text2") }
|
||||
</fui.Body1>
|
||||
<ul>
|
||||
{ !import.meta.env.FIREFOX &&
|
||||
<li>{ i18n.t("features.v3welcome.list.item1") }</li>
|
||||
}
|
||||
<li>{ i18n.t("features.v3welcome.list.item2") }</li>
|
||||
<li>{ i18n.t("features.v3welcome.list.item3") }</li>
|
||||
<li>{ i18n.t("features.v3welcome.list.item4") }</li>
|
||||
<li>{ i18n.t("features.v3welcome.list.item5") }</li>
|
||||
</ul>
|
||||
<fui.Body1>
|
||||
{ i18n.t("features.v3welcome.text3") }
|
||||
</fui.Body1>
|
||||
|
||||
</fui.DialogContent>
|
||||
|
||||
<fui.DialogActions>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="primary" as="a" { ...extLink(v3blogPost) }>
|
||||
{ i18n.t("features.v3welcome.actions.visit_blog") }
|
||||
</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle">
|
||||
{ i18n.t("common.actions.close") }
|
||||
</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</fui.DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalS
|
||||
},
|
||||
image:
|
||||
{
|
||||
display: "contents"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import WelcomeDialog from "../components/WelcomeDialog";
|
||||
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
|
||||
|
||||
export default function useWelcomeDialog(): void
|
||||
{
|
||||
const dialog = useDialog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
showWelcomeDialog.getValue().then(showWelcome =>
|
||||
{
|
||||
if (showWelcome || import.meta.env.DEV)
|
||||
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as useWelcomeDialog } from "./hooks/useWelcomeDialog";
|
||||
export { showWelcomeDialog } from "./utils/showWelcomeDialog";
|
||||
@@ -0,0 +1,6 @@
|
||||
export const showWelcomeDialog = storage.defineItem(
|
||||
"local:showWelcome",
|
||||
{
|
||||
fallback: false
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user