Skip to content

Commit f2b34a5

Browse files
fehmerMiodechottekbyseif21
authored
feat: allow user to use local file as background (@fehmer, @byseif21, @Miodec) (monkeytypegame#6663)
Allow the user to use a local file as custom background without uploading it to the server. Based on @byseif21 work in monkeytypegame#6630, thanks! --------- Co-authored-by: Miodec <[email protected]> Co-authored-by: Lukas <[email protected]> Co-authored-by: Seif Soliman <[email protected]>
1 parent b024e8e commit f2b34a5

File tree

10 files changed

+255
-85
lines changed

10 files changed

+255
-85
lines changed

frontend/package.json

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

frontend/src/html/pages/settings.html

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,11 +1307,47 @@
13071307
</button>
13081308
</div>
13091309
<div class="text">
1310-
Set an image url to be a custom background image. Cover fits the image
1311-
to cover the screen. Contain fits the image to be fully visible. Max
1312-
fits the image corner to corner.
1310+
Set an image url or local image to be a custom background image. Local
1311+
image always take priority over the image url. Cover fits the image to
1312+
cover the screen. Contain fits the image to be fully visible. Max fits
1313+
the image corner to corner.
1314+
<br />
1315+
<br />
1316+
Note: The local image is stored in your browser's local storage and will
1317+
not be uploaded to the server. This means that if you clear your
1318+
browser's local storage or use a different browser, the local image will
1319+
be lost.
13131320
</div>
13141321
<div class="inputs">
1322+
<div class="usingLocalImage">
1323+
<button class="no-auto-handle">
1324+
<i class="fas fa-trash fa-fw"></i>
1325+
remove local image
1326+
</button>
1327+
</div>
1328+
<div class="uploadContainer">
1329+
<label
1330+
for="customBackgroundUpload"
1331+
class="button"
1332+
aria-label="Select custom background image"
1333+
data-balloon-pos="left"
1334+
>
1335+
<i class="fas fa-file-import fa-fw"></i>
1336+
use local image
1337+
</label>
1338+
1339+
<input
1340+
type="file"
1341+
id="customBackgroundUpload"
1342+
accept="image/png,image/jpeg,image/jpg,image/gif"
1343+
style="display: none"
1344+
/>
1345+
</div>
1346+
<div class="separator">
1347+
<div class="line"></div>
1348+
or
1349+
<div class="line"></div>
1350+
</div>
13151351
<div class="inputAndButton">
13161352
<input
13171353
type="text"
@@ -1320,14 +1356,6 @@
13201356
tabindex="0"
13211357
onClick="this.select();"
13221358
/>
1323-
<span>
1324-
<button class="hidden remove no-auto-handle">
1325-
<i class="fas fa-trash fa-fw"></i>
1326-
</button>
1327-
<button class="save no-auto-handle">
1328-
<i class="fas fa-save fa-fw"></i>
1329-
</button>
1330-
</span>
13311359
</div>
13321360
<div class="buttons">
13331361
<button data-config-value="cover">cover</button>

frontend/src/styles/settings.scss

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,19 @@
110110
}
111111

112112
.statusIndicator {
113-
opacity: 0;
113+
visibility: hidden;
114+
}
115+
116+
input {
117+
padding-right: 0.5em !important;
114118
}
115119
&:has(input:focus),
116120
&:has([data-indicator-status="failed"]) {
117121
.statusIndicator {
118-
opacity: 1;
122+
visibility: visible;
123+
}
124+
input {
125+
padding-right: 2.2em !important;
119126
}
120127
}
121128
}
@@ -144,6 +151,41 @@
144151
}
145152
}
146153

154+
&[data-config-name="customBackgroundSize"] {
155+
.uploadContainer {
156+
grid-column: span 2;
157+
margin-bottom: 0.5em;
158+
margin-top: 0.5em;
159+
}
160+
label.button {
161+
width: 100%;
162+
}
163+
.separator {
164+
margin-bottom: 0.5rem;
165+
grid-column: span 2;
166+
// color: var(--sub-color);
167+
display: grid;
168+
gap: 1em;
169+
grid-template-columns: 1fr auto 1fr;
170+
place-items: center;
171+
}
172+
.line {
173+
width: 100%;
174+
height: 0.25em;
175+
border-radius: 0.25em;
176+
background: var(--sub-alt-color);
177+
}
178+
.usingLocalImage {
179+
display: grid;
180+
grid-template-columns: 1fr;
181+
place-items: center;
182+
margin-bottom: 0.5em;
183+
button {
184+
width: 100%;
185+
}
186+
}
187+
}
188+
147189
&[data-config-name="customBackgroundFilter"] {
148190
.groups {
149191
grid-area: buttons;

frontend/src/ts/controllers/theme-controller.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as Loader from "../elements/loader";
1212
import { debounce } from "throttle-debounce";
1313
import { ThemeName } from "@monkeytype/schemas/configs";
1414
import { ThemesList } from "../constants/themes";
15+
import fileStorage from "../utils/file-storage";
1516

1617
export let randomTheme: ThemeName | string | null = null;
1718
let isPreviewingTheme = false;
@@ -376,12 +377,22 @@ function applyCustomBackgroundSize(): void {
376377
}
377378
}
378379

379-
function applyCustomBackground(): void {
380+
export async function applyCustomBackground(): Promise<void> {
380381
// $(".customBackground").css({
381382
// backgroundImage: `url(${Config.customBackground})`,
382383
// backgroundAttachment: "fixed",
383384
// });
384-
if (Config.customBackground === "") {
385+
386+
let backgroundUrl = Config.customBackground;
387+
388+
//if there is a localBackgroundFile available, use it.
389+
const localBackgroundFile = await fileStorage.getFile("LocalBackgroundFile");
390+
391+
if (localBackgroundFile !== undefined) {
392+
backgroundUrl = localBackgroundFile;
393+
}
394+
395+
if (backgroundUrl === "") {
385396
$("#words").removeClass("noErrorBorder");
386397
$("#resultWordsHistory").removeClass("noErrorBorder");
387398
$(".customBackground img").remove();
@@ -392,7 +403,8 @@ function applyCustomBackground(): void {
392403
//use setAttribute for possible unsafe customBackground value
393404
const container = document.querySelector(".customBackground");
394405
const img = document.createElement("img");
395-
img.setAttribute("src", Config.customBackground);
406+
407+
img.setAttribute("src", backgroundUrl);
396408
img.setAttribute(
397409
"onError",
398410
"javascript:this.style.display='none'; window.dispatchEvent(new Event('customBackgroundFailed'))"
@@ -439,7 +451,7 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
439451
await set(Config.theme);
440452
}
441453
}
442-
applyCustomBackground();
454+
await applyCustomBackground();
443455
}
444456

445457
// this is here to prevent calling set / preview multiple times during a full config loading
@@ -461,7 +473,8 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
461473
await set(eventValue as string);
462474
}
463475
if (eventKey === "randomTheme" && eventValue === "off") await clearRandom();
464-
if (eventKey === "customBackground") applyCustomBackground();
476+
if (eventKey === "customBackground") await applyCustomBackground();
477+
465478
if (eventKey === "customBackgroundSize") applyCustomBackgroundSize();
466479
if (eventKey === "autoSwitchTheme") {
467480
if (eventValue as boolean) {

frontend/src/ts/elements/input-validation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export type Validation<T> = {
3232

3333
/** custom debounce delay for `isValid` call. defaults to 100 */
3434
debounceDelay?: number;
35+
36+
/** Resets the value to the current config if empty */
37+
resetIfEmpty?: false;
3538
};
3639
/**
3740
* Create input handler for validated input element.
@@ -213,7 +216,7 @@ export function handleConfigInput<T extends ConfigKey>({
213216
}
214217

215218
const handleStore = (): void => {
216-
if (input.value === "") {
219+
if (input.value === "" && (validation?.resetIfEmpty ?? true)) {
217220
//use last config value, clear validation
218221
input.value = new String(Config[configName]).toString();
219222
input.dispatchEvent(new Event("input"));
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import FileStorage from "../../utils/file-storage";
2+
import * as Notifications from "../notifications";
3+
import { applyCustomBackground } from "../../controllers/theme-controller";
4+
5+
const parentEl = document.querySelector(
6+
".pageSettings .section[data-config-name='customBackgroundSize']"
7+
);
8+
const usingLocalImageEl = parentEl?.querySelector(".usingLocalImage");
9+
const separatorEl = parentEl?.querySelector(".separator");
10+
const uploadContainerEl = parentEl?.querySelector(".uploadContainer");
11+
const inputAndButtonEl = parentEl?.querySelector(".inputAndButton");
12+
13+
async function readFileAsDataURL(file: File): Promise<string> {
14+
return new Promise((resolve, reject) => {
15+
const reader = new FileReader();
16+
reader.onload = () => resolve(reader.result as string);
17+
reader.onerror = reject;
18+
reader.readAsDataURL(file);
19+
});
20+
}
21+
22+
export async function updateUI(): Promise<void> {
23+
if (await FileStorage.hasFile("LocalBackgroundFile")) {
24+
usingLocalImageEl?.classList.remove("hidden");
25+
separatorEl?.classList.add("hidden");
26+
uploadContainerEl?.classList.add("hidden");
27+
inputAndButtonEl?.classList.add("hidden");
28+
} else {
29+
usingLocalImageEl?.classList.add("hidden");
30+
separatorEl?.classList.remove("hidden");
31+
uploadContainerEl?.classList.remove("hidden");
32+
inputAndButtonEl?.classList.remove("hidden");
33+
}
34+
}
35+
36+
usingLocalImageEl
37+
?.querySelector("button")
38+
?.addEventListener("click", async () => {
39+
await FileStorage.deleteFile("LocalBackgroundFile");
40+
await updateUI();
41+
await applyCustomBackground();
42+
});
43+
44+
uploadContainerEl
45+
?.querySelector("input[type='file']")
46+
?.addEventListener("change", async (e) => {
47+
const fileInput = e.target as HTMLInputElement;
48+
const file = fileInput.files?.[0];
49+
50+
if (!file) {
51+
return;
52+
}
53+
54+
// check type
55+
if (!file.type.match(/image\/(jpeg|jpg|png|gif)/)) {
56+
Notifications.add("Unsupported image format", 0);
57+
fileInput.value = "";
58+
return;
59+
}
60+
61+
const dataUrl = await readFileAsDataURL(file);
62+
await FileStorage.storeFile("LocalBackgroundFile", dataUrl);
63+
64+
await updateUI();
65+
await applyCustomBackground();
66+
67+
fileInput.value = "";
68+
});

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ShowOptions } from "../utils/animated-modal";
3737
import { GenerateDataRequest } from "@monkeytype/contracts/dev";
3838
import { UserEmailSchema, UserNameSchema } from "@monkeytype/contracts/users";
3939
import { goToPage } from "../pages/leaderboards";
40+
import FileStorage from "../utils/file-storage";
4041

4142
type PopupKey =
4243
| "updateEmail"
@@ -784,6 +785,7 @@ list.resetAccount = new SimpleModal({
784785

785786
Notifications.add("Resetting settings...", 0);
786787
await UpdateConfig.reset();
788+
await FileStorage.deleteFile("LocalBackgroundFile");
787789

788790
Notifications.add("Resetting account...", 0);
789791
const response = await Ape.users.reset();
@@ -939,6 +941,7 @@ list.resetSettings = new SimpleModal({
939941
onlineOnly: true,
940942
execFn: async (): Promise<ExecReturn> => {
941943
await UpdateConfig.reset();
944+
await FileStorage.deleteFile("LocalBackgroundFile");
942945
return {
943946
status: 1,
944947
message: "Settings reset",

0 commit comments

Comments
 (0)