mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-10 13:26:07 +08:00
Custom themes storage (#2660)
* Fixed typo
* Created method for adding theme in the UserDAO:
* Created function for checking if custom theme object is valid
* Exported the isThemeValid function
* Added controller for adding customTheme
* Created route for adding custom theme
* Created rateLimit for adding customTheme
* Fixed typo
* Fixed incorrect color length
* Added method for removing, getting and editing custom themes on the backend
* Moved validations from controllers to routes and some aesthetic changes in the user dao
* Started working on frontend and some minor changes in the backend
- Commandline support for custom themes
- Allow user to shift to their custom theme using Shift-click
- Updated the backend to be compatible with some changes
- Create a new custom theme for users with old system to prevent their custom theme loss
* Fixed custom theme type in ape and now new custom theme is created if user clicks the custom tab and doesn't already have one
* Fixed ape type issue
* Format html file
* Fixed wrong tab being active
* Created new custom theme edit section
* Fixed bug where user theme would have impact on icons with custom theme
* Update customThemes API
* Updated the custom theme sharing option to work with multiple custom themes
* Started working on the UI for custom theme buttons
* Added DOM event for clicking custom theme buttons
* Updated the updateActiveButton to work with multiple custom themes
* Removed favorite button for themes and fixed bug where double theme buttons were being added
* Fixed bug where preset theme buttons were not appearing if user has applied custom theme on website load and refreshed
* Moved DOM event for sharing custom theme to more appropriate place
* Integrated the save custom theme button with the changes
* Fixed bug with custom theme tab buttons and theme buttons
* Fixed commented div
* Replaced 'sds' with a meaningful message for custom theme buttons
* Integrated the delete button for deleting custom themes and fixed bug where id of newly added custom theme was not set properly
* Integrated the add button and name field for custom themes editing
* Added addCustomThemeWrapper element
I added it before but seems like vscode and other editors can't handle large files
* Removed some debug statements
* Removed some more debug statements
* Used parial types for custom theme. Thanks Bruce
* Removed unnecssary try catch blocks. Thanks Bruce
* Rephrased custom theme API messages
* Set new theme fields explicitly to prevent validtion failures and rephrased API message
* Replaced let with const
* Replaced let with const for _id
* Replaced let with const and used nullish coalescing
* Improved code quality in User DAO
* Strict equality in user DAO
* Moved validation scheme to a variables at the top of file
* Fixed bugs with strict equality checks
* Renamed themeId to themeID for consistency
* Made customThemes a required type in db to remove unnecessary undefined checks
* Uncommented GET API endpoint
* Prevent colorId being updated on custom theme name chnage
* Removed debug log
* Added loader on api calls
* Commenced shift from customThemeIndex to customThemeId
* Added required to themeColors schema
* Temp fix for validation fail for customThemeId
* Changed default value of customThemeId back to ''
* Temp fix for validation fail for customThemeId
* Fixed minor bug
* Fixed bug where account-controller would pass undefined to ThemeController.set
* Created methods in db.ts for adding, deleting and editing custom themes. Created new interface for raw custom themes and renamed ape methods
* Removed repeating code in account-controller
* Removed repeating code in theme-picker
* Removed setThemes in config
* Fixed minor bug
* Removed repeating code in user DAO
* Made custom themes available to registered users only
* Fixed minor bug
* Removed debug log and updated custom theme commands before showing list
* Added popup for confirming custom theme deletion
* Added custom option for random theme
* Minor improvement
* Workaround for local config firing before firebase initalization
* Removed debug log and created workaround for migration
* Added legacy customTheme config field
* Replaced workaround
* Changed put to patch
* Changed put to patch
* Added customTheme field back
* Integrated customTheme into to feature
* Added notifications for users when they access custom theme cmd option without being logged in
* Removed debug logs and comments
* Replaced literals with constant. Thanks Bruception
* Fixed wrong querySelector parameters and reset custom theme colors after deleting a custom theme
* added notification on save
* duplicating object instead of referencing
* Added return type on function
* Fixed wrong notification code
* spreading default config instead of referencing
* added index, psas, configs, presets
* camel_case
* added ape keys, leaderboards, results, quotes
* Modified setCustomTheme
* Modified setCustomThemeId
* Added tip for random themes settings
* Modified setCustomThemeId
* Now load custom theme before account loading
* Added custom theme compatibility for non-logged in users to theme-controller
* Now update tabs and buttons on custom theme config value change and modified boolean checks to use customTheme instead of customThemeId
* Fixed bug
* Refactoring in theme-controller.ts
* Enable custom theme support in commandline for logged out users
* More refactoring in theme-controller.ts
* Added custom theme compatibility for logged out users
* Removed double events in settings.ts and now turn on custom theme upon applying
* Fixed bug and recursive call
* Fixed bug
* Fixed random theme custom option
* Fix jquery wrong syntax
* Readded notification upon custom theme edit
* One notification upon error only
* Change notification type
* New custom themes now have default colors
* Notification on custom theme edit for non-logged in users
* Refresh buttons upon settings load
* missing gitignore
* updated message
* updated message
* setting config to unchanged when logging in to avoid issues with applying db config
* reverted some over complicated code, excessive auth checks
* removed customthemeid from config
* not setting custom theme id
* removed all customthemeid references
* removed commented code
* removed name field
* added edit button
* unused file
* removed popup
* removed add button, removed text
* removed duplicate code
* added simple popup checkbox support
* whitespace
* added custom theme popups
* removed warning when no custom themes were found
* removed add button click handler
* added function to save custom theme
* saving current theme not default
* removed custom theme id from default config
* not creating new theme by default, just applying
* reacting to customThemeColors save
* unnecessary function call
* removed unused code
* small refactor
* spacing
* unnecessary code
* turned off warnings for non null asertion
* showing theme name when randomising customs
* Revert "turned off warnings for non null asertion"
This reverts commit 433e1dc767.
* optional with default instead
* fixed custom theme colors always loaded on page load
* fixed custom theme buttons not showing up
* fixed various loading issues
* fixed custom theme edit styles
* showing custom in footer, removed unused code
* savaing custom theme colors
fixed typos
* changing theme
* updated custom theme buttons styling
* scaling custom theme buttons on hover
* not updating settings on theme event
* fixed quote id
* only showing custom themes when logged in
* updating save button text depending on auth state
* fixed double notification when trying to save too many custom themes
* fixed custom theme saving when signed out
* removed user check from db
* fixed exception when signed out user tried to open the custom themes command line
* ignoring file when compiling
* typo
* avoiding href errors
* setting href to an existing file
this fixes firefox custom themes not working
* better hex color regex
* spacing
* renamed function
* typo
* destructuring request
* removed unused function
* removed unused code
* removed unused code
* type fix
* removed non capturing group
* saving colors to config before saving custom theme
* encoding in base64
* added handler that can load themes in the old and new format from the url
Co-authored-by: Rizwan Mustafa <rizwanmustafa0000@gmail.com>
Co-authored-by: Rizwan Mustafa <69350358+rizwanmustafa@users.noreply.github.com>
This commit is contained in:
parent
7fa2827a60
commit
34e730c6fc
31 changed files with 8582 additions and 7727 deletions
|
|
@ -206,6 +206,45 @@ class UserController {
|
|||
return new MonkeyResponse("Leaderboard memory updated");
|
||||
}
|
||||
|
||||
static async getCustomThemes(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const customThemes = await UsersDAO.getThemes(uid);
|
||||
return new MonkeyResponse("Custom themes retrieved", customThemes);
|
||||
}
|
||||
|
||||
static async addCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { name, colors } = req.body;
|
||||
|
||||
const addedTheme = await UsersDAO.addTheme(uid, { name, colors });
|
||||
return new MonkeyResponse("Custom theme added", {
|
||||
theme: addedTheme,
|
||||
});
|
||||
}
|
||||
|
||||
static async removeCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { themeId } = req.body;
|
||||
await UsersDAO.removeTheme(uid, themeId);
|
||||
return new MonkeyResponse("Custom theme removed");
|
||||
}
|
||||
|
||||
static async editCustomTheme(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { themeId, theme } = req.body;
|
||||
|
||||
await UsersDAO.editTheme(uid, themeId, theme);
|
||||
return new MonkeyResponse("Custom theme updated");
|
||||
}
|
||||
|
||||
static async getPersonalBests(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,45 @@ const tagNameValidation = joi
|
|||
"string.max": "Tag name exceeds maximum of 16 characters",
|
||||
});
|
||||
|
||||
const customThemeNameValidation = joi
|
||||
.string()
|
||||
.max(16)
|
||||
.regex(/^[0-9a-zA-Z_.-]+$/)
|
||||
.required()
|
||||
.messages({
|
||||
"string.max": "The name must not exceed 16 characters",
|
||||
"string.pattern.base":
|
||||
"Name cannot contain special characters. Can include _ . and -",
|
||||
});
|
||||
|
||||
const customThemeColorsValidation = joi
|
||||
.array()
|
||||
.items(
|
||||
joi
|
||||
.string()
|
||||
.length(7)
|
||||
.regex(/^#[0-9a-fA-F]{6}$/)
|
||||
.messages({
|
||||
"string.pattern.base": "The colors must be valid hexadecimal",
|
||||
"string.length": "The colors must be 7 characters long",
|
||||
})
|
||||
)
|
||||
.length(9)
|
||||
.required()
|
||||
.messages({
|
||||
"array.length": "The colors array must have 9 colors",
|
||||
});
|
||||
|
||||
const customThemeIdValidation = joi
|
||||
.string()
|
||||
.length(24)
|
||||
.regex(/^[0-9a-fA-F]+$/)
|
||||
.required()
|
||||
.messages({
|
||||
"string.length": "The themeId must be 24 characters long",
|
||||
"string.pattern.base": "The themeId must be valid hexadecimal string",
|
||||
});
|
||||
|
||||
const usernameValidation = joi
|
||||
.string()
|
||||
.required()
|
||||
|
|
@ -178,6 +217,54 @@ router.delete(
|
|||
asyncHandler(UserController.clearTagPb)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/customThemes",
|
||||
RateLimit.userCustomThemeGet,
|
||||
authenticateRequest(),
|
||||
asyncHandler(UserController.getCustomThemes)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/customThemes",
|
||||
RateLimit.userCustomThemeAdd,
|
||||
authenticateRequest(),
|
||||
validateRequest({
|
||||
body: {
|
||||
name: customThemeNameValidation,
|
||||
colors: customThemeColorsValidation,
|
||||
},
|
||||
}),
|
||||
asyncHandler(UserController.addCustomTheme)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/customThemes",
|
||||
RateLimit.userCustomThemeRemove,
|
||||
authenticateRequest(),
|
||||
validateRequest({
|
||||
body: {
|
||||
themeId: customThemeIdValidation,
|
||||
},
|
||||
}),
|
||||
asyncHandler(UserController.removeCustomTheme)
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/customThemes",
|
||||
RateLimit.userCustomThemeEdit,
|
||||
authenticateRequest(),
|
||||
validateRequest({
|
||||
body: {
|
||||
themeId: customThemeIdValidation,
|
||||
theme: {
|
||||
name: customThemeNameValidation,
|
||||
colors: customThemeColorsValidation,
|
||||
},
|
||||
},
|
||||
}),
|
||||
asyncHandler(UserController.editCustomTheme)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/discord/link",
|
||||
RateLimit.userDiscordLink,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const CONFIG_SCHEMA = joi.object({
|
|||
themeDark: joi.string(),
|
||||
autoSwitchTheme: joi.boolean(),
|
||||
customTheme: joi.boolean(),
|
||||
customThemeId: joi.string().min(0).max(24),
|
||||
customThemeColors: joi
|
||||
.array()
|
||||
.items(joi.string().pattern(/^#([\da-f]{3}){1,2}$/i))
|
||||
|
|
@ -51,7 +52,9 @@ const CONFIG_SCHEMA = joi.object({
|
|||
indicateTypos: joi.string().valid("off", "below", "replace"),
|
||||
timerStyle: joi.string().valid("bar", "text", "mini"),
|
||||
colorfulMode: joi.boolean(),
|
||||
randomTheme: joi.string().valid("off", "on", "fav", "light", "dark"),
|
||||
randomTheme: joi
|
||||
.string()
|
||||
.valid("off", "on", "fav", "light", "dark", "custom"),
|
||||
timerColor: joi.string().valid("black", "sub", "text", "main"),
|
||||
timerOpacity: joi.number().valid(0.25, 0.5, 0.75, 1),
|
||||
stopOnError: joi.string().valid("off", "word", "letter"),
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class UsersDAO {
|
|||
}
|
||||
|
||||
static async addTag(uid, name) {
|
||||
let _id = new ObjectId();
|
||||
const _id = new ObjectId();
|
||||
await db
|
||||
.collection("users")
|
||||
.updateOne({ uid }, { $push: { tags: { _id, name } } });
|
||||
|
|
@ -298,6 +298,85 @@ class UsersDAO {
|
|||
}
|
||||
}
|
||||
|
||||
static themeDoesNotExist(customThemes, id) {
|
||||
return (
|
||||
(customThemes ?? []).filter((t) => t._id.toString() === id).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
static async addTheme(uid, theme) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (!user) throw new MonkeyError(404, "User not found", "Add custom theme");
|
||||
|
||||
if ((user.customThemes ?? []).length >= 10)
|
||||
throw new MonkeyError(409, "Too many custom themes");
|
||||
|
||||
const _id = new ObjectId();
|
||||
await db.collection("users").updateOne(
|
||||
{ uid },
|
||||
{
|
||||
$push: {
|
||||
customThemes: {
|
||||
_id,
|
||||
name: theme.name,
|
||||
colors: theme.colors,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
_id,
|
||||
name: theme.name,
|
||||
};
|
||||
}
|
||||
|
||||
static async removeTheme(uid, _id) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (!user)
|
||||
throw new MonkeyError(404, "User not found", "Remove custom theme");
|
||||
|
||||
if (this.themeDoesNotExist(user.customThemes, _id))
|
||||
throw new MonkeyError(404, "Custom theme not found");
|
||||
|
||||
return await db.collection("users").updateOne(
|
||||
{
|
||||
uid: uid,
|
||||
"customThemes._id": new ObjectId(_id),
|
||||
},
|
||||
{ $pull: { customThemes: { _id: new ObjectId(_id) } } }
|
||||
);
|
||||
}
|
||||
|
||||
static async editTheme(uid, _id, theme) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (!user)
|
||||
throw new MonkeyError(404, "User not found", "Edit custom theme");
|
||||
|
||||
if (this.themeDoesNotExist(user.customThemes, _id))
|
||||
throw new MonkeyError(404, "Custom Theme not found");
|
||||
|
||||
return await db.collection("users").updateOne(
|
||||
{
|
||||
uid: uid,
|
||||
"customThemes._id": new ObjectId(_id),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"customThemes.$.name": theme.name,
|
||||
"customThemes.$.colors": theme.colors,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async getThemes(uid) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (!user)
|
||||
throw new MonkeyError(404, "User not found", "Get custom themes");
|
||||
return user.customThemes ?? [];
|
||||
}
|
||||
|
||||
static async getPersonalBests(uid, mode, mode2) {
|
||||
const user = await db.collection("users").findOne({ uid });
|
||||
if (mode2) {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,34 @@ export const userTagsAdd = rateLimit({
|
|||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeGet = rateLimit({
|
||||
windowMs: ONE_HOUR,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getAddress,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeAdd = rateLimit({
|
||||
windowMs: ONE_HOUR,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getAddress,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeRemove = rateLimit({
|
||||
windowMs: ONE_HOUR,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getAddress,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeEdit = rateLimit({
|
||||
windowMs: ONE_HOUR,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getAddress,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userDiscordLink = rateLimit({
|
||||
windowMs: ONE_HOUR,
|
||||
max: 15 * REQUEST_MULTIPLIER,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,38 @@ export default function getUsersEndpoints(
|
|||
return await apeClient.delete(`${BASE_PATH}/tags/${tagId}/personalBest`);
|
||||
}
|
||||
|
||||
async function getCustomThemes(): Ape.EndpointData {
|
||||
return await apeClient.get(`${BASE_PATH}/customThemes`);
|
||||
}
|
||||
|
||||
async function editCustomTheme(
|
||||
themeId: string,
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
): Ape.EndpointData {
|
||||
const payload = {
|
||||
themeId: themeId,
|
||||
theme: {
|
||||
name: newTheme.name,
|
||||
colors: newTheme.colors,
|
||||
},
|
||||
};
|
||||
return await apeClient.patch(`${BASE_PATH}/customThemes`, { payload });
|
||||
}
|
||||
|
||||
async function deleteCustomTheme(themeId: string): Ape.EndpointData {
|
||||
const payload = {
|
||||
themeId: themeId,
|
||||
};
|
||||
return await apeClient.delete(`${BASE_PATH}/customThemes`, { payload });
|
||||
}
|
||||
|
||||
async function addCustomTheme(
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
): Ape.EndpointData {
|
||||
const payload = { name: newTheme.name, colors: newTheme.colors };
|
||||
return await apeClient.post(`${BASE_PATH}/customThemes`, { payload });
|
||||
}
|
||||
|
||||
async function linkDiscord(data: {
|
||||
tokenType: string;
|
||||
accessToken: string;
|
||||
|
|
@ -120,5 +152,9 @@ export default function getUsersEndpoints(
|
|||
deleteTagPersonalBest,
|
||||
linkDiscord,
|
||||
unlinkDiscord,
|
||||
getCustomThemes,
|
||||
addCustomTheme,
|
||||
editCustomTheme,
|
||||
deleteCustomTheme,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
9
frontend/src/scripts/ape/types/ape.d.ts
vendored
9
frontend/src/scripts/ape/types/ape.d.ts
vendored
|
|
@ -111,6 +111,15 @@ declare namespace Ape {
|
|||
) => EndpointData;
|
||||
updateEmail: (newEmail: string, previousEmail: string) => EndpointData;
|
||||
deletePersonalBests: Endpoint;
|
||||
getCustomThemes: () => EndpointData;
|
||||
addCustomTheme: (
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
) => EndpointData;
|
||||
editCustomTheme: (
|
||||
themeId: string,
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
) => EndpointData;
|
||||
deleteCustomTheme: (themeId: string) => EndpointData;
|
||||
getTags: Endpoint;
|
||||
createTag: (tagName: string) => EndpointData;
|
||||
editTag: (tagId: string, newName: string) => EndpointData;
|
||||
|
|
|
|||
|
|
@ -1210,10 +1210,12 @@ export function setThemeDark(name: string, nosave?: boolean): boolean {
|
|||
function setThemes(
|
||||
theme: string,
|
||||
customState: boolean,
|
||||
customThemeColors: string[],
|
||||
nosave?: boolean
|
||||
): boolean {
|
||||
if (!isConfigValueValid("themes", theme, ["string"])) return false;
|
||||
|
||||
config.customThemeColors = customThemeColors;
|
||||
config.theme = theme;
|
||||
config.customTheme = customState;
|
||||
saveToLocalStorage("theme", nosave);
|
||||
|
|
@ -1228,11 +1230,26 @@ export function setRandomTheme(
|
|||
): boolean {
|
||||
if (
|
||||
!isConfigValueValid("random theme", val, [
|
||||
["off", "on", "fav", "light", "dark"],
|
||||
["off", "on", "fav", "light", "dark", "custom"],
|
||||
])
|
||||
)
|
||||
return false;
|
||||
|
||||
if (val === "custom") {
|
||||
setCustomTheme(true);
|
||||
if (firebase.auth().currentUser === null) {
|
||||
config.randomTheme = val;
|
||||
return false;
|
||||
}
|
||||
if (!DB.getSnapshot()) return true;
|
||||
if (DB.getSnapshot().customThemes.length === 0) {
|
||||
Notifications.add("You need to create a custom theme first", 0);
|
||||
config.randomTheme = "off";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (val !== "off" && val !== "custom") setCustomTheme(false);
|
||||
|
||||
config.randomTheme = val;
|
||||
saveToLocalStorage("randomTheme", nosave);
|
||||
ConfigEvent.dispatch("randomTheme", config.randomTheme);
|
||||
|
|
@ -1279,7 +1296,7 @@ export function setCustomThemeColors(
|
|||
// applyCustomThemeColors();
|
||||
}
|
||||
saveToLocalStorage("customThemeColors", nosave);
|
||||
ConfigEvent.dispatch("customThemeColors", config.customThemeColors);
|
||||
ConfigEvent.dispatch("customThemeColors", config.customThemeColors, nosave);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1607,13 +1624,15 @@ export function apply(
|
|||
}
|
||||
);
|
||||
if (configObj !== undefined && configObj !== null) {
|
||||
setCustomThemeColors(configObj.customThemeColors, true);
|
||||
setThemeLight(configObj.themeLight, true);
|
||||
setThemeDark(configObj.themeDark, true);
|
||||
setAutoSwitchTheme(configObj.autoSwitchTheme, true);
|
||||
setThemes(configObj.theme, configObj.customTheme, true);
|
||||
// setTheme(configObj.theme, true);
|
||||
// setCustomTheme(configObj.customTheme, true, true);
|
||||
setThemes(
|
||||
configObj.theme,
|
||||
configObj.customTheme,
|
||||
configObj.customThemeColors,
|
||||
true
|
||||
);
|
||||
setCustomLayoutfluid(configObj.customLayoutfluid, true);
|
||||
setCustomBackground(configObj.customBackground, true);
|
||||
setCustomBackgroundSize(configObj.customBackgroundSize, true);
|
||||
|
|
@ -1624,7 +1643,6 @@ export function apply(
|
|||
setQuoteLength(configObj.quoteLength, true);
|
||||
setWordCount(configObj.words, true);
|
||||
setLanguage(configObj.language, true);
|
||||
// setSavedLayout(configObj.savedLayout, true);
|
||||
setLayout(configObj.layout, true);
|
||||
setFontSize(configObj.fontSize, true);
|
||||
setFreedomMode(configObj.freedomMode, true);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import * as PaceCaret from "../test/pace-caret";
|
|||
import * as CommandlineLists from "../elements/commandline-lists";
|
||||
import * as TagController from "./tag-controller";
|
||||
import * as ResultTagsPopup from "../popups/result-tags-popup";
|
||||
import * as URLHandler from "../utils/url-handler";
|
||||
|
||||
export const gmailProvider = new firebase.auth.GoogleAuthProvider();
|
||||
// const githubProvider = new firebase.auth.GithubAuthProvider();
|
||||
|
|
@ -280,21 +281,7 @@ const authListener = firebase.auth().onAuthStateChanged(async function (user) {
|
|||
}, 125 / 2);
|
||||
}
|
||||
|
||||
let theme = Misc.findGetParameter("customTheme");
|
||||
if (theme !== null) {
|
||||
try {
|
||||
theme = theme.split(",");
|
||||
UpdateConfig.setCustomThemeColors(theme);
|
||||
Notifications.add("Custom theme applied.", 1);
|
||||
} catch (e) {
|
||||
Notifications.add(
|
||||
"Something went wrong. Reverting to default custom colors.",
|
||||
0
|
||||
);
|
||||
UpdateConfig.setCustomThemeColors(Config.defaultConfig.customThemeColors);
|
||||
}
|
||||
UpdateConfig.setCustomTheme(true);
|
||||
}
|
||||
URLHandler.loadCustomThemeFromUrl();
|
||||
if (/challenge_.+/g.test(window.location.pathname)) {
|
||||
Notifications.add(
|
||||
"Challenge links temporarily disabled. Please use the command line to load the challenge manually",
|
||||
|
|
@ -312,6 +299,7 @@ const authListener = firebase.auth().onAuthStateChanged(async function (user) {
|
|||
});
|
||||
|
||||
export function signIn() {
|
||||
UpdateConfig.setChangedBeforeDb(false);
|
||||
authListener();
|
||||
$(".pageLogin .preloader").removeClass("hidden");
|
||||
$(".pageLogin .button").addClass("disabled");
|
||||
|
|
@ -366,6 +354,7 @@ export function signIn() {
|
|||
}
|
||||
|
||||
export async function signInWithGoogle() {
|
||||
UpdateConfig.setChangedBeforeDb(false);
|
||||
$(".pageLogin .preloader").removeClass("hidden");
|
||||
$(".pageLogin .button").addClass("disabled");
|
||||
authListener();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import * as ThemeColors from "../elements/theme-colors";
|
||||
import * as ChartController from "./chart-controller";
|
||||
import * as Misc from "../misc";
|
||||
import Config from "../config";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import tinycolor from "tinycolor2";
|
||||
import * as BackgroundFilter from "../elements/custom-background-filter";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as DB from "../db";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
|
||||
let isPreviewingTheme = false;
|
||||
export let randomTheme: string | null = null;
|
||||
|
|
@ -76,18 +78,29 @@ const loadStyle = async function (name: string): Promise<void> {
|
|||
link.onload = (): void => {
|
||||
resolve();
|
||||
};
|
||||
link.href = `themes/${name}.css`;
|
||||
if (name === "custom") {
|
||||
link.href = `themes/serika_dark.css`;
|
||||
} else {
|
||||
link.href = `themes/${name}.css`;
|
||||
}
|
||||
|
||||
const headScript = document.querySelector("#currentTheme") as Element;
|
||||
headScript.replaceWith(link);
|
||||
});
|
||||
};
|
||||
|
||||
export function apply(themeName: string, isPreview = false): void {
|
||||
// export function changeCustomTheme(themeId: string, nosave = false): void {
|
||||
// const customThemes = DB.getSnapshot().customThemes;
|
||||
// const colors = customThemes.find((e) => e._id === themeId)
|
||||
// ?.colors as string[];
|
||||
// UpdateConfig.setCustomThemeColors(colors, nosave);
|
||||
// }
|
||||
|
||||
function apply(themeName: string, isCustom: boolean, isPreview = false): void {
|
||||
clearCustomTheme();
|
||||
|
||||
let name = "serika_dark";
|
||||
if (themeName !== "custom") {
|
||||
if (!isCustom) {
|
||||
name = themeName;
|
||||
Misc.swapElements(
|
||||
$('.pageSettings [tabContent="custom"]'),
|
||||
|
|
@ -95,7 +108,7 @@ export function apply(themeName: string, isPreview = false): void {
|
|||
250
|
||||
);
|
||||
} else {
|
||||
//is custom
|
||||
name = "custom";
|
||||
Misc.swapElements(
|
||||
$('.pageSettings [tabContent="preset"]'),
|
||||
$('.pageSettings [tabContent="custom"]'),
|
||||
|
|
@ -109,12 +122,22 @@ export function apply(themeName: string, isPreview = false): void {
|
|||
// $("#currentTheme").attr("href", `themes/${name}.css`);
|
||||
loadStyle(name).then(() => {
|
||||
ThemeColors.update();
|
||||
if (themeName === "custom") {
|
||||
if (isCustom) {
|
||||
let colorValues = Config.customThemeColors;
|
||||
const snapshot = DB.getSnapshot();
|
||||
if (isCustom && !isPreview && snapshot) {
|
||||
const customColors =
|
||||
snapshot.customThemes.find((e) => e._id === themeName)?.colors ?? [];
|
||||
if (customColors.length > 0)
|
||||
UpdateConfig.setCustomThemeColors(customColors);
|
||||
}
|
||||
if (themeName !== "custom" && snapshot) {
|
||||
const customThemes = snapshot.customThemes;
|
||||
const customThemeById = customThemes.find((e) => e._id === themeName);
|
||||
colorValues = customThemeById?.colors as string[];
|
||||
}
|
||||
colorVars.forEach((e, index) => {
|
||||
document.documentElement.style.setProperty(
|
||||
e,
|
||||
Config.customThemeColors[index]
|
||||
);
|
||||
document.documentElement.style.setProperty(e, colorValues[index]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +150,9 @@ export function apply(themeName: string, isPreview = false): void {
|
|||
}
|
||||
if (!isPreview) {
|
||||
ThemeColors.getAll().then((colors) => {
|
||||
$(".current-theme .text").text(themeName.replace(/_/g, " "));
|
||||
$(".current-theme .text").text(
|
||||
isCustom ? "custom" : themeName.replace(/_/g, " ")
|
||||
);
|
||||
$(".keymap-key").attr("style", "");
|
||||
ChartController.updateAllChartColors();
|
||||
updateFavicon(128, 32);
|
||||
|
|
@ -137,13 +162,17 @@ export function apply(themeName: string, isPreview = false): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function preview(themeName: string, randomTheme = false): void {
|
||||
export function preview(
|
||||
themeIdentifier: string,
|
||||
isCustom: boolean,
|
||||
randomTheme = false
|
||||
): void {
|
||||
isPreviewingTheme = true;
|
||||
apply(themeName, true && !randomTheme);
|
||||
apply(themeIdentifier, isCustom, !randomTheme);
|
||||
}
|
||||
|
||||
export function set(themeName: string): void {
|
||||
apply(themeName);
|
||||
export function set(themeIdentifier: string, isCustom: boolean): void {
|
||||
apply(themeIdentifier, isCustom);
|
||||
}
|
||||
|
||||
export function clearPreview(): void {
|
||||
|
|
@ -151,15 +180,15 @@ export function clearPreview(): void {
|
|||
isPreviewingTheme = false;
|
||||
randomTheme = null;
|
||||
if (Config.customTheme) {
|
||||
apply("custom");
|
||||
apply("custom", true);
|
||||
} else {
|
||||
apply(Config.theme);
|
||||
apply(Config.theme, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function randomizeTheme(): void {
|
||||
let randomList;
|
||||
let randomList: string[] | MonkeyTypes.CustomTheme[];
|
||||
Misc.getThemesList().then((themes) => {
|
||||
if (Config.randomTheme === "fav" && Config.favThemes.length > 0) {
|
||||
randomList = Config.favThemes;
|
||||
|
|
@ -171,19 +200,32 @@ export function randomizeTheme(): void {
|
|||
randomList = themes
|
||||
.filter((t) => tinycolor(t.bgColor).isDark())
|
||||
.map((t) => t.name);
|
||||
} else {
|
||||
} else if (Config.randomTheme === "on") {
|
||||
randomList = themes.map((t) => {
|
||||
return t.name;
|
||||
});
|
||||
} else {
|
||||
randomList = DB.getSnapshot().customThemes.map((ct) => ct._id);
|
||||
}
|
||||
|
||||
const previousTheme = randomTheme;
|
||||
randomTheme = randomList[Math.floor(Math.random() * randomList.length)];
|
||||
|
||||
// if (Config.randomTheme === "custom") {
|
||||
// changeCustomTheme(randomTheme, true);
|
||||
// } else {
|
||||
preview(randomTheme, true);
|
||||
// }
|
||||
|
||||
if (previousTheme != randomTheme) {
|
||||
// Notifications.add(randomTheme.replace(/_/g, " "), 0);
|
||||
let name = randomTheme.replace(/_/g, " ");
|
||||
if (Config.randomTheme === "custom") {
|
||||
name = (
|
||||
DB.getSnapshot().customThemes.find((ct) => ct._id === randomTheme)
|
||||
?.name ?? "custom"
|
||||
).replace(/_/g, " ");
|
||||
}
|
||||
Notifications.add(name, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -229,35 +271,37 @@ window
|
|||
?.addEventListener("change", (event) => {
|
||||
if (!Config.autoSwitchTheme || Config.customTheme) return;
|
||||
if (event.matches) {
|
||||
set(Config.themeDark);
|
||||
set(Config.themeDark, false);
|
||||
} else {
|
||||
set(Config.themeLight);
|
||||
set(Config.themeLight, false);
|
||||
}
|
||||
});
|
||||
|
||||
ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
|
||||
if (eventKey === "customTheme")
|
||||
eventValue ? set("custom") : set(Config.theme);
|
||||
eventValue ? set("custom", true) : set(Config.theme, false);
|
||||
if (eventKey === "customThemeColors")
|
||||
nosave ? preview("custom", true) : set("custom", true);
|
||||
if (eventKey === "theme") {
|
||||
clearPreview();
|
||||
set(eventValue as string);
|
||||
set(eventValue as string, false);
|
||||
}
|
||||
if (eventKey === "setThemes") {
|
||||
clearPreview();
|
||||
if (eventValue) {
|
||||
set("custom");
|
||||
set("custom", true);
|
||||
} else {
|
||||
if (Config.autoSwitchTheme) {
|
||||
if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
set(Config.themeDark);
|
||||
set(Config.themeDark, false);
|
||||
} else {
|
||||
set(Config.themeLight);
|
||||
set(Config.themeLight, false);
|
||||
}
|
||||
} else {
|
||||
set(Config.theme);
|
||||
set(Config.theme, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -270,12 +314,12 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
|
|||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
set(Config.themeDark);
|
||||
set(Config.themeDark, false);
|
||||
} else {
|
||||
set(Config.themeLight);
|
||||
set(Config.themeLight, false);
|
||||
}
|
||||
} else {
|
||||
set(Config.theme);
|
||||
set(Config.theme, false);
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
|
@ -287,7 +331,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
|
|||
) &&
|
||||
!nosave
|
||||
) {
|
||||
set(Config.themeLight);
|
||||
set(Config.themeLight, false);
|
||||
}
|
||||
if (
|
||||
eventKey === "themeDark" &&
|
||||
|
|
@ -296,6 +340,6 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
|
|||
window.matchMedia("(prefers-color-scheme: dark)").matches &&
|
||||
!nosave
|
||||
) {
|
||||
set(Config.themeDark);
|
||||
set(Config.themeDark, false);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export async function initSnapshot(): Promise<
|
|||
custom: { custom: [] },
|
||||
},
|
||||
name: undefined,
|
||||
customThemes: [],
|
||||
presets: [],
|
||||
tags: [],
|
||||
favouriteThemes: [],
|
||||
|
|
@ -114,6 +115,7 @@ export async function initSnapshot(): Promise<
|
|||
// LoadingPage.updateBar(48);
|
||||
// }
|
||||
// LoadingPage.updateText("Downloading tags...");
|
||||
snap.customThemes = userData.customThemes ?? [];
|
||||
snap.tags = tagsData;
|
||||
snap.tags = snap.tags?.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
|
|
@ -183,6 +185,92 @@ export async function getUserResults(): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
export function getCustomThemeById(
|
||||
themeID: string
|
||||
): MonkeyTypes.CustomTheme | undefined {
|
||||
return dbSnapshot.customThemes.find((t) => t._id === themeID);
|
||||
}
|
||||
|
||||
export async function addCustomTheme(
|
||||
theme: MonkeyTypes.RawCustomTheme
|
||||
): Promise<boolean> {
|
||||
if (dbSnapshot === null) return false;
|
||||
|
||||
if (dbSnapshot.customThemes.length >= 10) {
|
||||
Notifications.add("Too many custom themes!", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await Ape.users.addCustomTheme(theme);
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error adding custom theme: " + response.message, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCustomTheme: MonkeyTypes.CustomTheme = {
|
||||
...theme,
|
||||
_id: response.data.theme._id as string,
|
||||
};
|
||||
|
||||
dbSnapshot.customThemes.push(newCustomTheme);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function editCustomTheme(
|
||||
themeId: string,
|
||||
newTheme: MonkeyTypes.RawCustomTheme
|
||||
): Promise<boolean> {
|
||||
const user = firebase.auth().currentUser;
|
||||
if (user === null) return false;
|
||||
if (dbSnapshot === null) return false;
|
||||
|
||||
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
|
||||
if (!customTheme) {
|
||||
Notifications.add(
|
||||
"Editing failed: Custom theme with id: " + themeId + " does not exist",
|
||||
-1
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await Ape.users.editCustomTheme(themeId, newTheme);
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error editing custom theme: " + response.message, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCustomTheme: MonkeyTypes.CustomTheme = {
|
||||
...newTheme,
|
||||
_id: themeId,
|
||||
};
|
||||
|
||||
dbSnapshot.customThemes[dbSnapshot.customThemes.indexOf(customTheme)] =
|
||||
newCustomTheme;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteCustomTheme(themeId: string): Promise<boolean> {
|
||||
const user = firebase.auth().currentUser;
|
||||
if (user === null) return false;
|
||||
if (dbSnapshot === null) return false;
|
||||
|
||||
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
|
||||
if (!customTheme) return false;
|
||||
|
||||
const response = await Ape.users.deleteCustomTheme(themeId);
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Error deleting custom theme: " + response.message, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
dbSnapshot.customThemes = dbSnapshot.customThemes.filter(
|
||||
(t) => t._id !== themeId
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getUserHighestWpm<M extends MonkeyTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
|
|
|
|||
|
|
@ -1147,6 +1147,21 @@ const commandsRandomTheme: MonkeyTypes.CommandsGroup = {
|
|||
UpdateConfig.setRandomTheme("dark");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "setRandomCustom",
|
||||
display: "custom",
|
||||
configValue: "custom",
|
||||
exec: (): void => {
|
||||
if (firebase.auth().currentUser === null) {
|
||||
Notifications.add(
|
||||
"Multiple custom themes are available to logged in users only",
|
||||
0
|
||||
);
|
||||
return;
|
||||
}
|
||||
UpdateConfig.setRandomTheme("custom");
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -1212,8 +1227,8 @@ export const commandsEnableAds: MonkeyTypes.CommandsGroup = {
|
|||
],
|
||||
};
|
||||
|
||||
const commandsCustomTheme: MonkeyTypes.CommandsGroup = {
|
||||
title: "Custom theme...",
|
||||
export const customThemeCommands: MonkeyTypes.CommandsGroup = {
|
||||
title: "Custom theme",
|
||||
configKey: "customTheme",
|
||||
list: [
|
||||
{
|
||||
|
|
@ -1235,6 +1250,41 @@ const commandsCustomTheme: MonkeyTypes.CommandsGroup = {
|
|||
],
|
||||
};
|
||||
|
||||
export const customThemeListCommands: MonkeyTypes.CommandsGroup = {
|
||||
title: "Custom themes list...",
|
||||
// configKey: "customThemeId",
|
||||
list: [],
|
||||
};
|
||||
|
||||
export function updateCustomThemeListCommands(): void {
|
||||
if (firebase.auth().currentUser === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
customThemeListCommands.list = [];
|
||||
|
||||
if (DB.getSnapshot().customThemes.length < 0) {
|
||||
Notifications.add("You need to create a custom theme first", 0);
|
||||
return;
|
||||
}
|
||||
DB.getSnapshot().customThemes.forEach((theme) => {
|
||||
customThemeListCommands.list.push({
|
||||
id: "setCustomThemeId" + theme._id,
|
||||
display: theme.name,
|
||||
configValue: theme._id,
|
||||
hover: (): void => {
|
||||
ThemeController.preview(theme._id, true);
|
||||
},
|
||||
exec: (): void => {
|
||||
// UpdateConfig.setCustomThemeId(theme._id);
|
||||
UpdateConfig.setCustomTheme(true);
|
||||
ThemeController.set(theme._id, true);
|
||||
},
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsCaretStyle: MonkeyTypes.CommandsGroup = {
|
||||
title: "Change caret style...",
|
||||
configKey: "caretStyle",
|
||||
|
|
@ -2365,7 +2415,7 @@ Misc.getThemesList().then((themes) => {
|
|||
configValue: theme.name,
|
||||
hover: (): void => {
|
||||
// previewTheme(theme.name);
|
||||
ThemeController.preview(theme.name);
|
||||
ThemeController.preview(theme.name, false);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme.name);
|
||||
|
|
@ -2404,7 +2454,7 @@ export function updateThemeCommands(): void {
|
|||
display: theme.replace(/_/g, " "),
|
||||
hover: (): void => {
|
||||
// previewTheme(theme);
|
||||
ThemeController.preview(theme);
|
||||
ThemeController.preview(theme, false);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme);
|
||||
|
|
@ -2419,7 +2469,7 @@ export function updateThemeCommands(): void {
|
|||
display: theme.name.replace(/_/g, " "),
|
||||
hover: (): void => {
|
||||
// previewTheme(theme.name);
|
||||
ThemeController.preview(theme.name);
|
||||
ThemeController.preview(theme.name, false);
|
||||
},
|
||||
exec: (): void => {
|
||||
UpdateConfig.setTheme(theme.name);
|
||||
|
|
@ -2798,7 +2848,17 @@ export const defaultCommands: MonkeyTypes.CommandsGroup = {
|
|||
id: "setCustomTheme",
|
||||
display: "Custom theme...",
|
||||
icon: "fa-palette",
|
||||
subgroup: commandsCustomTheme,
|
||||
subgroup: customThemeCommands,
|
||||
},
|
||||
{
|
||||
id: "setCustomThemeId",
|
||||
display: "Custom themes...",
|
||||
icon: "fa-palette",
|
||||
subgroup: customThemeListCommands,
|
||||
beforeSubgroup: (): void => updateCustomThemeListCommands(),
|
||||
available: (): boolean => {
|
||||
return firebase.auth().currentUser !== null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "changeRandomTheme",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import Config, * as UpdateConfig from "../config";
|
|||
import * as Focus from "../test/focus";
|
||||
import * as CommandlineLists from "./commandline-lists";
|
||||
import * as TestUI from "../test/test-ui";
|
||||
import * as DB from "../db";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
|
||||
let commandLineMouseMode = false;
|
||||
|
||||
|
|
@ -681,9 +683,29 @@ $(document).on("click", "#testModesNotice .text-button", (event) => {
|
|||
|
||||
$(document).on("click", "#bottom .leftright .right .current-theme", (e) => {
|
||||
if (e.shiftKey) {
|
||||
UpdateConfig.setCustomTheme(!Config.customTheme);
|
||||
if (!Config.customTheme) {
|
||||
if (firebase.auth().currentUser !== null) {
|
||||
if (DB.getSnapshot().customThemes.length < 1) {
|
||||
Notifications.add("No custom themes!", 0);
|
||||
UpdateConfig.setCustomTheme(false);
|
||||
// UpdateConfig.setCustomThemeId("");
|
||||
return;
|
||||
}
|
||||
// if (!DB.getCustomThemeById(Config.customThemeId)) {
|
||||
// // Turn on the first custom theme
|
||||
// const firstCustomThemeId = DB.getSnapshot().customThemes[0]._id;
|
||||
// UpdateConfig.setCustomThemeId(firstCustomThemeId);
|
||||
// }
|
||||
}
|
||||
UpdateConfig.setCustomTheme(true);
|
||||
} else UpdateConfig.setCustomTheme(false);
|
||||
} else {
|
||||
CommandlineLists.pushCurrent(CommandlineLists.themeCommands);
|
||||
if (Config.customTheme) CommandlineLists.updateCustomThemeListCommands();
|
||||
CommandlineLists.pushCurrent(
|
||||
Config.customTheme
|
||||
? CommandlineLists.customThemeListCommands
|
||||
: CommandlineLists.themeCommands
|
||||
);
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import * as PresetController from "../controllers/preset-controller";
|
|||
import * as ThemePicker from "../settings/theme-picker";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as ImportExportSettingsPopup from "../popups/import-export-settings-popup";
|
||||
import * as CustomThemePopup from "../popups/custom-theme-popup";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import * as ApeKeysPopup from "../popups/ape-keys-popup";
|
||||
|
|
@ -387,6 +386,7 @@ async function initGroups(): Promise<void> {
|
|||
export function reset(): void {
|
||||
$(".pageSettings .section.themes .favThemes.buttons").empty();
|
||||
$(".pageSettings .section.themes .allThemes.buttons").empty();
|
||||
$(".pageSettings .section.themes .allCustomThemes.buttons").empty();
|
||||
$(".pageSettings .section.languageGroups .buttons").empty();
|
||||
$(".pageSettings select").empty().select2("destroy");
|
||||
$(".pageSettings .section.funbox .buttons").empty();
|
||||
|
|
@ -705,7 +705,7 @@ export function update(): void {
|
|||
refreshPresetsSettingsSection();
|
||||
// LanguagePicker.setActiveGroup(); Shifted from grouped btns to combo-box
|
||||
setActiveFunboxButton();
|
||||
ThemePicker.updateActiveTab();
|
||||
ThemePicker.updateActiveTab(true);
|
||||
ThemePicker.setCustomInputs(true);
|
||||
updateDiscordSection();
|
||||
updateAuthSections();
|
||||
|
|
@ -926,29 +926,6 @@ $("#exportSettingsButton").on("click", () => {
|
|||
);
|
||||
});
|
||||
|
||||
$("#shareCustomThemeButton").on("click", () => {
|
||||
const share: string[] = [];
|
||||
$.each(
|
||||
$(".pageSettings .section.customTheme [type='color']"),
|
||||
(_, element) => {
|
||||
share.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
|
||||
const url =
|
||||
"https://monkeytype.com?" +
|
||||
Misc.objectToQueryString({ customTheme: share });
|
||||
|
||||
navigator.clipboard.writeText(url).then(
|
||||
function () {
|
||||
Notifications.add("URL Copied to clipboard", 0);
|
||||
},
|
||||
function () {
|
||||
CustomThemePopup.show(url);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".pageSettings .sectionGroupTitle").on("click", (e) => {
|
||||
toggleSettingsGroup($(e.currentTarget).attr("group") as string);
|
||||
});
|
||||
|
|
@ -1047,7 +1024,7 @@ export function setEventDisabled(value: boolean): void {
|
|||
}
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (configEventDisabled || eventKey === "saveToLocalStorage") return;
|
||||
if (ActivePage.get() === "settings") {
|
||||
if (ActivePage.get() === "settings" && eventKey !== "theme") {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ function updateList(): void {
|
|||
}
|
||||
|
||||
function updateQuoteLength(index: number): void {
|
||||
const len = ($(
|
||||
`#quoteApprovePopup .quote[id=${index}] .text`
|
||||
).val() as string)?.length;
|
||||
const len = (
|
||||
$(`#quoteApprovePopup .quote[id=${index}] .text`).val() as string
|
||||
)?.length;
|
||||
$(`#quoteApprovePopup .quote[id=${index}] .length`).text(
|
||||
"Quote length: " + len
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@ import * as Loader from "../elements/loader";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
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";
|
||||
|
||||
type Input = {
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
initVal: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
let activePopup: SimplePopup | null = null;
|
||||
|
|
@ -129,6 +131,14 @@ class SimplePopup {
|
|||
autocomplete="off"
|
||||
>${input.initVal}</textarea>
|
||||
`);
|
||||
} else if (input.type === "checkbox") {
|
||||
el.find(".inputs").append(`
|
||||
<label class="checkbox">
|
||||
<input type="checkbox">
|
||||
<div class="customTextCheckbox"></div>
|
||||
${input.label}
|
||||
</label>
|
||||
`);
|
||||
} else {
|
||||
el.find(".inputs").append(`
|
||||
<input
|
||||
|
|
@ -167,7 +177,11 @@ class SimplePopup {
|
|||
if (!this.canClose) return;
|
||||
const vals: string[] = [];
|
||||
$.each($("#simplePopup input"), (_, el) => {
|
||||
vals.push($(el).val() as string);
|
||||
if ($(el).is(":checkbox")) {
|
||||
vals.push($(el).is(":checked") ? "true" : "false");
|
||||
} else {
|
||||
vals.push($(el).val() as string);
|
||||
}
|
||||
});
|
||||
this.execFn(this, ...vals);
|
||||
this.hide();
|
||||
|
|
@ -934,6 +948,94 @@ list["deleteCustomText"] = new SimplePopup(
|
|||
}
|
||||
);
|
||||
|
||||
list["updateCustomTheme"] = new SimplePopup(
|
||||
"updateCustomTheme",
|
||||
"text",
|
||||
"Update Custom Theme",
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
placeholder: "Name",
|
||||
initVal: "",
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
initVal: "false",
|
||||
label: "Update custom theme to current colors",
|
||||
},
|
||||
],
|
||||
"",
|
||||
"Update",
|
||||
async (_thisPopup, name, updateColors) => {
|
||||
const snapshot = DB.getSnapshot();
|
||||
|
||||
const customTheme = snapshot.customThemes.find(
|
||||
(t) => t._id === _thisPopup.parameters[0]
|
||||
);
|
||||
if (customTheme === undefined) {
|
||||
Notifications.add("Custom theme does not exist!", -1);
|
||||
return;
|
||||
}
|
||||
|
||||
let newColors: string[] = [];
|
||||
if (updateColors === "true") {
|
||||
$.each(
|
||||
$(".pageSettings .customTheme .customThemeEdit [type='color']"),
|
||||
(_index, element) => {
|
||||
newColors.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
newColors = customTheme.colors;
|
||||
}
|
||||
|
||||
const newTheme = {
|
||||
name: name,
|
||||
colors: newColors,
|
||||
};
|
||||
Loader.show();
|
||||
await DB.editCustomTheme(customTheme._id, newTheme);
|
||||
Loader.hide();
|
||||
UpdateConfig.setCustomThemeColors(newColors);
|
||||
Notifications.add("Custom theme updated", 1);
|
||||
ThemePicker.refreshButtons();
|
||||
},
|
||||
(_thisPopup) => {
|
||||
const snapshot = DB.getSnapshot();
|
||||
|
||||
const customTheme = snapshot.customThemes.find(
|
||||
(t) => t._id === _thisPopup.parameters[0]
|
||||
);
|
||||
if (!customTheme) return;
|
||||
_thisPopup.inputs[0].initVal = customTheme.name;
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
list["deleteCustomTheme"] = new SimplePopup(
|
||||
"deleteCustomTheme",
|
||||
"text",
|
||||
"Delete Custom Theme",
|
||||
[],
|
||||
"Are you sure?",
|
||||
"Delete",
|
||||
async (_thisPopup) => {
|
||||
Loader.show();
|
||||
await DB.deleteCustomTheme(_thisPopup.parameters[0]);
|
||||
Loader.hide();
|
||||
Notifications.add("Custom theme deleted", 1);
|
||||
ThemePicker.refreshButtons();
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
$(".pageSettings .section.discordIntegration #unlinkDiscordButton").on(
|
||||
"click",
|
||||
() => {
|
||||
|
|
@ -977,6 +1079,26 @@ $(`#customTextPopup .buttonsTop .saveCustomText`).on("click", () => {
|
|||
list["saveCustomText"].show();
|
||||
});
|
||||
|
||||
$(document).on(
|
||||
"click",
|
||||
".pageSettings .section.themes .customTheme .delButton",
|
||||
(e) => {
|
||||
const $parentElement = $(e.currentTarget).parent(".customTheme.button");
|
||||
const customThemeId = $parentElement.attr("customThemeId") as string;
|
||||
list["deleteCustomTheme"].show([customThemeId]);
|
||||
}
|
||||
);
|
||||
|
||||
$(document).on(
|
||||
"click",
|
||||
".pageSettings .section.themes .customTheme .editButton",
|
||||
(e) => {
|
||||
const $parentElement = $(e.currentTarget).parent(".customTheme.button");
|
||||
const customThemeId = $parentElement.attr("customThemeId") as string;
|
||||
list["updateCustomTheme"].show([customThemeId]);
|
||||
}
|
||||
);
|
||||
|
||||
$(document).on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .list .savedText .button.delete`,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,19 @@ import * as Misc from "../misc";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import * as ThemeColors from "../elements/theme-colors";
|
||||
import * as ChartController from "../controllers/chart-controller";
|
||||
import * as CustomThemePopup from "../popups/custom-theme-popup";
|
||||
import * as Loader from "../elements/loader";
|
||||
import * as DB from "../db";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
|
||||
export function updateActiveButton(): void {
|
||||
let activeThemeName = Config.theme;
|
||||
if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) {
|
||||
activeThemeName = ThemeController.randomTheme;
|
||||
if (
|
||||
Config.randomTheme !== "off" &&
|
||||
Config.randomTheme !== "custom" &&
|
||||
ThemeController.randomTheme !== null
|
||||
) {
|
||||
activeThemeName = ThemeController.randomTheme as string;
|
||||
}
|
||||
$(`.pageSettings .section.themes .theme`).removeClass("active");
|
||||
$(`.pageSettings .section.themes .theme[theme=${activeThemeName}]`).addClass(
|
||||
|
|
@ -90,43 +98,86 @@ function updateColors(
|
|||
}
|
||||
|
||||
export async function refreshButtons(): Promise<void> {
|
||||
const favThemesEl = $(
|
||||
".pageSettings .section.themes .favThemes.buttons"
|
||||
).empty();
|
||||
const themesEl = $(
|
||||
".pageSettings .section.themes .allThemes.buttons"
|
||||
).empty();
|
||||
if (Config.customTheme) {
|
||||
// Update custom theme buttons
|
||||
const customThemesEl = $(
|
||||
".pageSettings .section.themes .allCustomThemes.buttons"
|
||||
).empty();
|
||||
const addButton = $(".pageSettings .section.themes .addCustomThemeButton");
|
||||
|
||||
let activeThemeName = Config.theme;
|
||||
if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) {
|
||||
activeThemeName = ThemeController.randomTheme;
|
||||
}
|
||||
if (firebase.auth().currentUser === null) {
|
||||
$(
|
||||
".pageSettings .section.themes .customThemeEdit .saveCustomThemeButton"
|
||||
).text("save");
|
||||
return;
|
||||
} else {
|
||||
$(
|
||||
".pageSettings .section.themes .customThemeEdit .saveCustomThemeButton"
|
||||
).text("save as new");
|
||||
}
|
||||
|
||||
const themes = await Misc.getSortedThemesList();
|
||||
//first show favourites
|
||||
if (Config.favThemes.length > 0) {
|
||||
favThemesEl.css({ paddingBottom: "1rem" });
|
||||
themes.forEach((theme) => {
|
||||
// @ts-ignore TODO: Remove this comment once the config.js is converted to ts
|
||||
if (Config.favThemes.includes(theme.name)) {
|
||||
const activeTheme = activeThemeName === theme.name ? "active" : "";
|
||||
favThemesEl.append(
|
||||
`<div class="theme button ${activeTheme}" theme='${
|
||||
theme.name
|
||||
}' style="color:${theme.mainColor};background:${theme.bgColor}">
|
||||
<div class="activeIndicator"><i class="fas fa-circle"></i></div>
|
||||
<div class="text">${theme.name.replace(/_/g, " ")}</div>
|
||||
<div class="favButton active"><i class="fas fa-star"></i></div></div>`
|
||||
);
|
||||
}
|
||||
addButton.removeClass("hidden");
|
||||
|
||||
const customThemes = DB.getSnapshot().customThemes;
|
||||
|
||||
customThemes.forEach((customTheme) => {
|
||||
// const activeTheme =
|
||||
// Config.customThemeId === customTheme._id ? "active" : "";
|
||||
const bgColor = customTheme.colors[0];
|
||||
const mainColor = customTheme.colors[1];
|
||||
|
||||
customThemesEl.append(
|
||||
`<div class="customTheme button" customThemeId='${customTheme._id}'
|
||||
style="color:${mainColor};background:${bgColor}">
|
||||
<div class="editButton"><i class="fas fa-pen"></i></div>
|
||||
<div class="text">${customTheme.name.replace(/_/g, " ")}</div>
|
||||
<div class="delButton"><i class="fas fa-trash fa-fw"></i></div>
|
||||
</div>`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
favThemesEl.css({ paddingBottom: "0" });
|
||||
}
|
||||
//then the rest
|
||||
themes.forEach((theme) => {
|
||||
// @ts-ignore TODO: Remove this comment once the config.js is converted to ts
|
||||
if (!Config.favThemes.includes(theme.name)) {
|
||||
// Update theme buttons
|
||||
const favThemesEl = $(
|
||||
".pageSettings .section.themes .favThemes.buttons"
|
||||
).empty();
|
||||
const themesEl = $(
|
||||
".pageSettings .section.themes .allThemes.buttons"
|
||||
).empty();
|
||||
|
||||
let activeThemeName = Config.theme;
|
||||
if (
|
||||
Config.randomTheme !== "off" &&
|
||||
Config.randomTheme !== "custom" &&
|
||||
ThemeController.randomTheme !== null
|
||||
) {
|
||||
activeThemeName = ThemeController.randomTheme as string;
|
||||
}
|
||||
|
||||
const themes = await Misc.getSortedThemesList();
|
||||
//first show favourites
|
||||
if (Config.favThemes.length > 0) {
|
||||
favThemesEl.css({ paddingBottom: "1rem" });
|
||||
themes.forEach((theme) => {
|
||||
if (Config.favThemes.includes(theme.name)) {
|
||||
const activeTheme = activeThemeName === theme.name ? "active" : "";
|
||||
favThemesEl.append(
|
||||
`<div class="theme button ${activeTheme}" theme='${theme.name}'
|
||||
style="color:${theme.mainColor};background:${theme.bgColor}">
|
||||
<div class="activeIndicator"><i class="fas fa-circle"></i></div>
|
||||
<div class="text">${theme.name.replace(/_/g, " ")}</div>
|
||||
<div class="favButton active"><i class="fas fa-star"></i></div></div>`
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
favThemesEl.css({ paddingBottom: "0" });
|
||||
}
|
||||
//then the rest
|
||||
themes.forEach((theme) => {
|
||||
if (Config.favThemes.includes(theme.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTheme = activeThemeName === theme.name ? "active" : "";
|
||||
themesEl.append(
|
||||
`<div class="theme button ${activeTheme}" theme='${
|
||||
|
|
@ -136,8 +187,8 @@ export async function refreshButtons(): Promise<void> {
|
|||
<div class="text">${theme.name.replace(/_/g, " ")}</div>
|
||||
<div class="favButton"><i class="far fa-star"></i></div></div>`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
updateActiveButton();
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +207,6 @@ export function setCustomInputs(noThemeUpdate = false): void {
|
|||
}
|
||||
|
||||
function toggleFavourite(themeName: string): void {
|
||||
// @ts-ignore TODO: Remove this comment once config.js is converted to typescript
|
||||
if (Config.favThemes.includes(themeName)) {
|
||||
// already favourite, remove
|
||||
UpdateConfig.setFavThemes(Config.favThemes.filter((t) => t !== themeName));
|
||||
|
|
@ -170,21 +220,46 @@ function toggleFavourite(themeName: string): void {
|
|||
refreshButtons();
|
||||
}
|
||||
|
||||
export function updateActiveTab(): void {
|
||||
$(".pageSettings .section.themes .tabs .button").removeClass("active");
|
||||
if (!Config.customTheme) {
|
||||
$(".pageSettings .section.themes .tabs .button[tab='preset']").addClass(
|
||||
"active"
|
||||
);
|
||||
export function saveCustomThemeColors(): void {
|
||||
const newColors: string[] = [];
|
||||
$.each(
|
||||
$(".pageSettings .customTheme .customThemeEdit [type='color']"),
|
||||
(_index, element) => {
|
||||
newColors.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
UpdateConfig.setCustomThemeColors(newColors);
|
||||
Notifications.add("Custom theme saved", 1);
|
||||
}
|
||||
|
||||
export function updateActiveTab(forced = false): void {
|
||||
// Set force to true only when some change for the active tab has taken place
|
||||
// Prevent theme buttons from being added twice by doing an update only when the state has changed
|
||||
const $presetTabButton = $(
|
||||
".pageSettings .section.themes .tabs .button[tab='preset']"
|
||||
);
|
||||
const $customTabButton = $(
|
||||
".pageSettings .section.themes .tabs .button[tab='custom']"
|
||||
);
|
||||
|
||||
if (Config.customTheme) {
|
||||
$presetTabButton.removeClass("active");
|
||||
if (!$customTabButton.hasClass("active") || forced) {
|
||||
$customTabButton.addClass("active");
|
||||
refreshButtons();
|
||||
}
|
||||
} else {
|
||||
$(".pageSettings .section.themes .tabs .button[tab='custom']").addClass(
|
||||
"active"
|
||||
);
|
||||
$customTabButton.removeClass("active");
|
||||
if (!$presetTabButton.hasClass("active") || forced) {
|
||||
$presetTabButton.addClass("active");
|
||||
refreshButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add events to the DOM
|
||||
|
||||
// Handle click on theme: preset or custom tab
|
||||
$(".pageSettings .section.themes .tabs .button").on("click", (e) => {
|
||||
$(".pageSettings .section.themes .tabs .button").removeClass("active");
|
||||
const $target = $(e.currentTarget);
|
||||
|
|
@ -197,6 +272,20 @@ $(".pageSettings .section.themes .tabs .button").on("click", (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Handle click on custom theme button
|
||||
$(document).on(
|
||||
"click",
|
||||
".pageSettings .section.themes .customTheme.button",
|
||||
(e) => {
|
||||
// Do not apply if user wanted to delete it
|
||||
if ($(e.target).hasClass("delButton")) return;
|
||||
if ($(e.target).hasClass("editButton")) return;
|
||||
const customThemeId = $(e.currentTarget).attr("customThemeId") ?? "";
|
||||
ThemeController.set(customThemeId, true);
|
||||
}
|
||||
);
|
||||
|
||||
// Handle click on favorite preset theme button
|
||||
$(document).on(
|
||||
"click",
|
||||
".pageSettings .section.themes .theme .favButton",
|
||||
|
|
@ -210,6 +299,7 @@ $(document).on(
|
|||
}
|
||||
);
|
||||
|
||||
// Handle click on preset theme button
|
||||
$(document).on("click", ".pageSettings .section.themes .theme.button", (e) => {
|
||||
const theme = $(e.currentTarget).attr("theme");
|
||||
if (!$(e.target).hasClass("favButton") && theme !== undefined) {
|
||||
|
|
@ -238,12 +328,14 @@ $(
|
|||
|
||||
$(".pageSettings .section.themes .tabContainer .customTheme input[type=text]")
|
||||
.on("blur", (e) => {
|
||||
if (e.target.id === "name") return;
|
||||
const $colorVar = $(e.currentTarget).attr("id") as string;
|
||||
const $pickedColor = $(e.currentTarget).val() as string;
|
||||
|
||||
updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor);
|
||||
})
|
||||
.on("keypress", function (e) {
|
||||
if (e.target.id === "name") return;
|
||||
if (e.code === "Enter") {
|
||||
$(this).attr("disabled", "disabled");
|
||||
const $colorVar = $(e.currentTarget).attr("id") as string;
|
||||
|
|
@ -254,19 +346,6 @@ $(".pageSettings .section.themes .tabContainer .customTheme input[type=text]")
|
|||
}
|
||||
});
|
||||
|
||||
$(".pageSettings .saveCustomThemeButton").on("click", () => {
|
||||
const save: Array<string> = [];
|
||||
$.each(
|
||||
$(".pageSettings .section.customTheme [type='color']"),
|
||||
(_index, element) => {
|
||||
save.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
UpdateConfig.setCustomThemeColors(save);
|
||||
ThemeController.set("custom");
|
||||
Notifications.add("Custom theme colors saved", 1);
|
||||
});
|
||||
|
||||
$(".pageSettings #loadCustomColorsFromPreset").on("click", () => {
|
||||
// previewTheme(Config.theme);
|
||||
$("#currentTheme").attr("href", `themes/${Config.theme}.css`);
|
||||
|
|
@ -306,3 +385,50 @@ $(".pageSettings #loadCustomColorsFromPreset").on("click", () => {
|
|||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Handles click on share custom theme button
|
||||
$("#shareCustomThemeButton").on("click", () => {
|
||||
const share: string[] = [];
|
||||
$.each(
|
||||
$(".pageSettings .customTheme .customThemeEdit [type='color']"),
|
||||
(_, element) => {
|
||||
share.push($(element).attr("value") as string);
|
||||
}
|
||||
);
|
||||
|
||||
const url =
|
||||
"https://monkeytype.com?customTheme=" + btoa(JSON.stringify(share));
|
||||
|
||||
navigator.clipboard.writeText(url).then(
|
||||
function () {
|
||||
Notifications.add("URL Copied to clipboard", 0);
|
||||
},
|
||||
function () {
|
||||
CustomThemePopup.show(url);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".pageSettings .saveCustomThemeButton").on("click", async () => {
|
||||
saveCustomThemeColors();
|
||||
if (firebase.auth().currentUser) {
|
||||
const newCustomTheme = {
|
||||
name: "custom",
|
||||
colors: Config.customThemeColors,
|
||||
};
|
||||
|
||||
Loader.show();
|
||||
const response = await DB.addCustomTheme(newCustomTheme);
|
||||
Loader.hide();
|
||||
if (response) {
|
||||
updateActiveTab(true);
|
||||
}
|
||||
} else {
|
||||
updateActiveTab(true);
|
||||
}
|
||||
});
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (eventKey === "customThemeId") refreshButtons();
|
||||
// if (eventKey === "customTheme") updateActiveTab();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -404,8 +404,8 @@ export function restart(
|
|||
if (TestUI.resultVisible) {
|
||||
if (
|
||||
Config.randomTheme !== "off" &&
|
||||
!PageTransition.get() &&
|
||||
!Config.customTheme
|
||||
!PageTransition.get()
|
||||
// && Config.customThemeId === ""
|
||||
) {
|
||||
ThemeController.randomizeTheme();
|
||||
}
|
||||
|
|
|
|||
12
frontend/src/scripts/types/types.d.ts
vendored
12
frontend/src/scripts/types/types.d.ts
vendored
|
|
@ -50,7 +50,7 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type TimerStyle = "bar" | "text" | "mini";
|
||||
|
||||
type RandomTheme = "off" | "on" | "fav" | "light" | "dark";
|
||||
type RandomTheme = "off" | "on" | "fav" | "light" | "dark" | "custom";
|
||||
|
||||
type TimerColor = "black" | "sub" | "text" | "main";
|
||||
|
||||
|
|
@ -188,6 +188,15 @@ declare namespace MonkeyTypes {
|
|||
active?: boolean;
|
||||
}
|
||||
|
||||
interface RawCustomTheme {
|
||||
name: string;
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
interface CustomTheme extends RawCustomTheme {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
time: number;
|
||||
started: number;
|
||||
|
|
@ -407,6 +416,7 @@ declare namespace MonkeyTypes {
|
|||
verified?: boolean;
|
||||
personalBests?: PersonalBests;
|
||||
name?: string;
|
||||
customThemes: CustomTheme[];
|
||||
presets?: Preset[];
|
||||
tags?: Tag[];
|
||||
favouriteThemes?: string[];
|
||||
|
|
|
|||
40
frontend/src/scripts/utils/url-handler.ts
Normal file
40
frontend/src/scripts/utils/url-handler.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import * as Misc from "../misc";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
|
||||
export function loadCustomThemeFromUrl(): void {
|
||||
const getValue = Misc.findGetParameter("customTheme");
|
||||
if (getValue === null) return;
|
||||
|
||||
const urlEncoded = getValue.split(",");
|
||||
let base64decoded = null;
|
||||
try {
|
||||
base64decoded = JSON.parse(atob(getValue) ?? "");
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
let colorArray = [];
|
||||
if (Array.isArray(urlEncoded) && urlEncoded.length === 9) {
|
||||
colorArray = urlEncoded;
|
||||
} else if (Array.isArray(base64decoded) && base64decoded.length === 9) {
|
||||
colorArray = base64decoded;
|
||||
}
|
||||
|
||||
if (colorArray.length === 0)
|
||||
return Notifications.add("Invalid custom theme ", 0);
|
||||
|
||||
const oldCustomTheme = Config.customTheme;
|
||||
const oldCustomThemeColors = Config.customThemeColors;
|
||||
try {
|
||||
UpdateConfig.setCustomThemeColors(colorArray);
|
||||
Notifications.add("Custom theme applied", 1);
|
||||
|
||||
if (!Config.customTheme) UpdateConfig.setCustomTheme(true);
|
||||
} catch (e) {
|
||||
Notifications.add("Something went wrong. Reverting to previous state.", 0);
|
||||
console.error(e);
|
||||
UpdateConfig.setCustomTheme(oldCustomTheme);
|
||||
UpdateConfig.setCustomThemeColors(oldCustomThemeColors);
|
||||
}
|
||||
}
|
||||
|
|
@ -1049,3 +1049,14 @@
|
|||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
#customThemesWrapper {
|
||||
#customThemesEdit {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,10 +131,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.customTheme {
|
||||
& .allCustomThemes.buttons {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
& .customThemeEdit {
|
||||
grid-row: 3;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
justify-items: stretch;
|
||||
gap: 0.5rem 2rem;
|
||||
gap: 0.5rem 1rem;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
place-content: center start;
|
||||
}
|
||||
|
||||
& p {
|
||||
grid-area: unset;
|
||||
|
|
@ -294,6 +311,7 @@
|
|||
height: auto;
|
||||
|
||||
&.customTheme {
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 1rem;
|
||||
.colorText {
|
||||
color: var(--text-color);
|
||||
|
|
@ -307,25 +325,6 @@
|
|||
}
|
||||
|
||||
.theme.button {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
.text {
|
||||
color: inherit;
|
||||
}
|
||||
.activeIndicator {
|
||||
overflow: hidden;
|
||||
width: 1.25rem;
|
||||
transition: 0.25s;
|
||||
opacity: 0;
|
||||
color: inherit;
|
||||
.far {
|
||||
margin: 0;
|
||||
}
|
||||
&.active {
|
||||
width: 1.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.favButton {
|
||||
overflow: hidden;
|
||||
width: 1.25rem;
|
||||
|
|
@ -350,6 +349,62 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customTheme.button {
|
||||
.delButton,
|
||||
.editButton {
|
||||
overflow: hidden;
|
||||
width: 1.25rem;
|
||||
transition: 0.25s;
|
||||
opacity: 0;
|
||||
.far,
|
||||
.fas {
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.active {
|
||||
width: 1.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
.editButton {
|
||||
width: 1.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
.delButton {
|
||||
width: 1.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customTheme.button,
|
||||
.theme.button {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
.text {
|
||||
color: inherit;
|
||||
}
|
||||
.activeIndicator {
|
||||
overflow: hidden;
|
||||
width: 1.25rem;
|
||||
transition: 0.25s;
|
||||
opacity: 0;
|
||||
color: inherit;
|
||||
.far {
|
||||
margin: 0;
|
||||
}
|
||||
&.active {
|
||||
width: 1.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.activeIndicator {
|
||||
opacity: 1;
|
||||
|
|
@ -405,7 +460,7 @@
|
|||
}
|
||||
|
||||
&.randomTheme .buttons {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -642,6 +642,12 @@
|
|||
<div class="button"><i class="fas fa-plus"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="customThemesWrapper" class="popupWrapper hidden">
|
||||
<div id="customThemesEdit" action="" customThemeId="">
|
||||
<div class="title"></div>
|
||||
<div class="button"><i class="fas fa-plus"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resultEditTagsPanelWrapper" class="popupWrapper hidden">
|
||||
<div id="resultEditTagsPanel" resultid="">
|
||||
<div class="buttons"></div>
|
||||
|
|
@ -3604,7 +3610,8 @@
|
|||
one. The random themes are not saved to your config. If set to
|
||||
'favorite' only favorite themes will be randomized. If set to
|
||||
'light' or 'dark', only presets with light or dark background
|
||||
colors will be randomized, respectively.
|
||||
colors will be randomized, respectively. If set to 'custom',
|
||||
custom themes will be randomized.
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
|
|
@ -3647,6 +3654,14 @@
|
|||
>
|
||||
dark
|
||||
</div>
|
||||
<div
|
||||
class="button"
|
||||
randomTheme="custom"
|
||||
tabindex="0"
|
||||
onclick="this.blur();"
|
||||
>
|
||||
custom
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section themes fullWidth">
|
||||
|
|
@ -3680,203 +3695,208 @@
|
|||
<div class="tabContainer">
|
||||
<div
|
||||
tabContent="custom"
|
||||
class="tabContent section customTheme hidden"
|
||||
class="tabContent customTheme hidden"
|
||||
>
|
||||
<label class="colorText">background</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--bg-color-txt"
|
||||
/>
|
||||
<label
|
||||
for="--bg-color"
|
||||
class="button"
|
||||
<div class="allCustomThemes buttons"></div>
|
||||
<div class="customThemeEdit">
|
||||
<label class="colorText">background</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--bg-color-txt"
|
||||
/>
|
||||
<label
|
||||
for="--bg-color"
|
||||
class="button"
|
||||
style="
|
||||
color: var(--text-color);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
"
|
||||
>
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--bg-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">main</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--main-color-txt"
|
||||
/>
|
||||
<label for="--main-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--main-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">caret</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--caret-color-txt"
|
||||
/>
|
||||
<label for="--caret-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--caret-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">sub</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--sub-color-txt"
|
||||
/>
|
||||
<label for="--sub-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--sub-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">text</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--text-color-txt"
|
||||
/>
|
||||
<label for="--text-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--text-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<label class="colorText">error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--error-color-txt"
|
||||
/>
|
||||
<label for="--error-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--error-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">extra error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--error-extra-color-txt"
|
||||
/>
|
||||
<label for="--error-extra-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--error-extra-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>colorful mode</p>
|
||||
|
||||
<label class="colorText">error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--colorful-error-color-txt"
|
||||
/>
|
||||
<label for="--colorful-error-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--colorful-error-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="colorText">extra error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--colorful-error-extra-color-txt"
|
||||
/>
|
||||
<label
|
||||
for="--colorful-error-extra-color"
|
||||
class="button"
|
||||
>
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--colorful-error-extra-color"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
color: var(--text-color);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-column: 1/5;
|
||||
gap: 0.5rem;
|
||||
"
|
||||
>
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--bg-color"
|
||||
/>
|
||||
<div class="button" id="loadCustomColorsFromPreset">
|
||||
load from preset
|
||||
</div>
|
||||
<div class="button" id="shareCustomThemeButton">
|
||||
share
|
||||
</div>
|
||||
<div class="button saveCustomThemeButton">
|
||||
save as new
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="colorText">main</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--main-color-txt"
|
||||
/>
|
||||
<label for="--main-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--main-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">caret</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--caret-color-txt"
|
||||
/>
|
||||
<label for="--caret-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--caret-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">sub</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--sub-color-txt"
|
||||
/>
|
||||
<label for="--sub-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--sub-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">text</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--text-color-txt"
|
||||
/>
|
||||
<label for="--text-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--text-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<label class="colorText">error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--error-color-txt"
|
||||
/>
|
||||
<label for="--error-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--error-color"
|
||||
/>
|
||||
</div>
|
||||
<label class="colorText">extra error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--error-extra-color-txt"
|
||||
/>
|
||||
<label for="--error-extra-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--error-extra-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>colorful mode</p>
|
||||
|
||||
<label class="colorText">error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--colorful-error-color-txt"
|
||||
/>
|
||||
<label for="--colorful-error-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--colorful-error-color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="colorText">extra error</label>
|
||||
<div class="colorPicker inputAndButton">
|
||||
<input
|
||||
type="text"
|
||||
value="#000000"
|
||||
class="input"
|
||||
id="--colorful-error-extra-color-txt"
|
||||
/>
|
||||
<label for="--colorful-error-extra-color" class="button">
|
||||
<i class="fas fa-fw fa-palette"></i>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
value="#000000"
|
||||
id="--colorful-error-extra-color"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div
|
||||
style="
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
grid-auto-flow: column;
|
||||
grid-column: 1/5;
|
||||
"
|
||||
> -->
|
||||
<div
|
||||
class="button"
|
||||
id="loadCustomColorsFromPreset"
|
||||
style="grid-column: 1/3"
|
||||
>
|
||||
load from preset
|
||||
</div>
|
||||
|
||||
<div class="button" id="shareCustomThemeButton">share</div>
|
||||
<div class="button saveCustomThemeButton">save</div>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
<div tabContent="preset" class="tabContent">
|
||||
<div class="favThemes buttons"></div>
|
||||
|
|
|
|||
|
|
@ -54,11 +54,7 @@
|
|||
},
|
||||
{
|
||||
"name": "afrikaans",
|
||||
"languages": [
|
||||
"afrikaans",
|
||||
"afrikaans_1k",
|
||||
"afrikaans_10k"
|
||||
]
|
||||
"languages": ["afrikaans", "afrikaans_1k", "afrikaans_10k"]
|
||||
},
|
||||
{
|
||||
"name": "mongolian",
|
||||
|
|
|
|||
|
|
@ -174,4 +174,3 @@
|
|||
"#[test]"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,209 +1,209 @@
|
|||
{
|
||||
"name": "nepali",
|
||||
"leftToRight": true,
|
||||
"ligatures": true,
|
||||
"noLazyMode": true,
|
||||
"bcp47": "ne-NP",
|
||||
"words": [
|
||||
"अघि",
|
||||
"अथवा",
|
||||
"अनि",
|
||||
"अनिरुद्र",
|
||||
"अनुभव",
|
||||
"अन्तमा",
|
||||
"अभियान",
|
||||
"अमूल्य",
|
||||
"अरू",
|
||||
"अवधि",
|
||||
"अवस्था",
|
||||
"अविनवन",
|
||||
"असलपन",
|
||||
"आम्दानी",
|
||||
"आयोजना",
|
||||
"आर्यब",
|
||||
"आविष्कार",
|
||||
"आशिश",
|
||||
"उत्साह",
|
||||
"उपलब्ध",
|
||||
"उपायले",
|
||||
"एक",
|
||||
"एक्लै",
|
||||
"ओछ्यान",
|
||||
"कथा",
|
||||
"कपाल",
|
||||
"कपास",
|
||||
"कम्पनी",
|
||||
"काँध",
|
||||
"काट्ने",
|
||||
"काठमाडौँ",
|
||||
"काम",
|
||||
"किन",
|
||||
"किनेको",
|
||||
"कोट",
|
||||
"खल्ती",
|
||||
"खानेकुरा",
|
||||
"खासगरि",
|
||||
"खेतीयोग्य",
|
||||
"ख्याल",
|
||||
"गए",
|
||||
"गएर",
|
||||
"गति",
|
||||
"गरे",
|
||||
"गर्नु",
|
||||
"गाईवस्तुका",
|
||||
"घटना",
|
||||
"घटाउनु",
|
||||
"चिट्ठी",
|
||||
"चिसोपन",
|
||||
"छ",
|
||||
"छाडेका",
|
||||
"छिट्टै",
|
||||
"छेउछाउ",
|
||||
"जंगल",
|
||||
"जनता",
|
||||
"जन्म",
|
||||
"जरा",
|
||||
"जिम्मेवारी",
|
||||
"जूता",
|
||||
"जो",
|
||||
"जोगाउन",
|
||||
"टोपी",
|
||||
"ठाउँ",
|
||||
"त",
|
||||
"तर",
|
||||
"तालिम",
|
||||
"ती",
|
||||
"त्यसपछि",
|
||||
"त्यसले",
|
||||
"त्यसैले",
|
||||
"थान",
|
||||
"थाल्यो",
|
||||
"थुप्रै",
|
||||
"दश",
|
||||
"दाउरा",
|
||||
"दाबी",
|
||||
"दिन",
|
||||
"दुईवटा",
|
||||
"न",
|
||||
"नजिकैा",
|
||||
"नमस्कार",
|
||||
"नरहेपछि",
|
||||
"नर्सरी",
|
||||
"नसक्ने",
|
||||
"निकेश",
|
||||
"निर्धारित",
|
||||
"नेतृत्व",
|
||||
"नेपाल",
|
||||
"नै",
|
||||
"पछि",
|
||||
"पत्नी",
|
||||
"पनि",
|
||||
"पन्ध्र",
|
||||
"परिवर्तन",
|
||||
"परेवा",
|
||||
"पहिले",
|
||||
"पाउनु",
|
||||
"पिच",
|
||||
"पुरस्कार",
|
||||
"पुस्तक",
|
||||
"पृथ्वी",
|
||||
"पौडी",
|
||||
"प्रत्येक",
|
||||
"प्रदान",
|
||||
"प्रयास",
|
||||
"प्रयोग",
|
||||
"प्रस्ताव",
|
||||
"फाइदा",
|
||||
"फैलियो",
|
||||
"बढी",
|
||||
"बढीको",
|
||||
"बन्न",
|
||||
"बर्खा",
|
||||
"बाहिर",
|
||||
"बिनिसा",
|
||||
"बिप्लप",
|
||||
"बिरूवा",
|
||||
"बिहान",
|
||||
"बीजहरू",
|
||||
"बेचन",
|
||||
"भए",
|
||||
"भएको",
|
||||
"भनि",
|
||||
"भयो",
|
||||
"भाग",
|
||||
"भाग्य",
|
||||
"भात",
|
||||
"भाइ",
|
||||
"भिजाउने",
|
||||
"म",
|
||||
"मन",
|
||||
"मरूभूमि",
|
||||
"मरे",
|
||||
"मन्दिर",
|
||||
"मह",
|
||||
"महादेशमा",
|
||||
"महिना",
|
||||
"महिला",
|
||||
"माटो",
|
||||
"माटोको",
|
||||
"मिल्ने",
|
||||
"याम",
|
||||
"यी",
|
||||
"यो",
|
||||
"र",
|
||||
"रकम",
|
||||
"रखेदेख",
|
||||
"रमाइलो",
|
||||
"राष्ट्रिय",
|
||||
"रिस",
|
||||
"रूख",
|
||||
"रोपेका",
|
||||
"रोपेको",
|
||||
"लक्ष्य",
|
||||
"लगाउने",
|
||||
"लाख",
|
||||
"लागि",
|
||||
"ल्याइए",
|
||||
"ल्याएर",
|
||||
"वन",
|
||||
"वरिपरि",
|
||||
"विद्यालय",
|
||||
"विपरीत",
|
||||
"विभाग",
|
||||
"विशेष",
|
||||
"वेहोरेर",
|
||||
"व्यक्ति",
|
||||
"व्यवस्था",
|
||||
"व्यवस्थापन",
|
||||
"शिविर",
|
||||
"शुभम",
|
||||
"सक्छन्",
|
||||
"सक्दैन",
|
||||
"सज्जा",
|
||||
"सफलता",
|
||||
"सबै",
|
||||
"समय",
|
||||
"समाज",
|
||||
"समूह",
|
||||
"सम्झौता",
|
||||
"सम्माना",
|
||||
"सय",
|
||||
"साँझ",
|
||||
"साथ",
|
||||
"साधारण",
|
||||
"सानै",
|
||||
"सामूहिक",
|
||||
"सार्वजनिक",
|
||||
"सुख्खा",
|
||||
"सुधार्न",
|
||||
"सुरूमा",
|
||||
"सौन्दर्य",
|
||||
"स्याहार",
|
||||
"हाम्रो",
|
||||
"हावाहुरी",
|
||||
"हुन",
|
||||
"हुर्किने",
|
||||
"हेरचाह",
|
||||
"हैन"
|
||||
]
|
||||
}
|
||||
"name": "nepali",
|
||||
"leftToRight": true,
|
||||
"ligatures": true,
|
||||
"noLazyMode": true,
|
||||
"bcp47": "ne-NP",
|
||||
"words": [
|
||||
"अघि",
|
||||
"अथवा",
|
||||
"अनि",
|
||||
"अनिरुद्र",
|
||||
"अनुभव",
|
||||
"अन्तमा",
|
||||
"अभियान",
|
||||
"अमूल्य",
|
||||
"अरू",
|
||||
"अवधि",
|
||||
"अवस्था",
|
||||
"अविनवन",
|
||||
"असलपन",
|
||||
"आम्दानी",
|
||||
"आयोजना",
|
||||
"आर्यब",
|
||||
"आविष्कार",
|
||||
"आशिश",
|
||||
"उत्साह",
|
||||
"उपलब्ध",
|
||||
"उपायले",
|
||||
"एक",
|
||||
"एक्लै",
|
||||
"ओछ्यान",
|
||||
"कथा",
|
||||
"कपाल",
|
||||
"कपास",
|
||||
"कम्पनी",
|
||||
"काँध",
|
||||
"काट्ने",
|
||||
"काठमाडौँ",
|
||||
"काम",
|
||||
"किन",
|
||||
"किनेको",
|
||||
"कोट",
|
||||
"खल्ती",
|
||||
"खानेकुरा",
|
||||
"खासगरि",
|
||||
"खेतीयोग्य",
|
||||
"ख्याल",
|
||||
"गए",
|
||||
"गएर",
|
||||
"गति",
|
||||
"गरे",
|
||||
"गर्नु",
|
||||
"गाईवस्तुका",
|
||||
"घटना",
|
||||
"घटाउनु",
|
||||
"चिट्ठी",
|
||||
"चिसोपन",
|
||||
"छ",
|
||||
"छाडेका",
|
||||
"छिट्टै",
|
||||
"छेउछाउ",
|
||||
"जंगल",
|
||||
"जनता",
|
||||
"जन्म",
|
||||
"जरा",
|
||||
"जिम्मेवारी",
|
||||
"जूता",
|
||||
"जो",
|
||||
"जोगाउन",
|
||||
"टोपी",
|
||||
"ठाउँ",
|
||||
"त",
|
||||
"तर",
|
||||
"तालिम",
|
||||
"ती",
|
||||
"त्यसपछि",
|
||||
"त्यसले",
|
||||
"त्यसैले",
|
||||
"थान",
|
||||
"थाल्यो",
|
||||
"थुप्रै",
|
||||
"दश",
|
||||
"दाउरा",
|
||||
"दाबी",
|
||||
"दिन",
|
||||
"दुईवटा",
|
||||
"न",
|
||||
"नजिकैा",
|
||||
"नमस्कार",
|
||||
"नरहेपछि",
|
||||
"नर्सरी",
|
||||
"नसक्ने",
|
||||
"निकेश",
|
||||
"निर्धारित",
|
||||
"नेतृत्व",
|
||||
"नेपाल",
|
||||
"नै",
|
||||
"पछि",
|
||||
"पत्नी",
|
||||
"पनि",
|
||||
"पन्ध्र",
|
||||
"परिवर्तन",
|
||||
"परेवा",
|
||||
"पहिले",
|
||||
"पाउनु",
|
||||
"पिच",
|
||||
"पुरस्कार",
|
||||
"पुस्तक",
|
||||
"पृथ्वी",
|
||||
"पौडी",
|
||||
"प्रत्येक",
|
||||
"प्रदान",
|
||||
"प्रयास",
|
||||
"प्रयोग",
|
||||
"प्रस्ताव",
|
||||
"फाइदा",
|
||||
"फैलियो",
|
||||
"बढी",
|
||||
"बढीको",
|
||||
"बन्न",
|
||||
"बर्खा",
|
||||
"बाहिर",
|
||||
"बिनिसा",
|
||||
"बिप्लप",
|
||||
"बिरूवा",
|
||||
"बिहान",
|
||||
"बीजहरू",
|
||||
"बेचन",
|
||||
"भए",
|
||||
"भएको",
|
||||
"भनि",
|
||||
"भयो",
|
||||
"भाग",
|
||||
"भाग्य",
|
||||
"भात",
|
||||
"भाइ",
|
||||
"भिजाउने",
|
||||
"म",
|
||||
"मन",
|
||||
"मरूभूमि",
|
||||
"मरे",
|
||||
"मन्दिर",
|
||||
"मह",
|
||||
"महादेशमा",
|
||||
"महिना",
|
||||
"महिला",
|
||||
"माटो",
|
||||
"माटोको",
|
||||
"मिल्ने",
|
||||
"याम",
|
||||
"यी",
|
||||
"यो",
|
||||
"र",
|
||||
"रकम",
|
||||
"रखेदेख",
|
||||
"रमाइलो",
|
||||
"राष्ट्रिय",
|
||||
"रिस",
|
||||
"रूख",
|
||||
"रोपेका",
|
||||
"रोपेको",
|
||||
"लक्ष्य",
|
||||
"लगाउने",
|
||||
"लाख",
|
||||
"लागि",
|
||||
"ल्याइए",
|
||||
"ल्याएर",
|
||||
"वन",
|
||||
"वरिपरि",
|
||||
"विद्यालय",
|
||||
"विपरीत",
|
||||
"विभाग",
|
||||
"विशेष",
|
||||
"वेहोरेर",
|
||||
"व्यक्ति",
|
||||
"व्यवस्था",
|
||||
"व्यवस्थापन",
|
||||
"शिविर",
|
||||
"शुभम",
|
||||
"सक्छन्",
|
||||
"सक्दैन",
|
||||
"सज्जा",
|
||||
"सफलता",
|
||||
"सबै",
|
||||
"समय",
|
||||
"समाज",
|
||||
"समूह",
|
||||
"सम्झौता",
|
||||
"सम्माना",
|
||||
"सय",
|
||||
"साँझ",
|
||||
"साथ",
|
||||
"साधारण",
|
||||
"सानै",
|
||||
"सामूहिक",
|
||||
"सार्वजनिक",
|
||||
"सुख्खा",
|
||||
"सुधार्न",
|
||||
"सुरूमा",
|
||||
"सौन्दर्य",
|
||||
"स्याहार",
|
||||
"हाम्रो",
|
||||
"हावाहुरी",
|
||||
"हुन",
|
||||
"हुर्किने",
|
||||
"हेरचाह",
|
||||
"हैन"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -3038,88 +3038,88 @@
|
|||
"id": 510
|
||||
},
|
||||
{
|
||||
"text": "Da sprach die Fee zum Hexer: \"Solchen Rat geb ich dir: Zieh eiserne Stiefel an, nimm einen eisernen Wanderstab. Geh in den eisernen Stiefeln bis ans Ende der Welt, den Weg vor dir aber sollst du mit dem Stab ertasten und mit deinen Tränen netzen. Geh durch Feuer und Wasser, halte nicht inne, schau nicht zurück. Wenn aber die Sohlen durchgelaufen sind, wenn der eiserne Stab verschlissen ist, wenn deine Augen von Wind und Hitze so trocken geworden sind, dass keine Träne mehr hervorquillt, dann wirst du am Ende der Welt das finden, was du suchst, und das, was du liebst. Vielleicht.\" Und der Hexer ging durch Feuer und Wasser, schaute niemals zurück. Doch er nahm weder eiserne Stiefel mit noch einen Wanderstab. Nur sein Hexerschwert nahm er mit. Er hörte nicht auf die Worte der Fee. Und er tat gut daran, denn es war eine böse Fee.",
|
||||
"source": "Geralt-Saga – Feuertaufe",
|
||||
"length": 837,
|
||||
"id": 511
|
||||
"text": "Da sprach die Fee zum Hexer: \"Solchen Rat geb ich dir: Zieh eiserne Stiefel an, nimm einen eisernen Wanderstab. Geh in den eisernen Stiefeln bis ans Ende der Welt, den Weg vor dir aber sollst du mit dem Stab ertasten und mit deinen Tränen netzen. Geh durch Feuer und Wasser, halte nicht inne, schau nicht zurück. Wenn aber die Sohlen durchgelaufen sind, wenn der eiserne Stab verschlissen ist, wenn deine Augen von Wind und Hitze so trocken geworden sind, dass keine Träne mehr hervorquillt, dann wirst du am Ende der Welt das finden, was du suchst, und das, was du liebst. Vielleicht.\" Und der Hexer ging durch Feuer und Wasser, schaute niemals zurück. Doch er nahm weder eiserne Stiefel mit noch einen Wanderstab. Nur sein Hexerschwert nahm er mit. Er hörte nicht auf die Worte der Fee. Und er tat gut daran, denn es war eine böse Fee.",
|
||||
"source": "Geralt-Saga – Feuertaufe",
|
||||
"length": 837,
|
||||
"id": 511
|
||||
},
|
||||
{
|
||||
"text": "Ich habe in meinem Leben viele Militärs kennengelernt. Ich kannte Marschälle, Generäle, Heergrafen und Hetmane, die Triumphatoren zahlreicher Feldzüge und Schlachten. Ich habe mir ihre Erzählungen und Erinnerungen angehört. Ich habe sie über Karten gebeugt gesehen, wie sie darauf verschiedenfarbige Striche zeichneten, Pläne machten, die Strategie bedachten. In diesem Krieg auf dem Papier passte alles, alles funktionierte, alles war klar und in vorbildlicher Ordnung. So muss es sein, erklärten die Militärs. Eine Armee heißt ja vor allem Ordnung und Disziplin. Umso erstaunlicher ist es, dass der wirkliche Krieg - und ein paar wirkliche Kriege habe ich gesehen – im Hinblick auf Ordnung und Disziplin einem in Flammen stehenden Bordell zum Verwechseln ähnlich sieht.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 771,
|
||||
"id": 512
|
||||
"text": "Ich habe in meinem Leben viele Militärs kennengelernt. Ich kannte Marschälle, Generäle, Heergrafen und Hetmane, die Triumphatoren zahlreicher Feldzüge und Schlachten. Ich habe mir ihre Erzählungen und Erinnerungen angehört. Ich habe sie über Karten gebeugt gesehen, wie sie darauf verschiedenfarbige Striche zeichneten, Pläne machten, die Strategie bedachten. In diesem Krieg auf dem Papier passte alles, alles funktionierte, alles war klar und in vorbildlicher Ordnung. So muss es sein, erklärten die Militärs. Eine Armee heißt ja vor allem Ordnung und Disziplin. Umso erstaunlicher ist es, dass der wirkliche Krieg - und ein paar wirkliche Kriege habe ich gesehen – im Hinblick auf Ordnung und Disziplin einem in Flammen stehenden Bordell zum Verwechseln ähnlich sieht.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 771,
|
||||
"id": 512
|
||||
},
|
||||
{
|
||||
"text": "Ich danke dir für die anerkennenden Worte über die Dichter und die Dichtkunst. Und für die Belehrung in Sachen Bogenschießen. Eine gute Waffe ist das, der Bogen. Wisst ihr was? Ich glaube, dass sich die Kriegskunstgenau in diese Richtung entwickeln wird. In den künftigen Kriegen wird man auf Distanz kämpfen. Man wird eine Waffe mit so großer Reichweite erfinden, dass die Gegner einander umbringen können, ohne sich auch nur zu sehen.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 436,
|
||||
"id": 513
|
||||
"text": "Ich danke dir für die anerkennenden Worte über die Dichter und die Dichtkunst. Und für die Belehrung in Sachen Bogenschießen. Eine gute Waffe ist das, der Bogen. Wisst ihr was? Ich glaube, dass sich die Kriegskunstgenau in diese Richtung entwickeln wird. In den künftigen Kriegen wird man auf Distanz kämpfen. Man wird eine Waffe mit so großer Reichweite erfinden, dass die Gegner einander umbringen können, ohne sich auch nur zu sehen.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 436,
|
||||
"id": 513
|
||||
},
|
||||
{
|
||||
"text": "Vampir, auch Wurdulak, heißet ein toter Mensch, so vom Chaos wieder zum Leben erweckt ward. Wiewohl er das erste Leben verloren hat, gewinnet er das zweite des Nachts. Er verlässt den Sarg beim Lichte des Mondes, und kann allein dahin gehen, wohin des Mondes Strahlen ihn führen; er kommet über schlafende Jungfern oder junge Bauernknechte und sauget, sonder sie zu wecken, ihr süßes Blut.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 389,
|
||||
"id": 514
|
||||
"text": "Vampir, auch Wurdulak, heißet ein toter Mensch, so vom Chaos wieder zum Leben erweckt ward. Wiewohl er das erste Leben verloren hat, gewinnet er das zweite des Nachts. Er verlässt den Sarg beim Lichte des Mondes, und kann allein dahin gehen, wohin des Mondes Strahlen ihn führen; er kommet über schlafende Jungfern oder junge Bauernknechte und sauget, sonder sie zu wecken, ihr süßes Blut.",
|
||||
"source": "Geralt-Saga - Feuertaufe",
|
||||
"length": 389,
|
||||
"id": 514
|
||||
},
|
||||
{
|
||||
"text": "Wer Blut vergossen hat und Blut getrunken hat wird mit Blut bezahlen. Keine drei Tage vergehen, dann wird eins im anderen sterben, und dann stirbt etwas in jedem. Langsam werden sie sterben, Stück um Stück… Und wenn am Ende die eisernen Stiefel durchgelaufen sind und die Tränen trocken, dann stirbt das wenige, was bleibt. Es stirbt sogar das, was niemals stirbt.",
|
||||
"source": "Geralt-Saga - Feuertaufe - Weissagung",
|
||||
"length": 364,
|
||||
"id": 515
|
||||
"text": "Wer Blut vergossen hat und Blut getrunken hat wird mit Blut bezahlen. Keine drei Tage vergehen, dann wird eins im anderen sterben, und dann stirbt etwas in jedem. Langsam werden sie sterben, Stück um Stück… Und wenn am Ende die eisernen Stiefel durchgelaufen sind und die Tränen trocken, dann stirbt das wenige, was bleibt. Es stirbt sogar das, was niemals stirbt.",
|
||||
"source": "Geralt-Saga - Feuertaufe - Weissagung",
|
||||
"length": 364,
|
||||
"id": 515
|
||||
},
|
||||
{
|
||||
"text": "Geralt löste den Geldbeutel vom Gürtel, hielt ihn bei den Riemen und wog ihn in der Hand. \"Du kannst mich nicht bestechen\", erklärte der Zerberus stolz. \"Das habe ich nicht vor.\" Der Türhüter war zu massig, als dass seine Reflexe ihm erlaubt hätten, sich gegen den raschen Schlag eines gewöhnlichen Menschen zu decken. Vor dem Schlag eines Hexers bekam er nicht einmal die Augen zu. Mit metallischem Klang donnerte ihm der Beutel an die Schläfe. Er stürzte gegen die Tür und hielt sich mit beiden Händen am Rahmen. Geralt riss ihn davon mit einem Tritt ins Knie los, stieß mit der Schulter zu und benutzte nochmals den Geldbeutel. Die Augen des Türstehers trübten sich und liefen zu einem urkomischen Schielen auseinander, die Beine klappten unter ihm wie zwei Federmesser zusammen. Der Hexer sah, dass der Koloss, obwohl schon fast bewusstlos, noch immer mit den Armen herumfuchtelte, und versetzte ihm noch einen dritten Schlag, mitten auf den Scheitel. \"Geld\", murmelte er, \"öffnet alle Türen.\"",
|
||||
"source": "Geralt-Saga - Der letzte Wunsch",
|
||||
"length": 997,
|
||||
"id": 516
|
||||
"text": "Geralt löste den Geldbeutel vom Gürtel, hielt ihn bei den Riemen und wog ihn in der Hand. \"Du kannst mich nicht bestechen\", erklärte der Zerberus stolz. \"Das habe ich nicht vor.\" Der Türhüter war zu massig, als dass seine Reflexe ihm erlaubt hätten, sich gegen den raschen Schlag eines gewöhnlichen Menschen zu decken. Vor dem Schlag eines Hexers bekam er nicht einmal die Augen zu. Mit metallischem Klang donnerte ihm der Beutel an die Schläfe. Er stürzte gegen die Tür und hielt sich mit beiden Händen am Rahmen. Geralt riss ihn davon mit einem Tritt ins Knie los, stieß mit der Schulter zu und benutzte nochmals den Geldbeutel. Die Augen des Türstehers trübten sich und liefen zu einem urkomischen Schielen auseinander, die Beine klappten unter ihm wie zwei Federmesser zusammen. Der Hexer sah, dass der Koloss, obwohl schon fast bewusstlos, noch immer mit den Armen herumfuchtelte, und versetzte ihm noch einen dritten Schlag, mitten auf den Scheitel. \"Geld\", murmelte er, \"öffnet alle Türen.\"",
|
||||
"source": "Geralt-Saga - Der letzte Wunsch",
|
||||
"length": 997,
|
||||
"id": 516
|
||||
},
|
||||
{
|
||||
"text": "Von der Liebe wissen wir nicht viel. Mit der Liebe ist es wie mit einer Birne. Die Birne ist süß und hat eine Form. Versucht einmal, die Form einer Birne zu definieren.",
|
||||
"source": "Geralt-Saga - Die Zeit der Verachtung",
|
||||
"length": 168,
|
||||
"id": 517
|
||||
"text": "Von der Liebe wissen wir nicht viel. Mit der Liebe ist es wie mit einer Birne. Die Birne ist süß und hat eine Form. Versucht einmal, die Form einer Birne zu definieren.",
|
||||
"source": "Geralt-Saga - Die Zeit der Verachtung",
|
||||
"length": 168,
|
||||
"id": 517
|
||||
},
|
||||
{
|
||||
"text": "Zu sagen, ich hätte sie gekannt, wäre eine Übertreibung. Ich glaube, außer dem Hexer und der Zauberin kannte sie niemand wirklich. Als ich sie zum ersten Mal sah, machte sie überhaupt keinen großen Eindruck auf mich, selbst unter den ziemlich unheimlichen Begleitumständen. Ich habe Leute gekannt, die behaupteten, sofort, bei der ersten Begegnung den Hauch des Todes gespürt zu haben, der dieses Mädchen umgab. Mir jedoch kam sie ganz gewöhnlich vor, aber ich wusste ja, dass sie nicht gewöhnlich war – daher habe ich mich besonders bemüht, sie eingehend zu betrachten, in ihr das Ungewöhnliche zu entdecken, zu erfühlen. Doch ich beobachtete nichts und fühlte nichts. Nichts, was die späteren tragischen Ereignisse hätte signalisieren, ankündigen, ahnen lassen können. Die Ereignisse, deren Ursache sie war. Und die Ereignisse, die sie selbst herbeiführte.",
|
||||
"source": "Geralt-Saga - Die Zeit der Verachtung",
|
||||
"length": 858,
|
||||
"id": 518
|
||||
"text": "Zu sagen, ich hätte sie gekannt, wäre eine Übertreibung. Ich glaube, außer dem Hexer und der Zauberin kannte sie niemand wirklich. Als ich sie zum ersten Mal sah, machte sie überhaupt keinen großen Eindruck auf mich, selbst unter den ziemlich unheimlichen Begleitumständen. Ich habe Leute gekannt, die behaupteten, sofort, bei der ersten Begegnung den Hauch des Todes gespürt zu haben, der dieses Mädchen umgab. Mir jedoch kam sie ganz gewöhnlich vor, aber ich wusste ja, dass sie nicht gewöhnlich war – daher habe ich mich besonders bemüht, sie eingehend zu betrachten, in ihr das Ungewöhnliche zu entdecken, zu erfühlen. Doch ich beobachtete nichts und fühlte nichts. Nichts, was die späteren tragischen Ereignisse hätte signalisieren, ankündigen, ahnen lassen können. Die Ereignisse, deren Ursache sie war. Und die Ereignisse, die sie selbst herbeiführte.",
|
||||
"source": "Geralt-Saga - Die Zeit der Verachtung",
|
||||
"length": 858,
|
||||
"id": 518
|
||||
},
|
||||
{
|
||||
"text": "Wenn sich nach Einbruch der Dunkelheit noch jemand zu der Hütte mit dem eingesackten und moosbewachsenen Strohdach geschlichen hätte, wenn er hineingeschaut hätte, hätte er im Licht der Flammen und der Glut in der Feuerstelle einen graubärtigen Greis erblickt, über einen Haufen Felle gebeugt. Er hätte auch ein aschblondes Mädchen mit einer hässlichen Narbe auf der Wange erblickt, einer Narbe, die so gar nicht zu den grünen Augen passte, die groß waren wie bei einem Kind. Doch niemand konnte das sehen. Die Hütte stand mitten im Röhrich, in einem Sumpfland, in das sich niemand wagte.",
|
||||
"source": "Geralt-Saga - Der Schwalbenturm",
|
||||
"length": 588,
|
||||
"id": 519
|
||||
"text": "Wenn sich nach Einbruch der Dunkelheit noch jemand zu der Hütte mit dem eingesackten und moosbewachsenen Strohdach geschlichen hätte, wenn er hineingeschaut hätte, hätte er im Licht der Flammen und der Glut in der Feuerstelle einen graubärtigen Greis erblickt, über einen Haufen Felle gebeugt. Er hätte auch ein aschblondes Mädchen mit einer hässlichen Narbe auf der Wange erblickt, einer Narbe, die so gar nicht zu den grünen Augen passte, die groß waren wie bei einem Kind. Doch niemand konnte das sehen. Die Hütte stand mitten im Röhrich, in einem Sumpfland, in das sich niemand wagte.",
|
||||
"source": "Geralt-Saga - Der Schwalbenturm",
|
||||
"length": 588,
|
||||
"id": 519
|
||||
},
|
||||
{
|
||||
"text": "Die Welt ist voller offensichtlicher Dinge, die offensichtlich niemand sieht.",
|
||||
"source": "Sherlock Holmes – der Hund von Baskerville",
|
||||
"length": 77,
|
||||
"id": 520
|
||||
"text": "Die Welt ist voller offensichtlicher Dinge, die offensichtlich niemand sieht.",
|
||||
"source": "Sherlock Holmes – der Hund von Baskerville",
|
||||
"length": 77,
|
||||
"id": 520
|
||||
},
|
||||
{
|
||||
"text": "Sterben, das hatte ich gelernt, war kein Mannschaftssport. Es war ein einsames Unterfangen. Alle, die ich liebte, standen am trockenen Ufer, während ich allein in einem Boot saß, das sich langsam von der Küste entfernte, und niemand konnte etwas tun. Sie konnten nur dabei zusehen.",
|
||||
"source": "Full Tilt",
|
||||
"length": 281,
|
||||
"id": 521
|
||||
"text": "Sterben, das hatte ich gelernt, war kein Mannschaftssport. Es war ein einsames Unterfangen. Alle, die ich liebte, standen am trockenen Ufer, während ich allein in einem Boot saß, das sich langsam von der Küste entfernte, und niemand konnte etwas tun. Sie konnten nur dabei zusehen.",
|
||||
"source": "Full Tilt",
|
||||
"length": 281,
|
||||
"id": 521
|
||||
},
|
||||
{
|
||||
"text": "In der Sowjetunion ist jeder Arbeiter ein Staatsangestellter, und es gibt ein Sprichwort: So lange die Bosse vorgeben, uns zu bezahlen, werden wir vorgeben zu arbeiten.",
|
||||
"source": "Jagd auf Roter Oktober (frei übersetzt)",
|
||||
"length": 168,
|
||||
"id": 522
|
||||
"text": "In der Sowjetunion ist jeder Arbeiter ein Staatsangestellter, und es gibt ein Sprichwort: So lange die Bosse vorgeben, uns zu bezahlen, werden wir vorgeben zu arbeiten.",
|
||||
"source": "Jagd auf Roter Oktober (frei übersetzt)",
|
||||
"length": 168,
|
||||
"id": 522
|
||||
},
|
||||
{
|
||||
"text": "Es ist keine Schande, etwas nicht zu wissen, aber es ist eine, nichts lernen zu wollen.",
|
||||
"source": "City of Fallen Magic – Mit Feuer und Zinn",
|
||||
"length": 87,
|
||||
"id": 523
|
||||
"text": "Es ist keine Schande, etwas nicht zu wissen, aber es ist eine, nichts lernen zu wollen.",
|
||||
"source": "City of Fallen Magic – Mit Feuer und Zinn",
|
||||
"length": 87,
|
||||
"id": 523
|
||||
},
|
||||
{
|
||||
"text": "Birka war einst ein reiches Dorf gewesen, bezaubert und außergewöhnlich malerisch gelegen – seine gelben Stroh- und roten Ziegeldächer füllten dicht an dicht einen Talkessel mit steilen, bewaldeten Hängen, die je nach Jahreszeit ihre Farbe änderten. Vor allem im Herbst erfreute der Anblick von Birka das ästhetische Auge und das empfindsame Herz. So war es bis zu dem Zeitpunkt, da die Siedlung ihren Namen änderte.",
|
||||
"source": "Geralt-Saga - Der Schwalbenturm",
|
||||
"length": 416,
|
||||
"id": 524
|
||||
"text": "Birka war einst ein reiches Dorf gewesen, bezaubert und außergewöhnlich malerisch gelegen – seine gelben Stroh- und roten Ziegeldächer füllten dicht an dicht einen Talkessel mit steilen, bewaldeten Hängen, die je nach Jahreszeit ihre Farbe änderten. Vor allem im Herbst erfreute der Anblick von Birka das ästhetische Auge und das empfindsame Herz. So war es bis zu dem Zeitpunkt, da die Siedlung ihren Namen änderte.",
|
||||
"source": "Geralt-Saga - Der Schwalbenturm",
|
||||
"length": 416,
|
||||
"id": 524
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,53 @@
|
|||
{
|
||||
"language": "nepali",
|
||||
"groups": [
|
||||
[0, 100],
|
||||
[101, 300],
|
||||
[301, 600],
|
||||
[601, 9999]
|
||||
],
|
||||
"quotes": [
|
||||
{
|
||||
"text": "फुलको आँखामा फुलै संसार\nकाँडाको आँखामा काँडै संसार\nझुल्किन्छ है छायाँ बस्तु अनुसार\nकाँडाको आँखामा काँडै संसार",
|
||||
"source": "फुलको आँखामा - Ani Choying Dolma",
|
||||
"length": 109,
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"text": "सिन्दुली गढी घुमेर हेर्दा\nसुन्तली माई\nकतिमा राम्रो दरवार\nमार्यो नी माया ले मार्यो",
|
||||
"source": "सिन्दुली गढी घुमेर हेर्दा",
|
||||
"length": 81,
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"text": "जीवनमा सबैभन्दा ठूलो महिमा भनेको कहिल्यै पनि नलडनु हो, र उठनु हो हरेक चोटि जब हामी लडछौं",
|
||||
"source": "नेलसन मण्डेला",
|
||||
"length": 88,
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"text": "कुन मन्दिरमा जान्छौ यात्री,\nकुन मन्दिरमा जानेहो?\nकुन सामग्री पुजा गने\nसाथ कसोरी लानेहो ?\nमावनसहरूका कााँध चढी\nकुन देिपुरीमा जानेहो?",
|
||||
"source": "यात्री",
|
||||
"length": 131,
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"text": "बिकल्प त थुप्रै थिऐ\nनि:शब्द रहनु मेरै गल्ति थियो\nसारांश खोज्छु कथा पढनु आघि\nम त्यहि खोजिमा हराउनु मेरै गल्ति थियो,\nहर्ष को स्पर्श हुन्छ तब\nजब थकाई मर्छ पुरा दिनको\nनसोचेको पल पनि पल-पलाई रहन्छ, जब आभास हुन्छ तिम्रो",
|
||||
"source": "अविनवन भट्टराई",
|
||||
"length": 218,
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"text": "एउटा अग्लो भिरमा प्रेमको मन्दिर बनाई, तिमीलाई देउता थाप्ने छू\nधोकाकी देवी नाम राखी, पुजारी म स्वयमं नै बन्ने छू\nतिम्रो बिछोडको अग्निले हवन गरी, बगाएका आशुले जल चढाउने छू\nचर्किएको मुटू समाती कटाईएका ति रातका निन्द्रा नैवद टक्र्याउने छू\nहाम्रा पिरतीका कुराकानीलाई भजन बनाई बिहान बेलुकी गाउने छू\nअनि तिमीलाई छोडी जानेको पीडा बुझाउने छु",
|
||||
"source": "अनिरुद्र",
|
||||
"length": 333,
|
||||
"id": 6
|
||||
},
|
||||
{
|
||||
"text": "जीवनका भोगाइ सबै रङ्गमन्चको खेल न हो\nधनदौलत, यौवन, उर्लिने खहरेको भेल न हो\nकोही पर्दाबाहिर त कोही पर्दाभित्र जाल बुन्छन्\nकठपुतलीझैँ अर्कैले नचाउँछ जगत् एक झेल न हो",
|
||||
"source": "दुर्गाकिरण तिवारी",
|
||||
"length": 163,
|
||||
"id": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"language": "nepali",
|
||||
"groups": [
|
||||
[0, 100],
|
||||
[101, 300],
|
||||
[301, 600],
|
||||
[601, 9999]
|
||||
],
|
||||
"quotes": [
|
||||
{
|
||||
"text": "फुलको आँखामा फुलै संसार\nकाँडाको आँखामा काँडै संसार\nझुल्किन्छ है छायाँ बस्तु अनुसार\nकाँडाको आँखामा काँडै संसार",
|
||||
"source": "फुलको आँखामा - Ani Choying Dolma",
|
||||
"length": 109,
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"text": "सिन्दुली गढी घुमेर हेर्दा\nसुन्तली माई\nकतिमा राम्रो दरवार\nमार्यो नी माया ले मार्यो",
|
||||
"source": "सिन्दुली गढी घुमेर हेर्दा",
|
||||
"length": 81,
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"text": "जीवनमा सबैभन्दा ठूलो महिमा भनेको कहिल्यै पनि नलडनु हो, र उठनु हो हरेक चोटि जब हामी लडछौं",
|
||||
"source": "नेलसन मण्डेला",
|
||||
"length": 88,
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"text": "कुन मन्दिरमा जान्छौ यात्री,\nकुन मन्दिरमा जानेहो?\nकुन सामग्री पुजा गने\nसाथ कसोरी लानेहो ?\nमावनसहरूका कााँध चढी\nकुन देिपुरीमा जानेहो?",
|
||||
"source": "यात्री",
|
||||
"length": 131,
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"text": "बिकल्प त थुप्रै थिऐ\nनि:शब्द रहनु मेरै गल्ति थियो\nसारांश खोज्छु कथा पढनु आघि\nम त्यहि खोजिमा हराउनु मेरै गल्ति थियो,\nहर्ष को स्पर्श हुन्छ तब\nजब थकाई मर्छ पुरा दिनको\nनसोचेको पल पनि पल-पलाई रहन्छ, जब आभास हुन्छ तिम्रो",
|
||||
"source": "अविनवन भट्टराई",
|
||||
"length": 218,
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"text": "एउटा अग्लो भिरमा प्रेमको मन्दिर बनाई, तिमीलाई देउता थाप्ने छू\nधोकाकी देवी नाम राखी, पुजारी म स्वयमं नै बन्ने छू\nतिम्रो बिछोडको अग्निले हवन गरी, बगाएका आशुले जल चढाउने छू\nचर्किएको मुटू समाती कटाईएका ति रातका निन्द्रा नैवद टक्र्याउने छू\nहाम्रा पिरतीका कुराकानीलाई भजन बनाई बिहान बेलुकी गाउने छू\nअनि तिमीलाई छोडी जानेको पीडा बुझाउने छु",
|
||||
"source": "अनिरुद्र",
|
||||
"length": 333,
|
||||
"id": 6
|
||||
},
|
||||
{
|
||||
"text": "जीवनका भोगाइ सबै रङ्गमन्चको खेल न हो\nधनदौलत, यौवन, उर्लिने खहरेको भेल न हो\nकोही पर्दाबाहिर त कोही पर्दाभित्र जाल बुन्छन्\nकठपुतलीझैँ अर्कैले नचाउँछ जगत् एक झेल न हो",
|
||||
"source": "दुर्गाकिरण तिवारी",
|
||||
"length": 163,
|
||||
"id": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue