Skip to content

Commit 660f8ab

Browse files
committed
impr: store custom text in indexedDB (@fehmer)
1 parent 238a2c7 commit 660f8ab

File tree

8 files changed

+132
-180
lines changed

8 files changed

+132
-180
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"hangul-js": "0.2.6",
100100
"howler": "2.2.3",
101101
"html2canvas": "1.4.1",
102+
"idb": "8.0.3",
102103
"jquery": "3.7.1",
103104
"jquery-color": "2.2.0",
104105
"jquery.easing": "1.4.1",

frontend/src/ts/modals/save-custom-text.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function hide(): void {
3232
void modal.hide();
3333
}
3434

35-
function save(): boolean {
35+
async function save(): Promise<boolean> {
3636
const name = $("#saveCustomTextModal .textName").val() as string;
3737
const checkbox = $("#saveCustomTextModal .isLongText").prop(
3838
"checked"
@@ -48,7 +48,11 @@ function save(): boolean {
4848
return false;
4949
}
5050

51-
const saved = CustomText.setCustomText(name, state.textToSave, checkbox);
51+
const saved = await CustomText.setCustomText(
52+
name,
53+
state.textToSave,
54+
checkbox
55+
);
5256
if (saved) {
5357
CustomTextState.setCustomTextName(name, checkbox);
5458
Notifications.add("Custom text saved", 1);
@@ -98,9 +102,9 @@ async function setup(modalEl: HTMLElement): Promise<void> {
98102
level: 0,
99103
},
100104
});
101-
modalEl.addEventListener("submit", (e) => {
105+
modalEl.addEventListener("submit", async (e) => {
102106
e.preventDefault();
103-
if (save()) hide();
107+
if (await save()) hide();
104108
});
105109
modalEl.querySelector(".textName")?.addEventListener("input", (e) => {
106110
const val = (e.target as HTMLInputElement).value;

frontend/src/ts/modals/saved-texts.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,19 @@ async function fill(): Promise<void> {
9191
}
9292
);
9393

94-
$("#savedTextsModal .list .savedText .button.name").on("click", (e) => {
94+
$("#savedTextsModal .list .savedText .button.name").on("click", async (e) => {
9595
const name = $(e.target).text();
9696
CustomTextState.setCustomTextName(name, false);
97-
const text = getSavedText(name, false);
97+
const text = await getSavedText(name, false);
9898
hide({ modalChainData: { text, long: false } });
9999
});
100100

101101
$("#savedTextsModal .listLong .savedLongText .button.name").on(
102102
"click",
103-
(e) => {
103+
async (e) => {
104104
const name = $(e.target).text();
105105
CustomTextState.setCustomTextName(name, true);
106-
const text = getSavedText(name, true);
106+
const text = await getSavedText(name, true);
107107
hide({ modalChainData: { text, long: true } });
108108
}
109109
);
@@ -124,8 +124,8 @@ function hide(hideOptions?: HideOptions<OutgoingData>): void {
124124
});
125125
}
126126

127-
function getSavedText(name: string, long: boolean): string {
128-
let text = CustomText.getCustomText(name, long);
127+
async function getSavedText(name: string, long: boolean): Promise<string> {
128+
let text = await CustomText.getCustomText(name, long);
129129
if (long) {
130130
text = text.slice(CustomText.getCustomTextLongProgress(name));
131131
}

frontend/src/ts/modals/simple-modals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ list.resetProgressCustomTextLong = new SimpleModal({
10741074
buttonText: "reset",
10751075
execFn: async (_thisPopup): Promise<ExecReturn> => {
10761076
CustomText.setCustomTextLongProgress(_thisPopup.parameters[0] as string, 0);
1077-
const text = CustomText.getCustomText(
1077+
const text = await CustomText.getCustomText(
10781078
_thisPopup.parameters[0] as string,
10791079
true
10801080
);

frontend/src/ts/test/custom-text.ts

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,27 @@ import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
66
import { z } from "zod";
77
import { CompletedEventCustomTextSchema } from "@monkeytype/contracts/schemas/results";
88
import { deepClone } from "../utils/misc";
9+
import { DBSchema, IDBPDatabase, openDB } from "idb";
910

11+
type CustomTextDB = DBSchema & {
12+
customTexts: {
13+
key: string;
14+
value: {
15+
text: string;
16+
};
17+
};
18+
customLongTexts: {
19+
key: string;
20+
value: {
21+
text: string;
22+
progress: number;
23+
};
24+
};
25+
};
26+
27+
//Legacy storage
1028
const CustomTextObjectSchema = z.record(z.string(), z.string());
1129
type CustomTextObject = z.infer<typeof CustomTextObjectSchema>;
12-
1330
const CustomTextLongObjectSchema = z.record(
1431
z.string(),
1532
z.object({ text: z.string(), progress: z.number() })
@@ -21,13 +38,53 @@ const customTextLS = new LocalStorageWithSchema({
2138
schema: CustomTextObjectSchema,
2239
fallback: {},
2340
});
41+
2442
//todo maybe add migrations here?
2543
const customTextLongLS = new LocalStorageWithSchema({
2644
key: "customTextLong",
2745
schema: CustomTextLongObjectSchema,
2846
fallback: {},
2947
});
3048

49+
export async function getDB(): Promise<IDBPDatabase<CustomTextDB>> {
50+
return await openDB<CustomTextDB>("customTexts", 1, {
51+
async upgrade(db, oldVersion, _newVersion, tx, _event) {
52+
if (oldVersion === 0) {
53+
console.debug("Initialize indexedDB for customTexts from localStorage");
54+
55+
//create objectStores
56+
await db.createObjectStore("customTexts");
57+
await db.createObjectStore("customLongTexts");
58+
59+
const ctStore = tx.objectStore("customTexts");
60+
const longCtStore = tx.objectStore("customLongTexts");
61+
62+
//copy from old localStorage
63+
await Promise.all([
64+
...Object.entries(customTextLS.get()).map(async ([key, value]) =>
65+
ctStore.add({ text: value }, key)
66+
),
67+
...Object.entries(customTextLongLS.get()).map(async ([key, value]) =>
68+
longCtStore.add({ text: value.text, progress: value.progress }, key)
69+
),
70+
tx.done,
71+
]);
72+
73+
console.debug("Remove localStorage after migration");
74+
//TODO:
75+
//customTextLS.destroy();
76+
//customTextLongLS.destroy();
77+
}
78+
},
79+
});
80+
}
81+
82+
window.globalThis["db"] = {
83+
get: getDB,
84+
getText: getCustomText,
85+
setText: setCustomText,
86+
};
87+
3188
export const CustomTextSettingsSchema = CompletedEventCustomTextSchema.omit({
3289
textLen: true,
3390
}).extend({
@@ -140,56 +197,38 @@ export function getData(): CustomTextSettings {
140197
return customTextSettings.get();
141198
}
142199

143-
export function getCustomText(name: string, long = false): string[] {
144-
if (long) {
145-
const customTextLong = getLocalStorageLong();
146-
const customText = customTextLong[name];
147-
if (customText === undefined)
148-
throw new Error(`Custom text ${name} not found`);
149-
return customText.text.split(/ +/);
150-
} else {
151-
const customText = getLocalStorage()[name];
152-
if (customText === undefined)
153-
throw new Error(`Custom text ${name} not found`);
154-
return customText.split(/ +/);
155-
}
200+
export async function getCustomText(
201+
name: string,
202+
long = false
203+
): Promise<string[]> {
204+
const db = await getDB();
205+
const customText = await db.get(
206+
long ? "customLongTexts" : "customTexts",
207+
name
208+
);
209+
if (customText === undefined)
210+
throw new Error(`Custom text ${name} not found`);
211+
212+
return customText.text.split(/ +/);
156213
}
157214

158-
export function setCustomText(
215+
export async function setCustomText(
159216
name: string,
160217
text: string | string[],
161218
long = false
162-
): boolean {
163-
if (long) {
164-
const customText = getLocalStorageLong();
165-
166-
customText[name] = {
167-
text: "",
168-
progress: 0,
169-
};
170-
171-
const textByName = customText[name];
172-
if (textByName === undefined) {
173-
throw new Error("Custom text not found");
174-
}
175-
176-
if (typeof text === "string") {
177-
textByName.text = text;
219+
): Promise<boolean> {
220+
const db = await getDB();
221+
const textToStore = typeof text === "string" ? text : text.join(" ");
222+
try {
223+
if (long) {
224+
await db.put("customLongTexts", { text: textToStore, progress: 0 }, name);
178225
} else {
179-
textByName.text = text.join(" ");
226+
await db.put("customTexts", { text: textToStore }, name);
180227
}
181-
182-
return setLocalStorageLong(customText);
183-
} else {
184-
const customText = getLocalStorage();
185-
186-
if (typeof text === "string") {
187-
customText[name] = text;
188-
} else {
189-
customText[name] = text.join(" ");
190-
}
191-
192-
return setLocalStorage(customText);
228+
return true;
229+
} catch (e) {
230+
console.debug("Storing to indexedDb failed: ", e);
231+
return false;
193232
}
194233
}
195234

frontend/src/ts/test/test-logic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,13 +1083,13 @@ export async function finish(difficultyFailed = false): Promise<void> {
10831083
important: true,
10841084
});
10851085

1086-
let newText = CustomText.getCustomText(customTextName, true);
1086+
let newText = await CustomText.getCustomText(customTextName, true);
10871087
newText = newText.slice(newProgress);
10881088
CustomText.setText(newText);
10891089
} else {
10901090
// They finished the test
10911091
CustomText.setCustomTextLongProgress(customTextName, 0);
1092-
const text = CustomText.getCustomText(customTextName, true);
1092+
const text = await CustomText.getCustomText(customTextName, true);
10931093
CustomText.setText(text);
10941094
Notifications.add("Long custom text completed", 1, {
10951095
duration: 5,

frontend/src/ts/utils/local-storage-with-schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,8 @@ export class LocalStorageWithSchema<T> {
118118
return false;
119119
}
120120
}
121+
122+
public destroy(): void {
123+
window.localStorage.removeItem(this.key);
124+
}
121125
}

0 commit comments

Comments
 (0)