mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-24 14:56:23 +08:00
feat(settings): allow user to pick a local font (@fehmer, @miodec) (#6794)
This commit is contained in:
parent
9c41fd5d04
commit
f759b0ce89
8 changed files with 199 additions and 36 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
71
frontend/src/ts/elements/settings/custom-font-picker.ts
Normal file
71
frontend/src/ts/elements/settings/custom-font-picker.ts
Normal 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 = "";
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ type FileDB = DBSchema & {
|
|||
};
|
||||
};
|
||||
|
||||
type Filename = "LocalBackgroundFile";
|
||||
type Filename = "LocalBackgroundFile" | "LocalFontFamilyFile";
|
||||
|
||||
class FileStorage {
|
||||
private dbPromise: Promise<IDBPDatabase<FileDB>>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue