Skip to content

Commit f759b0c

Browse files
authored
feat(settings): allow user to pick a local font (@fehmer, @Miodec) (monkeytypegame#6794)
1 parent 9c41fd5 commit f759b0c

File tree

8 files changed

+199
-36
lines changed

8 files changed

+199
-36
lines changed

frontend/src/html/head.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,6 @@
111111
<meta http-equiv="Cache-Control" content="no-store" />
112112
<link rel="stylesheet" href="styles/vendor.scss" />
113113
<link rel="stylesheet" href="styles/index.scss" />
114+
115+
<style class="customFont" type="text/css"></style>
114116
</head>

frontend/src/html/pages/settings.html

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1147,7 +1147,44 @@
11471147
<i class="fas fa-fw fa-link"></i>
11481148
</button>
11491149
</div>
1150-
<!-- <div class="text">Change the font family for the site</div> -->
1150+
<div class="text">
1151+
Change the font family used by the website. Using a local font will
1152+
override your choice.
1153+
<br />
1154+
Note: Local fonts are not sent to the server and will not persist across
1155+
devices.
1156+
</div>
1157+
<div class="topRight">
1158+
<div class="usingLocalFont">
1159+
<button class="no-auto-handle">
1160+
<i class="fas fa-trash fa-fw"></i>
1161+
remove local font
1162+
</button>
1163+
</div>
1164+
<div class="uploadContainer">
1165+
<label
1166+
for="customFontUpload"
1167+
class="button"
1168+
aria-label="Select custom font"
1169+
data-balloon-pos="left"
1170+
>
1171+
<i class="fas fa-file-import fa-fw"></i>
1172+
use local font
1173+
</label>
1174+
1175+
<input
1176+
type="file"
1177+
id="customFontUpload"
1178+
accept="font/woff,font/woff2,font/ttf,font/otf"
1179+
style="display: none"
1180+
/>
1181+
</div>
1182+
<div class="separator">
1183+
<div class="line"></div>
1184+
or
1185+
<div class="line"></div>
1186+
</div>
1187+
</div>
11511188
<div class="buttons"></div>
11521189
</div>
11531190
<div class="section" data-config-name="keymapMode">

frontend/src/styles/settings.scss

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@
8787
row-gap: 0.5rem;
8888
align-items: start;
8989

90+
&.fullWidth {
91+
// grid-template-columns: 2fr 1fr;
92+
grid-template-areas:
93+
"title tabs"
94+
"text text"
95+
"buttons buttons";
96+
column-gap: 2rem;
97+
// row-gap: 0.5rem;
98+
99+
.buttons {
100+
margin-left: 0;
101+
display: grid;
102+
grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
103+
gap: 0.5rem;
104+
}
105+
}
106+
90107
.inputAndButton {
91108
display: grid;
92109
grid-template-columns: auto min-content;
@@ -151,15 +168,8 @@
151168
}
152169
}
153170

171+
&[data-config-name="fontFamily"],
154172
&[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-
}
163173
.separator {
164174
margin-bottom: 0.5rem;
165175
grid-column: span 2;
@@ -175,6 +185,44 @@
175185
border-radius: 0.25em;
176186
background: var(--sub-alt-color);
177187
}
188+
}
189+
190+
&[data-config-name="fontFamily"] {
191+
grid-template-areas:
192+
"title title"
193+
"text tabs"
194+
"buttons buttons";
195+
.topRight {
196+
grid-area: tabs;
197+
align-self: end;
198+
.separator {
199+
margin-bottom: 0;
200+
}
201+
.usingLocalFont {
202+
button {
203+
width: 100%;
204+
}
205+
}
206+
.uploadContainer {
207+
label {
208+
width: 100%;
209+
margin-bottom: 0.5em;
210+
}
211+
}
212+
}
213+
}
214+
215+
&[data-config-name="customBackgroundSize"] {
216+
//TOOD
217+
.uploadContainer {
218+
grid-column: span 2;
219+
margin-bottom: 0.5em;
220+
margin-top: 0.5em;
221+
}
222+
label.button {
223+
width: 100%;
224+
}
225+
178226
.usingLocalImage {
179227
display: grid;
180228
grid-template-columns: 1fr;
@@ -468,7 +516,6 @@
468516
}
469517

470518
&.themes {
471-
grid-template-columns: 2fr 1fr;
472519
grid-template-areas:
473520
"title tabs"
474521
"text text"
@@ -518,23 +565,6 @@
518565
}
519566
}
520567

