feat(settings): allow user to pick a local font (@fehmer, @miodec) (#6794)

This commit is contained in:
Christian Fehmer 2025-08-04 15:50:02 +02:00 committed by GitHub
parent 9c41fd5d04
commit f759b0ce89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 199 additions and 36 deletions

View file

@ -111,4 +111,6 @@
<meta http-equiv="Cache-Control" content="no-store" />
<link rel="stylesheet" href="styles/vendor.scss" />
<link rel="stylesheet" href="styles/index.scss" />
<style class="customFont" type="text/css"></style>
</head>

View file

@ -1147,7 +1147,44 @@
<i class="fas fa-fw fa-link"></i>
</button>
</div>
<!-- <div class="text">Change the font family for the site</div> -->
<div class="text">
Change the font family used by the website. Using a local font will
override your choice.
<br />
Note: Local fonts are not sent to the server and will not persist across
devices.
</div>
<div class="topRight">
<div class="usingLocalFont">
<button class="no-auto-handle">
<i class="fas fa-trash fa-fw"></i>
remove local font
</button>
</div>
<div class="uploadContainer">
<label
for="customFontUpload"
class="button"
aria-label="Select custom font"
data-balloon-pos="left"
>
<i class="fas fa-file-import fa-fw"></i>
use local font
</label>
<input
type="file"
id="customFontUpload"
accept="font/woff,font/woff2,font/ttf,font/otf"
style="display: none"
/>
</div>
<div class="separator">
<div class="line"></div>
or
<div class="line"></div>
</div>
</div>
<div class="buttons"></div>
</div>
<div class="section" data-config-name="keymapMode">

View file

@ -87,6 +87,23 @@
row-gap: 0.5rem;
align-items: start;
&.fullWidth {
// grid-template-columns: 2fr 1fr;
grid-template-areas:
"title tabs"
"text text"
"buttons buttons";
column-gap: 2rem;
// row-gap: 0.5rem;
.buttons {
margin-left: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
gap: 0.5rem;
}
}
.inputAndButton {
display: grid;
grid-template-columns: auto min-content;
@ -151,15 +168,8 @@
}
}
&[data-config-name="fontFamily"],
&[data-config-name="customBackgroundSize"] {
.uploadContainer {
grid-column: span 2;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
label.button {
width: 100%;
}
.separator {
margin-bottom: 0.5rem;
grid-column: span 2;
@ -175,6 +185,44 @@
border-radius: 0.25em;
background: var(--sub-alt-color);
}
}
&[data-config-name="fontFamily"] {
grid-template-areas:
"title title"
"text tabs"
"buttons buttons";
.topRight {
grid-area: tabs;
align-self: end;
.separator {
margin-bottom: 0;
}
.usingLocalFont {
button {
width: 100%;
}
}
.uploadContainer {
label {
width: 100%;
margin-bottom: 0.5em;
}
}
}
}
&[data-config-name="customBackgroundSize"] {
//TOOD
.uploadContainer {
grid-column: span 2;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
label.button {
width: 100%;
}
.usingLocalImage {
display: grid;
grid-template-columns: 1fr;
@ -468,7 +516,6 @@
}
&.themes {
grid-template-columns: 2fr 1fr;
grid-template-areas:
"title tabs"
"text text"
@ -518,23 +565,6 @@
}
}
&.fullWidth {
grid-template-columns: 2fr 1fr;
grid-template-areas:
"title tabs"
"text text"
"buttons buttons";
column-gap: 2rem;
// row-gap: 0.5rem;
.buttons {
margin-left: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
gap: 0.5rem;
}
}
&.passwordAuthSettings {
.buttons {
grid-template-rows: repeat(auto-fill, 1fr);

View file

@ -428,6 +428,32 @@ export async function applyCustomBackground(): Promise<void> {
}
}
export async function applyFontFamily(): Promise<void> {
let font = Config.fontFamily.replace(/_/g, " ");
const localFont = await fileStorage.getFile("LocalFontFamilyFile");
if (localFont === undefined) {
//use config font
$(".customFont").empty();
} else {
font = "LOCALCUSTOM";
$(".customFont").html(`
@font-face{
font-family: LOCALCUSTOM;
src: url(${localFont});
font-weight: 400;
font-style: normal;
font-display: block;
}`);
}
document.documentElement.style.setProperty(
"--font",
`"${font}", "Roboto Mono", "Vazirmatn", monospace`
);
}
window
.matchMedia?.("(prefers-color-scheme: dark)")
?.addEventListener?.("change", (event) => {

View file

@ -0,0 +1,71 @@
import FileStorage from "../../utils/file-storage";
import * as Notifications from "../notifications";
import { applyFontFamily } from "../../controllers/theme-controller";
const parentEl = document.querySelector(
".pageSettings .section[data-config-name='fontFamily']"
);
const usingLocalFontEl = parentEl?.querySelector(".usingLocalFont");
const separatorEl = parentEl?.querySelector(".separator");
const uploadContainerEl = parentEl?.querySelector(".uploadContainer");
const inputAndButtonEl = parentEl?.querySelector(".buttons");
async function readFileAsDataURL(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
export async function updateUI(): Promise<void> {
if (await FileStorage.hasFile("LocalFontFamilyFile")) {
usingLocalFontEl?.classList.remove("hidden");
separatorEl?.classList.add("hidden");
uploadContainerEl?.classList.add("hidden");
inputAndButtonEl?.classList.add("hidden");
} else {
usingLocalFontEl?.classList.add("hidden");
separatorEl?.classList.remove("hidden");
uploadContainerEl?.classList.remove("hidden");
inputAndButtonEl?.classList.remove("hidden");
}
}
usingLocalFontEl
?.querySelector("button")
?.addEventListener("click", async () => {
await FileStorage.deleteFile("LocalFontFamilyFile");
await updateUI();
await applyFontFamily();
});
uploadContainerEl
?.querySelector("input[type='file']")
?.addEventListener("change", async (e) => {
const fileInput = e.target as HTMLInputElement;
const file = fileInput.files?.[0];
if (!file) {
return;
}
// check type
if (!file.type.match(/font\/(woff|woff2|ttf|otf)/)) {
Notifications.add(
"Unsupported font format, must be woff, woff2, ttf or otf.",
0
);
fileInput.value = "";
return;
}
const dataUrl = await readFileAsDataURL(file);
await FileStorage.storeFile("LocalFontFamilyFile", dataUrl);
await updateUI();
await applyFontFamily();
fileInput.value = "";
});

View file

@ -40,6 +40,7 @@ import { z } from "zod";
import { handleConfigInput } from "../elements/input-validation";
import { Fonts } from "../constants/fonts";
import * as CustomBackgroundPicker from "../elements/settings/custom-background-picker";
import * as CustomFontPicker from "../elements/settings/custom-font-picker";
let settingsInitialized = false;
@ -854,6 +855,7 @@ export async function update(
ThemePicker.setCustomInputs(true);
await CustomBackgroundPicker.updateUI();
await updateFilterSectionVisibility();
await CustomFontPicker.updateUI();
const setInputValue = (
key: ConfigKey,

View file

@ -10,6 +10,7 @@ import { isDevEnvironment } from "./utils/misc";
import { isCustomTextLong } from "./states/custom-text-name";
import { canQuickRestart } from "./utils/quick-restart";
import { FontName } from "@monkeytype/schemas/fonts";
import { applyFontFamily } from "./controllers/theme-controller";
let isPreviewingFont = false;
export function previewFontFamily(font: FontName): void {
@ -118,7 +119,7 @@ $(window).on("resize", () => {
debouncedEvent();
});
ConfigEvent.subscribe((eventKey, value) => {
ConfigEvent.subscribe(async (eventKey) => {
if (eventKey === "quickRestart") updateKeytips();
if (eventKey === "showKeyTips") {
if (Config.showKeyTips) {
@ -128,12 +129,6 @@ ConfigEvent.subscribe((eventKey, value) => {
}
}
if (eventKey === "fontFamily") {
document.documentElement.style.setProperty(
"--font",
`"${(value as string).replace(
/_/g,
" "
)}", "Roboto Mono", "Vazirmatn", monospace`
);
await applyFontFamily();
}
});

View file

@ -7,7 +7,7 @@ type FileDB = DBSchema & {
};
};
type Filename = "LocalBackgroundFile";
type Filename = "LocalBackgroundFile" | "LocalFontFamilyFile";
class FileStorage {
private dbPromise: Promise<IDBPDatabase<FileDB>>;