Initial commit: Add MathHomeworkHelper project with web and Android components
Some checks reported errors
continuous-integration/drone Build encountered an error

This commit is contained in:
Juan José Gutiérrez de Quevedo Pérez 2025-11-25 11:07:51 +01:00
commit f9558008e1
37 changed files with 5318 additions and 0 deletions

View 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'
}

View 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

View 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>

View 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>

View 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');
}
}

View 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;
}
}

View 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();

View 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": "Λάθος. Προσπάθησε ξανά! 💪"
}

View 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! 💪"
}

View 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! 💪"
}

View 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! 💪"
}

View file

@ -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();
}
}
}

View 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>

View 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>

View 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>