Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getChrome } from "@code-chronicles/util/browser-extensions/chrome/getChrome";

import { SETTINGS_ATTRIBUTE, SETTINGS_STORAGE_KEY } from "../constants.ts";
import { PUBLIC_SETTINGS_STORAGE_KEY } from "../shared/public-settings/constants.ts";
import { publicSettingsZodType } from "../shared/public-settings/publicSettingsZodType.ts";
import { writePublicSettingsToDocumentAttribute } from "../shared/public-settings/writePublicSettingsToDocumentAttribute.ts";

/**
* Entry point for the extension content script that will run in an isolated
Expand All @@ -18,10 +20,15 @@ async function main(): Promise<void> {
return;
}

const settings = await chrome.storage.sync.get(SETTINGS_STORAGE_KEY);
document.documentElement.setAttribute(
SETTINGS_ATTRIBUTE,
JSON.stringify(settings[SETTINGS_STORAGE_KEY]),
const unsafePublicSettings: unknown = (
await chrome.storage.sync.get(PUBLIC_SETTINGS_STORAGE_KEY)
)[PUBLIC_SETTINGS_STORAGE_KEY];
if (unsafePublicSettings == null) {
return;
}

writePublicSettingsToDocumentAttribute(
publicSettingsZodType.parse(unsafePublicSettings),
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { isNonArrayObject } from "@code-chronicles/util/isNonArrayObject";
import { isString } from "@code-chronicles/util/isString";

import { difficultyZodType, type Difficulty } from "../problemDifficulties.ts";

export function isArrayOfDataByDifficulty(
arr: unknown[],
): arr is ({ difficulty: string } & Record<string, unknown>)[] {
): arr is ({ difficulty: Difficulty } & Record<string, unknown>)[] {
return arr.every(
(elem) => isNonArrayObject(elem) && isString(elem.difficulty),
(elem) =>
isNonArrayObject(elem) &&
difficultyZodType.safeParse(elem.difficulty).success,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { isString } from "@code-chronicles/util/isString";
import { assignFunctionCosmeticProperties } from "@code-chronicles/util/object-properties/assignFunctionCosmeticProperties";
import { NullReactElement } from "@code-chronicles/util/browser-extensions/NullReactElement";

import { difficultyZodType } from "../problemDifficulties.ts";

type CreateElementFn = (
this: unknown,
elementType: unknown,
Expand All @@ -23,7 +25,7 @@ export function patchJsxFactory<T extends CreateElementFn>(
Array.isArray(props.items) &&
props.items.some(
(it: Record<string, unknown>) =>
isString(it.value) && /^easy$/i.test(it.value),
difficultyZodType.safeParse(it.value).success,
)
) {
return createElementFn.apply(this, [NullReactElement, {}]);
Expand All @@ -35,6 +37,7 @@ export function patchJsxFactory<T extends CreateElementFn>(
if (
isNonArrayObject(props) &&
isString(props.category) &&
// TODO: use the preferred difficulty
/^(?:medium|hard)$/i.test(props.category)
) {
return createElementFn.apply(this, [NullReactElement, {}]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { stringToCase, type Case } from "@code-chronicles/util/stringToCase";

import { isArrayOfDataByDifficulty } from "./isArrayOfDataByDifficulty.ts";
import { PREFERRED_STRING_CASE, STRING_CASE_CHECKERS } from "./stringCase.ts";
import type { Difficulty } from "../problemDifficulties.ts";

/**
* Some of the LeetCode GraphQL data is aggregate statistics about problems
Expand All @@ -18,6 +19,7 @@ import { PREFERRED_STRING_CASE, STRING_CASE_CHECKERS } from "./stringCase.ts";
*/
export function rewriteLeetCodeAggregateDataForDifficulty(
arr: unknown[],
uncasedPreferredDifficulty: Difficulty,
): unknown[] {
// Do nothing if it's not the kind of data we're looking for.
if (!isArrayOfDataByDifficulty(arr)) {
Expand All @@ -38,27 +40,33 @@ export function rewriteLeetCodeAggregateDataForDifficulty(

// Prepare some difficulty strings that will come in handy below.
const allDifficulty = stringToCase("all", difficultyStringCase);
const easyDifficulty = stringToCase("easy", difficultyStringCase);
const casedPreferredDifficulty = stringToCase(
uncasedPreferredDifficulty,
difficultyStringCase,
);

const elementsByDifficulty = groupBy(arr, (elem) =>
stringToCase(elem.difficulty, difficultyStringCase),
);

// If we have a single "All" item and items with difficulties besides
// "All" and "Easy", we will get rid of the extra items, and instead use
// a single "Easy" item that's a copy of the "All" item with an updated
// difficulty.
// If we have a single "All" item and items with difficulties besides "All"
// and the preferred difficulty, we will get rid of the extra items, and
// instead use a single item that's a copy of the "All" item with an updated
// difficulty to be the preferred one.
if (
elementsByDifficulty.get(allDifficulty)?.length === 1 &&
[...elementsByDifficulty.keys()].some(
(difficulty) =>
difficulty !== allDifficulty && difficulty !== easyDifficulty,
difficulty !== allDifficulty && difficulty !== casedPreferredDifficulty,
)
) {
const allElement = only(
nullthrows(elementsByDifficulty.get(allDifficulty)),
);
return [allElement, { ...allElement, difficulty: easyDifficulty }];
return [
allElement,
{ ...allElement, difficulty: casedPreferredDifficulty },
];
}

// Another option is that we don't have an "All" item. In this case we
Expand All @@ -71,18 +79,19 @@ export function rewriteLeetCodeAggregateDataForDifficulty(
if (
[...elementsByDifficulty.values()].every((group) => group.length === 1) &&
[...elementsByDifficulty.keys()].some(
(difficulty) => difficulty !== easyDifficulty,
(difficulty) => difficulty !== casedPreferredDifficulty,
)
) {
return [
mergeObjects(arr, (values, key) => {
if (key === "difficulty") {
return easyDifficulty;
return casedPreferredDifficulty;
}

if (isArrayOfNumbers(values)) {
const total = sum(values);

// TODO: weighted average
if (key === "percentage") {
return total / (values.length || 1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,30 @@ import { isString } from "@code-chronicles/util/isString";
import { mapObjectValues } from "@code-chronicles/util/mapObjectValues";
import { stringToCase } from "@code-chronicles/util/stringToCase";

import { SETTINGS_ATTRIBUTE } from "../constants.ts";
import { rewriteLeetCodeAggregateDataForDifficulty } from "./rewriteLeetCodeAggregateDataForDifficulty.ts";
import { PREFERRED_STRING_CASE, STRING_CASE_CHECKERS } from "./stringCase.ts";
import type { Difficulty } from "../usePreferredDifficulty.ts";

let preferredDifficulty: Difficulty | null = null;
function getPreferredDifficulty(prevJsonParse: typeof JSON.parse): Difficulty {
if (preferredDifficulty == null) {
try {
preferredDifficulty = (prevJsonParse(
String(document.documentElement.getAttribute(SETTINGS_ATTRIBUTE)),
) ?? "Easy") as Difficulty;
} catch (err) {
console.error(err);
preferredDifficulty = "Easy";
}
}

return preferredDifficulty;
import { difficultyZodType } from "../problemDifficulties.ts";
import type { PublicSettings } from "../shared/public-settings/publicSettingsZodType.ts";
import { readPublicSettingsFromDocumentAttribute } from "../shared/public-settings/readPublicSettingsFromDocumentAttribute.ts";

let publicSettings: PublicSettings | null = null;
function getPublicSettings(prevJsonParse: typeof JSON.parse): PublicSettings {
return (publicSettings ??=
readPublicSettingsFromDocumentAttribute(prevJsonParse));
}

export function rewriteLeetCodeGraphQLData(
value: unknown,
prevJsonParse: typeof JSON.parse,
): unknown {
if (Array.isArray(value)) {
const { preferredDifficulty } = getPublicSettings(prevJsonParse);

// Arrays get some extra processing.
const rewrittenValue = rewriteLeetCodeAggregateDataForDifficulty(value);
const rewrittenValue = rewriteLeetCodeAggregateDataForDifficulty(
value,
preferredDifficulty,
);

// Recursively process array values.
return rewrittenValue.map((value) =>
Expand All @@ -46,11 +42,12 @@ export function rewriteLeetCodeGraphQLData(
}

// Rewrite difficulty strings!
if (isString(value) && /^(?:easy|medium|hard)$/i.test(value)) {
if (isString(value) && difficultyZodType.safeParse(value).success) {
const stringCase =
STRING_CASE_CHECKERS.find(([, checker]) => checker(value))?.[0] ??
PREFERRED_STRING_CASE;
return stringToCase(getPreferredDifficulty(prevJsonParse), stringCase);
const { preferredDifficulty } = getPublicSettings(prevJsonParse);
return stringToCase(preferredDifficulty, stringCase);
}

// Pass everything else through unchanged.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React from "react";

import {
usePreferredDifficulty,
DIFFICULTIES,
type Difficulty,
} from "../../usePreferredDifficulty.ts";
import { usePreferredDifficulty } from "../../usePreferredDifficulty.ts";
import { DIFFICULTIES, difficultyZodType } from "../../problemDifficulties.ts";

function Options() {
const [preferredDifficulty, setPreferredDifficulty] =
Expand All @@ -17,7 +14,7 @@ function Options() {
<select
value={preferredDifficulty}
onChange={(ev) =>
setPreferredDifficulty(ev.target.value as Difficulty)
setPreferredDifficulty(difficultyZodType.parse(ev.target.value))
}
>
{DIFFICULTIES.map((difficultyOption) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import nullthrows from "nullthrows";
import { z } from "zod";

export const DIFFICULTIES = ["Easy", "Medium", "Hard"] as const;

export const difficultyZodType = z
.string()
.transform((s) =>
nullthrows(DIFFICULTIES.find((d) => d.toLowerCase() === s.toLowerCase())),
);

export type Difficulty = z.infer<typeof difficultyZodType>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const PUBLIC_SETTINGS_ATTRIBUTE =
"data-leetcode-zen-mode-public-settings";

export const PUBLIC_SETTINGS_STORAGE_KEY = "public-settings";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PublicSettings } from "./publicSettingsZodType.ts";

export function getDefaultPublicSettings(): PublicSettings {
return {
preferredDifficulty: "Easy",
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

import { difficultyZodType } from "../../problemDifficulties.ts";

export const publicSettingsZodType = z.object({
preferredDifficulty: difficultyZodType,
});

export type PublicSettings = z.infer<typeof publicSettingsZodType>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PUBLIC_SETTINGS_ATTRIBUTE } from "./constants.ts";
import { getDefaultPublicSettings } from "./getDefaultPublicSettings.ts";
import {
publicSettingsZodType,
type PublicSettings,
} from "./publicSettingsZodType.ts";

export function readPublicSettingsFromDocumentAttribute(
parseFn: typeof JSON.parse = JSON.parse,
): PublicSettings {
try {
const attribute = document.documentElement.getAttribute(
PUBLIC_SETTINGS_ATTRIBUTE,
);
if (attribute != null) {
return publicSettingsZodType.parse(parseFn(attribute));
}
} catch (err) {
console.error(err);
}

return getDefaultPublicSettings();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PUBLIC_SETTINGS_ATTRIBUTE } from "./constants.ts";
import type { PublicSettings } from "./publicSettingsZodType.ts";

export function writePublicSettingsToDocumentAttribute(
settings: PublicSettings,
): void {
document.documentElement.setAttribute(
PUBLIC_SETTINGS_ATTRIBUTE,
JSON.stringify(settings),
);
}
10 changes: 7 additions & 3 deletions workspaces/leetcode-zen-mode/src/extension/useChromeStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getUniqueId } from "@code-chronicles/util/getUniqueId";

let storage: Record<string, unknown> | null = null;

async function writeChanges(): Promise<void> {
async function readLatestStorageAndNotify(): Promise<void> {
const chrome = getChrome();
if (!chrome) {
return;
Expand All @@ -20,13 +20,15 @@ let promise: Promise<void> | null = null;

function getSnapshot(): Record<string, unknown> {
if (!storage) {
throw (promise ??= writeChanges().finally(() => {
throw (promise ??= readLatestStorageAndNotify().finally(() => {
promise = null;

// We never clear the listener but that seems ok.
getChrome()?.storage.sync.onChanged.addListener((changes) => {
if (!storage) {
writeChanges();
// We weren't able to read the storage for some reason... let's try
// again?
readLatestStorageAndNotify();
return;
}

Expand All @@ -49,6 +51,7 @@ function getSnapshot(): Record<string, unknown> {
return storage;
}

// TODO: use a proper event emitter
const subscribers = new Map<string, () => void>();

function subscribe(sub: () => void): () => void {
Expand All @@ -63,6 +66,7 @@ function notifySubscribers(): void {
}
}

// TODO: move this hook to the util package
export function useChromeStorage(): Record<string, unknown> {
return useSyncExternalStore(subscribe, getSnapshot, () => ({}));
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { z } from "zod";

import { getChrome } from "@code-chronicles/util/browser-extensions/chrome/getChrome";

import { useChromeStorage } from "./useChromeStorage.ts";
import { SETTINGS_STORAGE_KEY } from "./constants.ts";

export const DIFFICULTIES = ["Easy", "Medium", "Hard"] as const;

const difficultyZodType = z.enum(DIFFICULTIES);
import { PUBLIC_SETTINGS_STORAGE_KEY } from "./shared/public-settings/constants.ts";
import {
DIFFICULTIES,
difficultyZodType,
type Difficulty,
} from "./problemDifficulties.ts";

export type Difficulty = z.infer<typeof difficultyZodType>;
// TODO: set the whole public settings

async function setPreferredDifficulty(
newPreferredDifficulty: Difficulty,
): Promise<void> {
await getChrome()?.storage.sync.set({
[SETTINGS_STORAGE_KEY]: newPreferredDifficulty,
[PUBLIC_SETTINGS_STORAGE_KEY]: newPreferredDifficulty,
});
}

Expand All @@ -26,7 +25,7 @@ export function usePreferredDifficulty(): [
const storage = useChromeStorage();

const parseResult = difficultyZodType.safeParse(
storage[SETTINGS_STORAGE_KEY],
storage[PUBLIC_SETTINGS_STORAGE_KEY],
);
const preferredDifficulty: Difficulty = parseResult.success
? parseResult.data
Expand Down
Loading