Merge pull request #1475 from lukew3/mongo

Mongo
This commit is contained in:
Jack 2021-06-06 14:45:08 +01:00 committed by GitHub
commit 25a23dde3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 21111 additions and 15977 deletions

View file

@ -17,7 +17,6 @@ Sometimes your browser has old files cached and the bug you are experiencing mig
<!-- **Did it happen in incognito mode?**
Sometimes things work in incognito mode, which allows me to further track down the issue. -->
**To Reproduce** <!-- Steps to reproduce the behavior: -->
1. Go to '...'
@ -35,5 +34,4 @@ Sometimes things work in incognito mode, which allows me to further track down t
- Browser [] <!-- e.g. Chrome, Firefox, Safari, etc... -->
- Browser Version [] <!-- e.g. 22 -->
**Additional context** <!-- Add any other context about the problem here. -->

View file

@ -1,29 +1,27 @@
<!-- please read the comments -->
<!-- Adding a language or a theme? For languages, make sure to edit the `_list.json`, `_groups.json` files, and add the `language.json` file as well. For themes, make sure to add the `theme.css` file. It will not work if you don't follow these steps!
<!-- Adding a language or a theme? For languages, make sure to edit the `_list.json`, `_groups.json` files, and add the `language.json` file as well. For themes, make sure to add the `theme.css` file. It will not work if you don't follow these steps!
If your change is visual (mainly themes) it would be extra awesome if you could include a screenshot.
If your change is visual (mainly themes) it would be extra awesome if you could include a screenshot.
-->
### Description
<!-- Please describe the change(s) made in your PR -->
<!-- please check the items you have completed -->
### Checklist
### Checklist
- [] I have read the [`CODE_OF_CONDUCT.md`](https://github.com/Miodec/monkeytype/blob/master/CODE_OF_CONDUCT.md) and the [`CONTRIBUTING.md`](https://github.com/Miodec/monkeytype/blob/master/CONTRIBUTING.md)
- [] I checked if my PR has any bugs or other issues that could reduce the stability of the project
- [] I understand that the maintainer has the right to reject my contribution and it may not get accepted.
- [] If my PR is a new language or theme I modified the appropriate files to incorporate the language or theme <!-- Delete if that is not the case-->
- [] If my PR is a new language or theme I modified the appropriate files to incorporate the language or theme <!-- Delete if that is not the case-->
<!-- the issue(s) your PR resolves if any (delete if that is not the case) -->
<!-- please also reference any issues and or PRs related to your pull request -->
Resolves #
<!-- pro tip: you can check checkboxes by putting an x inside the brackets [x] -->

View file

@ -41,16 +41,16 @@
- Click on `Email/Password`, enable it and save
- Click on `Google`, add a support email and save
1. Enable Firebase Firestore
## Prerequisite - Mongo Setup
- In the Firebase console, go to Cloud Firestore
- Create database
- Start in test mode
- Select default location and enable
1. Install [Mongodb Community Edition](https://docs.mongodb.com/manual/administration/install-community/) and ensure that it is running
1. Optional - Install [Mongodb-compass](https://www.mongodb.com/try/download/compass?tck=docs_compass). This tool can be used to see and manipulate your data visually.
1. To connect, type `mongodb://localhost:27017` in the connection string box and press connect. The monkeytype database will be created and shown` after the server is started.
## Building and Running
1. Run `npm install` in the project root directory to install dependencies.
1. Run `npm ci` in the project root directory to install dependencies.
1. Run `npm run start:dev` to start a local dev server on port 5000. It will watch for changes and rebuild when you edit files in `src/` or `public/`. Use ctrl+c to stop it.
- Run `firebase use <your-project-id>` if you run into any errors for this.

113
backend/migrate.js Normal file
View file

@ -0,0 +1,113 @@
const admin = require("firebase-admin");
const mongoose = require("mongoose");
const { User } = require("./models/user");
const { Leaderboard } = require("./models/leaderboard");
const { Stats } = require("./models/stats");
const { BotCommand } = require("./models/bot-command");
const serviceAccount = require("../functions/serviceAccountKey.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
var db = admin.firestore();
const port = process.env.PORT || "5005";
mongoose.connect("mongodb://localhost:27017/monkeytype", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Database should be completely clear before this is ran in order to prevent overlapping documents
// Migrate users
userCount = 1;
db.collection("users")
// .where("name","==","mio")
.get()
.then((querySnapshot) => {
// console.log('start of foreach');
querySnapshot.forEach( async (userDoc) => {
let newUser;
try{
let data = userDoc.data();
data._id = userDoc.id;
newUser = new User(data);
newUser.uid = userDoc.id;
newUser.globalStats = {
started: userDoc.data().startedTests,
completed: userDoc.data().completedTests,
time: userDoc.data().timeTyping,
};
let tagIdDict = {};
let tagsSnapshot = await db.collection(`users/${userDoc.id}/tags`).get();
tagsSnapshot.forEach((tagDoc) => {
let formattedTag = tagDoc.data();
formattedTag._id = mongoose.Types.ObjectId(); //generate new objectId
tagIdDict[tagDoc.id] = formattedTag._id; //save pair of ids in memory to determine what to set new id as in result tags
newUser.tags.push(formattedTag);
console.log(`Tag ${tagDoc.id} saved for user ${userCount}`);
});
let resultsSnapshot = await db.collection(`users/${userDoc.id}/results`).get();
let resCount = 1;
resultsSnapshot.forEach((result) => {
let formattedResult = result.data();
if(formattedResult.tags != undefined){
formattedResult.tags.forEach((tag, index) => {
if (tagIdDict[tag])
formattedResult.tags[index] = tagIdDict[tag];
});
}
newUser.results.push(formattedResult);
console.log(`Result ${resCount} saved for user ${userCount}`);
resCount++;
});
newUser.results.sort((a, b) => {
return a.timestamp - b.timestamp;
});
let presetsSnapshot = await db.collection(`users/${userDoc.id}/presets`).get();
presetsSnapshot.forEach((preset) => {
newUser.presets.push(preset.data());
});
await newUser.save();
console.log(`User ${userCount} (${newUser.uid}) saved`);
userCount++;
}catch(e){
// throw e;
console.log(`User ${userCount} (${newUser.uid}) failed: ${e.message}`);
userCount++;
}
});
// console.log('end of foreach');
});
//not tested because I can't get leaderboards to work on my fork for some reason
db.collection("leaderboards")
.get()
.then((leaderboardsSnapshot) => {
leaderboardsSnapshot.forEach((lbDoc) => {
let newLb = new Leaderboard(lbDoc.data());
newLb.save();
});
});
//migrate bot-commands
db.collection("bot-commands")
.get()
.then((botCommandsSnapshot) => {
botCommandsSnapshot.forEach((bcDoc) => {
let newBotCommand = new BotCommand(bcDoc.data());
newBotCommand.save();
});
});
//migrate public stats
db.collection("public")
.doc("stats")
.get()
.then((ret) => {
let stats = ret.data();
let newStats = new Stats(stats);
newStats.save();
});

View file

@ -0,0 +1,20 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const botCommandSchema = new Schema(
{
command: { type: String, required: true },
arguments: [{ type: Schema.Types.Mixed }],
executedTimestamp: { type: Date },
requestTimestamp: { type: Date },
executed: { type: Boolean, default: false },
status: { type: String },
},
{
timestamps: true,
}
);
const BotCommand = mongoose.model("BotCommand", botCommandSchema);
module.exports = { BotCommand };

View file

@ -0,0 +1,32 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const leaderboardEntrySchema = new Schema({
name: { type: String },
wpm: { type: Number },
raw: { type: Number },
acc: { type: Number },
consistency: { type: {} }, //can be null
mode: { type: String }, //not sure why mode and mode2 are needed
mode2: { type: Number },
timestamp: { type: Date },
hidden: { type: Boolean },
});
const leaderboardSchema = new Schema(
{
resetTime: { type: Date }, //or Number, only on daily lb
size: { type: Number, required: true },
board: [{ type: leaderboardEntrySchema }], //contents of leaderbaord
mode: { type: String, required: true }, //only equal to 'time' for now
mode2: { type: Number, required: true }, //only equal to 15 and 60 for now
type: { type: String, required: true }, //global or local
},
{
timestamps: true,
}
);
const Leaderboard = mongoose.model("Leaderboard", leaderboardSchema);
module.exports = { Leaderboard };

17
backend/models/stats.js Normal file
View file

@ -0,0 +1,17 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const statsSchema = new Schema(
{
completedTests: { type: Number, default: 0 },
startedTests: { type: Number, default: 0 },
timeTyping: { type: Number, default: 0 },
},
{
timestamps: true,
}
);
const Stats = mongoose.model("Stats", statsSchema);
module.exports = { Stats };

View file

@ -0,0 +1,78 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const configSchema = new Schema({
theme: { type: String },
customTheme: { type: Boolean },
customThemeColors: [{ type: String }],
favThemes: [{ type: String }],
showKeyTips: { type: Boolean },
showLiveWpm: { type: Boolean },
showTimerProgress: { type: Boolean },
smoothCaret: { type: Boolean },
quickTab: { type: Boolean },
punctuation: { type: Boolean },
numbers: { type: Boolean },
words: { type: Number },
time: { type: Number },
mode: { type: String },
quoteLength: [{ type: Number }],
language: { type: String },
fontSize: { type: Number },
freedomMode: { type: Boolean },
difficulty: { type: String },
blindMode: { type: Boolean },
quickEnd: { type: Boolean },
caretStyle: { type: String },
paceCaretStyle: { type: String },
flipTestColors: { type: Boolean },
capsLockBackspace: { type: Boolean },
layout: { type: String },
confidenceMode: { type: String },
indicateTypos: { type: Boolean },
timerStyle: { type: String },
colorfulMode: { type: Boolean },
randomTheme: { type: String }, //feels like this should be a boolean
timerColor: { type: String },
timerOpacity: { type: String }, //maybe should be a number
stopOnError: { type: String },
showAllLines: { type: Boolean },
keymapMode: { type: String },
keymapStyle: { type: String },
keymapLegendStyle: { type: String },
keymapLayout: { type: String },
fontFamily: { type: String },
smoothLineScroll: { type: Boolean },
alwaysShowDecimalPlaces: { type: Boolean },
alwaysShowWordsHistory: { type: Boolean },
singleListCommandLine: { type: String },
playSoundOnError: { type: Boolean },
playSoundOnClick: { type: String },
startGraphsAtZero: { type: Boolean },
swapEscAndTab: { type: Boolean },
showOutOfFocusWarning: { type: Boolean },
paceCaret: { type: String },
paceCaretCustomSpeed: { type: Number },
pageWidth: { type: String },
chartAccuracy: { type: Boolean },
chartStyle: { type: String },
minWpm: { type: String },
minWpmCustomSpeed: { type: Number },
highlightMode: { type: String },
alwaysShowCPM: { type: Boolean },
enableAds: { type: String },
hideExtraLetters: { type: Boolean },
strictSpace: { type: Boolean },
minAcc: { type: String },
minAccCustom: { type: Number },
showLiveAcc: { type: Boolean },
monkey: { type: Boolean },
repeatQuotes: { type: String },
oppositeShiftMode: { type: String },
customBackground: { type: String },
customBackgroundSize: { type: String },
customBackgroundFilter: [{ type: Number }],
customLayoutfluid: { type: String },
});
module.exports = { configSchema };

View file

@ -0,0 +1,10 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const { configSchema } = require("./config");
const presetSchema = new Schema({
name: { type: String, required: true },
config: { type: configSchema }, //not sure if preset config always follows config schema
});
module.exports = { presetSchema };

View file

@ -0,0 +1,38 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const resultSchema = new Schema({
wpm: { type: Number, required: true },
rawWpm: { type: Number, required: true },
correctChars: { type: Number, required: true },
incorrectChars: { type: Number, required: true },
allChars: { type: Number, required: true },
acc: { type: Number, required: true },
mode: { type: String, required: true }, //is this always string type?
mode2: { type: String, required: true }, //is this always number type? not always
quoteLength: { type: Number, required: false },
timestamp: { type: Number, required: true }, //can this be removed if timestamps are added to mongoose
language: { type: String, default: "english" },
restartCount: { type: Number, required: true },
incompleteTestSeconds: { type: Number, required: true },
testDuration: { type: Number, required: true },
afkDuration: { type: Number, required: true },
theme: { type: String, required: true },
tags: [{ type: String }], //the id of each tag
keySpacing: { type: String, default: "removed" }, //not sure what this or keyDuration is
keyDuration: { type: String, default: "removed" },
consistency: { type: Number, required: true },
keyConsistency: { type: Number, required: true },
chartData: {
//should chartData have it's own schema?
wpm: [{ type: Number }],
raw: [{ type: Number }],
err: [{ type: Number }],
},
customText: { type: Schema.Types.Mixed },
keySpacingStats: { type: Schema.Types.Mixed }, //not sure that this needs to exist, it's set as null in all of mine
name: { type: String, required: true }, //name of the user who took the test //should probably be typistName/username or something
isPb: { type: Boolean, default: false },
});
module.exports = { resultSchema };

View file

@ -0,0 +1,9 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const tagSchema = new Schema({
name: { type: String, required: true },
personalBests: { type: Schema.Types.Mixed },
});
module.exports = { tagSchema };

62
backend/models/user.js Normal file
View file

@ -0,0 +1,62 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const { configSchema } = require("./subschemas/config");
const { resultSchema } = require("./subschemas/result");
const { tagSchema } = require("./subschemas/tag");
const { presetSchema } = require("./subschemas/preset");
const userSchema = new Schema(
{
_id: { type: String },
results: [{ type: resultSchema, default: {} }],
personalBests: {
custom: { type: Schema.Types.Mixed, default: {} },
time: { type: Schema.Types.Mixed, default: {} },
words: { type: Schema.Types.Mixed, default: {} },
zen: { type: Schema.Types.Mixed, default: {} },
},
name: { type: String, required: true },
uid: { type: String, required: true },
discordId: { type: String },
presets: [{ type: presetSchema, default: {} }],
tags: [{ type: tagSchema, default: {} }],
favouriteThemes: [],
refactored: { type: Boolean, default: true },
banned: { type: Boolean, default: false },
verified: { type: Boolean, default: false }, //Verified is actually whether or not discord account is connected
emailVerified: { type: Boolean, default: false },
lbMemory: {
time15: {
global: { type: Number, default: -1 },
daily: { type: Number, default: -1 },
},
time60: {
global: { type: Number, default: -1 },
daily: { type: Number, default: -1 },
},
},
globalStats: {
time: { type: Number, default: 0 },
started: { type: Number, default: 0 }, //number of started tests
completed: { type: Number, default: 0 },
},
email: { type: String },
config: { type: configSchema, default: {} },
bananas: {
t60bananas: { type: Number, default: 0 },
},
dailyLbWins: {
time15: { type: Number },
time60: { type: Number },
},
},
{
timestamps: true,
minimize: false, //allows empty objects to be saved to mongodb
}
);
const User = mongoose.model("User", userSchema);
module.exports = { User };

57
backend/mongo-todo.md Normal file
View file

@ -0,0 +1,57 @@
# Mongo todo
## Todo
- Make sure that the branch is ready for deployment
- Make sure that the bot can interact with the data on the express server
- Would be optimal if the bot were to run on the same server as the express server, so that the bot wouldn't have to access data through api routes
- Determine if generatePairingCode should be removed or migrated
- This function was commented out in index.js but is used in frontend
## Bugs
- Make sure that the bot is able to interact with the mongo database
- If bot is on same server, it could work with mongo directly, otherwise, more api routes are needed
- Do names have to be made lowercase before checking if a duplicate name is found?(that is when a new user is created or username is changed)
### Minor/efficiency bugs
- Does clearDailyLeaderboards cause a memory leak?
- Is filteredResults.reverse(); in account.js going to cause efficiency issues?
- For loop in account could work backwards instead, but this would add complexity
- Why does `if (page == "account") pageTransition = false;` get rid of endless account loading bug when accessing via url
- Name is not passed in user token/auth().currentUser
- Account button sometimes shows loading infinitely after a test
- Can't navigate to user until page is refreshed
- After refresh, pr is not saved
- Can't induce this error and doesn't occur often so adding it as minor bug
- lbmemory undefined if page not refreshed after user sign up?
- If you are in first place and you place on the leaderboard but not above yourself, you may get glb undefined error
- Might also occur if you are simply on the leaderboard and make the leaderboard but not above your current position
- Doesn't happen all the time
- Hidden property of leaderboard is unused
- Verified property of user is unused, set at false by default
- Can't find where the property would be set in the code
- Is this discord verified, if so, why do you need discord verified to be on leaderboard?
- Temporarily removed from leaderboard requirements
### Functions not found anywhere except for index.js
Might need to be migrated, might not. I'm not sure why these are in the file if they are not being used.
- getAllNames
- getAllUsers
- getPatreons
- requestTest
- incrementStartedTestCounter
- incrementTestCounter
### Possibilities
- Might be worthwhile to use redis to store userdata up to a certain point
- Users who have been requested in the last hour will be stored in the redis database so that their data can be sent again without having to search a large database
- After an hour without a new request they can be removed from memory
- Create a backup system to prevent loss of data
- Users should be able to export their data themselves
- Pretty much is just the user snap but without uid
- Could split server.js into multiple files for easier code management

1881
backend/server.js Normal file

File diff suppressed because it is too large Load diff

9
example.env Normal file
View file

@ -0,0 +1,9 @@
# Secrets can be generated at https://www.grc.com/passwords.htm
ACCESS_TOKEN_SECRET=6JlduNw96JONRtmg7Ru6tCW0UN42LQyzlHE0e03p2HO4m5Gm7PrYjRCinHCfeMM
REFRESH_TOKEN_SECRET=bnTfeI0J84XucqTWkHRPBCrewoJGIQySdHnL2bDrZp212tDyMG0fs5nf9aUXT9N
#Gmail login for email verification
#App password can be generated on account page under security, sigining in to Google
#Must enable 2 step verification before generating app password
MAIL_ADDRESS=youremail@gmail.com
MAIL_PASSWORD=cqvpgasbggytzfjq

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
const { task, src, dest, series, watch } = require("gulp");
const axios = require("axios");
const browserify = require("browserify");
const babelify = require("babelify");
const concat = require("gulp-concat");
@ -86,6 +87,7 @@ let eslintConfig = {
//refactored files, which should be es6 modules
//once all files are moved here, then can we use a bundler to its full potential
const refactoredSrc = [
"./src/js/axios-instance.js",
"./src/js/db.js",
"./src/js/cloud-functions.js",
"./src/js/misc.js",

6932
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@
"scripts": {
"postinstall": "cd functions && npm install",
"build": "npx gulp build",
"start:dev": "npm run build && concurrently --kill-others \"npx gulp watch\" \"firebase serve\"",
"start:dev": "npm run build && concurrently --kill-others \"npx gulp watch\" \"nodemon ./backend/server.js\" \"firebase serve\"",
"deploy:live:hosting": "npm run build && firebase deploy -P monkey-type --only hosting",
"deploy:live:functions": "npm run build && firebase deploy -P monkey-type --only functions",
"deploy:live": "npm run build && firebase deploy -P live"
@ -43,10 +43,17 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"chartjs-plugin-annotation": "^0.5.7",
"chartjs-plugin-trendline": "^0.2.2",
"cors": "^2.8.5",
"express": "^4.17.1",
"firebase-admin": "^9.9.0",
"helmet": "^4.6.0",
"howler": "^2.2.1",
"mongoose": "^5.12.12",
"nodemon": "^2.0.7",
"tinycolor2": "^1.4.2"
}
}

View file

@ -8,11 +8,11 @@ import * as Misc from "./misc";
import * as Settings from "./settings";
import * as ChallengeController from "./challenge-controller";
import Config from "./config";
import * as CloudFunctions from "./cloud-functions";
import * as AllTimeStats from "./all-time-stats";
import * as DB from "./db";
import * as TestLogic from "./test-logic";
import * as UI from "./ui";
import axiosInstance from "./axios-instance";
var gmailProvider = new firebase.auth.GoogleAuthProvider();
@ -135,8 +135,8 @@ function signUp() {
$(".pageLogin .register .button").removeClass("disabled");
return;
}
CloudFunctions.namecheck({ name: nname }).then((d) => {
axiosInstance.get(`/nameCheck/${nname}`).then((d) => {
console.log(d.data);
if (d.data.resultCode === -1) {
Notifications.add("Name unavailable", -1);
$(".pageLogin .preloader").addClass("hidden");
@ -158,23 +158,18 @@ function signUp() {
// Account has been created here.
// dontCheckUserName = true;
let usr = user.user;
//maybe there's a better place for the api call
axiosInstance.post("/signUp", {
name: nname,
uid: usr.uid,
email: email,
});
usr
.updateProfile({
displayName: nname,
})
.then(async function () {
// Update successful.
await firebase
.firestore()
.collection("users")
.doc(usr.uid)
.set({ name: nname }, { merge: true });
CloudFunctions.reserveName({ name: nname, uid: usr.uid }).catch(
(e) => {
console.error("Could not reserve name " + e);
throw "Could not reserve name";
}
);
usr.sendEmailVerification();
AllTimeStats.clear();
Notifications.add("Account created", 1, 3);
@ -197,11 +192,15 @@ function signUp() {
});
if (TestLogic.notSignedInLastResult !== null) {
TestLogic.setNotSignedInUid(usr.uid);
CloudFunctions.testCompleted({
uid: usr.uid,
obj: TestLogic.notSignedInLastResult,
});
DB.getSnapshot().results.push(TestLogic.notSignedInLastResult);
axiosInstance
.post("/testCompleted", {
obj: TestLogic.notSignedInLastResult,
})
.then(() => {
DB.getSnapshot().results.push(
TestLogic.notSignedInLastResult
);
});
}
UI.changePage("account");
usr.sendEmailVerification();

View file

@ -1,6 +1,5 @@
import * as DB from "./db";
import * as Misc from "./misc";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as ResultFilters from "./result-filters";
import * as ThemeColors from "./theme-colors";
@ -18,6 +17,7 @@ import * as Settings from "./settings";
import * as ThemePicker from "./theme-picker";
import * as AllTimeStats from "./all-time-stats";
import * as PbTables from "./pb-tables";
import axiosInstance from "./axios-instance";
export function getDataAndInit() {
DB.initSnapshot()
@ -30,11 +30,11 @@ export function getDataAndInit() {
let user = firebase.auth().currentUser;
if (snap.name === undefined) {
//verify username
if (Misc.isUsernameValid(user.displayName)) {
if (Misc.isUsernameValid(user.name)) {
//valid, just update
snap.name = user.displayName;
snap.name = user.name;
DB.setSnapshot(snap);
DB.updateName(user.uid, user.displayName);
DB.updateName(user.uid, user.name);
} else {
//invalid, get new
// Notifications.add("Invalid name", 0);
@ -49,21 +49,23 @@ export function getDataAndInit() {
promptVal = prompt(
"Your name is either invalid or unavailable (you also need to do this if you used Google Sign Up). Please provide a new display name (cannot be longer than 14 characters, can only contain letters, numbers, underscores, dots and dashes):"
);
cdnVal = await CloudFunctions.changeDisplayName({
uid: user.uid,
name: promptVal,
});
if (cdnVal.data.status === 1) {
alert("Name updated", 1);
location.reload();
} else if (cdnVal.data.status < 0) {
alert(cdnVal.data.message, 0);
}
axiosInstance
.post("/updateName", {
name: promptVal,
})
.then((cdnVal) => {
if (cdnVal.data.status === 1) {
alert("Name updated", 1);
location.reload();
} else if (cdnVal.data.status < 0) {
alert(cdnVal.data.message, 0);
}
});
}
}
}
if (snap.refactored === false) {
CloudFunctions.removeSmallTests({ uid: user.uid });
axiosInstance.post("/removeSmallTestsAndQPB");
}
if (!UpdateConfig.changedBeforeDb) {
if (Config.localStorageConfig === null) {
@ -230,7 +232,7 @@ function loadMoreLines(lineIndex) {
if (result.tags !== undefined && result.tags.length > 0) {
result.tags.forEach((tag) => {
DB.getSnapshot().tags.forEach((snaptag) => {
if (tag === snaptag.id) {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
@ -302,6 +304,7 @@ export function update() {
ChartController.accountHistory.updateColors();
ChartController.accountActivity.updateColors();
AllTimeStats.update();
PbTables.update();
let chartData = [];
@ -367,7 +370,6 @@ export function update() {
}
if (!ResultFilters.getFilter("difficulty", resdiff)) return;
if (!ResultFilters.getFilter("mode", result.mode)) return;
if (result.mode == "time") {
let timefilter = "custom";
if ([15, 30, 60, 120].includes(parseInt(result.mode2))) {
@ -399,7 +401,6 @@ export function update() {
)
return;
}
let langFilter = ResultFilters.getFilter("language", result.language);
if (
@ -409,13 +410,11 @@ export function update() {
langFilter = true;
}
if (!langFilter) return;
let puncfilter = "off";
if (result.punctuation) {
puncfilter = "on";
}
if (!ResultFilters.getFilter("punctuation", puncfilter)) return;
let numfilter = "off";
if (result.numbers) {
numfilter = "on";
@ -429,7 +428,6 @@ export function update() {
}
let tagHide = true;
if (result.tags === undefined || result.tags.length === 0) {
//no tags, show when no tag is enabled
if (DB.getSnapshot().tags.length > 0) {
@ -485,7 +483,6 @@ export function update() {
ResultFilters.reset();
ResultFilters.updateActive();
}
//filters done
//=======================================
@ -591,6 +588,7 @@ export function update() {
totalWpm += result.wpm;
});
filteredResults.reverse();
loadMoreLines();
////////
@ -648,7 +646,6 @@ export function update() {
});
lastTimestamp = date;
});
ChartController.accountActivity.data.datasets[0].data = activityChartData_time;
ChartController.accountActivity.data.datasets[1].data = activityChartData_avgWpm;
@ -737,9 +734,9 @@ export function update() {
Math.round(totalCons10 / Math.min(last10, consCount)) + "%"
);
}
$(".pageAccount .testsStarted .val").text(`${testCount + testRestarts}`);
console.log("Test count: " + testCount);
console.log("Test restarts: " + testRestarts);
$(".pageAccount .testsCompleted .val").text(
`${testCount}(${Math.floor(
(testCount / (testCount + testRestarts)) * 100
@ -834,7 +831,7 @@ $(".pageAccount #accountHistoryChart").click((e) => {
loadMoreLines(index);
$([document.documentElement, document.body]).animate(
{
scrollTop: $(`#result-${index}`).offset().top - ($(window).height()/2),
scrollTop: $(`#result-${index}`).offset().top - $(window).height() / 2,
},
500
);

View file

@ -99,7 +99,7 @@ export function getFilter(group, filter) {
export function loadTags(tags) {
tags.forEach((tag) => {
defaultResultFilters.tags[tag.id] = true;
defaultResultFilters.tags[tag._id] = true;
});
}
@ -305,7 +305,7 @@ export function updateTags() {
DB.getSnapshot().tags.forEach((tag) => {
$(
".pageAccount .content .filterButtons .buttonsAndTitle.tags .buttons"
).append(`<div class="button" filter="${tag.id}">${tag.name}</div>`);
).append(`<div class="button" filter="${tag._id}">${tag.name}</div>`);
});
} else {
$(".pageAccount .content .filterButtons .buttonsAndTitle.tags").addClass(
@ -412,7 +412,7 @@ $(".pageAccount .topFilters .button.currentConfigFilter").click((e) => {
DB.getSnapshot().tags.forEach((tag) => {
if (tag.active === true) {
filters["tags"]["none"] = false;
filters["tags"][tag.id] = true;
filters["tags"][tag._id] = true;
}
});

View file

@ -1,7 +1,7 @@
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import * as DB from "./db";
import axiosInstance from "./axios-instance";
export let data = null;
export function set(val) {
@ -11,13 +11,18 @@ export function set(val) {
export function verify(user) {
Notifications.add("Verifying", 0, 3);
data.uid = user.uid;
CloudFunctions.verifyUser(data).then((data) => {
if (data.data.status === 1) {
Notifications.add(data.data.message, 1);
DB.getSnapshot().discordId = data.data.did;
Settings.updateDiscordSection();
} else {
Notifications.add(data.data.message, -1);
}
});
axiosInstance
.post("/verifyDiscord", {
data: data,
})
.then((response) => {
if (response.data.status === 1) {
Notifications.add(response.data.message, 1);
DB.getSnapshot().discordId = response.data.did;
Settings.updateDiscordSection();
} else {
Notifications.add(response.data.message, -1);
}
});
}

42
src/js/axios-instance.js Normal file
View file

@ -0,0 +1,42 @@
import axios from "axios";
let baseURL;
if (window.location.hostname === "localhost") {
baseURL = "http://localhost:5005";
} else {
baseURL = "https://api.monkeytype.com";
}
const axiosInstance = axios.create({
baseURL: baseURL,
});
// Request interceptor for API calls
axiosInstance.interceptors.request.use(
async (config) => {
let idToken;
if (firebase.auth().currentUser != null) {
idToken = await firebase.auth().currentUser.getIdToken();
} else {
idToken = null;
}
if (idToken) {
config.headers = {
Authorization: `Bearer ${idToken}`,
Accept: "application/json",
"Content-Type": "application/json",
};
} else {
config.headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
}
return config;
},
(error) => {
Promise.reject(error);
}
);
export default axiosInstance;

View file

@ -1,45 +1,3 @@
export const testCompleted = firebase
.functions()
.httpsCallable("testCompleted");
export const addTag = firebase.functions().httpsCallable("addTag");
export const editTag = firebase.functions().httpsCallable("editTag");
export const removeTag = firebase.functions().httpsCallable("removeTag");
export const updateResultTags = firebase
.functions()
.httpsCallable("updateResultTags");
export const saveConfig = firebase.functions().httpsCallable("saveConfig");
export const addPreset = firebase.functions().httpsCallable("addPreset");
export const editPreset = firebase.functions().httpsCallable("editPreset");
export const removePreset = firebase.functions().httpsCallable("removePreset");
export const generatePairingCode = firebase
.functions()
.httpsCallable("generatePairingCode");
export const saveLbMemory = firebase.functions().httpsCallable("saveLbMemory");
export const unlinkDiscord = firebase
.functions()
.httpsCallable("unlinkDiscord");
export const verifyUser = firebase.functions().httpsCallable("verifyUser");
export const reserveName = firebase
.functions()
.httpsCallable("reserveDisplayName");
export const updateEmail = firebase.functions().httpsCallable("updateEmail");
export const namecheck = firebase
.functions()
.httpsCallable("checkNameAvailability");
export const getLeaderboard = firebase
.functions()
.httpsCallable("getLeaderboard");
export const clearTagPb = firebase.functions().httpsCallable("clearTagPb");
export const changeDisplayName = firebase
.functions()
.httpsCallable("changeDisplayName");
export const removeSmallTests = firebase
.functions()
.httpsCallable("removeSmallTestsAndQPB");
export const resetPersonalBests = firebase
.functions()
.httpsCallable("resetPersonalBests");
export const checkLeaderboards = firebase
.functions()
.httpsCallable("checkLeaderboards");
export function generatePairingCode(input) {
console.log("request data here");
}

View file

@ -214,11 +214,11 @@ export function updateTagCommands() {
}
commandsTags.list.push({
id: "toggleTag" + tag.id,
id: "toggleTag" + tag._id,
display: dis,
sticky: true,
exec: () => {
TagController.toggle(tag.id);
TagController.toggle(tag._id);
TestUI.updateModesNotice();
let txt = tag.name;
@ -229,11 +229,11 @@ export function updateTagCommands() {
}
if (Commandline.isSingleListCommandLineActive()) {
$(
`#commandLine .suggestions .entry[command='toggleTag${tag.id}']`
`#commandLine .suggestions .entry[command='toggleTag${tag._id}']`
).html("Change tags > " + txt);
} else {
$(
`#commandLine .suggestions .entry[command='toggleTag${tag.id}']`
`#commandLine .suggestions .entry[command='toggleTag${tag._id}']`
).html(txt);
}
},

View file

@ -1,15 +1,14 @@
import { loadTags } from "./result-filters";
import * as AccountButton from "./account-button";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
const db = firebase.firestore();
db.settings({ experimentalForceLongPolling: true });
import axiosInstance from "./axios-instance";
let dbSnapshot = null;
export function updateName(uid, name) {
db.collection(`users`).doc(uid).set({ name: name }, { merge: true });
axiosInstance.post("/updateName", {
name: name,
});
}
export function getSnapshot() {
@ -27,129 +26,18 @@ export function setSnapshot(newSnapshot) {
}
export async function initSnapshot() {
let user = firebase.auth().currentUser;
if (user == null) return false;
let snap = {
results: undefined,
personalBests: {},
name: undefined,
presets: [],
tags: [],
favouriteThemes: [],
refactored: false,
banned: undefined,
verified: undefined,
emailVerified: undefined,
lbMemory: {
time15: {
global: null,
daily: null,
},
time60: {
global: null,
daily: null,
},
},
globalStats: {
time: 0,
started: 0,
completed: 0,
},
};
try {
await db
.collection(`users/${user.uid}/tags/`)
.get()
.then((data) => {
data.docs.forEach((doc) => {
let tag = doc.data();
tag.id = doc.id;
if (tag.personalBests === undefined) {
tag.personalBests = {};
}
snap.tags.push(tag);
});
snap.tags = snap.tags.sort((a, b) => {
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
} else {
return 0;
}
});
})
.catch((e) => {
throw e;
});
await db
.collection(`users/${user.uid}/presets/`)
.get()
.then((data) => {
data.docs.forEach((doc) => {
// console.log(doc);
let preset = doc.data();
preset.id = doc.id;
snap.presets.push(preset);
});
snap.presets = snap.presets.sort((a, b) => {
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
} else {
return 0;
}
});
})
.catch((e) => {
throw e;
});
await db
.collection("users")
.doc(user.uid)
.get()
.then((res) => {
let data = res.data();
if (data === undefined) return;
if (data.personalBests !== undefined) {
snap.personalBests = data.personalBests;
}
snap.name = data.name;
snap.discordId = data.discordId;
snap.pairingCode =
data.discordPairingCode == null ? undefined : data.discordPairingCode;
snap.config = data.config;
snap.favouriteThemes =
data.favouriteThemes === undefined ? [] : data.favouriteThemes;
snap.refactored = data.refactored === true ? true : false;
snap.globalStats = {
time: data.timeTyping,
started: data.startedTests,
completed: data.completedTests,
};
snap.banned = data.banned;
snap.verified = data.verified;
snap.emailVerified = user.emailVerified;
try {
if (data.lbMemory.time15 !== undefined) {
snap.lbMemory.time15 = data.lbMemory.time15;
}
if (data.lbMemory.time60 !== undefined) {
snap.lbMemory.time60 = data.lbMemory.time60;
}
} catch {}
})
.catch((e) => {
throw e;
});
// console.log(snap.presets);
dbSnapshot = snap;
} catch (e) {
console.error(e);
}
loadTags(dbSnapshot.tags);
return dbSnapshot;
//send api request with token that returns tags, presets, and data needed for snap
if (firebase.auth().currentUser == null) return false;
await axiosInstance
.get("/fetchSnapshot")
.then((response) => {
dbSnapshot = response.data.snap;
loadTags(dbSnapshot.tags);
return dbSnapshot;
})
.catch((e) => {
console.error(e);
});
}
export async function getUserResults() {
@ -159,6 +47,16 @@ export async function getUserResults() {
if (dbSnapshot.results !== undefined) {
return true;
} else {
axiosInstance
.get("/userResults")
.then((response) => {
dbSnapshot.results = response.data.results;
})
.catch((error) => {
console.log(error);
});
}
/*
try {
return await db
.collection(`users/${user.uid}/results/`)
@ -171,6 +69,7 @@ export async function getUserResults() {
let result = doc.data();
result.id = doc.id;
//this should be done server-side
if (result.bailedOut === undefined) result.bailedOut = false;
if (result.blindMode === undefined) result.blindMode = false;
if (result.difficulty === undefined) result.difficulty = "normal";
@ -191,6 +90,7 @@ export async function getUserResults() {
return false;
}
}
*/
}
export async function getUserHighestWpm(
@ -403,7 +303,7 @@ export async function getLocalTagPB(
) {
function cont() {
let ret = 0;
let filteredtag = dbSnapshot.tags.filter((t) => t.id === tagId)[0];
let filteredtag = dbSnapshot.tags.filter((t) => t._id === tagId)[0];
try {
filteredtag.personalBests[mode][mode2].forEach((pb) => {
if (
@ -414,10 +314,10 @@ export async function getLocalTagPB(
ret = pb.wpm;
}
});
return ret;
} catch (e) {
return ret;
console.log(e);
}
return ret;
}
let retval;
@ -443,7 +343,7 @@ export async function saveLocalTagPB(
) {
if (mode == "quote") return;
function cont() {
let filteredtag = dbSnapshot.tags.filter((t) => t.id === tagId)[0];
let filteredtag = dbSnapshot.tags.filter((t) => t._id === tagId)[0];
try {
let found = false;
if (filteredtag.personalBests[mode][mode2] === undefined) {
@ -500,22 +400,27 @@ export async function saveLocalTagPB(
}
export function updateLbMemory(mode, mode2, type, value) {
//could dbSnapshot just be used here instead of getSnapshot()
getSnapshot().lbMemory[mode + mode2][type] = value;
}
export async function saveConfig(config) {
if (firebase.auth().currentUser !== null) {
AccountButton.loading(true);
CloudFunctions.saveConfig({
uid: firebase.auth().currentUser.uid,
obj: config,
}).then((d) => {
AccountButton.loading(false);
if (d.data.resultCode !== 1) {
Notifications.add(`Error saving config to DB! ${d.data.message}`, 4000);
}
return;
});
axiosInstance
.post("/saveConfig", {
obj: config,
})
.then((response) => {
AccountButton.loading(false);
if (response.data.resultCode !== 1) {
Notifications.add(
`Error saving config to DB! ${response.data.message}`,
4000
);
}
return;
});
}
}
@ -543,7 +448,7 @@ export async function saveConfig(config) {
// export async functio(tagId, wpm) {
// function cont() {
// dbSnapshot.tags.forEach((tag) => {
// if (tag.id === tagId) {
// if (tag._id === tagId) {
// tag.pb = wpm;
// }
// });

View file

@ -1,6 +1,7 @@
import * as CloudFunctions from "./cloud-functions";
import * as Loader from "./loader";
import * as Notifications from "./notifications";
import * as DB from "./db";
import axiosInstance from "./axios-instance";
let currentLeaderboard = "time_15";
@ -27,25 +28,10 @@ function update() {
let boardinfo = currentLeaderboard.split("_");
let uid = null;
if (firebase.auth().currentUser !== null) {
uid = firebase.auth().currentUser.uid;
}
Loader.show();
Promise.all([
CloudFunctions.getLeaderboard({
mode: boardinfo[0],
mode2: boardinfo[1],
type: "daily",
uid: uid,
}),
CloudFunctions.getLeaderboard({
mode: boardinfo[0],
mode2: boardinfo[1],
type: "global",
uid: uid,
}),
axiosInstance.get(`/getLeaderboard/daily/${boardinfo[0]}/${boardinfo[1]}`),
axiosInstance.get(`/getLeaderboard/global/${boardinfo[0]}/${boardinfo[1]}`),
])
.then((lbdata) => {
Loader.hide();
@ -53,7 +39,8 @@ function update() {
let globalData = lbdata[1].data;
//daily
let diffAsDate = new Date(dailyData.resetTime - Date.now());
let nextReset = new Date(dailyData.resetTime);
let diffAsDate = new Date(nextReset - Date.now());
let diffHours = diffAsDate.getUTCHours();
let diffMinutes = diffAsDate.getUTCMinutes();
@ -91,7 +78,8 @@ function update() {
dailyData.board.forEach((entry) => {
if (entry.hidden) return;
let meClassString = "";
if (entry.currentUser) {
//hacky way to get username because auth().currentUser.name isn't working after mongo switch
if (DB.getSnapshot() && entry.name == DB.getSnapshot().name) {
meClassString = ' class="me"';
$("#leaderboardsWrapper table.daily tfoot").html(`
<tr>
@ -174,7 +162,7 @@ function update() {
globalData.board.forEach((entry) => {
if (entry.hidden) return;
let meClassString = "";
if (entry.currentUser) {
if (DB.getSnapshot() && entry.name == DB.getSnapshot().name) {
meClassString = ' class="me"';
$("#leaderboardsWrapper table.global tfoot").html(`
<tr>

View file

@ -1,4 +1,5 @@
import * as Loader from "./loader";
import axiosInstance from "./axios-instance";
export function getuid() {
console.error("Only share this uid with Miodec and nobody else!");
@ -314,7 +315,8 @@ export function migrateFromCookies() {
export function sendVerificationEmail() {
Loader.show();
let cu = firebase.auth().currentUser;
cu.sendEmailVerification()
axiosInstance
.post("/sendEmailVerification", {})
.then(() => {
Loader.hide();
showNotification("Email sent to " + cu.email, 4000);

View file

@ -1,9 +1,9 @@
import * as Loader from "./loader";
import * as DB from "./db";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import * as Config from "./config";
import axiosInstance from "./axios-instance";
export function show(action, id, name) {
if (action === "add") {
@ -71,75 +71,80 @@ function apply() {
hide();
if (action === "add") {
Loader.show();
CloudFunctions.addPreset({
uid: firebase.auth().currentUser.uid,
obj: {
name: inputVal,
config: configChanges,
},
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset added", 1, 2);
DB.getSnapshot().presets.push({
axiosInstance
.post("/addPreset", {
obj: {
name: inputVal,
config: configChanges,
id: e.data.id,
});
Settings.update();
} else if (status === -1) {
Notifications.add("Invalid preset name", 0);
} else if (status === -2) {
Notifications.add("You can't add any more presets", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
},
})
.then((e) => {
console.log(e);
console.log("Should be ready to go away");
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset added", 1, 2);
DB.getSnapshot().presets.push({
name: inputVal,
config: configChanges,
_id: e.data.id,
});
Settings.update();
} else if (status === -1) {
Notifications.add("Invalid preset name", 0);
} else if (status === -2) {
Notifications.add("You can't add any more presets", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
} else if (action === "edit") {
Loader.show();
CloudFunctions.editPreset({
uid: firebase.auth().currentUser.uid,
name: inputVal,
presetid,
config: configChanges,
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset updated", 1);
let preset = DB.getSnapshot().presets.filter(
(preset) => preset.id == presetid
)[0];
preset.name = inputVal;
preset.config = configChanges;
Settings.update();
} else if (status === -1) {
Notifications.add("Invalid preset name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
axiosInstance
.post("/editPreset", {
presetName: inputVal,
presetid: presetid,
config: configChanges,
})
.then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset updated", 1);
let preset = DB.getSnapshot().presets.filter(
(preset) => preset._id == presetid
)[0];
preset.name = inputVal;
preset.config = configChanges;
Settings.update();
} else if (status === -1) {
Notifications.add("Invalid preset name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
} else if (action === "remove") {
Loader.show();
CloudFunctions.removePreset({
uid: firebase.auth().currentUser.uid,
presetid,
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset removed", 1);
DB.getSnapshot().presets.forEach((preset, index) => {
if (preset.id === presetid) {
DB.getSnapshot().presets.splice(index, 1);
}
});
Settings.update();
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
axiosInstance
.post("/removePreset", {
presetid,
})
.then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Preset removed", 1);
DB.getSnapshot().presets.forEach((preset, index) => {
if (preset._id === presetid) {
DB.getSnapshot().presets.splice(index, 1);
}
});
Settings.update();
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
}
}

View file

@ -2,9 +2,9 @@ import * as ResultTagsPopup from "./result-tags-popup";
import * as ResultFilters from "./result-filters";
import * as Loader from "./loader";
import * as DB from "./db";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import axiosInstance from "./axios-instance";
export function show(action, id, name) {
if (action === "add") {
@ -66,74 +66,77 @@ function apply() {
hide();
if (action === "add") {
Loader.show();
CloudFunctions.addTag({
uid: firebase.auth().currentUser.uid,
name: inputVal,
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag added", 1, 2);
DB.getSnapshot().tags.push({
name: inputVal,
id: e.data.id,
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status === -1) {
Notifications.add("Invalid tag name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
axiosInstance
.post("/addTag", {
tagName: inputVal,
})
.then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag added", 1, 2);
DB.getSnapshot().tags.push({
name: inputVal,
_id: e.data.id,
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status === -1) {
Notifications.add("Invalid tag name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
} else if (action === "edit") {
Loader.show();
CloudFunctions.editTag({
uid: firebase.auth().currentUser.uid,
name: inputVal,
tagid: tagid,
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag updated", 1);
DB.getSnapshot().tags.forEach((tag) => {
if (tag.id === tagid) {
tag.name = inputVal;
}
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status === -1) {
Notifications.add("Invalid tag name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
axiosInstance
.post("/editTag", {
tagName: inputVal,
tagId: tagid,
})
.then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag updated", 1);
DB.getSnapshot().tags.forEach((tag) => {
if (tag._id === tagid) {
tag.name = inputVal;
}
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status === -1) {
Notifications.add("Invalid tag name", 0);
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
} else if (action === "remove") {
Loader.show();
CloudFunctions.removeTag({
uid: firebase.auth().currentUser.uid,
tagid: tagid,
}).then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag removed", 1);
DB.getSnapshot().tags.forEach((tag, index) => {
if (tag.id === tagid) {
DB.getSnapshot().tags.splice(index, 1);
}
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
axiosInstance
.post("/removeTag", {
tagId: tagid,
})
.then((e) => {
Loader.hide();
let status = e.data.resultCode;
if (status === 1) {
Notifications.add("Tag removed", 1);
DB.getSnapshot().tags.forEach((tag, index) => {
if (tag._id === tagid) {
DB.getSnapshot().tags.splice(index, 1);
}
});
ResultTagsPopup.updateButtons();
Settings.update();
ResultFilters.updateTags();
} else if (status < -1) {
Notifications.add("Unknown error: " + e.data.message, -1);
}
});
}
}

View file

@ -1,7 +1,7 @@
import * as DB from "./db";
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import axiosInstance from "./axios-instance";
function show() {
if ($("#resultEditTagsPanelWrapper").hasClass("hidden")) {
@ -34,7 +34,7 @@ export function updateButtons() {
$("#resultEditTagsPanel .buttons").empty();
DB.getSnapshot().tags.forEach((tag) => {
$("#resultEditTagsPanel .buttons").append(
`<div class="button tag" tagid="${tag.id}">${tag.name}</div>`
`<div class="button tag" tagid="${tag._id}">${tag.name}</div>`
);
});
}
@ -85,65 +85,66 @@ $("#resultEditTagsPanel .confirmButton").click((e) => {
});
Loader.show();
hide();
CloudFunctions.updateResultTags({
uid: firebase.auth().currentUser.uid,
tags: newtags,
resultid: resultid,
}).then((r) => {
Loader.hide();
if (r.data.resultCode === 1) {
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results.forEach((result) => {
if (result.id === resultid) {
result.tags = newtags;
}
});
let tagNames = "";
if (newtags.length > 0) {
newtags.forEach((tag) => {
DB.getSnapshot().tags.forEach((snaptag) => {
if (tag === snaptag.id) {
tagNames += snaptag.name + ", ";
}
});
axiosInstance
.post("/updateResultTags", {
tags: newtags,
resultid: resultid,
})
.then((r) => {
Loader.hide();
if (r.data.resultCode === 1) {
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results.forEach((result) => {
if (result.id === resultid) {
result.tags = newtags;
}
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newtags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newtags);
}
let tagNames = "";
if (newtags.length > 0) {
newtags.forEach((tag) => {
DB.getSnapshot().tags.forEach((snaptag) => {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newtags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newtags);
}
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"tags",
restags
);
if (newtags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
1
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
tagNames
"tags",
restags
);
if (newtags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
1
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
tagNames
);
} else {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
"no tags"
);
}
} else {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
"no tags"
);
Notifications.add("Error updating tags: " + r.data.message, -1);
}
} else {
Notifications.add("Error updating tags: " + r.data.message, -1);
}
});
});
});

View file

@ -2,12 +2,12 @@ import * as Config from "./config";
import * as DB from "./db";
import * as Notifications from "./notifications";
import * as Settings from "./settings";
import * as TestLogic from './test-logic';
import * as TestLogic from "./test-logic";
export function apply(id) {
// console.log(DB.getSnapshot().presets);
DB.getSnapshot().presets.forEach((preset) => {
if (preset.id == id) {
if (preset._id == id) {
Config.apply(JSON.parse(JSON.stringify(preset.config)));
TestLogic.restart();
Notifications.add("Preset applied", 1, 2);

View file

@ -14,6 +14,8 @@ UpdateConfig.loadFromLocalStorage();
Misc.getReleasesFromGitHub();
$(document).ready(() => {
console.log("Ready started");
//this makes the center content visible
RouteController.handleInitialPageClasses(window.location.pathname);
if (window.location.pathname === "/") {
$("#top .config").removeClass("hidden");

View file

@ -8,6 +8,7 @@ import * as Notifications from "./notifications";
import * as DB from "./db";
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import axiosInstance from "./axios-instance";
import * as Funbox from "./funbox";
import * as TagController from "./tag-controller";
import * as PresetController from "./preset-controller";
@ -410,11 +411,11 @@ function showActiveTags() {
DB.getSnapshot().tags.forEach((tag) => {
if (tag.active === true) {
$(
`.pageSettings .section.tags .tagsList .tag[id='${tag.id}'] .active`
`.pageSettings .section.tags .tagsList .tag[id='${tag._id}'] .active`
).html('<i class="fas fa-check-square"></i>');
} else {
$(
`.pageSettings .section.tags .tagsList .tag[id='${tag.id}'] .active`
`.pageSettings .section.tags .tagsList .tag[id='${tag._id}'] .active`
).html('<i class="fas fa-square"></i>');
}
});
@ -461,7 +462,7 @@ function refreshTagsSettingsSection() {
tagPbString = `PB: ${tag.pb}`;
}
tagsEl.append(`
<div class="tag" id="${tag.id}">
<div class="tag" id="${tag._id}">
<div class="active" active="${tag.active}">
<i class="fas fa-${tag.active ? "check-" : ""}square"></i>
</div>
@ -484,7 +485,7 @@ function refreshPresetsSettingsSection() {
let presetsEl = $(".pageSettings .section.presets .presetsList").empty();
DB.getSnapshot().presets.forEach((preset) => {
presetsEl.append(`
<div class="buttons preset" id="${preset.id}">
<div class="buttons preset" id="${preset._id}">
<div class="button presetButton">
<div class="title">${preset.name}</div>
</div>
@ -674,9 +675,7 @@ $(".pageSettings .section.discordIntegration #unlinkDiscordButton").click(
(e) => {
if (confirm("Are you sure?")) {
Loader.show();
CloudFunctions.unlinkDiscord({
uid: firebase.auth().currentUser.uid,
}).then((ret) => {
axiosInstance.post("/unlinkDiscord").then((ret) => {
Loader.hide();
console.log(ret);
if (ret.data.status === 1) {

View file

@ -1,9 +1,9 @@
import * as Loader from "./loader";
import * as CloudFunctions from "./cloud-functions";
import * as Notifications from "./notifications";
import * as AccountController from "./account-controller";
import * as DB from "./db";
import * as Settings from "./settings";
import axiosInstance from "./axios-instance";
export let list = {};
class SimplePopup {
@ -151,26 +151,27 @@ list.updateEmail = new SimplePopup(
(previousEmail, newEmail) => {
try {
Loader.show();
CloudFunctions.updateEmail({
uid: firebase.auth().currentUser.uid,
previousEmail: previousEmail,
newEmail: newEmail,
}).then((data) => {
Loader.hide();
if (data.data.resultCode === 1) {
Notifications.add("Email updated", 0);
setTimeout(() => {
AccountController.signOut();
}, 1000);
} else if (data.data.resultCode === -1) {
Notifications.add("Current email doesn't match", 0);
} else {
Notifications.add(
"Something went wrong: " + JSON.stringify(data.data),
-1
);
}
});
axiosInstance
.post("/updateEmail", {
previousEmail: previousEmail,
newEmail: newEmail,
})
.then((data) => {
Loader.hide();
if (data.data.resultCode === 1) {
Notifications.add("Email updated", 0);
setTimeout(() => {
AccountController.signOut();
}, 1000);
} else if (data.data.resultCode === -1) {
Notifications.add("Current email doesn't match", 0);
} else {
Notifications.add(
"Something went wrong: " + JSON.stringify(data.data),
-1
);
}
});
} catch (e) {
Notifications.add("Something went wrong: " + e, -1);
}
@ -188,10 +189,10 @@ list.clearTagPb = new SimplePopup(
() => {
let tagid = eval("this.parameters[0]");
Loader.show();
CloudFunctions.clearTagPb({
uid: firebase.auth().currentUser.uid,
tagid: tagid,
})
axiosInstance
.post("/clearTagPb", {
tagid: tagid,
})
.then((res) => {
Loader.hide();
if (res.data.resultCode === 1) {
@ -245,10 +246,7 @@ list.resetPersonalBests = new SimplePopup(
() => {
try {
Loader.show();
CloudFunctions.resetPersonalBests({
uid: firebase.auth().currentUser.uid,
}).then((res) => {
axiosInstance.post("/resetPersonalBests").then((res) => {
if (res) {
Loader.hide();
Notifications.add(

View file

@ -1,7 +1,7 @@
import Config from "./config";
import {Howl, Howler} from 'howler';
import { Howl, Howler } from "howler";
let errorSound = new Howl({src:["../sound/error.wav"]});
let errorSound = new Howl({ src: ["../sound/error.wav"] });
let clickSounds = null;
export function init() {
@ -10,22 +10,22 @@ export function init() {
1: [
{
sounds: [
new Howl({src:"../sound/click1/click1_1.wav"}),
new Howl({src:"../sound/click1/click1_1.wav"}),
new Howl({ src: "../sound/click1/click1_1.wav" }),
new Howl({ src: "../sound/click1/click1_1.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click1/click1_2.wav"}),
new Howl({src:"../sound/click1/click1_2.wav"}),
new Howl({ src: "../sound/click1/click1_2.wav" }),
new Howl({ src: "../sound/click1/click1_2.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click1/click1_3.wav"}),
new Howl({src:"../sound/click1/click1_3.wav"}),
new Howl({ src: "../sound/click1/click1_3.wav" }),
new Howl({ src: "../sound/click1/click1_3.wav" }),
],
counter: 0,
},
@ -33,22 +33,22 @@ export function init() {
2: [
{
sounds: [
new Howl({src:"../sound/click2/click2_1.wav"}),
new Howl({src:"../sound/click2/click2_1.wav"}),
new Howl({ src: "../sound/click2/click2_1.wav" }),
new Howl({ src: "../sound/click2/click2_1.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click2/click2_2.wav"}),
new Howl({src:"../sound/click2/click2_2.wav"}),
new Howl({ src: "../sound/click2/click2_2.wav" }),
new Howl({ src: "../sound/click2/click2_2.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click2/click2_3.wav"}),
new Howl({src:"../sound/click2/click2_3.wav"}),
new Howl({ src: "../sound/click2/click2_3.wav" }),
new Howl({ src: "../sound/click2/click2_3.wav" }),
],
counter: 0,
},
@ -56,22 +56,22 @@ export function init() {
3: [
{
sounds: [
new Howl({src:"../sound/click3/click3_1.wav"}),
new Howl({src:"../sound/click3/click3_1.wav"}),
new Howl({ src: "../sound/click3/click3_1.wav" }),
new Howl({ src: "../sound/click3/click3_1.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click3/click3_2.wav"}),
new Howl({src:"../sound/click3/click3_2.wav"}),
new Howl({ src: "../sound/click3/click3_2.wav" }),
new Howl({ src: "../sound/click3/click3_2.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click3/click3_3.wav"}),
new Howl({src:"../sound/click3/click3_3.wav"}),
new Howl({ src: "../sound/click3/click3_3.wav" }),
new Howl({ src: "../sound/click3/click3_3.wav" }),
],
counter: 0,
},
@ -79,43 +79,43 @@ export function init() {
4: [
{
sounds: [
new Howl({src:"../sound/click4/click4_1.wav"}),
new Howl({src:"../sound/click4/click4_11.wav"}),
new Howl({ src: "../sound/click4/click4_1.wav" }),
new Howl({ src: "../sound/click4/click4_11.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click4/click4_2.wav"}),
new Howl({src:"../sound/click4/click4_22.wav"}),
new Howl({ src: "../sound/click4/click4_2.wav" }),
new Howl({ src: "../sound/click4/click4_22.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click4/click4_3.wav"}),
new Howl({src:"../sound/click4/click4_33.wav"}),
new Howl({ src: "../sound/click4/click4_3.wav" }),
new Howl({ src: "../sound/click4/click4_33.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click4/click4_4.wav"}),
new Howl({src:"../sound/click4/click4_44.wav"}),
new Howl({ src: "../sound/click4/click4_4.wav" }),
new Howl({ src: "../sound/click4/click4_44.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click4/click4_5.wav"}),
new Howl({src:"../sound/click4/click4_55.wav"}),
new Howl({ src: "../sound/click4/click4_5.wav" }),
new Howl({ src: "../sound/click4/click4_55.wav" }),
],
counter: 0,
},
{
sounds: [
new Howl({src:"../sound/click4/click4_6.wav"}),
new Howl({src:"../sound/click4/click4_66.wav"}),
new Howl({ src: "../sound/click4/click4_6.wav" }),
new Howl({ src: "../sound/click4/click4_66.wav" }),
],
counter: 0,
},

View file

@ -7,7 +7,7 @@ export function saveActiveToLocalStorage() {
try {
DB.getSnapshot().tags.forEach((tag) => {
if (tag.active === true) {
tags.push(tag.id);
tags.push(tag._id);
}
});
// let d = new Date();
@ -23,7 +23,7 @@ export function saveActiveToLocalStorage() {
export function toggle(tagid, nosave = false) {
DB.getSnapshot().tags.forEach((tag) => {
if (tag.id === tagid) {
if (tag._id === tagid) {
if (tag.active === undefined) {
tag.active = true;
} else {

View file

@ -6,8 +6,7 @@ import * as DB from "./db";
export let settings = null;
function resetCaretPosition() {
if (Config.paceCaret === "off" && !TestLogic.isPaceRepeat)
return;
if (Config.paceCaret === "off" && !TestLogic.isPaceRepeat) return;
if (!$("#paceCaret").hasClass("hidden")) {
$("#paceCaret").addClass("hidden");
}

View file

@ -1,8 +1,8 @@
import * as CloudFunctions from "./cloud-functions";
import * as DB from "./db";
import * as Notifications from "./notifications";
import Config from "./config";
import * as Misc from "./misc";
import axiosInstance from "./axios-instance";
let textTimeouts = [];
@ -160,17 +160,10 @@ export async function check(completedEvent) {
delete lbRes.keySpacing;
delete lbRes.keyDuration;
delete lbRes.chartData;
CloudFunctions.checkLeaderboards({
// uid: completedEvent.uid,
token: await firebase.auth().currentUser.getIdToken(),
// lbMemory: DB.getSnapshot().lbMemory,
// emailVerified: DB.getSnapshot().emailVerified,
// name: DB.getSnapshot().name,
// banned: DB.getSnapshot().banned,
// verified: DB.getSnapshot().verified,
// discordId: DB.getSnapshot().discordId,
result: lbRes,
})
axiosInstance
.post("/attemptAddToLeaderboards", {
result: lbRes,
})
.then((data) => {
if (data.data.status === -999) {
if (data.data.message === "Bad token") {

View file

@ -25,9 +25,9 @@ import * as OutOfFocus from "./out-of-focus";
import * as AccountButton from "./account-button";
import * as DB from "./db";
import * as ThemeColors from "./theme-colors";
import * as CloudFunctions from "./cloud-functions";
import * as TestLeaderboards from "./test-leaderboards";
import * as Replay from "./replay.js";
import axiosInstance from "./axios-instance";
import * as MonkeyPower from "./monkey-power";
let glarsesMode = false;
@ -1329,7 +1329,7 @@ export function finish(difficultyFailed = false) {
DB.getSnapshot().tags.forEach((tag) => {
if (tag.active === true) {
activeTags.push(tag);
activeTagsIds.push(tag.id);
activeTagsIds.push(tag._id);
}
});
} catch (e) {}
@ -1497,7 +1497,7 @@ export function finish(difficultyFailed = false) {
let annotationSide = "left";
activeTags.forEach(async (tag) => {
let tpb = await DB.getLocalTagPB(
tag.id,
tag._id,
Config.mode,
mode2,
Config.punctuation,
@ -1505,13 +1505,13 @@ export function finish(difficultyFailed = false) {
Config.difficulty
);
$("#result .stats .tags .bottom").append(`
<div tagid="${tag.id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.name}<i class="fas fa-crown hidden"></i></div>
<div tagid="${tag._id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.name}<i class="fas fa-crown hidden"></i></div>
`);
if (Config.mode != "quote") {
if (tpb < stats.wpm) {
//new pb for that tag
DB.saveLocalTagPB(
tag.id,
tag._id,
Config.mode,
mode2,
Config.punctuation,
@ -1523,12 +1523,11 @@ export function finish(difficultyFailed = false) {
consistency
);
$(
`#result .stats .tags .bottom div[tagid="${tag.id}"] .fas`
`#result .stats .tags .bottom div[tagid="${tag._id}"] .fas`
).removeClass("hidden");
$(`#result .stats .tags .bottom div[tagid="${tag.id}"]`).attr(
"aria-label",
"+" + Misc.roundTo2(stats.wpm - tpb)
);
$(
`#result .stats .tags .bottom div[tagid="${tag._id}"]`
).attr("aria-label", "+" + Misc.roundTo2(stats.wpm - tpb));
// console.log("new pb for tag " + tag.name);
} else {
ChartController.result.options.annotation.annotations.push({
@ -1577,10 +1576,10 @@ export function finish(difficultyFailed = false) {
AccountButton.loading(false);
Notifications.add("You are offline. Result not saved.", -1);
} else {
CloudFunctions.testCompleted({
uid: firebase.auth().currentUser.uid,
obj: completedEvent,
})
axiosInstance
.post("/testCompleted", {
obj: completedEvent,
})
.then((e) => {
AccountButton.loading(false);
if (e.data == null) {
@ -1629,7 +1628,7 @@ export function finish(difficultyFailed = false) {
DB.getSnapshot() !== null &&
DB.getSnapshot().results !== undefined
) {
DB.getSnapshot().results.unshift(completedEvent);
DB.getSnapshot().results.push(completedEvent);
if (DB.getSnapshot().globalStats.time == undefined) {
DB.getSnapshot().globalStats.time =
testtime +

View file

@ -108,6 +108,8 @@ export function swapElements(
}
export function changePage(page) {
//next line fixes endless loading when accessing account via url
if (page == "account") pageTransition = false;
if (pageTransition) {
return;
}
@ -204,6 +206,8 @@ export function changePage(page) {
}
}
//checking if the project is the development site
/*
if (firebase.app().options.projectId === "monkey-type-dev-67af4") {
$("#top .logo .bottom").text("monkey-dev");
$("head title").text("Monkey Dev");
@ -211,6 +215,7 @@ if (firebase.app().options.projectId === "monkey-type-dev-67af4") {
`<div class="devIndicator tr">DEV</div><div class="devIndicator bl">DEV</div>`
);
}
*/
if (window.location.hostname === "localhost") {
window.onerror = function (error) {
@ -218,7 +223,7 @@ if (window.location.hostname === "localhost") {
};
$("#top .logo .top").text("localhost");
$("head title").text($("head title").text() + " (localhost)");
firebase.functions().useFunctionsEmulator("http://localhost:5001");
//firebase.functions().useFunctionsEmulator("http://localhost:5001");
$("body").append(
`<div class="devIndicator tl">local</div><div class="devIndicator br">local</div>`
);

View file

@ -1,220 +1,220 @@
{
"name": "bulgarian",
"leftToRight": true,
"words": [
"а",
"аз",
"ако",
"ами",
"баща",
"без",
"беше",
"би",
"бил",
"бих",
"благодаря",
"бъде",
"бях",
"в",
"вас",
"веднага",
"вече",
"вечер",
"ви",
"видим",
"видя",
"вие",
"виж",
"винаги",
"време",
"все",
"всеки",
"всички",
"всичко",
"всъщност",
"във",
"ги",
"го",
"говоря",
"години",
"да",
"дай",
"дали",
"две",
"ден",
"днес",
"до",
"добър",
"дойде",
"докато",
"дори",
"доста",
"достатъчно",
"друг",
"друго",
"е",
"един",
"една",
"едно",
"ей",
"ела",
"ето",
"жена",
"живот",
"за",
"заедно",
"заради",
"затова",
"защо",
"защото",
"здравей",
"знае",
"знаете",
"знаеш",
"знам",
"значи",
"и",
"изглежда",
"или",
"им",
"има",
"имам",
"имаме",
"имате",
"имаш",
"иска",
"каза",
"казах",
"казвам",
"как",
"каква",
"какво",
"каквото",
"както",
"какъв",
"като",
"когато",
"което",
"които",
"кой",
"който",
"кола",
"колко",
"която",
"къде",
"към",
"ли",
"майка",
"малко",
"ме",
"между",
"мен",
"ми",
"мисля",
"много",
"мога",
"може",
"моля",
"момиче",
"моя",
"му",
"място",
"на",
"наистина",
"нали",
"направи",
"направя",
"наред",
"нас",
"начин",
"не",
"него",
"нека",
"неща",
"нещо",
"нея",
"ни",
"ние",
"никога",
"никой",
"нищо",
"но",
"нощ",
"нужда",
"някой",
"няколко",
"няма",
"нямам",
"обичам",
"от",
"отново",
"още",
"пак",
"пари",
"по",
"повече",
"под",
"после",
"правя",
"пред",
"предвид",
"преди",
"през",
"при",
"приятел",
"проблем",
"просто",
"път",
"работа",
"разбирам",
"с",
"са",
"сам",
"само",
"се",
"себе",
"сега",
"си",
"сигурен",
"скъпа",
"след",
"случи",
"сме",
"става",
"сте",
"стига",
"съжалявам",
"съм",
"със",
"също",
"тази",
"така",
"там",
"татко",
"те",
"теб",
"тези",
"ти",
"това",
"тогава",
"този",
"той",
"толкова",
"точно",
"три",
"трябва",
"тук",
"тя",
"тях",
"утре",
"хайде",
"харесвам",
"хора",
"хората",
"чакай",
"че",
"човек",
"ще",
"я"
]
}
"name": "bulgarian",
"leftToRight": true,
"words": [
"а",
"аз",
"ако",
"ами",
"баща",
"без",
"беше",
"би",
"бил",
"бих",
"благодаря",
"бъде",
"бях",
"в",
"вас",
"веднага",
"вече",
"вечер",
"ви",
"видим",
"видя",
"вие",
"виж",
"винаги",
"време",
"все",
"всеки",
"всички",
"всичко",
"всъщност",
"във",
"ги",
"го",
"говоря",
"години",
"да",
"дай",
"дали",
"две",
"ден",
"днес",
"до",
"добър",
"дойде",
"докато",
"дори",
"доста",
"достатъчно",
"друг",
"друго",
"е",
"един",
"една",
"едно",
"ей",
"ела",
"ето",
"жена",
"живот",
"за",
"заедно",
"заради",
"затова",
"защо",
"защото",
"здравей",
"знае",
"знаете",
"знаеш",
"знам",
"значи",
"и",
"изглежда",
"или",
"им",
"има",
"имам",
"имаме",
"имате",
"имаш",
"иска",
"каза",
"казах",
"казвам",
"как",
"каква",
"какво",
"каквото",
"както",
"какъв",
"като",
"когато",
"което",
"които",
"кой",
"който",
"кола",
"колко",
"която",
"къде",
"към",
"ли",
"майка",
"малко",
"ме",
"между",
"мен",
"ми",
"мисля",
"много",
"мога",
"може",
"моля",
"момиче",
"моя",
"му",
"място",
"на",
"наистина",
"нали",
"направи",
"направя",
"наред",
"нас",
"начин",
"не",
"него",
"нека",
"неща",
"нещо",
"нея",
"ни",
"ние",
"никога",
"никой",
"нищо",
"но",
"нощ",
"нужда",
"някой",
"няколко",
"няма",
"нямам",
"обичам",
"от",
"отново",
"още",
"пак",
"пари",
"по",
"повече",
"под",
"после",
"правя",
"пред",
"предвид",
"преди",
"през",
"при",
"приятел",
"проблем",
"просто",
"път",
"работа",
"разбирам",
"с",
"са",
"сам",
"само",
"се",
"себе",
"сега",
"си",
"сигурен",
"скъпа",
"след",
"случи",
"сме",
"става",
"сте",
"стига",
"съжалявам",
"съм",
"със",
"също",
"тази",
"така",
"там",
"татко",
"те",
"теб",
"тези",
"ти",
"това",
"тогава",
"този",
"той",
"толкова",
"точно",
"три",
"трябва",
"тук",
"тя",
"тях",
"утре",
"хайде",
"харесвам",
"хора",
"хората",
"чакай",
"че",
"човек",
"ще",
"я"
]
}

View file

@ -1,76 +1,75 @@
{
"name": "code_swift",
"leftToRight": true,
"words": [
"Class",
"deinit",
"Enum",
"extension",
"Func",
"import",
"Init",
"operator",
"private",
"protocol",
"public",
"static",
"struct",
"subscript",
"break",
"case",
"continue",
"default",
"do",
"else",
"for",
"return",
"switch",
"where",
"while",
"as",
"false",
"is",
"dynamicType",
"super",
"true",
"_COLUMN_",
"Let",
"in",
"_FILE_",
"internal",
"typealias",
"if",
"nil",
"var",
"self",
"unowned",
"_FUNCTION_",
"LINE",
"associativity",
"convenience",
"dynamic",
"didSet",
"precedence",
"final",
"get",
"infix",
"inout",
"right",
"set",
"type",
"lazy",
"left",
"mutating",
"none",
"weak",
"willSet",
"prefix",
"nonmutating",
"optional",
"override",
"postfix",
"Protocall",
"required"
]
}
"name": "code_swift",
"leftToRight": true,
"words": [
"Class",
"deinit",
"Enum",
"extension",
"Func",
"import",
"Init",
"operator",
"private",
"protocol",
"public",
"static",
"struct",
"subscript",
"break",
"case",
"continue",
"default",
"do",
"else",
"for",
"return",
"switch",
"where",
"while",
"as",
"false",
"is",
"dynamicType",
"super",
"true",
"_COLUMN_",
"Let",
"in",
"_FILE_",
"internal",
"typealias",
"if",
"nil",
"var",
"self",
"unowned",
"_FUNCTION_",
"LINE",
"associativity",
"convenience",
"dynamic",
"didSet",
"precedence",
"final",
"get",
"infix",
"inout",
"right",
"set",
"type",
"lazy",
"left",
"mutating",
"none",
"weak",
"willSet",
"prefix",
"nonmutating",
"optional",
"override",
"postfix",
"Protocall",
"required"
]
}

View file

@ -3,204 +3,204 @@
"leftToRight": true,
"words": [
"izany",
"no",
"izy",
"anaka",
"hoy",
"ny",
"efa",
"ka",
"zakao",
"tsia",
"rainy",
"tsy",
"avy",
"izao",
"fa",
"atao",
"hoe",
"tsara",
"anie",
"izay",
"teto",
"anao",
"nefa",
"nisy",
"hono",
"ho",
"aho",
"ry",
"dada",
"ve",
"ity",
"ary",
"aminy",
"dia",
"vita",
"teny",
"misy",
"olona",
"azo",
"eny",
"ianao",
"ilay",
"raha",
"sy",
"hita",
"tody",
"dieny",
"aoka",
"eto",
"mba",
"loza",
"be",
"soa",
"ratsy",
"mamy",
"na",
"samy",
"hafa",
"asa",
"tokoa",
"io",
"natao",
"ireny",
"any",
"azy",
"noho",
"hahay",
"tia",
"ihany",
"koa",
"sady",
"saina",
"ireo",
"poeta",
"anefa",
"mety",
"akory",
"maha",
"eo",
"avo",
"isaky",
"isika",
"tena",
"feo",
"ao",
"manao",
"adidy",
"araka",
"injao",
"amina",
"iza",
"inona",
"fomba",
"mandà",
"asany",
"afaka",
"irery",
"tonga",
"azoko",
"toa",
"mila",
"iray",
"hatao",
"andry",
"momba",
"kosa",
"anny",
"zony",
"hiasa",
"mbola",
"aina",
"mahay",
"biby",
"anaty",
"aza",
"kokoa",
"ampy",
"olana",
"hery",
"toy",
"ara",
"antsy",
"nosy",
"vao",
"maro",
"dihy",
"vary",
"sisa",
"maso",
"aleo",
"very",
"may",
"vava",
"tsapa",
"tany",
"taona",
"tiako",
"ery",
"anay",
"loha",
"lova",
"malagasy",
"omeny",
"lanja",
"afa",
"miala",
"inty",
"àry",
"tao",
"teo",
"miova",
"ray",
"iny",
"saigy",
"fiara",
"tsiny",
"resy",
"hisy",
"aminy",
"milza",
"liana",
"azao",
"vola",
"dimy",
"arivo",
"krizy",
"firy",
"tsena",
"feno",
"lasa",
"manko",
"kiry",
"miasa",
"voho",
"dika",
"roa",
"telo",
"lany",
"hay",
"dia",
"maty",
"kanto",
"sy",
"aty",
"amidy",
"foana",
"boky",
"jamba",
"adika",
"radio",
"te",
"nanao",
"ireto",
"an'ny",
"ala",
"hazo",
"tiana",
"aloka",
"manja",
"lahy"
"no",
"izy",
"anaka",
"hoy",
"ny",
"efa",
"ka",
"zakao",
"tsia",
"rainy",
"tsy",
"avy",
"izao",
"fa",
"atao",
"hoe",
"tsara",
"anie",
"izay",
"teto",
"anao",
"nefa",
"nisy",
"hono",
"ho",
"aho",
"ry",
"dada",
"ve",
"ity",
"ary",
"aminy",
"dia",
"vita",
"teny",
"misy",
"olona",
"azo",
"eny",
"ianao",
"ilay",
"raha",
"sy",
"hita",
"tody",
"dieny",
"aoka",
"eto",
"mba",
"loza",
"be",
"soa",
"ratsy",
"mamy",
"na",
"samy",
"hafa",
"asa",
"tokoa",
"io",
"natao",
"ireny",
"any",
"azy",
"noho",
"hahay",
"tia",
"ihany",
"koa",
"sady",
"saina",
"ireo",
"poeta",
"anefa",
"mety",
"akory",
"maha",
"eo",
"avo",
"isaky",
"isika",
"tena",
"feo",
"ao",
"manao",
"adidy",
"araka",
"injao",
"amina",
"iza",
"inona",
"fomba",
"mandà",
"asany",
"afaka",
"irery",
"tonga",
"azoko",
"toa",
"mila",
"iray",
"hatao",
"andry",
"momba",
"kosa",
"anny",
"zony",
"hiasa",
"mbola",
"aina",
"mahay",
"biby",
"anaty",
"aza",
"kokoa",
"ampy",
"olana",
"hery",
"toy",
"ara",
"antsy",
"nosy",
"vao",
"maro",
"dihy",
"vary",
"sisa",
"maso",
"aleo",
"very",
"may",
"vava",
"tsapa",
"tany",
"taona",
"tiako",
"ery",
"anay",
"loha",
"lova",
"malagasy",
"omeny",
"lanja",
"afa",
"miala",
"inty",
"àry",
"tao",
"teo",
"miova",
"ray",
"iny",
"saigy",
"fiara",
"tsiny",
"resy",
"hisy",
"aminy",
"milza",
"liana",
"azao",
"vola",
"dimy",
"arivo",
"krizy",
"firy",
"tsena",
"feno",
"lasa",
"manko",
"kiry",
"miasa",
"voho",
"dika",
"roa",
"telo",
"lany",
"hay",
"dia",
"maty",
"kanto",
"sy",
"aty",
"amidy",
"foana",
"boky",
"jamba",
"adika",
"radio",
"te",
"nanao",
"ireto",
"an'ny",
"ala",
"hazo",
"tiana",
"aloka",
"manja",
"lahy"
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -32873,35 +32873,35 @@
"length": 463,
"id": 5534
},
{
{
"text": "It is those who possess wisdom who are the greatest fools. History has shown us this. You could say that this is the final warning from God to those who resist.",
"source": "Steins;Gate",
"length": 160,
"id": 5535
},
{
"source": "Steins;Gate",
"length": 160,
"id": 5535
},
{
"text": "Remembering something that no one else can is a painful thing. You can't talk to anyone about it. No one will understand you. You'll be alone.",
"source": "Steins;Gate",
"length": 142,
"id": 5536
},
{
"source": "Steins;Gate",
"length": 142,
"id": 5536
},
{
"text": "No one knows what the future holds. That's why its potential is infinite.",
"source": "Steins;Gate",
"length": 73,
"id": 5537
},
{
"source": "Steins;Gate",
"length": 73,
"id": 5537
},
{
"text": "It feels like time is passing so quickly. Damn you, Einstein! Your science is crowding in on our kiss! He was right. The passage of time depends entirely on where you're standing. Relativity Theory... it's so romantic. But it's just so tragic too.",
"source": "Steins;Gate",
"length": 247,
"id": 5538
},
{
"text": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
"source": "Martin Fowler",
"length": 111,
"id": 5539
}
"source": "Steins;Gate",
"length": 247,
"id": 5538
},
{
"text": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
"source": "Martin Fowler",
"length": 111,
"id": 5539
}
]
}

View file

@ -137,7 +137,7 @@
"text": "Nežadu tavęs paleist, nežadu tavęs nuvilt, ir neketinu lakstyti ir apleisti. Sumanim neteks tau verkt, nereikės ištart sudie, nežadu tau pameluot, įskaudint",
"source": "Rikas Astlys - Nežadu Tavęs Paleist",
"length": 22,
"id": 22
"id": 22
},
{
"text": "Šaly diendaržio durų, ant didžiulės spalių krūvos, guli senas Brisius - žilas, apžabalęs. Matyti jisai dar mato, bet tik kaip per dūmus, ir savo žmogaus labai dažnai nebepažįsta. Sunki senatvė ir jam: visų užmirštas, apleistas. Patsai gerai jaučia, kad mažai kam bereikalingas.",

Binary file not shown.