521-
&.fullWidth {
522-
grid-template-columns: 2fr 1fr;
523-
grid-template-areas:
524-
"title tabs"
525-
"text text"
526-
"buttons buttons";
527-
column-gap: 2rem;
528-
// row-gap: 0.5rem;
529-
530-
.buttons {
531-
margin-left: 0;
532-
display: grid;
533-
grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
534-
gap: 0.5rem;
535-
}
536-
}
537-
538568
&.passwordAuthSettings {
539569
.buttons {
540570
grid-template-rows: repeat(auto-fill, 1fr);

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,32 @@ export async function applyCustomBackground(): Promise<void> {
428428
}
429429
}
430430

431+
export async function applyFontFamily(): Promise<void> {
432+
let font = Config.fontFamily.replace(/_/g, " ");
433+
434+
const localFont = await fileStorage.getFile("LocalFontFamilyFile");
435+
if (localFont === undefined) {
436+
//use config font
437+
$(".customFont").empty();
438+
} else {
439+
font = "LOCALCUSTOM";
440+
441+
$(".customFont").html(`
442+
@font-face{
443+
font-family: LOCALCUSTOM;
444+
src: url(${localFont});
445+
font-weight: 400;
446+
font-style: normal;
447+
font-display: block;
448+
}`);
449+
}
450+
451+
document.documentElement.style.setProperty(
452+
"--font",
453+
`"${font}", "Roboto Mono", "Vazirmatn", monospace`
454+
);
455+
}
456+
431457
window
432458
.matchMedia?.("(prefers-color-scheme: dark)")
433459
?.addEventListener?.("change", (event) => {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import FileStorage from "../../utils/file-storage";
2+
import * as Notifications from "../notifications";
3+
import { applyFontFamily } from "../../controllers/theme-controller";
4+
5+
const parentEl = document.querySelector(
6+
".pageSettings .section[data-config-name='fontFamily']"
7+
);
8+
const usingLocalFontEl = parentEl?.querySelector(".usingLocalFont");
9+
const separatorEl = parentEl?.querySelector(".separator");
10+
const uploadContainerEl = parentEl?.querySelector(".uploadContainer");
11+
const inputAndButtonEl = parentEl?.querySelector(".buttons");
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("LocalFontFamilyFile")) {
24+
usingLocalFontEl?.classList.remove("hidden");
25+
separatorEl?.classList.add("hidden");
26+
uploadContainerEl?.classList.add("hidden");
27+
inputAndButtonEl?.classList.add("hidden");
28+
} else {
29+
usingLocalFontEl?.classList.add("hidden");
30+
separatorEl?.classList.remove("hidden");
31+
uploadContainerEl?.classList.remove("hidden");
32+
inputAndButtonEl?.classList.remove("hidden");
33+
}
34+
}
35+
36+
usingLocalFontEl
37+
?.querySelector("button")
38+
?.addEventListener("click", async () => {
39+
await FileStorage.deleteFile("LocalFontFamilyFile");
40+
await updateUI();
41+
await applyFontFamily();
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(/font\/(woff|woff2|ttf|otf)/)) {
56+
Notifications.add(
57+
"Unsupported font format, must be woff, woff2, ttf or otf.",
58+
0
59+
);
60+
fileInput.value = "";
61+
return;
62+
}
63+
64+
const dataUrl = await readFileAsDataURL(file);
65+
await FileStorage.storeFile("LocalFontFamilyFile", dataUrl);
66+
67+
await updateUI();
68+
await applyFontFamily();
69+
70+
fileInput.value = "";
71+
});

frontend/src/ts/pages/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { z } from "zod";
4040
import { handleConfigInput } from "../elements/input-validation";
4141
import { Fonts } from "../constants/fonts";
4242
import * as CustomBackgroundPicker from "../elements/settings/custom-background-picker";
43+
import * as CustomFontPicker from "../elements/settings/custom-font-picker";
4344

4445
let settingsInitialized = false;
4546

@@ -854,6 +855,7 @@ export async function update(
854855
ThemePicker.setCustomInputs(true);
855856
await CustomBackgroundPicker.updateUI();
856857
await updateFilterSectionVisibility();
858+
await CustomFontPicker.updateUI();
857859

858860
const setInputValue = (
859861
key: ConfigKey,

frontend/src/ts/ui.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isDevEnvironment } from "./utils/misc";
1010
import { isCustomTextLong } from "./states/custom-text-name";
1111
import { canQuickRestart } from "./utils/quick-restart";
1212
import { FontName } from "@monkeytype/schemas/fonts";
13+
import { applyFontFamily } from "./controllers/theme-controller";
1314

1415
let isPreviewingFont = false;
1516
export function previewFontFamily(font: FontName): void {
@@ -118,7 +119,7 @@ $(window).on("resize", () => {
118119
debouncedEvent();
119120
});
120121

121-
ConfigEvent.subscribe((eventKey, value) => {
122+
ConfigEvent.subscribe(async (eventKey) => {
122123
if (eventKey === "quickRestart") updateKeytips();
123124
if (eventKey === "showKeyTips") {
124125
if (Config.showKeyTips) {
@@ -128,12 +129,6 @@ ConfigEvent.subscribe((eventKey, value) => {
128129
}
129130
}
130131
if (eventKey === "fontFamily") {
131-
document.documentElement.style.setProperty(
132-
"--font",
133-
`"${(value as string).replace(
134-
/_/g,
135-
" "
136-
)}", "Roboto Mono", "Vazirmatn", monospace`
137-
);
132+
await applyFontFamily();
138133
}
139134
});

frontend/src/ts/utils/file-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type FileDB = DBSchema & {
77
};
88
};
99

10-
type Filename = "LocalBackgroundFile";
10+
type Filename = "LocalBackgroundFile" | "LocalFontFamilyFile";
1111

1212
class FileStorage {
1313
private dbPromise: Promise<IDBPDatabase<FileDB>>;

0 commit comments

Comments
 (0)