Long Custom Text (#3543) rizwanmustafa miodec

* Create state for storing custom text name

* Save custom text name on click

* Add todo

* add some utility functions to custom-text.ts

* Set custom text name to empty upon modification

* now update the custom text progress in localStorage

* rework logic for updating progress in test-logic.ts

* more logic changes

* Keep progress in mind when starting next test after bailout

* reset test once they complete it and minor refactor

* Now set custom text progress to 0 when it is modified

* Add UI for continuing and change var name

* Reset progress if they start it again

* Move functions

* remove debug log

* replaced simple popup with custom popup

* fixed media query

* also setting opacity to 1

* saving long custom text into a separate object

* fixed incorrect saving function
fixed get custom text names function

* setting to empty object structure first

* long list style fix

* showing long texts
handling delete and progress reset

* renamed file
tracking if custom text is long

* unnecessary comment

* showing a warning that editing will disable progress tracking

* checking if text is long
updating progress

* added notifications

* setting custom text

* showing if progress tracking is working

* showing if progress tracking was disabled

* longer notification

* corrected button text

* joining with space

* checking if name is taken
added indicator

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Rizwan Mustafa 2022-10-04 20:42:52 +05:00 committed by GitHub
parent d2595bd1b1
commit 9bb778673a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 476 additions and 55 deletions

View file

@ -21,6 +21,25 @@
gap: 1rem;
width: 60vw;
.longCustomTextWarning {
background: rgba(0, 0, 0, 0.9);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: grid;
place-items: center center;
border-radius: var(--roundness);
.textAndButton {
width: 80%;
max-width: 20rem;
text-align: center;
display: grid;
gap: 1rem;
}
}
.buttonsTop {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@ -99,12 +118,44 @@
.savedText {
display: grid;
gap: 0.5rem;
grid-template-columns: 1fr 3rem;
grid-template-columns: 2fr 3rem;
.button .fas {
pointer-events: none;
}
}
}
.listLong {
display: grid;
gap: 1rem;
.savedText {
display: grid;
gap: 0.5rem;
grid-template-columns: 2fr 1fr 3rem;
.button .fas {
pointer-events: none;
}
}
}
}
}
#saveCustomTextPopupWrapper {
#saveCustomTextPopup {
color: var(--sub-color);
background: var(--bg-color);
border-radius: var(--roundness);
padding: 2rem;
display: grid;
gap: 1rem;
width: 400px;
max-height: 80vh;
overflow: auto;
.title {
font-size: 1.5rem;
color: var(--sub-color);
}
}
}

View file

@ -85,10 +85,6 @@
}
#customTextPopup {
width: 80vw !important;
.wordfilter.button {
width: 50% !important;
}
}
#leaderboardsWrapper {
#leaderboards {

View file

@ -6,6 +6,7 @@ import Config from "../config";
import * as TestWords from "../test/test-words";
import * as ConfigEvent from "../observables/config-event";
import { Auth } from "../firebase";
import * as CustomTextState from "../states/custom-text-name";
ConfigEvent.subscribe((eventKey) => {
if (
@ -57,6 +58,14 @@ export async function update(): Promise<void> {
}
}
const customTextName = CustomTextState.getCustomTextName();
const isLong = CustomTextState.isCustomTextLong();
if (Config.mode === "custom" && customTextName !== "" && isLong) {
$(".pageTest #testModesNotice").append(
`<div class="textButton noInteraction"><i class="fas fa-book"></i>tracking progress for ${customTextName}</div>`
);
}
if (TestState.activeChallenge) {
$(".pageTest #testModesNotice").append(
`<div class="textButton noInteraction"><i class="fas fa-award"></i>${TestState.activeChallenge.display}</div>`

View file

@ -1,4 +1,5 @@
import * as CustomText from "../test/custom-text";
import * as CustomTextState from "../states/custom-text-name";
import * as ManualRestart from "../test/manual-restart-tracker";
import * as TestLogic from "../test/test-logic";
import * as ChallengeController from "../controllers/challenge-controller";
@ -7,12 +8,22 @@ import * as Misc from "../utils/misc";
import * as WordFilterPopup from "./word-filter-popup";
import * as Notifications from "../elements/notifications";
import * as SavedTextsPopup from "./saved-texts-popup";
import * as SaveCustomTextPopup from "./save-custom-text-popup";
const wrapper = "#customTextPopupWrapper";
const popup = "#customTextPopup";
function updateLongTextWarning(): void {
if (CustomTextState.isCustomTextLong() === true) {
$(`${popup} .longCustomTextWarning`).removeClass("hidden");
} else {
$(`${popup} .longCustomTextWarning`).addClass("hidden");
}
}
export function show(): void {
if ($(wrapper).hasClass("hidden")) {
updateLongTextWarning();
if ($(`${popup} .checkbox input`).prop("checked")) {
$(`${popup} .inputs .randomInputFields`).removeClass("hidden");
} else {
@ -36,7 +47,9 @@ export function show(): void {
});
}
setTimeout(() => {
$(`${popup} textarea`).trigger("focus");
if (!CustomTextState.isCustomTextLong()) {
$(`${popup} textarea`).trigger("focus");
}
}, 150);
}
@ -99,9 +112,20 @@ $(`${popup} .inputs .checkbox input`).on("change", () => {
});
$(`${popup} textarea`).on("keypress", (e) => {
if (!$(`${popup} .longCustomTextWarning`).hasClass("hidden")) {
e.preventDefault();
return;
}
if (e.code === "Enter" && e.ctrlKey) {
$(`${popup} .button.apply`).trigger("click");
}
if (
CustomTextState.isCustomTextLong() &&
CustomTextState.getCustomTextName() !== ""
) {
CustomTextState.setCustomTextName("", undefined);
Notifications.add("Disabled long custom text progress tracking", 0, 5);
}
});
$(`${popup} .randomInputFields .wordcount input`).on("keypress", () => {
@ -216,3 +240,11 @@ $(document).on("keydown", (event) => {
event.preventDefault();
}
});
$(`#customTextPopup .buttonsTop .saveCustomText`).on("click", () => {
SaveCustomTextPopup.show();
});
$(`#customTextPopup .longCustomTextWarning .button`).on("click", () => {
$(`#customTextPopup .longCustomTextWarning`).addClass("hidden");
});

View file

@ -0,0 +1,99 @@
import * as CustomText from "../test/custom-text";
import * as Notifications from "../elements/notifications";
import * as CustomTextState from "../states/custom-text-name";
import { InputIndicator } from "../elements/input-indicator";
import { debounce } from "throttle-debounce";
const indicator = new InputIndicator($("#saveCustomTextPopup .textName"), {
available: {
icon: "fa-check",
level: 1,
},
unavailable: {
icon: "fa-times",
level: -1,
},
loading: {
icon: "fa-circle-notch",
spinIcon: true,
level: 0,
},
});
export async function show(): Promise<void> {
$("#saveCustomTextPopupWrapper").removeClass("hidden");
$("#customTextPopupWrapper").addClass("hidden");
$("#saveCustomTextPopupWrapper .textName").val("");
$("#saveCustomTextPopupWrapper .isLongText").prop("checked", false);
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
}
function hide(full = false): void {
$("#saveCustomTextPopupWrapper").addClass("hidden");
if (!full) {
$("#customTextPopupWrapper").removeClass("hidden").css("opacity", 1);
}
}
function save(): boolean {
const name = $("#saveCustomTextPopupWrapper .textName").val() as string;
const text = ($(`#customTextPopup textarea`).val() as string).normalize();
const checkbox = $("#saveCustomTextPopupWrapper .isLongText").prop("checked");
if (!name) {
Notifications.add("Custom text needs a name", 0);
return false;
}
CustomText.setCustomText(name, text, checkbox);
CustomTextState.setCustomTextName(name, checkbox);
Notifications.add("Custom text saved", 1);
return true;
}
$(document).on("click", `#saveCustomTextPopupWrapper .button.save`, () => {
if (save() === true) hide();
});
$("#saveCustomTextPopupWrapper").on("mousedown", (e) => {
if ($(e.target).attr("id") === "saveCustomTextPopupWrapper") {
hide();
}
});
function updateIndicatorAndButton(): void {
const val = $("#saveCustomTextPopup .textName").val() as string;
const checkbox = $("#saveCustomTextPopupWrapper .isLongText").prop("checked");
if (!val) {
indicator.hide();
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
} else {
const names = CustomText.getCustomTextNames(checkbox);
if (names.includes(val)) {
indicator.show("unavailable");
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
} else {
indicator.show("available");
$("#saveCustomTextPopupWrapper .button.save").removeClass("disabled");
}
}
}
const updateInputAndButtonDebounced = debounce(500, updateIndicatorAndButton);
$("#saveCustomTextPopup .textName").on("input", () => {
const val = $("#saveCustomTextPopup .textName").val() as string;
if (val.length > 0) {
indicator.show("loading");
updateInputAndButtonDebounced();
}
});
$("#saveCustomTextPopupWrapper .isLongText").on("change", () => {
const val = $("#saveCustomTextPopup .textName").val() as string;
if (val.length > 0) {
indicator.show("loading");
updateInputAndButtonDebounced();
}
});

View file

@ -1,4 +1,5 @@
import * as CustomText from "../test/custom-text";
import * as CustomTextState from "../states/custom-text-name";
export async function show(): Promise<void> {
const names = CustomText.getCustomTextNames();
@ -17,17 +18,48 @@ export async function show(): Promise<void> {
}
}
listEl.html(list);
const longNames = CustomText.getCustomTextNames(true);
const longListEl = $(`#savedTextsPopup .listLong`).empty();
let longList = "";
if (longNames.length === 0) {
longList += "<div>No saved long custom texts found</div>";
} else {
for (const name of longNames) {
longList += `<div class="savedText">
<div class="button name">${name}</div>
<div class="button ${
CustomText.getCustomTextLongProgress(name) <= 0 ? "disabled" : ""
} resetProgress">reset</div>
<div class="button delete">
<i class="fas fa-fw fa-trash"></i>
</div>
</div>`;
}
}
longListEl.html(longList);
$("#savedTextsPopupWrapper").removeClass("hidden");
$("#customTextPopupWrapper").addClass("hidden");
}
function hide(full = false): void {
$("#savedTextsPopupWrapper").addClass("hidden");
if (!full) $("#customTextPopupWrapper").removeClass("hidden");
if (!full) {
if (CustomTextState.isCustomTextLong() === true) {
$(`#customTextPopup .longCustomTextWarning`).removeClass("hidden");
} else {
$(`#customTextPopup .longCustomTextWarning`).addClass("hidden");
}
$("#customTextPopupWrapper").removeClass("hidden");
}
}
function applySaved(name: string): void {
const text = CustomText.getCustomText(name);
function applySaved(name: string, long: boolean): void {
let text = CustomText.getCustomText(name, long);
if (long) {
text = text.slice(CustomText.getCustomTextLongProgress(name));
}
$(`#customTextPopupWrapper textarea`).val(text.join(CustomText.delimiter));
}
@ -36,7 +68,8 @@ $(document).on(
`#savedTextsPopupWrapper .list .savedText .button.name`,
(e) => {
const name = $(e.target).text();
applySaved(name);
CustomTextState.setCustomTextName(name, false);
applySaved(name, false);
hide();
}
);
@ -49,6 +82,33 @@ $(document).on(
}
);
$(document).on(
"click",
`#savedTextsPopupWrapper .listLong .savedText .button.name`,
(e) => {
const name = $(e.target).text();
CustomTextState.setCustomTextName(name, true);
applySaved(name, true);
hide();
}
);
$(document).on(
"click",
`#savedTextsPopupWrapper .listLong .savedText .button.resetProgress`,
() => {
hide(true);
}
);
$(document).on(
"click",
`#savedTextsPopupWrapper .listLong .savedText .button.delete`,
() => {
hide(true);
}
);
$("#savedTextsPopupWrapper").on("mousedown", (e) => {
if ($(e.target).attr("id") === "savedTextsPopupWrapper") {
hide();

View file

@ -8,7 +8,6 @@ import * as Settings from "../pages/settings";
import * as ApeKeysPopup from "../popups/ape-keys-popup";
import * as ThemePicker from "../settings/theme-picker";
import * as CustomText from "../test/custom-text";
import * as CustomTextPopup from "../popups/custom-text-popup";
import * as SavedTextsPopup from "./saved-texts-popup";
import * as AccountButton from "../elements/account-button";
import { FirebaseError } from "firebase/app";
@ -1076,32 +1075,6 @@ list["editApeKey"] = new SimplePopup(
}
);
list["saveCustomText"] = new SimplePopup(
"saveCustomText",
"text",
"Save custom text",
[
{
placeholder: "Name",
initVal: "",
},
],
"",
"Save",
(_thisPopup, input) => {
const text = ($(`#customTextPopup textarea`).val() as string).normalize();
CustomText.setCustomText(input, text);
Notifications.add("Custom text saved", 1);
CustomTextPopup.show();
},
() => {
//
},
() => {
//
}
);
list["deleteCustomText"] = new SimplePopup(
"deleteCustomText",
"text",
@ -1122,6 +1095,49 @@ list["deleteCustomText"] = new SimplePopup(
}
);
list["deleteCustomTextLong"] = new SimplePopup(
"deleteCustomTextLong",
"text",
"Delete custom text",
[],
"Are you sure?",
"Delete",
(_thisPopup) => {
CustomText.deleteCustomText(_thisPopup.parameters[0], true);
Notifications.add("Custom text deleted", 1);
SavedTextsPopup.show();
},
(_thisPopup) => {
_thisPopup.text = `Are you sure you want to delete custom text ${_thisPopup.parameters[0]}?`;
},
() => {
//
}
);
list["resetProgressCustomTextLong"] = new SimplePopup(
"resetProgressCustomTextLong",
"text",
"Reset progress for custom text",
[],
"Are you sure?",
"Reset",
(_thisPopup) => {
CustomText.setCustomTextLongProgress(_thisPopup.parameters[0], 0);
Notifications.add("Custom text progress reset", 1);
SavedTextsPopup.show();
$(`#customTextPopupWrapper textarea`).val(
CustomText.getCustomText(_thisPopup.parameters[0], true).join(" ")
);
},
(_thisPopup) => {
_thisPopup.text = `Are you sure you want to reset your progress for custom text ${_thisPopup.parameters[0]}?`;
},
() => {
//
}
);
list["updateCustomTheme"] = new SimplePopup(
"updateCustomTheme",
"text",
@ -1262,10 +1278,6 @@ $("#apeKeysPopup .generateApeKey").on("click", () => {
list["generateApeKey"].show();
});
$(`#customTextPopup .buttonsTop .saveCustomText`).on("click", () => {
list["saveCustomText"].show();
});
$(document).on(
"click",
".pageSettings .section.themes .customTheme .delButton",
@ -1295,6 +1307,24 @@ $(document).on(
}
);
$(document).on(
"click",
`#savedTextsPopupWrapper .listLong .savedText .button.delete`,
(e) => {
const name = $(e.target).siblings(".button.name").text();
list["deleteCustomTextLong"].show([name]);
}
);
$(document).on(
"click",
`#savedTextsPopupWrapper .listLong .savedText .button.resetProgress`,
(e) => {
const name = $(e.target).siblings(".button.name").text();
list["resetProgressCustomTextLong"].show([name]);
}
);
$(document).on("click", "#apeKeysPopup table tbody tr .button.delete", (e) => {
const keyId = $(e.target).closest("tr").attr("keyId") as string;
list["deleteApeKey"].show([keyId]);

View file

@ -0,0 +1,18 @@
let customTestName = ""; // It should be empty when the text is not saved or a saved text has been modified
let isLong: boolean | undefined = false;
export function getCustomTextName(): string {
return customTestName;
}
export function isCustomTextLong(): boolean | undefined {
return isLong;
}
export function setCustomTextName(
newName: string,
long: boolean | undefined
): void {
customTestName = newName;
isLong = long;
}

View file

@ -19,6 +19,14 @@ export function setText(txt: string[]): void {
text = txt;
}
export function getText(): string {
return text.join(" ");
}
export function getTextArray(): string[] {
return text;
}
export function setIsWordRandom(val: boolean): void {
isWordRandom = val;
}
@ -41,33 +49,93 @@ export function setDelimiter(val: string): void {
type CustomTextObject = Record<string, string>;
export function getCustomText(name: string): string[] {
const customText = getCustomTextObject();
type CustomTextLongObject = Record<string, { text: string; progress: number }>;
return customText[name].split(/ +/);
export function getCustomText(name: string, long = false): string[] {
if (long) {
return getCustomTextLongObject()[name]["text"].split(/ +/);
} else {
return getCustomTextObject()[name].split(/ +/);
}
}
export function setCustomText(name: string, text: string | string[]): void {
const customText = getCustomTextObject();
export function setCustomText(
name: string,
text: string | string[],
long = false
): void {
if (long) {
const customText = getCustomTextLongObject();
if (typeof text === "string") customText[name] = text;
else customText[name] = text.join(" ");
customText[name] = {
text: "",
progress: 0,
};
window.localStorage.setItem("customText", JSON.stringify(customText));
if (typeof text === "string") {
customText[name]["text"] = text;
} else {
customText[name]["text"] = text.join(" ");
}
window.localStorage.setItem("customTextLong", JSON.stringify(customText));
} else {
const customText = getCustomTextObject();
if (typeof text === "string") {
customText[name] = text;
} else {
customText[name] = text.join(" ");
}
window.localStorage.setItem("customText", JSON.stringify(customText));
}
}
export function deleteCustomText(name: string): void {
const customText = getCustomTextObject();
export function deleteCustomText(name: string, long = false): void {
const customText = long ? getCustomTextLongObject() : getCustomTextObject();
if (customText[name]) delete customText[name];
window.localStorage.setItem("customText", JSON.stringify(customText));
if (long) {
window.localStorage.setItem("customTextLong", JSON.stringify(customText));
} else {
window.localStorage.setItem("customText", JSON.stringify(customText));
}
}
export function getCustomTextLongProgress(name: string): number {
const customText = getCustomTextLongObject();
return customText[name]["progress"] ?? 0;
}
export function setCustomTextLongProgress(
name: string,
progress: number
): void {
const customTextProgress = getCustomTextLongObject();
customTextProgress[name]["progress"] = progress;
window.localStorage.setItem(
"customTextLong",
JSON.stringify(customTextProgress)
);
}
function getCustomTextObject(): CustomTextObject {
return JSON.parse(window.localStorage.getItem("customText") ?? "{}");
}
export function getCustomTextNames(): string[] {
return Object.keys(getCustomTextObject());
function getCustomTextLongObject(): CustomTextLongObject {
return JSON.parse(window.localStorage.getItem("customTextLong") ?? "{}");
}
export function getCustomTextNames(long = false): string[] {
if (long) {
return Object.keys(getCustomTextLongObject());
} else {
return Object.keys(getCustomTextObject());
}
}

View file

@ -6,6 +6,7 @@ import * as Misc from "../utils/misc";
import QuotesController from "../controllers/quotes-controller";
import * as Notifications from "../elements/notifications";
import * as CustomText from "./custom-text";
import * as CustomTextState from "../states/custom-text-name";
import * as TestStats from "./test-stats";
import * as PractiseWords from "./practise-words";
import * as ShiftTracker from "./shift-tracker";
@ -1604,6 +1605,29 @@ export async function finish(difficultyFailed = false): Promise<void> {
// test is valid
const customTextName = CustomTextState.getCustomTextName();
const isLong = CustomTextState.isCustomTextLong();
if (Config.mode === "custom" && customTextName !== "" && isLong) {
// Let's update the custom text progress
if (TestInput.bailout) {
// They bailed out
const newProgress =
CustomText.getCustomTextLongProgress(customTextName) +
TestInput.input.getHistory().length;
CustomText.setCustomTextLongProgress(customTextName, newProgress);
Notifications.add("Long custom text progress saved", 1, 5);
let newText = CustomText.getCustomText(customTextName, true);
newText = newText.slice(newProgress);
CustomText.setText(newText);
} else {
// They finished the test
CustomText.setCustomTextLongProgress(customTextName, 0);
CustomText.setText(CustomText.getCustomText(customTextName, true));
Notifications.add("Long custom text completed", 1, 5);
}
}
if (!dontSave) {
TodayTracker.addSeconds(
completedEvent.testDuration +

View file

@ -438,7 +438,16 @@
<div class="title">saved texts</div>
<div class="buttons"></div>
</div>
<textarea class="textarea" placeholder="Custom text"></textarea>
<div style="position: relative">
<div class="longCustomTextWarning">
<div class="textAndButton">
A long custom text is currently loaded. Editing the text will disable
progress tracking.
<div class="button">Got it</div>
</div>
</div>
<textarea class="textarea" placeholder="Custom text"></textarea>
</div>
<div class="inputs">
<label class="checkbox">
<input type="checkbox" />
@ -496,6 +505,31 @@
<div id="savedTextsPopup">
<div class="title">Saved texts</div>
<div class="list"></div>
<div class="title">Saved long texts</div>
<div class="listLong"></div>
</div>
</div>
<div id="saveCustomTextPopupWrapper" class="popupWrapper hidden">
<div id="saveCustomTextPopup">
<div class="title">Save custom text</div>
<input class="textName" type="text" placeholder="name" />
<div>
<label class="checkbox">
<input type="checkbox" class="isLongText" />
<div class="customTextCheckbox">
<div class="check">
<i class="fas fa-fw fa-check"></i>
</div>
</div>
Long text (book mode)
<span>
Disables editing this text but allows you to save progress when
bailing out. You can then load this text again to continue where you
left off.
</span>
</label>
</div>
<div class="button save">save</div>
</div>
</div>
<div id="wordFilterPopupWrapper" class="popupWrapper hidden">