chore(messages): update settings keys for multiple languages
This commit is contained in:
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "فكر خطوة بخطوة", "problem": "المشكلة:", "withoutMagic": "بدون الكلمات السحرية:", "addMagicWords": "أضف الكلمات السحرية!", "magicWordsActive": "تمت إضافة الكلمات السحرية!", "nextStep": "اكشف الخطوة التالية", "withMagic": "مع التفكير خطوة بخطوة:", "retry": "حاول مرة أخرى" },
|
||||
"promptLab": { "title": "مختبر الأوامر", "progress": "التحسينات", "yourPrompt": "أمرك:", "aiSays": "رد الذكاء الاصطناعي:", "addDetails": "أضف تحسينات:", "success": "أمرك الآن محدد جداً!", "retry": "ابدأ من جديد" },
|
||||
"wordPredictor": { "title": "كيف يفكر الذكاء الاصطناعي", "instruction": "الذكاء الاصطناعي يخمن الكلمة التالية الأكثر احتمالاً. هل يمكنك التفكير مثله؟", "aiThinks": "الذكاء الاصطناعي يقرأ:", "thinkingDefault": "همم، أي كلمة ستكون منطقية هنا؟", "check": "تحقق من تخميني!", "correct": "أنت تفكر مثل الذكاء الاصطناعي!", "tryAgain": "ليس تماماً! الذكاء الاصطناعي يختار الكلمة الأكثر احتمالاً.", "retry": "حاول مرة أخرى" },
|
||||
"settings": { "title": "الإعدادات", "language": "اللغة", "progress": "تقدمك", "stars": "النجوم", "completed": "المكتمل", "resetTitle": "إعادة تعيين التقدم", "resetButton": "إعادة تعيين كل التقدم", "resetWarning": "سيحذف هذا كل نجومك وتقدمك. هل أنت متأكد؟", "resetConfirm": "نعم، أعد تعيين كل شيء", "resetComplete": "تم إعادة التعيين! جارٍ إعادة التحميل...", "cancel": "إلغاء" }
|
||||
"settings": { "title": "الإعدادات", "music": "الموسيقى", "language": "اللغة", "progress": "تقدمك", "stars": "النجوم", "completed": "المكتمل", "resetTitle": "إعادة تعيين التقدم", "resetButton": "إعادة تعيين كل التقدم", "resetWarning": "سيحذف هذا كل نجومك وتقدمك. هل أنت متأكد؟", "resetConfirm": "نعم، أعد تعيين كل شيء", "resetComplete": "تم إعادة التعيين! جارٍ إعادة التحميل...", "cancel": "إلغاء" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Addım-addım Düşün", "problem": "Problem:", "withoutMagic": "Sehrli sözlər olmadan:", "addMagicWords": "Sehrli Sözləri Əlavə Et!", "magicWordsActive": "Sehrli sözlər əlavə edildi!", "nextStep": "Növbəti Addımı Göstər", "withMagic": "Addım-addım düşünmə ilə:", "retry": "Yenidən cəhd et" },
|
||||
"promptLab": { "title": "Prompt Laboratoriyası", "progress": "Təkmilləşdirmələr", "yourPrompt": "Sənin Promptun:", "aiSays": "AI Cavabı:", "addDetails": "Təkmilləşdirmə əlavə et:", "success": "Promptun indi çox spesifikdir!", "retry": "Yenidən başla" },
|
||||
"wordPredictor": { "title": "AI Necə Düşünür", "instruction": "AI ən çox ehtimal olunan növbəti sözü təxmin edir. AI kimi düşünə bilərsən?", "aiThinks": "AI oxuyur:", "thinkingDefault": "Hmm, burada hansı söz ən məntiqli olardı?", "check": "Cavabımı yoxla!", "correct": "AI kimi düşünürsən!", "tryAgain": "Tam deyil! AI ən çox ehtimal olunan sözü seçir.", "retry": "Yenidən cəhd et" },
|
||||
"settings": { "title": "Parametrlər", "language": "Dil", "progress": "Tərəqqin", "stars": "Ulduzlar", "completed": "Tamamlandı", "resetTitle": "Tərəqqini Sıfırla", "resetButton": "Hamısını Sıfırla", "resetWarning": "Bu bütün ulduzları və tərəqqini siləcək. Əminsən?", "resetConfirm": "Bəli, Hamısını Sıfırla", "resetComplete": "Tərəqqi sıfırlandı! Yenidən yüklənir...", "cancel": "Ləğv et" }
|
||||
"settings": { "title": "Tənzimləmələr", "music": "Musiqi", "language": "Dil", "progress": "Tərəqqin", "stars": "Ulduzlar", "completed": "Tamamlandı", "resetTitle": "Tərəqqini Sıfırla", "resetButton": "Hamısını Sıfırla", "resetWarning": "Bu bütün ulduzları və tərəqqini siləcək. Əminsən?", "resetConfirm": "Bəli, Hamısını Sıfırla", "resetComplete": "Tərəqqi sıfırlandı! Yenidən yüklənir...", "cancel": "Ləğv et" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Schritt für Schritt denken", "problem": "Das Problem:", "withoutMagic": "Ohne Zauberwörter:", "addMagicWords": "Füge Zauberwörter hinzu!", "magicWordsActive": "Zauberwörter hinzugefügt!", "nextStep": "Nächsten Schritt zeigen", "withMagic": "Mit Schritt-für-Schritt-Denken:", "retry": "Nochmal versuchen" },
|
||||
"promptLab": { "title": "Prompt-Labor", "progress": "Verbesserungen", "yourPrompt": "Dein Prompt:", "aiSays": "KI-Antwort:", "addDetails": "Verbesserungen hinzufügen:", "success": "Dein Prompt ist jetzt super spezifisch!", "retry": "Von vorn" },
|
||||
"wordPredictor": { "title": "Wie KI denkt", "instruction": "KI errät das wahrscheinlichste nächste Wort. Kannst du wie KI denken?", "aiThinks": "KI liest:", "thinkingDefault": "Hmm, welches Wort würde hier Sinn machen?", "check": "Meine Antwort prüfen!", "correct": "Du denkst wie KI!", "tryAgain": "Nicht ganz! KI wählt das wahrscheinlichste Wort.", "retry": "Nochmal versuchen" },
|
||||
"settings": { "title": "Einstellungen", "language": "Sprache", "progress": "Dein Fortschritt", "stars": "Sterne", "completed": "Abgeschlossen", "resetTitle": "Fortschritt zurücksetzen", "resetButton": "Alles zurücksetzen", "resetWarning": "Dies löscht alle Sterne und Fortschritt. Bist du sicher?", "resetConfirm": "Ja, alles zurücksetzen", "resetComplete": "Fortschritt zurückgesetzt! Wird neu geladen...", "cancel": "Abbrechen" }
|
||||
"settings": { "title": "Einstellungen", "music": "Musik", "language": "Sprache", "progress": "Dein Fortschritt", "stars": "Sterne", "completed": "Abgeschlossen", "resetTitle": "Fortschritt zurücksetzen", "resetButton": "Alles zurücksetzen", "resetWarning": "Dies löscht alle Sterne und Fortschritt. Bist du sicher?", "resetConfirm": "Ja, alles zurücksetzen", "resetComplete": "Fortschritt zurückgesetzt! Wird neu geladen...", "cancel": "Abbrechen" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Σκέψου Βήμα-Βήμα", "problem": "Το Πρόβλημα:", "withoutMagic": "Χωρίς μαγικές λέξεις:", "addMagicWords": "Πρόσθεσε Μαγικές Λέξεις!", "magicWordsActive": "Μαγικές λέξεις προστέθηκαν!", "nextStep": "Αποκάλυψη Επόμενου Βήματος", "withMagic": "Με σκέψη βήμα-βήμα:", "retry": "Προσπάθησε ξανά" },
|
||||
"promptLab": { "title": "Εργαστήριο Prompts", "progress": "Βελτιώσεις", "yourPrompt": "Το Prompt σου:", "aiSays": "Απάντηση AI:", "addDetails": "Πρόσθεσε βελτιώσεις:", "success": "Το prompt σου είναι τώρα πολύ συγκεκριμένο!", "retry": "Από την αρχή" },
|
||||
"wordPredictor": { "title": "Πώς Σκέφτεται η AI", "instruction": "Η AI μαντεύει την πιο πιθανή επόμενη λέξη. Μπορείς να σκεφτείς σαν AI;", "aiThinks": "Η AI διαβάζει:", "thinkingDefault": "Χμμ, ποια λέξη θα είχε νόημα εδώ;", "check": "Έλεγξε την απάντησή μου!", "correct": "Σκέφτεσαι σαν AI!", "tryAgain": "Όχι ακριβώς! Η AI διαλέγει την πιο πιθανή λέξη.", "retry": "Προσπάθησε ξανά" },
|
||||
"settings": { "title": "Ρυθμίσεις", "language": "Γλώσσα", "progress": "Η Πρόοδός σου", "stars": "Αστέρια", "completed": "Ολοκληρώθηκε", "resetTitle": "Επαναφορά Προόδου", "resetButton": "Επαναφορά Όλων", "resetWarning": "Αυτό θα διαγράψει όλα τα αστέρια και την πρόοδο. Είσαι σίγουρος;", "resetConfirm": "Ναι, Επαναφορά Όλων", "resetComplete": "Η πρόοδος επαναφέρθηκε! Επαναφόρτωση...", "cancel": "Ακύρωση" }
|
||||
"settings": { "title": "Ρυθμίσεις", "music": "Μουσική", "language": "Γλώσσα", "progress": "Η Πρόοδός σου", "stars": "Αστέρια", "completed": "Ολοκληρώθηκε", "resetTitle": "Επαναφορά Προόδου", "resetButton": "Επαναφορά Όλων", "resetWarning": "Αυτό θα διαγράψει όλα τα αστέρια και την πρόοδο. Είσαι σίγουρος;", "resetConfirm": "Ναι, Επαναφορά Όλων", "resetComplete": "Η πρόοδος επαναφέρθηκε! Επαναφόρτωση...", "cancel": "Ακύρωση" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1633,6 +1633,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"music": "Music",
|
||||
"language": "Language",
|
||||
"progress": "Your Progress",
|
||||
"stars": "Stars",
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Piensa Paso a Paso", "problem": "El Problema:", "withoutMagic": "Sin palabras mágicas:", "addMagicWords": "¡Añade las Palabras Mágicas!", "magicWordsActive": "¡Palabras mágicas añadidas!", "nextStep": "Revelar Siguiente Paso", "withMagic": "Con pensamiento paso a paso:", "retry": "Intentar de nuevo" },
|
||||
"promptLab": { "title": "Laboratorio de Prompts", "progress": "Mejoras", "yourPrompt": "Tu Prompt:", "aiSays": "Respuesta de IA:", "addDetails": "Añadir mejoras:", "success": "¡Tu prompt ahora es súper específico!", "retry": "Empezar de nuevo" },
|
||||
"wordPredictor": { "title": "Cómo Piensa la IA", "instruction": "La IA adivina la palabra más probable. ¿Puedes pensar como IA?", "aiThinks": "La IA está leyendo:", "thinkingDefault": "Hmm, ¿qué palabra tendría más sentido aquí?", "check": "¡Comprobar mi respuesta!", "correct": "¡Piensas como IA!", "tryAgain": "¡No del todo! La IA elige la palabra más probable.", "retry": "Intentar de nuevo" },
|
||||
"settings": { "title": "Configuración", "language": "Idioma", "progress": "Tu Progreso", "stars": "Estrellas", "completed": "Completados", "resetTitle": "Restablecer Progreso", "resetButton": "Restablecer Todo", "resetWarning": "Esto eliminará todas tus estrellas y progreso. ¿Estás seguro?", "resetConfirm": "Sí, Restablecer Todo", "resetComplete": "¡Progreso restablecido! Recargando...", "cancel": "Cancelar" }
|
||||
"settings": { "title": "Configuración", "music": "Música", "language": "Idioma", "progress": "Tu Progreso", "stars": "Estrellas", "completed": "Completados", "resetTitle": "Restablecer Progreso", "resetButton": "Restablecer Todo", "resetWarning": "Esto eliminará todas tus estrellas y progreso. ¿Estás seguro?", "resetConfirm": "Sí, Restablecer Todo", "resetComplete": "¡Progreso restablecido! Recargando...", "cancel": "Cancelar" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "قدم به قدم فکر کن", "problem": "مسئله:", "withoutMagic": "بدون کلمات جادویی:", "addMagicWords": "کلمات جادویی اضافه کن!", "magicWordsActive": "کلمات جادویی اضافه شد!", "nextStep": "قدم بعدی نشان بده", "withMagic": "با فکر قدم به قدم:", "retry": "دوباره تلاش کن" },
|
||||
"promptLab": { "title": "آزمایشگاه پرامپت", "progress": "بهبودها", "yourPrompt": "پرامپت تو:", "aiSays": "پاسخ هوش مصنوعی:", "addDetails": "بهبود اضافه کن:", "success": "پرامپتت الان خیلی مشخصه!", "retry": "از اول" },
|
||||
"wordPredictor": { "title": "هوش مصنوعی چطور فکر میکند", "instruction": "هوش مصنوعی محتملترین کلمه بعدی حدس میزند. میتونی مثل هوش مصنوعی فکر کنی?", "aiThinks": "هوش مصنوعی میخواند:", "thinkingDefault": "هوم، کدام کلمه اینجا منطقیتره؟", "check": "جوابم بررسی کن!", "correct": "مثل هوش مصنوعی فکر میکنی!", "tryAgain": "دقیقاً نه! هوش مصنوعی محتملترین کلمه انتخاب میکند.", "retry": "دوباره تلاش کن" },
|
||||
"settings": { "title": "تنظیمات", "language": "زبان", "progress": "پیشرفت تو", "stars": "ستارهها", "completed": "تکمیل شده", "resetTitle": "بازنشانی پیشرفت", "resetButton": "همه بازنشانی", "resetWarning": "این همه ستارهها و پیشرفت پاک میکند. مطمئنی؟", "resetConfirm": "بله، همه بازنشانی", "resetComplete": "پیشرفت بازنشانی شد! بارگذاری مجدد...", "cancel": "لغو" }
|
||||
"settings": { "title": "تنظیمات", "music": "موسیقی", "language": "زبان", "progress": "پیشرفت تو", "stars": "ستارهها", "completed": "تکمیل شده", "resetTitle": "بازنشانی پیشرفت", "resetButton": "همه بازنشانی", "resetWarning": "این همه ستارهها و پیشرفت پاک میکند. مطمئنی؟", "resetConfirm": "بله، همه بازنشانی", "resetComplete": "پیشرفت بازنشانی شد! بارگذاری مجدد...", "cancel": "لغو" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Penser Étape par Étape", "problem": "Le Problème :", "withoutMagic": "Sans mots magiques :", "addMagicWords": "Ajoute les Mots Magiques !", "magicWordsActive": "Mots magiques ajoutés !", "nextStep": "Révéler l'Étape Suivante", "withMagic": "Avec la pensée étape par étape :", "retry": "Réessayer" },
|
||||
"promptLab": { "title": "Labo de Prompts", "progress": "Améliorations", "yourPrompt": "Ton Prompt :", "aiSays": "Réponse de l'IA :", "addDetails": "Ajouter des améliorations :", "success": "Ton prompt est maintenant super spécifique !", "retry": "Recommencer" },
|
||||
"wordPredictor": { "title": "Comment l'IA Pense", "instruction": "L'IA devine le mot suivant le plus probable. Peux-tu penser comme l'IA ?", "aiThinks": "L'IA lit :", "thinkingDefault": "Hmm, quel mot aurait le plus de sens ici ?", "check": "Vérifier ma réponse !", "correct": "Tu penses comme l'IA !", "tryAgain": "Pas tout à fait ! L'IA choisit le mot le plus probable.", "retry": "Réessayer" },
|
||||
"settings": { "title": "Paramètres", "language": "Langue", "progress": "Ta Progression", "stars": "Étoiles", "completed": "Complétés", "resetTitle": "Réinitialiser la Progression", "resetButton": "Tout Réinitialiser", "resetWarning": "Cela supprimera toutes tes étoiles et ta progression. Es-tu sûr ?", "resetConfirm": "Oui, Tout Réinitialiser", "resetComplete": "Progression réinitialisée ! Rechargement...", "cancel": "Annuler" }
|
||||
"settings": { "title": "Paramètres", "music": "Musique", "language": "Langue", "progress": "Ta Progression", "stars": "Étoiles", "completed": "Complétés", "resetTitle": "Réinitialiser la Progression", "resetButton": "Tout Réinitialiser", "resetWarning": "Cela supprimera toutes tes étoiles et ta progression. Es-tu sûr ?", "resetConfirm": "Oui, Tout Réinitialiser", "resetComplete": "Progression réinitialisée ! Rechargement...", "cancel": "Annuler" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "חשוב צעד אחר צעד", "problem": "הבעיה:", "withoutMagic": "בלי מילות קסם:", "addMagicWords": "הוסף את מילות הקסם!", "magicWordsActive": "מילות הקסם נוספו!", "nextStep": "גלה את הצעד הבא", "withMagic": "עם חשיבה צעד אחר צעד:", "retry": "נסה שוב" },
|
||||
"promptLab": { "title": "מעבדת פרומפטים", "progress": "שיפורים", "yourPrompt": "הפרומפט שלך:", "aiSays": "תשובת AI:", "addDetails": "הוסף שיפורים:", "success": "הפרומפט שלך עכשיו ספציפי מאוד!", "retry": "התחל מחדש" },
|
||||
"wordPredictor": { "title": "איך AI חושב", "instruction": "AI מנחש את המילה הסבירה ביותר. אתה יכול לחשוב כמו AI?", "aiThinks": "AI קורא:", "thinkingDefault": "הממ, איזו מילה תהיה הגיונית כאן?", "check": "בדוק את התשובה שלי!", "correct": "אתה חושב כמו AI!", "tryAgain": "לא בדיוק! AI בוחר את המילה הסבירה ביותר.", "retry": "נסה שוב" },
|
||||
"settings": { "title": "הגדרות", "language": "שפה", "progress": "ההתקדמות שלך", "stars": "כוכבים", "completed": "הושלם", "resetTitle": "אפס התקדמות", "resetButton": "אפס הכל", "resetWarning": "זה ימחק את כל הכוכבים וההתקדמות. בטוח?", "resetConfirm": "כן, אפס הכל", "resetComplete": "התקדמות אופסה! טוען מחדש...", "cancel": "ביטול" }
|
||||
"settings": { "title": "הגדרות", "music": "מוזיקה", "language": "שפה", "progress": "ההתקדמות שלך", "stars": "כוכבים", "completed": "הושלם", "resetTitle": "אפס התקדמות", "resetButton": "אפס הכל", "resetWarning": "זה ימחק את כל הכוכבים וההתקדמות. בטוח?", "resetConfirm": "כן, אפס הכל", "resetComplete": "התקדמות אופסה! טוען מחדש...", "cancel": "ביטול" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Pensa Passo dopo Passo", "problem": "Il Problema:", "withoutMagic": "Senza parole magiche:", "addMagicWords": "Aggiungi le Parole Magiche!", "magicWordsActive": "Parole magiche aggiunte!", "nextStep": "Rivela Passo Successivo", "withMagic": "Con il pensiero passo dopo passo:", "retry": "Riprova" },
|
||||
"promptLab": { "title": "Laboratorio Prompt", "progress": "Miglioramenti", "yourPrompt": "Il Tuo Prompt:", "aiSays": "Risposta IA:", "addDetails": "Aggiungi miglioramenti:", "success": "Il tuo prompt ora è super specifico!", "retry": "Ricomincia" },
|
||||
"wordPredictor": { "title": "Come Pensa l'IA", "instruction": "L'IA indovina la parola più probabile. Puoi pensare come l'IA?", "aiThinks": "L'IA sta leggendo:", "thinkingDefault": "Hmm, quale parola avrebbe più senso qui?", "check": "Verifica la mia risposta!", "correct": "Pensi come l'IA!", "tryAgain": "Non proprio! L'IA sceglie la parola più probabile.", "retry": "Riprova" },
|
||||
"settings": { "title": "Impostazioni", "language": "Lingua", "progress": "I Tuoi Progressi", "stars": "Stelle", "completed": "Completati", "resetTitle": "Reimposta Progressi", "resetButton": "Reimposta Tutto", "resetWarning": "Questo cancellerà tutte le stelle e i progressi. Sei sicuro?", "resetConfirm": "Sì, Reimposta Tutto", "resetComplete": "Progressi reimpostati! Ricaricamento...", "cancel": "Annulla" }
|
||||
"settings": { "title": "Impostazioni", "music": "Musica", "language": "Lingua", "progress": "I Tuoi Progressi", "stars": "Stelle", "completed": "Completati", "resetTitle": "Reimposta Progressi", "resetButton": "Reimposta Tutto", "resetWarning": "Questo cancellerà tutte le stelle e i progressi. Sei sicuro?", "resetConfirm": "Sì, Reimposta Tutto", "resetComplete": "Progressi reimpostati! Ricaricamento...", "cancel": "Annulla" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "ステップバイステップで考えよう", "problem": "問題:", "withoutMagic": "魔法の言葉なし:", "addMagicWords": "魔法の言葉を追加!", "magicWordsActive": "魔法の言葉を追加した!", "nextStep": "次のステップを見る", "withMagic": "ステップバイステップ思考で:", "retry": "もう一度" },
|
||||
"promptLab": { "title": "プロンプトラボ", "progress": "改善", "yourPrompt": "あなたのプロンプト:", "aiSays": "AIの応答:", "addDetails": "改善を追加:", "success": "プロンプトがとても具体的になった!", "retry": "最初から" },
|
||||
"wordPredictor": { "title": "AIの考え方", "instruction": "AIは最も可能性の高い次の単語を推測する。AIのように考えられる?", "aiThinks": "AIが読んでいる:", "thinkingDefault": "うーん、ここでどの単語が一番意味がある?", "check": "答えをチェック!", "correct": "AIのように考えてる!", "tryAgain": "違う!AIは最も可能性の高い単語を選ぶ。", "retry": "もう一度" },
|
||||
"settings": { "title": "設定", "language": "言語", "progress": "あなたの進捗", "stars": "スター", "completed": "完了", "resetTitle": "進捗をリセット", "resetButton": "すべてリセット", "resetWarning": "すべてのスターと進捗が削除されます。よろしいですか?", "resetConfirm": "はい、すべてリセット", "resetComplete": "進捗をリセットしました!再読み込み中...", "cancel": "キャンセル" }
|
||||
"settings": { "title": "設定", "music": "音楽", "language": "言語", "progress": "あなたの進捗", "stars": "スター", "completed": "完了", "resetTitle": "進捗をリセット", "resetButton": "すべてリセット", "resetWarning": "すべてのスターと進捗が削除されます。よろしいですか?", "resetConfirm": "はい、すべてリセット", "resetComplete": "進捗をリセットしました!再読み込み中...", "cancel": "キャンセル" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "단계별로 생각하기", "problem": "문제:", "withoutMagic": "마법의 단어 없이:", "addMagicWords": "마법의 단어를 추가하세요!", "magicWordsActive": "마법의 단어가 추가됐어요!", "nextStep": "다음 단계 보기", "withMagic": "단계별 사고로:", "retry": "다시 시도" },
|
||||
"promptLab": { "title": "프롬프트 연구소", "progress": "개선", "yourPrompt": "당신의 프롬프트:", "aiSays": "AI 응답:", "addDetails": "개선 추가:", "success": "프롬프트가 아주 구체적이 됐어요!", "retry": "처음부터" },
|
||||
"wordPredictor": { "title": "AI가 생각하는 방법", "instruction": "AI는 가장 가능성 높은 다음 단어를 추측해요. AI처럼 생각할 수 있나요?", "aiThinks": "AI가 읽고 있어요:", "thinkingDefault": "음, 어떤 단어가 여기에 가장 맞을까?", "check": "내 답 확인!", "correct": "AI처럼 생각하고 있어요!", "tryAgain": "아니에요! AI는 가장 가능성 높은 단어를 선택해요.", "retry": "다시 시도" },
|
||||
"settings": { "title": "설정", "language": "언어", "progress": "진행 상황", "stars": "별", "completed": "완료", "resetTitle": "진행 상황 초기화", "resetButton": "모두 초기화", "resetWarning": "모든 별과 진행 상황이 삭제됩니다. 확실한가요?", "resetConfirm": "네, 모두 초기화", "resetComplete": "진행 상황 초기화됨! 새로고침 중...", "cancel": "취소" }
|
||||
"settings": { "title": "설정", "music": "음악", "language": "언어", "progress": "진행 상황", "stars": "별", "completed": "완료", "resetTitle": "진행 상황 초기화", "resetButton": "모두 초기화", "resetWarning": "모든 별과 진행 상황이 삭제됩니다. 확실한가요?", "resetConfirm": "네, 모두 초기화", "resetComplete": "진행 상황 초기화됨! 새로고침 중...", "cancel": "취소" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1633,6 +1633,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"music": "Muziek",
|
||||
"language": "Taal",
|
||||
"progress": "Jouw voortgang",
|
||||
"stars": "Sterren",
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Pense Passo a Passo", "problem": "O Problema:", "withoutMagic": "Sem palavras mágicas:", "addMagicWords": "Adicione as Palavras Mágicas!", "magicWordsActive": "Palavras mágicas adicionadas!", "nextStep": "Revelar Próximo Passo", "withMagic": "Com pensamento passo a passo:", "retry": "Tentar de novo" },
|
||||
"promptLab": { "title": "Laboratório de Prompts", "progress": "Melhorias", "yourPrompt": "Seu Prompt:", "aiSays": "Resposta da IA:", "addDetails": "Adicionar melhorias:", "success": "Seu prompt agora é super específico!", "retry": "Recomeçar" },
|
||||
"wordPredictor": { "title": "Como a IA Pensa", "instruction": "A IA adivinha a palavra mais provável. Você consegue pensar como IA?", "aiThinks": "A IA está lendo:", "thinkingDefault": "Hmm, qual palavra faria mais sentido aqui?", "check": "Verificar minha resposta!", "correct": "Você pensa como IA!", "tryAgain": "Não exatamente! A IA escolhe a palavra mais provável.", "retry": "Tentar de novo" },
|
||||
"settings": { "title": "Configurações", "language": "Idioma", "progress": "Seu Progresso", "stars": "Estrelas", "completed": "Completados", "resetTitle": "Redefinir Progresso", "resetButton": "Redefinir Tudo", "resetWarning": "Isso apagará todas as estrelas e progresso. Tem certeza?", "resetConfirm": "Sim, Redefinir Tudo", "resetComplete": "Progresso redefinido! Recarregando...", "cancel": "Cancelar" }
|
||||
"settings": { "title": "Configurações", "music": "Música", "language": "Idioma", "progress": "Seu Progresso", "stars": "Estrelas", "completed": "Completados", "resetTitle": "Redefinir Progresso", "resetButton": "Redefinir Tudo", "resetWarning": "Isso apagará todas as estrelas e progresso. Tem certeza?", "resetConfirm": "Sim, Redefinir Tudo", "resetComplete": "Progresso redefinido! Recarregando...", "cancel": "Cancelar" }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "Думай пошагово", "problem": "Проблема:", "withoutMagic": "Без волшебных слов:", "addMagicWords": "Добавь волшебные слова!", "magicWordsActive": "Волшебные слова добавлены!", "nextStep": "Показать следующий шаг", "withMagic": "С пошаговым мышлением:", "retry": "Попробуй снова" },
|
||||
"promptLab": { "title": "Лаборатория промптов", "progress": "Улучшения", "yourPrompt": "Твой промпт:", "aiSays": "Ответ ИИ:", "addDetails": "Добавь улучшения:", "success": "Твой промпт теперь очень конкретный!", "retry": "Сначала" },
|
||||
"wordPredictor": { "title": "Как думает ИИ", "instruction": "ИИ угадывает наиболее вероятное следующее слово. Можешь думать как ИИ?", "aiThinks": "ИИ читает:", "thinkingDefault": "Хмм, какое слово имело бы смысл здесь?", "check": "Проверь мой ответ!", "correct": "Ты думаешь как ИИ!", "tryAgain": "Не совсем! ИИ выбирает наиболее вероятное слово.", "retry": "Попробуй снова" },
|
||||
"settings": { "title": "Настройки", "language": "Язык", "progress": "Твой прогресс", "stars": "Звёзды", "completed": "Завершено", "resetTitle": "Сбросить прогресс", "resetButton": "Сбросить всё", "resetWarning": "Это удалит все звёзды и прогресс. Уверен?", "resetConfirm": "Да, сбросить всё", "resetComplete": "Прогресс сброшен! Перезагрузка...", "cancel": "Отмена" }
|
||||
"settings": { "title": "Настройки", "music": "Музыка", "language": "Язык", "progress": "Твой прогресс", "stars": "Звёзды", "completed": "Завершено", "resetTitle": "Сбросить прогресс", "resetButton": "Сбросить всё", "resetWarning": "Это удалит все звёзды и прогресс. Уверен?", "resetConfirm": "Да, сбросить всё", "resetComplete": "Прогресс сброшен! Перезагрузка...", "cancel": "Отмена" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1566,6 +1566,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ayarlar",
|
||||
"music": "Müzik",
|
||||
"language": "Dil",
|
||||
"progress": "İlerlemeniz",
|
||||
"stars": "Yıldız",
|
||||
|
||||
+1
-1
@@ -1445,6 +1445,6 @@
|
||||
"stepByStep": { "title": "逐步思考", "problem": "问题:", "withoutMagic": "没有魔法词语:", "addMagicWords": "添加魔法词语!", "magicWordsActive": "魔法词语已添加!", "nextStep": "显示下一步", "withMagic": "逐步思考:", "retry": "再试一次" },
|
||||
"promptLab": { "title": "提示词实验室", "progress": "改进", "yourPrompt": "你的提示词:", "aiSays": "AI回应:", "addDetails": "添加改进:", "success": "你的提示词现在非常具体!", "retry": "重新开始" },
|
||||
"wordPredictor": { "title": "AI如何思考", "instruction": "AI猜测最可能的下一个词。你能像AI一样思考吗?", "aiThinks": "AI正在阅读:", "thinkingDefault": "嗯,哪个词在这里最有意义?", "check": "检查我的答案!", "correct": "你像AI一样思考!", "tryAgain": "不太对!AI选择最可能的词。", "retry": "再试一次" },
|
||||
"settings": { "title": "设置", "language": "语言", "progress": "你的进度", "stars": "星星", "completed": "已完成", "resetTitle": "重置进度", "resetButton": "重置全部", "resetWarning": "这将删除所有星星和进度。确定吗?", "resetConfirm": "是的,重置全部", "resetComplete": "进度已重置!正在重新加载...", "cancel": "取消" }
|
||||
"settings": { "title": "设置", "music": "音乐", "language": "语言", "progress": "你的进度", "stars": "星星", "completed": "已完成", "resetTitle": "重置进度", "resetButton": "重置全部", "resetWarning": "这将删除所有星星和进度。确定吗?", "resetConfirm": "是的,重置全部", "resetComplete": "进度已重置!正在重新加载...", "cancel": "取消" }
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
import { Schoolbell } from "next/font/google";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { KidsHeader } from "@/components/kids/layout/kids-header";
|
||||
import { BackgroundMusic } from "@/components/kids/layout/background-music";
|
||||
import { MusicProvider } from "@/components/kids/layout/background-music";
|
||||
import { LevelProvider } from "@/components/kids/providers/level-context";
|
||||
|
||||
const RTL_LOCALES = ["ar", "he", "fa"];
|
||||
@@ -40,6 +40,7 @@ export default async function KidsLayout({
|
||||
|
||||
return (
|
||||
<LevelProvider>
|
||||
<MusicProvider>
|
||||
<div
|
||||
className={`fixed inset-0 flex flex-col text-xl light ${kidsFont.className}`}
|
||||
data-theme="light"
|
||||
@@ -86,8 +87,8 @@ export default async function KidsLayout({
|
||||
<main className="flex-1 min-h-0 overflow-hidden">
|
||||
{children}
|
||||
</main>
|
||||
<BackgroundMusic />
|
||||
</div>
|
||||
</MusicProvider>
|
||||
</LevelProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, createContext, useContext } from "react";
|
||||
|
||||
const MUSIC_ENABLED_KEY = "kids-music-enabled";
|
||||
const MUSIC_VOLUME_KEY = "kids-music-volume";
|
||||
|
||||
// Shared audio instance and context for cross-component communication
|
||||
interface MusicContextType {
|
||||
isPlaying: boolean;
|
||||
volume: number;
|
||||
setIsPlaying: (playing: boolean) => void;
|
||||
setVolume: (volume: number) => void;
|
||||
audioRef: React.RefObject<HTMLAudioElement | null>;
|
||||
}
|
||||
|
||||
const MusicContext = createContext<MusicContextType | null>(null);
|
||||
|
||||
export function useMusicContext() {
|
||||
return useContext(MusicContext);
|
||||
}
|
||||
|
||||
// Pixel art speaker icons
|
||||
function PixelSpeakerOn() {
|
||||
@@ -30,304 +48,125 @@ function PixelSpeakerOff() {
|
||||
);
|
||||
}
|
||||
|
||||
// 8-bit chiptune music generator using Web Audio API
|
||||
class ChiptunePlayer {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private masterGain: GainNode | null = null;
|
||||
private isPlaying = false;
|
||||
private intervalId: number | null = null;
|
||||
private step = 0;
|
||||
// Provider component that manages the audio element
|
||||
export function MusicProvider({ children }: { children: React.ReactNode }) {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [volume, setVolume] = useState(0.3);
|
||||
const [hasInteracted, setHasInteracted] = useState(false);
|
||||
|
||||
// Fun bouncy 8-bit melody - playful and catchy!
|
||||
private melody = [
|
||||
// Part 1: Bouncy intro
|
||||
523.25, 0, 659.25, 0, 783.99, 0, 659.25, 0, // C5 - E5 - G5 - E5 (bouncy)
|
||||
698.46, 0, 783.99, 0, 880.00, 783.99, 659.25, 0, // F5 - G5 - A5 G5 E5 (climb up!)
|
||||
// Part 2: Silly descending
|
||||
783.99, 698.46, 659.25, 587.33, 523.25, 0, 392.00, 0, // G5 F5 E5 D5 C5 - G4 (slide down)
|
||||
440.00, 493.88, 523.25, 0, 659.25, 0, 523.25, 0, // A4 B4 C5 - E5 - C5 (pop back up)
|
||||
// Part 3: Playful jumps
|
||||
392.00, 523.25, 392.00, 523.25, 659.25, 783.99, 659.25, 0, // G4 C5 G4 C5 E5 G5 E5 (jumping!)
|
||||
880.00, 0, 783.99, 0, 659.25, 523.25, 587.33, 659.25, // A5 - G5 - E5 C5 D5 E5
|
||||
// Part 4: Fun ending
|
||||
783.99, 0, 659.25, 0, 523.25, 587.33, 659.25, 783.99, // G5 - E5 - C5 D5 E5 G5
|
||||
880.00, 783.99, 659.25, 523.25, 392.00, 0, 523.25, 0, // A5 G5 E5 C5 G4 - C5 (finish!)
|
||||
];
|
||||
|
||||
// Funky bass line - groovy!
|
||||
private bass = [
|
||||
// Part 1
|
||||
130.81, 0, 130.81, 164.81, 196.00, 0, 164.81, 0, // C3 - C3 E3 G3 - E3
|
||||
174.61, 0, 196.00, 0, 220.00, 196.00, 164.81, 0, // F3 - G3 - A3 G3 E3
|
||||
// Part 2
|
||||
196.00, 174.61, 164.81, 146.83, 130.81, 0, 98.00, 0, // G3 F3 E3 D3 C3 - G2
|
||||
110.00, 123.47, 130.81, 0, 164.81, 0, 130.81, 0, // A2 B2 C3 - E3 - C3
|
||||
// Part 3
|
||||
98.00, 130.81, 98.00, 130.81, 164.81, 196.00, 164.81, 0, // G2 C3 G2 C3 E3 G3 E3
|
||||
220.00, 0, 196.00, 0, 164.81, 130.81, 146.83, 164.81, // A3 - G3 - E3 C3 D3 E3
|
||||
// Part 4
|
||||
196.00, 0, 164.81, 0, 130.81, 146.83, 164.81, 196.00, // G3 - E3 - C3 D3 E3 G3
|
||||
220.00, 196.00, 164.81, 130.81, 98.00, 0, 130.81, 0, // A3 G3 E3 C3 G2 - C3
|
||||
];
|
||||
|
||||
async start() {
|
||||
if (this.isPlaying) return;
|
||||
// Load saved preferences
|
||||
useEffect(() => {
|
||||
const savedEnabled = localStorage.getItem(MUSIC_ENABLED_KEY);
|
||||
const savedVolume = localStorage.getItem(MUSIC_VOLUME_KEY);
|
||||
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
||||
|
||||
// Resume audio context if suspended (required by browsers)
|
||||
if (this.audioContext.state === "suspended") {
|
||||
await this.audioContext.resume();
|
||||
if (savedVolume) {
|
||||
setVolume(parseFloat(savedVolume));
|
||||
}
|
||||
|
||||
// Default to enabled if not set
|
||||
if (savedEnabled === null || savedEnabled === "true") {
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Create and manage audio element
|
||||
useEffect(() => {
|
||||
if (!audioRef.current) {
|
||||
const audio = new Audio("/kids-music.mp3");
|
||||
audio.loop = true;
|
||||
audio.volume = volume;
|
||||
audioRef.current = audio;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
}
|
||||
|
||||
this.masterGain = this.audioContext.createGain();
|
||||
this.masterGain.gain.value = 0.2;
|
||||
this.masterGain.connect(this.audioContext.destination);
|
||||
this.isPlaying = true;
|
||||
this.step = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Play notes at upbeat tempo
|
||||
this.intervalId = window.setInterval(() => this.playStep(), 150);
|
||||
} catch (error) {
|
||||
console.error("Failed to start audio:", error);
|
||||
// Update volume when it changes
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = volume;
|
||||
}
|
||||
}
|
||||
localStorage.setItem(MUSIC_VOLUME_KEY, volume.toString());
|
||||
}, [volume]);
|
||||
|
||||
private playStep() {
|
||||
if (!this.audioContext || !this.masterGain) return;
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
const noteIndex = this.step % this.melody.length;
|
||||
|
||||
// Play melody (square wave for 8-bit sound) - skip if 0 (rest)
|
||||
if (this.melody[noteIndex] > 0) {
|
||||
this.playNote(this.melody[noteIndex], now, 0.12, "square", 0.12);
|
||||
}
|
||||
// Handle play/pause
|
||||
useEffect(() => {
|
||||
localStorage.setItem(MUSIC_ENABLED_KEY, isPlaying.toString());
|
||||
|
||||
// Play bass (triangle wave) - louder and deeper, skip if 0
|
||||
if (this.bass[noteIndex] > 0) {
|
||||
this.playNote(this.bass[noteIndex], now, 0.14, "triangle", 0.15);
|
||||
// Add sub-bass for extra punch
|
||||
this.playNote(this.bass[noteIndex] / 2, now, 0.14, "sine", 0.08);
|
||||
}
|
||||
|
||||
// Aggressive dubstep-style percussion
|
||||
if (this.step % 8 === 0) {
|
||||
this.playDrum(now, "kick");
|
||||
this.playDrum(now + 0.15, "kick"); // Double kick
|
||||
} else if (this.step % 8 === 4) {
|
||||
this.playDrum(now, "snare");
|
||||
} else if (this.step % 2 === 1) {
|
||||
this.playDrum(now, "hihat");
|
||||
}
|
||||
if (!audioRef.current) return;
|
||||
|
||||
// Wobble bass on every other beat for dubstep feel
|
||||
if (this.step % 4 === 2) {
|
||||
this.playWobble(now);
|
||||
}
|
||||
|
||||
this.step++;
|
||||
}
|
||||
|
||||
private playNote(freq: number, time: number, duration: number, type: OscillatorType, volume: number) {
|
||||
if (!this.audioContext || !this.masterGain) return;
|
||||
|
||||
const osc = this.audioContext.createOscillator();
|
||||
const gain = this.audioContext.createGain();
|
||||
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
|
||||
gain.gain.setValueAtTime(volume, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + duration);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.masterGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + duration);
|
||||
}
|
||||
|
||||
private playDrum(time: number, type: "kick" | "hihat" | "snare" = "hihat") {
|
||||
if (!this.audioContext || !this.masterGain) return;
|
||||
|
||||
if (type === "kick") {
|
||||
// Heavy dubstep kick - deep and punchy
|
||||
const osc = this.audioContext.createOscillator();
|
||||
const gain = this.audioContext.createGain();
|
||||
|
||||
osc.type = "sine";
|
||||
osc.frequency.setValueAtTime(180, time);
|
||||
osc.frequency.exponentialRampToValueAtTime(30, time + 0.12);
|
||||
|
||||
gain.gain.setValueAtTime(0.5, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.masterGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.2);
|
||||
|
||||
// Add click for attack
|
||||
const click = this.audioContext.createOscillator();
|
||||
const clickGain = this.audioContext.createGain();
|
||||
click.type = "square";
|
||||
click.frequency.value = 800;
|
||||
clickGain.gain.setValueAtTime(0.15, time);
|
||||
clickGain.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
|
||||
click.connect(clickGain);
|
||||
clickGain.connect(this.masterGain);
|
||||
click.start(time);
|
||||
click.stop(time + 0.02);
|
||||
|
||||
} else if (type === "snare") {
|
||||
// Aggressive dubstep snare - noise + tone
|
||||
const bufferSize = this.audioContext.sampleRate * 0.15;
|
||||
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const noise = this.audioContext.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const noiseGain = this.audioContext.createGain();
|
||||
noiseGain.gain.setValueAtTime(0.3, time);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);
|
||||
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = "bandpass";
|
||||
filter.frequency.value = 3000;
|
||||
filter.Q.value = 1;
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(noiseGain);
|
||||
noiseGain.connect(this.masterGain);
|
||||
|
||||
noise.start(time);
|
||||
noise.stop(time + 0.15);
|
||||
|
||||
// Add tone body
|
||||
const tone = this.audioContext.createOscillator();
|
||||
const toneGain = this.audioContext.createGain();
|
||||
tone.type = "triangle";
|
||||
tone.frequency.setValueAtTime(200, time);
|
||||
tone.frequency.exponentialRampToValueAtTime(100, time + 0.05);
|
||||
toneGain.gain.setValueAtTime(0.2, time);
|
||||
toneGain.gain.exponentialRampToValueAtTime(0.001, time + 0.08);
|
||||
tone.connect(toneGain);
|
||||
toneGain.connect(this.masterGain);
|
||||
tone.start(time);
|
||||
tone.stop(time + 0.08);
|
||||
|
||||
if (isPlaying && hasInteracted) {
|
||||
audioRef.current.play().catch(() => {
|
||||
// Autoplay blocked, will retry on user interaction
|
||||
});
|
||||
} else {
|
||||
// Hi-hat - crispy noise
|
||||
const bufferSize = this.audioContext.sampleRate * 0.04;
|
||||
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
audioRef.current.pause();
|
||||
}
|
||||
}, [isPlaying, hasInteracted]);
|
||||
|
||||
// Listen for first user interaction to enable autoplay
|
||||
useEffect(() => {
|
||||
const handleInteraction = () => {
|
||||
setHasInteracted(true);
|
||||
if (isPlaying && audioRef.current) {
|
||||
audioRef.current.play().catch(() => {});
|
||||
}
|
||||
|
||||
const noise = this.audioContext.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const gain = this.audioContext.createGain();
|
||||
gain.gain.setValueAtTime(0.08, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.04);
|
||||
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = "highpass";
|
||||
filter.frequency.value = 9000;
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.masterGain);
|
||||
|
||||
noise.start(time);
|
||||
noise.stop(time + 0.04);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private playWobble(time: number) {
|
||||
if (!this.audioContext || !this.masterGain) return;
|
||||
document.addEventListener("click", handleInteraction, { once: true });
|
||||
document.addEventListener("keydown", handleInteraction, { once: true });
|
||||
document.addEventListener("touchstart", handleInteraction, { once: true });
|
||||
|
||||
// Classic dubstep wobble bass
|
||||
const osc = this.audioContext.createOscillator();
|
||||
const gain = this.audioContext.createGain();
|
||||
const lfo = this.audioContext.createOscillator();
|
||||
const lfoGain = this.audioContext.createGain();
|
||||
|
||||
osc.type = "sawtooth";
|
||||
osc.frequency.value = 55; // Low A
|
||||
|
||||
// LFO for wobble effect
|
||||
lfo.type = "sine";
|
||||
lfo.frequency.value = 8; // Wobble speed
|
||||
lfoGain.gain.value = 400;
|
||||
|
||||
lfo.connect(lfoGain);
|
||||
|
||||
// Create filter for wobble
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = "lowpass";
|
||||
filter.frequency.value = 400;
|
||||
filter.Q.value = 8;
|
||||
|
||||
lfoGain.connect(filter.frequency);
|
||||
|
||||
gain.gain.setValueAtTime(0.2, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.3);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.masterGain);
|
||||
|
||||
lfo.start(time);
|
||||
osc.start(time);
|
||||
lfo.stop(time + 0.3);
|
||||
osc.stop(time + 0.3);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("click", handleInteraction);
|
||||
document.removeEventListener("keydown", handleInteraction);
|
||||
document.removeEventListener("touchstart", handleInteraction);
|
||||
};
|
||||
}, [isPlaying]);
|
||||
|
||||
stop() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
getIsPlaying() {
|
||||
return this.isPlaying;
|
||||
}
|
||||
return (
|
||||
<MusicContext.Provider value={{ isPlaying, volume, setIsPlaying, setVolume, audioRef }}>
|
||||
{children}
|
||||
</MusicContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function MusicButton() {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const playerRef = useRef<ChiptunePlayer | null>(null);
|
||||
const context = useMusicContext();
|
||||
|
||||
// Fallback for when not wrapped in provider
|
||||
const [localPlaying, setLocalPlaying] = useState(false);
|
||||
const localAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const isPlaying = context?.isPlaying ?? localPlaying;
|
||||
const setIsPlaying = context?.setIsPlaying ?? setLocalPlaying;
|
||||
|
||||
const toggleMusic = useCallback(async () => {
|
||||
if (!playerRef.current) {
|
||||
playerRef.current = new ChiptunePlayer();
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
playerRef.current.stop();
|
||||
setIsPlaying(false);
|
||||
const toggleMusic = useCallback(() => {
|
||||
if (context) {
|
||||
context.setIsPlaying(!context.isPlaying);
|
||||
} else {
|
||||
await playerRef.current.start();
|
||||
setIsPlaying(true);
|
||||
// Fallback behavior
|
||||
if (!localAudioRef.current) {
|
||||
localAudioRef.current = new Audio("/kids-music.mp3");
|
||||
localAudioRef.current.loop = true;
|
||||
localAudioRef.current.volume = 0.3;
|
||||
}
|
||||
|
||||
if (localPlaying) {
|
||||
localAudioRef.current.pause();
|
||||
setLocalPlaying(false);
|
||||
} else {
|
||||
localAudioRef.current.play().catch(() => {});
|
||||
setLocalPlaying(true);
|
||||
}
|
||||
}
|
||||
}, [isPlaying]);
|
||||
}, [context, localPlaying]);
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -341,6 +180,41 @@ export function MusicButton() {
|
||||
);
|
||||
}
|
||||
|
||||
// Volume slider component for settings
|
||||
export function MusicVolumeSlider() {
|
||||
const context = useMusicContext();
|
||||
|
||||
if (!context) return null;
|
||||
|
||||
const { volume, setVolume, isPlaying, setIsPlaying } = context;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium text-sm transition-colors ${
|
||||
isPlaying
|
||||
? "bg-[#22C55E] text-white"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{isPlaying ? "🔊 On" : "🔇 Off"}
|
||||
</button>
|
||||
<span className="text-sm text-[#5D4037]">{Math.round(volume * 100)}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={volume * 100}
|
||||
onChange={(e) => setVolume(parseInt(e.target.value) / 100)}
|
||||
className="w-full h-2 bg-[#D4A574] rounded-lg appearance-none cursor-pointer accent-[#8B4513]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy export for backwards compatibility
|
||||
export function BackgroundMusic() {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,8 @@ import { cn } from "@/lib/utils";
|
||||
import { clearAllProgress, getTotalStars, getCompletedLevelsCount } from "@/lib/kids/progress";
|
||||
import { setLocale } from "@/lib/i18n/client";
|
||||
import { analyticsKids } from "@/lib/analytics";
|
||||
import { Settings, X, Globe, Trash2, Check } from "lucide-react";
|
||||
import { Settings, X, Globe, Trash2, Check, Volume2 } from "lucide-react";
|
||||
import { MusicVolumeSlider } from "./background-music";
|
||||
|
||||
const SUPPORTED_LOCALES = [
|
||||
{ code: "en", label: "English", flag: "🇺🇸" },
|
||||
@@ -91,7 +92,7 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-[#FEF3C7] border-4 border-[#8B4513] rounded-xl p-6 w-full max-w-md animate-in zoom-in-95 fade-in duration-200">
|
||||
<div className="relative bg-[#FEF3C7] border-4 border-[#8B4513] rounded-xl p-6 w-full max-w-md max-h-[90vh] overflow-y-auto animate-in zoom-in-95 fade-in duration-200">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -106,6 +107,35 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
|
||||
{t("title")}
|
||||
</h2>
|
||||
|
||||
{/* Music Section */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-bold text-[#5D4037] mb-3 flex items-center gap-2">
|
||||
<Volume2 className="w-5 h-5" />
|
||||
{t("music")}
|
||||
</h3>
|
||||
<MusicVolumeSlider />
|
||||
<p className="mt-3 text-xs text-[#8B7355]">
|
||||
Music by{" "}
|
||||
<a
|
||||
href="https://pixabay.com/users/djartmusic-46653586/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=301272"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-[#8B4513]"
|
||||
>
|
||||
Krzysztof Szymanski
|
||||
</a>{" "}
|
||||
from{" "}
|
||||
<a
|
||||
href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=301272"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-[#8B4513]"
|
||||
>
|
||||
Pixabay
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Language Section */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-bold text-[#5D4037] mb-3 flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user