diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b30f42..91a5bdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Fixed
+
+- „Von Level 1 neu starten" setzt den Galerie-Speicherstatus zurück (vorher blieb „In Galerie speichern" ggf. gesperrt)
+- Ergebnis-Feedback wechselte bei jedem Re-Render, weil die rotierende Nachricht im Render gewählt wurde — jetzt einmal pro Bewertung; DE/EN-Texte für 1–2 Sterne laufen nicht mehr auseinander
+
### Changed
- Startseite entschlackt: „Spiel starten" ist die einzige auffällige Aktion (ohne Dauer-Puls); Tägliche Challenge als dezenter Textlink darunter (ohne ⚡, Farbrahmen, Countdown); Level/Galerie/Freies Malen als einfarbige Textlinks im Footer; Statistik als eine Textzeile statt drei Emoji-Karten; Streak-Badge und schwebende Sterne entfernt — die Serie steht nur noch in Malis Begrüßung
diff --git a/components/__tests__/GamePhaseComponents.test.tsx b/components/__tests__/GamePhaseComponents.test.tsx
index fa508ac..ccf8c3b 100644
--- a/components/__tests__/GamePhaseComponents.test.tsx
+++ b/components/__tests__/GamePhaseComponents.test.tsx
@@ -316,6 +316,16 @@ describe('ResultPhase', () => {
expect(texts).toContain('feedback-en-5');
});
+ it('selects feedback once per rating, not on every re-render', () => {
+ const { getRatingFeedback } = require('../../services/RatingManager');
+ getRatingFeedback.mockClear();
+ const { rerender } = render();
+ rerender(
+ ,
+ );
+ expect(getRatingFeedback).toHaveBeenCalledTimes(1);
+ });
+
it('shows saved label when savedToGallery is true', () => {
const { UNSAFE_getAllByType } = render();
const texts = getAllTexts(UNSAFE_getAllByType);
diff --git a/components/game/ResultPhase.tsx b/components/game/ResultPhase.tsx
index 89b87fe..35e37ea 100644
--- a/components/game/ResultPhase.tsx
+++ b/components/game/ResultPhase.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal } from 'react-native';
import DrawingCanvas from '@components/DrawingCanvas';
import LevelImageDisplay from '@components/LevelImageDisplay';
@@ -50,13 +50,17 @@ export default function ResultPhase({
}
}, [isLastLevel, userRating]);
- const getFeedbackText = (rating: number) => {
- if (rating >= 1 && rating <= 5) {
- const feedback = getRatingFeedback(rating as StarRating);
- return currentLang === 'en' ? feedback.messageEn : feedback.message;
- }
- return t('game.result.tapStars');
- };
+ // Feedback einmal pro Bewertung auswählen: getRatingFeedback() rotiert durch
+ // Nachrichten, ein Aufruf im Render ließ den Text bei jedem Re-Render wechseln.
+ const feedback = useMemo(
+ () => (userRating >= 1 && userRating <= 5 ? getRatingFeedback(userRating as StarRating) : null),
+ [userRating],
+ );
+ const feedbackText = feedback
+ ? currentLang === 'en'
+ ? feedback.messageEn
+ : feedback.message
+ : t('game.result.tapStars');
const imageSize = Math.min(Math.max(screenWidth * 0.44, 140), 220);
@@ -230,7 +234,7 @@ export default function ResultPhase({
)}
- {getFeedbackText(userRating)}
+ {feedbackText}
diff --git a/services/RatingManager.ts b/services/RatingManager.ts
index 798b145..3037be7 100644
--- a/services/RatingManager.ts
+++ b/services/RatingManager.ts
@@ -25,11 +25,12 @@ const lowRatingMessages = {
let lowRatingMessageIndex = 0;
-function getRotatingLowRatingMessage(language: 'de' | 'en' = 'de'): string {
- const messages = lowRatingMessages[language];
- const message = messages[lowRatingMessageIndex % messages.length];
+// DE und EN als Paar mit demselben Index (vorher zwei getrennte Aufrufe,
+// die den Zähler doppelt weiterschoben und die Sprachen auseinanderlaufen ließen).
+function getRotatingLowRatingMessages(): { de: string; en: string } {
+ const idx = lowRatingMessageIndex % lowRatingMessages.de.length;
lowRatingMessageIndex++;
- return message;
+ return { de: lowRatingMessages.de[idx], en: lowRatingMessages.en[idx] };
}
/**
@@ -91,14 +92,16 @@ function getRotatingHighRatingMessages(stars: 4 | 5): { de: string; en: string }
export function getRatingFeedback(stars: StarRating, language: 'de' | 'en' = 'de'): RatingFeedback {
switch (stars) {
case 1:
- case 2:
+ case 2: {
+ const low = getRotatingLowRatingMessages();
return {
stars,
- message: getRotatingLowRatingMessage('de'),
- messageEn: getRotatingLowRatingMessage('en'),
+ message: low.de,
+ messageEn: low.en,
animation: 'none',
sound: 'huchhu.mp3',
};
+ }
case 3:
return {
diff --git a/services/__tests__/useGamePhase.test.ts b/services/__tests__/useGamePhase.test.ts
index 771959b..39e189b 100644
--- a/services/__tests__/useGamePhase.test.ts
+++ b/services/__tests__/useGamePhase.test.ts
@@ -290,6 +290,24 @@ describe('useGamePhase', () => {
expect(clearCanvas).toHaveBeenCalled();
});
+ it('resets savedToGallery when restarting from level 1', async () => {
+ const { result } = renderGamePhase({
+ initialLevel: 5,
+ drawingPaths: [{ points: [{ x: 0, y: 0 }], color: '#000', strokeWidth: 3 }],
+ });
+
+ await act(async () => {
+ await result.current.saveToGallery();
+ });
+ expect(result.current.savedToGallery).toBe(true);
+
+ act(() => {
+ result.current.restartFromLevel1();
+ });
+
+ expect(result.current.savedToGallery).toBe(false);
+ });
+
// --- Replay ---
it('starts replay', () => {
diff --git a/services/useGamePhase.ts b/services/useGamePhase.ts
index 86bd102..132912f 100644
--- a/services/useGamePhase.ts
+++ b/services/useGamePhase.ts
@@ -291,6 +291,7 @@ export function useGamePhase({
const restartFromLevel1 = () => {
setPhase('memorize');
setUserRating(0);
+ setSavedToGallery(false);
clearCanvas();
setLevelNumber(1); // triggers level-init useEffect
};