Initial commit: Add MathHomeworkHelper project with web and Android components
Some checks reported errors
continuous-integration/drone Build encountered an error
Some checks reported errors
continuous-integration/drone Build encountered an error
This commit is contained in:
commit
f9558008e1
37 changed files with 5318 additions and 0 deletions
39
MathHomeworkHelper/app/build.gradle
Normal file
39
MathHomeworkHelper/app/build.gradle
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.example.mathhomeworkhelper'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.mathhomeworkhelper"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
18
MathHomeworkHelper/app/proguard-rules.pro
vendored
Normal file
18
MathHomeworkHelper/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# This is a configuration file for ProGuard.
|
||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||
|
||||
-dontusemixedcaseclassnames
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
-dontwarn com.google.gson.**
|
||||
-keep class com.google.gson.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.example.mathhomeworkhelper.** { *; }
|
||||
|
||||
# Preserve line numbers for debugging stack traces
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
28
MathHomeworkHelper/app/src/main/AndroidManifest.xml
Normal file
28
MathHomeworkHelper/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.mathhomeworkhelper">
|
||||
|
||||
<!-- Internet permission for WebView -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MathHomeworkHelper">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
86
MathHomeworkHelper/app/src/main/assets/index.html
Normal file
86
MathHomeworkHelper/app/src/main/assets/index.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Math Homework Helper - Multiplication Practice</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Language Selector -->
|
||||
<div class="language-selector">
|
||||
<div class="language-flags" id="language-flags"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 id="title">🎓 Math Homework Helper</h1>
|
||||
<p class="subtitle" id="subtitle">Multiplication Practice</p>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- Difficulty Slider Section -->
|
||||
<section class="difficulty-section">
|
||||
<label for="difficulty-slider" class="difficulty-label">
|
||||
Difficulty Level: <span id="difficulty-display">1</span>
|
||||
</label>
|
||||
<div class="slider-container">
|
||||
<span class="difficulty-text">Easy</span>
|
||||
<input
|
||||
type="range"
|
||||
id="difficulty-slider"
|
||||
min="1"
|
||||
max="5"
|
||||
value="1"
|
||||
class="slider"
|
||||
>
|
||||
<span class="difficulty-text">Hard</span>
|
||||
</div>
|
||||
<div class="difficulty-info">
|
||||
<p id="difficulty-description">1 digit × 1 digit (e.g., 5 × 7)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Problem Display Section -->
|
||||
<section class="problem-section">
|
||||
<div class="problem-display" id="problem-display">
|
||||
<!-- Problem will be rendered here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Check Answer Button -->
|
||||
<section class="answer-section">
|
||||
<button id="submit-btn" class="submit-btn" style="width: 100%;">Check Answer</button>
|
||||
</section>
|
||||
|
||||
<!-- Feedback Section -->
|
||||
<section class="feedback-section">
|
||||
<div id="feedback" class="feedback"></div>
|
||||
</section>
|
||||
|
||||
<!-- Score Section -->
|
||||
<section class="score-section">
|
||||
<div class="score-box">
|
||||
<p class="score-label">Points:</p>
|
||||
<p class="score-value" id="points-score">0/20</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- New Problem Button -->
|
||||
<button id="new-problem-btn" class="new-problem-btn">New Problem</button>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Victory Modal -->
|
||||
<div id="victory-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>🎉 Congratulations! 🎉</h2>
|
||||
<p>You've reached 20 points!</p>
|
||||
<button id="play-again-btn" class="modal-btn">Play Again</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="translations.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
651
MathHomeworkHelper/app/src/main/assets/script.js
Normal file
651
MathHomeworkHelper/app/src/main/assets/script.js
Normal file
|
|
@ -0,0 +1,651 @@
|
|||
// Game State
|
||||
let gameState = {
|
||||
difficulty: 1,
|
||||
currentProblem: null,
|
||||
points: 0,
|
||||
totalAnswers: 0
|
||||
};
|
||||
|
||||
// Difficulty configurations (will be updated with translations)
|
||||
let difficultyConfig = {
|
||||
1: {
|
||||
num1Min: 1, num1Max: 9,
|
||||
num2Min: 1, num2Max: 9,
|
||||
descriptionKey: 'difficulty1'
|
||||
},
|
||||
2: {
|
||||
num1Min: 10, num1Max: 99,
|
||||
num2Min: 1, num2Max: 9,
|
||||
descriptionKey: 'difficulty2'
|
||||
},
|
||||
3: {
|
||||
num1Min: 10, num1Max: 99,
|
||||
num2Min: 10, num2Max: 99,
|
||||
descriptionKey: 'difficulty3'
|
||||
},
|
||||
4: {
|
||||
num1Min: 100, num1Max: 999,
|
||||
num2Min: 10, num2Max: 99,
|
||||
descriptionKey: 'difficulty4'
|
||||
},
|
||||
5: {
|
||||
num1Min: 10000, num1Max: 99999,
|
||||
num2Min: 100, num2Max: 999,
|
||||
descriptionKey: 'difficulty5'
|
||||
}
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const difficultySlider = document.getElementById('difficulty-slider');
|
||||
const difficultyDisplay = document.getElementById('difficulty-display');
|
||||
const difficultyDescription = document.getElementById('difficulty-description');
|
||||
const problemDisplay = document.getElementById('problem-display');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const feedbackDiv = document.getElementById('feedback');
|
||||
const pointsScoreDisplay = document.getElementById('points-score');
|
||||
const newProblemBtn = document.getElementById('new-problem-btn');
|
||||
const victoryModal = document.getElementById('victory-modal');
|
||||
const playAgainBtn = document.getElementById('play-again-btn');
|
||||
|
||||
// Event Listeners
|
||||
difficultySlider.addEventListener('change', handleDifficultyChange);
|
||||
submitBtn.addEventListener('click', handleSubmitAnswer);
|
||||
newProblemBtn.addEventListener('click', generateNewProblem);
|
||||
playAgainBtn.addEventListener('click', resetGame);
|
||||
|
||||
// Initialize
|
||||
window.addEventListener('load', async () => {
|
||||
await i18n.initialize();
|
||||
initializeLanguageSelector();
|
||||
updateUIText();
|
||||
generateNewProblem();
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle difficulty slider change
|
||||
*/
|
||||
function handleDifficultyChange() {
|
||||
gameState.difficulty = parseInt(difficultySlider.value);
|
||||
difficultyDisplay.textContent = gameState.difficulty;
|
||||
difficultyDescription.textContent = difficultyConfig[gameState.difficulty].description;
|
||||
generateNewProblem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random number between min and max (inclusive)
|
||||
*/
|
||||
function getRandomNumber(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new multiplication problem
|
||||
*/
|
||||
function generateNewProblem() {
|
||||
const config = difficultyConfig[gameState.difficulty];
|
||||
const num1 = getRandomNumber(config.num1Min, config.num1Max);
|
||||
const num2 = getRandomNumber(config.num2Min, config.num2Max);
|
||||
const answer = num1 * num2;
|
||||
|
||||
gameState.currentProblem = {
|
||||
num1,
|
||||
num2,
|
||||
answer
|
||||
};
|
||||
|
||||
displayProblem();
|
||||
clearFeedback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate intermediate steps for vertical multiplication
|
||||
*/
|
||||
function calculateIntermediateSteps(num1, num2) {
|
||||
const num2Str = num2.toString();
|
||||
const steps = [];
|
||||
|
||||
// Calculate partial products for each digit of num2
|
||||
for (let i = num2Str.length - 1; i >= 0; i--) {
|
||||
const digit = parseInt(num2Str[i]);
|
||||
const partialProduct = num1 * digit;
|
||||
const shiftAmount = num2Str.length - 1 - i;
|
||||
steps.push({
|
||||
digit,
|
||||
product: partialProduct,
|
||||
shift: shiftAmount
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the multiplication problem
|
||||
*/
|
||||
function displayProblem() {
|
||||
const { num1, num2 } = gameState.currentProblem;
|
||||
const difficulty = gameState.difficulty;
|
||||
|
||||
if (difficulty === 1) {
|
||||
// Horizontal layout for 1x1 with result
|
||||
const answer = gameState.currentProblem.answer.toString();
|
||||
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||
for (let i = 0; i < answer.length; i++) {
|
||||
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||
}
|
||||
resultHTML += '</div>';
|
||||
|
||||
problemDisplay.innerHTML = `
|
||||
<div class="multiplication-horizontal">
|
||||
<span class="number">${num1}</span>
|
||||
<span class="operator">×</span>
|
||||
<span class="number">${num2}</span>
|
||||
<span class="operator">=</span>
|
||||
${resultHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners to result digit inputs
|
||||
setTimeout(() => {
|
||||
addResultDigitInputListeners();
|
||||
}, 0);
|
||||
} else {
|
||||
// Vertical layout for 2+ digits
|
||||
const num1Str = num1.toString();
|
||||
const num2Str = num2.toString();
|
||||
|
||||
// Calculate intermediate steps for reference
|
||||
const steps = calculateIntermediateSteps(num1, num2);
|
||||
gameState.currentProblem.steps = steps;
|
||||
|
||||
// Create input fields for intermediate steps only if num2 has more than 1 digit
|
||||
let intermediateHTML = '';
|
||||
if (num2 > 9) {
|
||||
// Calculate the maximum width needed for intermediate steps
|
||||
const maxResultWidth = gameState.currentProblem.answer.toString().length;
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const stepProduct = steps[i].product.toString();
|
||||
const totalWidth = stepProduct.length + steps[i].shift;
|
||||
intermediateHTML += '<div class="line" style="justify-content: flex-start; gap: 5px;">';
|
||||
|
||||
// Each row is offset one position to the left
|
||||
// Add offset spaces at the beginning (left side)
|
||||
for (let offset = 0; offset < i; offset++) {
|
||||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||
}
|
||||
|
||||
// Add empty spaces for shift
|
||||
for (let s = 0; s < steps[i].shift; s++) {
|
||||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||
}
|
||||
|
||||
// Add input fields for each digit
|
||||
for (let j = 0; j < stepProduct.length; j++) {
|
||||
intermediateHTML += `<input type="text" class="digit-input intermediate-input" maxlength="1" data-step="${i}" data-pos="${j}" />`;
|
||||
}
|
||||
|
||||
// Add trailing empty spaces on the right
|
||||
const trailingSpaces = maxResultWidth - totalWidth - i;
|
||||
for (let e = 0; e < trailingSpaces; e++) {
|
||||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||
}
|
||||
|
||||
intermediateHTML += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Create input fields for final result
|
||||
const answer = gameState.currentProblem.answer.toString();
|
||||
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||
for (let i = 0; i < answer.length; i++) {
|
||||
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||
}
|
||||
resultHTML += '</div>';
|
||||
|
||||
let html = `
|
||||
<div class="multiplication-vertical">
|
||||
<div class="line">
|
||||
<span>${num1Str}</span>
|
||||
</div>
|
||||
<div class="line">
|
||||
<span class="operator">×</span>
|
||||
<span>${num2Str}</span>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
`;
|
||||
|
||||
if (num2 > 9) {
|
||||
html += `
|
||||
<div id="intermediate-steps">
|
||||
${intermediateHTML}
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div id="result-steps">
|
||||
${resultHTML}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
problemDisplay.innerHTML = html;
|
||||
|
||||
// Add event listeners to digit inputs
|
||||
setTimeout(() => {
|
||||
addDigitInputListeners();
|
||||
addResultDigitInputListeners();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listeners to digit input fields
|
||||
*/
|
||||
function addDigitInputListeners() {
|
||||
const digitInputs = document.querySelectorAll('.digit-input:not(.result-input)');
|
||||
|
||||
digitInputs.forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => {
|
||||
// Only allow digits
|
||||
if (!/^\d?$/.test(e.target.value)) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to next input if digit entered
|
||||
if (e.target.value && index < digitInputs.length - 1) {
|
||||
digitInputs[index + 1].focus();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||
digitInputs[index - 1].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listeners to result digit input fields
|
||||
* Navigation: left after filling, wrap to rightmost of next line
|
||||
*/
|
||||
function addResultDigitInputListeners() {
|
||||
const resultInputs = document.querySelectorAll('.result-input');
|
||||
|
||||
resultInputs.forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => {
|
||||
// Only allow digits
|
||||
if (!/^\d?$/.test(e.target.value)) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to LEFT if digit entered
|
||||
if (e.target.value) {
|
||||
if (index > 0) {
|
||||
// Move to previous input (left)
|
||||
resultInputs[index - 1].focus();
|
||||
} else if (index === 0) {
|
||||
// At the leftmost position, wrap to rightmost of previous line
|
||||
const allLines = document.querySelectorAll('#result-steps .line, #intermediate-steps .line');
|
||||
if (allLines.length > 1) {
|
||||
const prevLineInputs = allLines[allLines.length - 2].querySelectorAll('input');
|
||||
if (prevLineInputs.length > 0) {
|
||||
prevLineInputs[prevLineInputs.length - 1].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !e.target.value && index < resultInputs.length - 1) {
|
||||
resultInputs[index + 1].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which digits are incorrect in the answer
|
||||
*/
|
||||
function findIncorrectDigits(userAnswer, correctAnswer) {
|
||||
const userStr = userAnswer.toString();
|
||||
const correctStr = correctAnswer.toString();
|
||||
const incorrectPositions = [];
|
||||
|
||||
// Pad the shorter string with leading zeros for comparison
|
||||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||
const userPadded = userStr.padStart(maxLen, '0');
|
||||
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (userPadded[i] !== correctPadded[i]) {
|
||||
incorrectPositions.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return incorrectPositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display answer with highlighted incorrect digits
|
||||
*/
|
||||
function displayAnswerWithErrors(userAnswer, correctAnswer) {
|
||||
const incorrectPositions = findIncorrectDigits(userAnswer, correctAnswer);
|
||||
const userStr = userAnswer.toString();
|
||||
const correctStr = correctAnswer.toString();
|
||||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||
const userPadded = userStr.padStart(maxLen, '0');
|
||||
|
||||
let html = '<div style="font-size: 1.5em; margin-top: 15px;"><strong>Your answer:</strong> ';
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const digit = userPadded[i];
|
||||
if (incorrectPositions.includes(i)) {
|
||||
html += `<span style="border: 3px solid #dc2626; padding: 5px 8px; margin: 0 2px; display: inline-block; background-color: #fee2e2;">${digit}</span>`;
|
||||
} else {
|
||||
html += digit;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<br><strong>Correct answer:</strong> ${correctStr}</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user answer from input boxes
|
||||
*/
|
||||
function getUserAnswer() {
|
||||
const resultInputs = document.querySelectorAll('.result-input');
|
||||
let answer = '';
|
||||
resultInputs.forEach(input => {
|
||||
answer += input.value;
|
||||
});
|
||||
return answer === '' ? null : parseInt(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user intermediate steps from input boxes
|
||||
*/
|
||||
function getUserIntermediateSteps() {
|
||||
const steps = [];
|
||||
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||
|
||||
intermediateLines.forEach((line, lineIndex) => {
|
||||
const inputs = line.querySelectorAll('input');
|
||||
let stepValue = '';
|
||||
inputs.forEach(input => {
|
||||
stepValue += input.value;
|
||||
});
|
||||
if (stepValue) {
|
||||
steps.push(parseInt(stepValue));
|
||||
}
|
||||
});
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle answer submission
|
||||
*/
|
||||
function handleSubmitAnswer() {
|
||||
const userAnswer = getUserAnswer();
|
||||
const correctAnswer = gameState.currentProblem.answer;
|
||||
|
||||
if (userAnswer === null) {
|
||||
showFeedback('Please enter an answer!', 'incorrect');
|
||||
return;
|
||||
}
|
||||
|
||||
gameState.totalAnswers++;
|
||||
|
||||
// Check intermediate steps if they exist
|
||||
const intermediateStepsDiv = document.getElementById('intermediate-steps');
|
||||
let intermediateStepsCorrect = true;
|
||||
let intermediateErrorMessage = '';
|
||||
|
||||
if (intermediateStepsDiv) {
|
||||
const steps = gameState.currentProblem.steps;
|
||||
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||
|
||||
intermediateLines.forEach((line, lineIndex) => {
|
||||
const inputs = line.querySelectorAll('input');
|
||||
let userStepValue = '';
|
||||
inputs.forEach(input => {
|
||||
userStepValue += input.value;
|
||||
});
|
||||
|
||||
if (userStepValue) {
|
||||
const userStepNum = parseInt(userStepValue);
|
||||
const correctStepNum = steps[lineIndex].product;
|
||||
|
||||
if (userStepNum !== correctStepNum) {
|
||||
intermediateStepsCorrect = false;
|
||||
intermediateErrorMessage += `<br>Step ${lineIndex + 1}: You wrote ${userStepNum}, correct is ${correctStepNum}`;
|
||||
|
||||
// Highlight incorrect digits in this step
|
||||
const correctStr = correctStepNum.toString();
|
||||
const userStr = userStepValue;
|
||||
inputs.forEach((input, digitIndex) => {
|
||||
if (digitIndex < userStr.length && userStr[digitIndex] !== correctStr[correctStr.length - userStr.length + digitIndex]) {
|
||||
input.classList.add('error');
|
||||
} else if (digitIndex < userStr.length) {
|
||||
input.classList.add('correct');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (userAnswer === correctAnswer && intermediateStepsCorrect) {
|
||||
gameState.points += 1;
|
||||
showFeedback('🎉 Correct! +1 point', 'correct');
|
||||
updateScore();
|
||||
|
||||
// Check if reached 20 points
|
||||
if (gameState.points >= 20) {
|
||||
showVictoryModal();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
generateNewProblem();
|
||||
}, 1500);
|
||||
} else {
|
||||
gameState.points = Math.max(0, gameState.points - 2);
|
||||
|
||||
// Mark incorrect result boxes with red color
|
||||
if (userAnswer !== correctAnswer) {
|
||||
const resultInputs = document.querySelectorAll('.result-input');
|
||||
const userStr = userAnswer.toString();
|
||||
const correctStr = correctAnswer.toString();
|
||||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||
const userPadded = userStr.padStart(maxLen, '0');
|
||||
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||
|
||||
resultInputs.forEach((input, index) => {
|
||||
if (userPadded[index] !== correctPadded[index]) {
|
||||
input.classList.add('error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
feedbackDiv.textContent = '❌ Your answer is wrong, check the red boxes';
|
||||
feedbackDiv.className = 'feedback incorrect';
|
||||
updateScore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show feedback message
|
||||
*/
|
||||
function showFeedback(message, type) {
|
||||
feedbackDiv.textContent = message;
|
||||
feedbackDiv.className = `feedback ${type}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear feedback message
|
||||
*/
|
||||
function clearFeedback() {
|
||||
feedbackDiv.textContent = '';
|
||||
feedbackDiv.className = 'feedback empty';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update score display
|
||||
*/
|
||||
function updateScore() {
|
||||
pointsScoreDisplay.textContent = `${gameState.points}/20`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show victory modal and create confetti
|
||||
*/
|
||||
function showVictoryModal() {
|
||||
victoryModal.classList.add('show');
|
||||
createConfetti();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create confetti animation
|
||||
*/
|
||||
function createConfetti() {
|
||||
const colors = ['#2563eb', '#7c3aed', '#dc2626', '#16a34a', '#ea580c', '#f59e0b'];
|
||||
const confettiCount = 50;
|
||||
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.className = 'confetti';
|
||||
confetti.style.left = Math.random() * 100 + '%';
|
||||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
confetti.style.delay = Math.random() * 0.5 + 's';
|
||||
confetti.style.animationDuration = (Math.random() * 2 + 2.5) + 's';
|
||||
document.body.appendChild(confetti);
|
||||
|
||||
// Remove confetti element after animation
|
||||
setTimeout(() => {
|
||||
confetti.remove();
|
||||
}, 3500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset game for new round
|
||||
*/
|
||||
function resetGame() {
|
||||
gameState.points = 0;
|
||||
gameState.totalAnswers = 0;
|
||||
victoryModal.classList.remove('show');
|
||||
updateScore();
|
||||
generateNewProblem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize language selector
|
||||
*/
|
||||
function initializeLanguageSelector() {
|
||||
const languageFlagsContainer = document.getElementById('language-flags');
|
||||
const languages = i18n.getSupportedLanguages();
|
||||
|
||||
languageFlagsContainer.innerHTML = '';
|
||||
|
||||
Object.entries(languages).forEach(([langCode, langInfo]) => {
|
||||
const flagButton = document.createElement('button');
|
||||
flagButton.className = 'language-flag';
|
||||
flagButton.textContent = langInfo.flag;
|
||||
flagButton.title = langInfo.name;
|
||||
flagButton.setAttribute('data-tooltip', langInfo.name);
|
||||
|
||||
if (langCode === i18n.getCurrentLanguage()) {
|
||||
flagButton.classList.add('active');
|
||||
}
|
||||
|
||||
flagButton.addEventListener('click', () => {
|
||||
changeLanguage(langCode);
|
||||
});
|
||||
|
||||
languageFlagsContainer.appendChild(flagButton);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change language and update UI
|
||||
*/
|
||||
function changeLanguage(langCode) {
|
||||
i18n.setLanguage(langCode);
|
||||
|
||||
// Update active flag
|
||||
document.querySelectorAll('.language-flag').forEach(flag => {
|
||||
flag.classList.remove('active');
|
||||
});
|
||||
const languages = i18n.getSupportedLanguages();
|
||||
const flagButtons = document.querySelectorAll('.language-flag');
|
||||
const langCodes = Object.keys(languages);
|
||||
flagButtons.forEach((btn, index) => {
|
||||
if (langCodes[index] === langCode) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
updateUIText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all UI text based on current language
|
||||
*/
|
||||
function updateUIText() {
|
||||
// Update header
|
||||
document.getElementById('title').textContent = '🎓 ' + i18n.t('title');
|
||||
document.getElementById('subtitle').textContent = i18n.t('subtitle');
|
||||
|
||||
// Update difficulty label
|
||||
const difficultyLabel = document.querySelector('.difficulty-label');
|
||||
if (difficultyLabel) {
|
||||
difficultyLabel.innerHTML = i18n.t('difficultyLevel') + ' <span id="difficulty-display">' + gameState.difficulty + '</span>';
|
||||
}
|
||||
|
||||
// Update difficulty text labels
|
||||
const difficultyTexts = document.querySelectorAll('.difficulty-text');
|
||||
if (difficultyTexts.length >= 2) {
|
||||
difficultyTexts[0].textContent = i18n.t('easy');
|
||||
difficultyTexts[1].textContent = i18n.t('hard');
|
||||
}
|
||||
|
||||
// Update difficulty description
|
||||
const config = difficultyConfig[gameState.difficulty];
|
||||
if (config && config.descriptionKey) {
|
||||
document.getElementById('difficulty-description').textContent = i18n.t(config.descriptionKey);
|
||||
}
|
||||
|
||||
// Update buttons
|
||||
document.getElementById('submit-btn').textContent = i18n.t('checkAnswer');
|
||||
document.getElementById('new-problem-btn').textContent = i18n.t('newProblem');
|
||||
|
||||
// Update score label
|
||||
const scoreLabel = document.querySelector('.score-label');
|
||||
if (scoreLabel) {
|
||||
scoreLabel.textContent = i18n.t('points');
|
||||
}
|
||||
|
||||
// Update modal
|
||||
const modalTitle = document.querySelector('.modal-content h2');
|
||||
const modalText = document.querySelector('.modal-content p');
|
||||
const playAgainButton = document.getElementById('play-again-btn');
|
||||
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = i18n.t('congratulations');
|
||||
}
|
||||
if (modalText) {
|
||||
modalText.textContent = i18n.t('youReached20Points');
|
||||
}
|
||||
if (playAgainButton) {
|
||||
playAgainButton.textContent = i18n.t('playAgain');
|
||||
}
|
||||
}
|
||||
591
MathHomeworkHelper/app/src/main/assets/styles.css
Normal file
591
MathHomeworkHelper/app/src/main/assets/styles.css
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
background: #87CEEB;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Language Selector */
|
||||
.language-selector {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.language-flags {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.language-flag:hover {
|
||||
background: #f0f0f0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.language-flag.active {
|
||||
background: #87CEEB;
|
||||
border-color: #2563eb;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.language-flag-tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-flag-tooltip:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8em;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 5px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 3em;
|
||||
color: #2563eb;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.3em;
|
||||
color: #7c3aed;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Difficulty Section */
|
||||
.difficulty-section {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||
border-radius: 20px;
|
||||
padding: 25px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 25px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.difficulty-label {
|
||||
display: block;
|
||||
font-size: 1.3em;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#difficulty-display {
|
||||
background: white;
|
||||
color: #dc2626;
|
||||
padding: 5px 15px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.difficulty-text {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.95em;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.difficulty-info {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 10px 15px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#difficulty-description {
|
||||
color: white;
|
||||
font-size: 0.95em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Problem Section */
|
||||
.problem-section {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.problem-display {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
font-family: 'Courier New', monospace;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Horizontal multiplication (1x1) */
|
||||
.multiplication-horizontal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.multiplication-horizontal .number {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.multiplication-horizontal .operator {
|
||||
font-size: 2.5em;
|
||||
color: #f5576c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Vertical multiplication (2+ digits) */
|
||||
.multiplication-vertical {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.multiplication-vertical .line {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
margin: 5px 0;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.multiplication-vertical .operator {
|
||||
color: #f5576c;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.multiplication-vertical .separator {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #333;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.digit-input {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
font-size: 1.4em;
|
||||
text-align: center;
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.digit-input:focus {
|
||||
outline: none;
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 8px rgba(124, 58, 237, 0.4);
|
||||
background-color: #f0f4ff;
|
||||
}
|
||||
|
||||
.digit-input.error {
|
||||
border-color: #dc2626;
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.digit-input.correct {
|
||||
border-color: #16a34a;
|
||||
background-color: #dcfce7;
|
||||
}
|
||||
|
||||
/* Answer Section */
|
||||
.answer-section {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.answer-input {
|
||||
flex: 1;
|
||||
padding: 18px;
|
||||
font-size: 1.3em;
|
||||
border: 3px solid #2563eb;
|
||||
border-radius: 15px;
|
||||
outline: none;
|
||||
transition: all 0.3s;
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.answer-input:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.answer-input::placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 18px 30px;
|
||||
font-size: 1.2em;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 5px 15px rgba(37, 99, 235, 0.4);
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.6);
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Feedback Section */
|
||||
.feedback-section {
|
||||
min-height: 80px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
min-width: 100%;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.feedback.correct {
|
||||
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
|
||||
color: #2d5016;
|
||||
box-shadow: 0 5px 15px rgba(132, 250, 176, 0.4);
|
||||
}
|
||||
|
||||
.feedback.incorrect {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
color: #8b0000;
|
||||
box-shadow: 0 5px 15px rgba(250, 112, 154, 0.4);
|
||||
}
|
||||
|
||||
.feedback.empty {
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Score Section */
|
||||
.score-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.score-box {
|
||||
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||
padding: 20px 30px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.3);
|
||||
min-width: 120px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 0.95em;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 2.5em;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* New Problem Button */
|
||||
.new-problem-btn {
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
font-size: 1.3em;
|
||||
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 5px 15px rgba(220, 38, 38, 0.4);
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.new-problem-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.6);
|
||||
}
|
||||
|
||||
.new-problem-btn:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Confetti Animation */
|
||||
@keyframes confetti-fall {
|
||||
to {
|
||||
transform: translateY(100vh) rotateZ(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
pointer-events: none;
|
||||
animation: confetti-fall 3s ease-in forwards;
|
||||
}
|
||||
|
||||
/* Modal Popup */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalBounce 0.5s ease-out;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@keyframes modalBounce {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 2.5em;
|
||||
color: #16a34a;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
font-size: 1.3em;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 15px 40px;
|
||||
font-size: 1.2em;
|
||||
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.4);
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.6);
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.2em;
|
||||
}
|
||||
|
||||
.problem-section {
|
||||
padding: 25px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.problem-display {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.answer-input,
|
||||
.submit-btn {
|
||||
font-size: 1.1em;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.score-box {
|
||||
min-width: 100px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
184
MathHomeworkHelper/app/src/main/assets/translations.js
Normal file
184
MathHomeworkHelper/app/src/main/assets/translations.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
// Translation Manager
|
||||
class TranslationManager {
|
||||
constructor() {
|
||||
this.currentLanguage = 'en'; // Will be set during initialization
|
||||
this.translations = {};
|
||||
this.supportedLanguages = {
|
||||
'en': { name: 'English', flag: '🇬🇧' },
|
||||
'es': { name: 'Español', flag: '🇪🇸' },
|
||||
'sv': { name: 'Svenska', flag: '🇸🇪' },
|
||||
'el': { name: 'Ελληνικά', flag: '🇬🇷' }
|
||||
};
|
||||
|
||||
// Country to language mapping
|
||||
this.countryToLanguage = {
|
||||
// Spanish-speaking countries
|
||||
'ES': 'es', 'MX': 'es', 'AR': 'es', 'CO': 'es', 'PE': 'es', 'VE': 'es',
|
||||
'CL': 'es', 'EC': 'es', 'BO': 'es', 'PY': 'es', 'UY': 'es', 'CU': 'es',
|
||||
'DO': 'es', 'GT': 'es', 'HN': 'es', 'SV': 'es', 'NI': 'es', 'CR': 'es',
|
||||
'PA': 'es', 'BZ': 'es', 'EQ': 'es',
|
||||
// Swedish-speaking countries
|
||||
'SE': 'sv', 'FI': 'sv', 'AX': 'sv',
|
||||
// Greek-speaking countries
|
||||
'GR': 'el', 'CY': 'el',
|
||||
// Default to English for all others
|
||||
};
|
||||
}
|
||||
|
||||
// Detect language with geolocation fallback
|
||||
async detectLanguageWithGeolocation() {
|
||||
// Step 1: Check localStorage for saved preference
|
||||
const savedLanguage = localStorage.getItem('preferredLanguage');
|
||||
if (savedLanguage && this.supportedLanguages[savedLanguage]) {
|
||||
console.log('Using saved language preference:', savedLanguage);
|
||||
return savedLanguage;
|
||||
}
|
||||
|
||||
// Step 2: Try browser language detection
|
||||
const browserLang = this.detectBrowserLanguage();
|
||||
if (browserLang !== 'en') {
|
||||
console.log('Using browser language:', browserLang);
|
||||
return browserLang;
|
||||
}
|
||||
|
||||
// Step 3: Try geolocation API to get country and map to language
|
||||
try {
|
||||
const countryCode = await this.getCountryFromGeolocation();
|
||||
if (countryCode) {
|
||||
const language = this.countryToLanguage[countryCode] || 'en';
|
||||
console.log('Using geolocation-based language:', language, 'from country:', countryCode);
|
||||
return language;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Geolocation failed, continuing with fallback:', error.message);
|
||||
}
|
||||
|
||||
// Step 4: Fallback to English
|
||||
console.log('Falling back to English');
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// Detect browser language
|
||||
detectBrowserLanguage() {
|
||||
const browserLang = navigator.language || navigator.userLanguage;
|
||||
const langCode = browserLang.split('-')[0];
|
||||
|
||||
// Check if the detected language is supported
|
||||
if (this.supportedLanguages[langCode]) {
|
||||
return langCode;
|
||||
}
|
||||
|
||||
// Return 'en' if not supported (will trigger geolocation)
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// Get country code from geolocation API
|
||||
async getCountryFromGeolocation() {
|
||||
const timeout = 5000; // 5 second timeout
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// Try ip-api.com (free, no auth required)
|
||||
const response = await fetch('https://ipapi.co/json/', {
|
||||
signal: controller.signal,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API returned status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const countryCode = data.country_code;
|
||||
|
||||
if (countryCode && typeof countryCode === 'string') {
|
||||
console.log('Geolocation API returned country:', countryCode);
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
throw new Error('No country code in response');
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.warn('Geolocation API request timed out');
|
||||
} else {
|
||||
console.warn('Geolocation API error:', error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Load translation file
|
||||
async loadTranslation(language) {
|
||||
try {
|
||||
const response = await fetch(`translations/${language}.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${language} translation`);
|
||||
}
|
||||
this.translations[language] = await response.json();
|
||||
this.currentLanguage = language;
|
||||
localStorage.setItem('preferredLanguage', language);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error loading translation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get translated string
|
||||
t(key) {
|
||||
if (this.translations[this.currentLanguage] && this.translations[this.currentLanguage][key]) {
|
||||
return this.translations[this.currentLanguage][key];
|
||||
}
|
||||
// Fallback to English if key not found
|
||||
if (this.translations['en'] && this.translations['en'][key]) {
|
||||
return this.translations['en'][key];
|
||||
}
|
||||
// Return key if no translation found
|
||||
return key;
|
||||
}
|
||||
|
||||
// Get current language
|
||||
getCurrentLanguage() {
|
||||
return this.currentLanguage;
|
||||
}
|
||||
|
||||
// Get all supported languages
|
||||
getSupportedLanguages() {
|
||||
return this.supportedLanguages;
|
||||
}
|
||||
|
||||
// Set language
|
||||
setLanguage(language) {
|
||||
if (this.supportedLanguages[language]) {
|
||||
this.currentLanguage = language;
|
||||
localStorage.setItem('preferredLanguage', language);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize translations
|
||||
async initialize() {
|
||||
// Detect language using geolocation fallback chain
|
||||
this.currentLanguage = await this.detectLanguageWithGeolocation();
|
||||
|
||||
// Load all supported language files
|
||||
const loadPromises = Object.keys(this.supportedLanguages).map(lang =>
|
||||
this.loadTranslation(lang)
|
||||
);
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
// Ensure current language is set correctly
|
||||
if (!this.translations[this.currentLanguage]) {
|
||||
this.currentLanguage = 'en';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
const i18n = new TranslationManager();
|
||||
20
MathHomeworkHelper/app/src/main/assets/translations/el.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/el.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": "Βοηθός Μαθηματικών Εργασιών",
|
||||
"subtitle": "Εξάσκηση Πολλαπλασιασμού",
|
||||
"difficultyLevel": "Επίπεδο Δυσκολίας:",
|
||||
"easy": "Εύκολο",
|
||||
"hard": "Δύσκολο",
|
||||
"difficulty1": "1 ψηφίο × 1 ψηφίο (π.χ. 5 × 7)",
|
||||
"difficulty2": "1 ψηφίο × 2 ψηφία (π.χ. 5 × 23)",
|
||||
"difficulty3": "2 ψηφία × 2 ψηφία (π.χ. 23 × 45)",
|
||||
"difficulty4": "2 ψηφία × 3 ψηφία (π.χ. 23 × 456)",
|
||||
"difficulty5": "3 ψηφία × 3 ψηφία (π.χ. 234 × 567)",
|
||||
"checkAnswer": "Έλεγχος Απάντησης",
|
||||
"points": "Πόντοι:",
|
||||
"newProblem": "Νέο Πρόβλημα",
|
||||
"congratulations": "🎉 Συγχαρητήρια! 🎉",
|
||||
"youReached20Points": "Έφτασες τους 20 πόντους!",
|
||||
"playAgain": "Παίξε Ξανά",
|
||||
"correct": "Σωστό! Εξαιρετική δουλειά! 🎉",
|
||||
"incorrect": "Λάθος. Προσπάθησε ξανά! 💪"
|
||||
}
|
||||
20
MathHomeworkHelper/app/src/main/assets/translations/en.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/en.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": "Math Homework Helper",
|
||||
"subtitle": "Multiplication Practice",
|
||||
"difficultyLevel": "Difficulty Level:",
|
||||
"easy": "Easy",
|
||||
"hard": "Hard",
|
||||
"difficulty1": "1 digit × 1 digit (e.g., 5 × 7)",
|
||||
"difficulty2": "1 digit × 2 digits (e.g., 5 × 23)",
|
||||
"difficulty3": "2 digits × 2 digits (e.g., 23 × 45)",
|
||||
"difficulty4": "2 digits × 3 digits (e.g., 23 × 456)",
|
||||
"difficulty5": "3 digits × 3 digits (e.g., 234 × 567)",
|
||||
"checkAnswer": "Check Answer",
|
||||
"points": "Points:",
|
||||
"newProblem": "New Problem",
|
||||
"congratulations": "🎉 Congratulations! 🎉",
|
||||
"youReached20Points": "You've reached 20 points!",
|
||||
"playAgain": "Play Again",
|
||||
"correct": "Correct! Great job! 🎉",
|
||||
"incorrect": "Incorrect. Try again! 💪"
|
||||
}
|
||||
20
MathHomeworkHelper/app/src/main/assets/translations/es.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/es.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": "Ayudante de Tareas de Matemáticas",
|
||||
"subtitle": "Práctica de Multiplicación",
|
||||
"difficultyLevel": "Nivel de Dificultad:",
|
||||
"easy": "Fácil",
|
||||
"hard": "Difícil",
|
||||
"difficulty1": "1 dígito × 1 dígito (p. ej., 5 × 7)",
|
||||
"difficulty2": "1 dígito × 2 dígitos (p. ej., 5 × 23)",
|
||||
"difficulty3": "2 dígitos × 2 dígitos (p. ej., 23 × 45)",
|
||||
"difficulty4": "2 dígitos × 3 dígitos (p. ej., 23 × 456)",
|
||||
"difficulty5": "3 dígitos × 3 dígitos (p. ej., 234 × 567)",
|
||||
"checkAnswer": "Verificar Respuesta",
|
||||
"points": "Puntos:",
|
||||
"newProblem": "Nuevo Problema",
|
||||
"congratulations": "🎉 ¡Felicitaciones! 🎉",
|
||||
"youReached20Points": "¡Has alcanzado 20 puntos!",
|
||||
"playAgain": "Jugar de Nuevo",
|
||||
"correct": "¡Correcto! ¡Excelente trabajo! 🎉",
|
||||
"incorrect": "Incorrecto. ¡Intenta de nuevo! 💪"
|
||||
}
|
||||
20
MathHomeworkHelper/app/src/main/assets/translations/sv.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/sv.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": "Matematikläxhjälp",
|
||||
"subtitle": "Multiplikationspraktik",
|
||||
"difficultyLevel": "Svårighetsnivå:",
|
||||
"easy": "Lätt",
|
||||
"hard": "Svårt",
|
||||
"difficulty1": "1 siffra × 1 siffra (t.ex. 5 × 7)",
|
||||
"difficulty2": "1 siffra × 2 siffror (t.ex. 5 × 23)",
|
||||
"difficulty3": "2 siffror × 2 siffror (t.ex. 23 × 45)",
|
||||
"difficulty4": "2 siffror × 3 siffror (t.ex. 23 × 456)",
|
||||
"difficulty5": "3 siffror × 3 siffror (t.ex. 234 × 567)",
|
||||
"checkAnswer": "Kontrollera Svar",
|
||||
"points": "Poäng:",
|
||||
"newProblem": "Nytt Problem",
|
||||
"congratulations": "🎉 Grattis! 🎉",
|
||||
"youReached20Points": "Du har nått 20 poäng!",
|
||||
"playAgain": "Spela Igen",
|
||||
"correct": "Rätt! Bra jobbat! 🎉",
|
||||
"incorrect": "Fel. Försök igen! 💪"
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.example.mathhomeworkhelper;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private WebView webView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
webView = findViewById(R.id.webview);
|
||||
|
||||
// Configure WebView settings
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
|
||||
// Enable JavaScript (required for your app)
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
|
||||
// Enable DOM storage
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
|
||||
// Enable local storage
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
|
||||
// Set user agent
|
||||
webSettings.setUserAgentString(webSettings.getUserAgentString());
|
||||
|
||||
// Enable zoom controls
|
||||
webSettings.setBuiltInZoomControls(true);
|
||||
webSettings.setDisplayZoomControls(false);
|
||||
|
||||
// Set default zoom level
|
||||
webSettings.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM);
|
||||
|
||||
// Allow file access
|
||||
webSettings.setAllowFileAccess(true);
|
||||
webSettings.setAllowContentAccess(true);
|
||||
|
||||
// For Android 5.0 and above, allow mixed content
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
}
|
||||
|
||||
// Load the local HTML file from assets
|
||||
webView.loadUrl("file:///android_asset/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Allow back navigation within the WebView
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
MathHomeworkHelper/app/src/main/res/layout/activity_main.xml
Normal file
12
MathHomeworkHelper/app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
5
MathHomeworkHelper/app/src/main/res/values/strings.xml
Normal file
5
MathHomeworkHelper/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Math Homework Helper</string>
|
||||
<string name="app_title">Math Homework Helper</string>
|
||||
</resources>
|
||||
24
MathHomeworkHelper/app/src/main/res/values/styles.xml
Normal file
24
MathHomeworkHelper/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.MathHomeworkHelper" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorError">@color/red_500</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="colorOnError">@color/white</item>
|
||||
<item name="colorOnBackground">@color/black</item>
|
||||
<item name="colorOnSurface">@color/black</item>
|
||||
</style>
|
||||
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="red_500">#FFFF0000</color>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue