Update to v1.0.5: Redesigned Guided Relaxation (Body Scan, Visualization, Grounding)
This commit is contained in:
@@ -1222,72 +1222,119 @@ function quickRelax() {
|
||||
startGuidedRelaxation();
|
||||
}
|
||||
|
||||
// --- Guided Relaxation (5-4-3-2-1 Grounding) ---
|
||||
// --- Guided Relaxation (Mindfulness System) ---
|
||||
let relaxationState = {
|
||||
mode: null, // 'grounding', 'body_scan', 'visualization'
|
||||
step: 0,
|
||||
isActive: false,
|
||||
steps: [] // Will be populated dynamically based on language
|
||||
isPaused: false,
|
||||
steps: [],
|
||||
timer: null,
|
||||
duration: 0
|
||||
};
|
||||
|
||||
function getRelaxationSteps() {
|
||||
// Data Providers
|
||||
function getGroundingSteps() {
|
||||
return [
|
||||
{
|
||||
title: t('guided_sight_title'),
|
||||
instruction: t('guided_sight_instruction'),
|
||||
sub: t('guided_sight_sub'),
|
||||
count: 5,
|
||||
icon: "👁️",
|
||||
color: "#64B5F6"
|
||||
},
|
||||
{
|
||||
title: t('guided_touch_title'),
|
||||
instruction: t('guided_touch_instruction'),
|
||||
sub: t('guided_touch_sub'),
|
||||
count: 4,
|
||||
icon: "✋",
|
||||
color: "#81C784"
|
||||
},
|
||||
{
|
||||
title: t('guided_sound_title'),
|
||||
instruction: t('guided_sound_instruction'),
|
||||
sub: t('guided_sound_sub'),
|
||||
count: 3,
|
||||
icon: "👂",
|
||||
color: "#FFB74D"
|
||||
},
|
||||
{
|
||||
title: t('guided_smell_title'),
|
||||
instruction: t('guided_smell_instruction'),
|
||||
sub: t('guided_smell_sub'),
|
||||
count: 2,
|
||||
icon: "👃",
|
||||
color: "#BA68C8"
|
||||
},
|
||||
{
|
||||
title: t('guided_taste_title'),
|
||||
instruction: t('guided_taste_instruction'),
|
||||
sub: t('guided_taste_sub'),
|
||||
count: 1,
|
||||
icon: "👅",
|
||||
color: "#E57373"
|
||||
}
|
||||
{ title: t('guided_sight_title'), instruction: t('guided_sight_instruction'), sub: t('guided_sight_sub'), count: 5, icon: "👁️", color: "#64B5F6" },
|
||||
{ title: t('guided_touch_title'), instruction: t('guided_touch_instruction'), sub: t('guided_touch_sub'), count: 4, icon: "✋", color: "#81C784" },
|
||||
{ title: t('guided_sound_title'), instruction: t('guided_sound_instruction'), sub: t('guided_sound_sub'), count: 3, icon: "👂", color: "#FFB74D" },
|
||||
{ title: t('guided_smell_title'), instruction: t('guided_smell_instruction'), sub: t('guided_smell_sub'), count: 2, icon: "👃", color: "#BA68C8" },
|
||||
{ title: t('guided_taste_title'), instruction: t('guided_taste_instruction'), sub: t('guided_taste_sub'), count: 1, icon: "👅", color: "#E57373" }
|
||||
];
|
||||
}
|
||||
|
||||
function getBodyScanSteps() {
|
||||
return [
|
||||
{ instruction: t('scan_intro'), duration: 5000 },
|
||||
{ instruction: t('scan_feet'), duration: 8000 },
|
||||
{ instruction: t('scan_legs'), duration: 8000 },
|
||||
{ instruction: t('scan_stomach'), duration: 8000 },
|
||||
{ instruction: t('scan_chest'), duration: 8000 },
|
||||
{ instruction: t('scan_shoulders'), duration: 8000 },
|
||||
{ instruction: t('scan_face'), duration: 8000 },
|
||||
{ instruction: t('scan_outro'), duration: 6000 }
|
||||
];
|
||||
}
|
||||
|
||||
function getVisualizationSteps() {
|
||||
return [
|
||||
{ instruction: t('vis_intro'), duration: 6000 },
|
||||
{ instruction: t('vis_step1'), duration: 10000 },
|
||||
{ instruction: t('vis_step2'), duration: 10000 },
|
||||
{ instruction: t('vis_step3'), duration: 10000 },
|
||||
{ instruction: t('vis_step4'), duration: 10000 },
|
||||
{ instruction: t('vis_outro'), duration: 8000 }
|
||||
];
|
||||
}
|
||||
|
||||
// Entry Point
|
||||
function startGuidedRelaxation() {
|
||||
relaxationState.step = 0;
|
||||
relaxationState.isActive = true;
|
||||
relaxationState.steps = getRelaxationSteps();
|
||||
relaxationState = { mode: null, step: 0, isActive: true, isPaused: false, steps: [], timer: null, duration: 0 };
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'guided-relaxation-overlay';
|
||||
overlay.className = 'guided-overlay';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
renderRelaxationStep();
|
||||
// Add styles dynamically if not present (using the styles we wrote to guided-styles.css but injecting here for simplicity in single file context if needed, but best to rely on CSS file)
|
||||
// Assuming guided-styles.css is linked or merged. For safety, we rely on the styles.css update.
|
||||
|
||||
renderModeSelection();
|
||||
}
|
||||
|
||||
function renderRelaxationStep() {
|
||||
function renderModeSelection() {
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="guided-controls">
|
||||
<button class="icon-btn" onclick="closeGuidedRelaxation()">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="guided-title-large">${t('guided_select_mode')}</h2>
|
||||
|
||||
<div class="guided-mode-grid">
|
||||
<div class="guided-mode-card" onclick="startSession('grounding')">
|
||||
<span class="guided-mode-icon">🌿</span>
|
||||
<span class="guided-mode-title">${t('mode_grounding')}</span>
|
||||
</div>
|
||||
<div class="guided-mode-card" onclick="startSession('body_scan')">
|
||||
<span class="guided-mode-icon">🧘</span>
|
||||
<span class="guided-mode-title">${t('mode_body_scan')}</span>
|
||||
</div>
|
||||
<div class="guided-mode-card" onclick="startSession('visualization')">
|
||||
<span class="guided-mode-icon">🌄</span>
|
||||
<span class="guided-mode-title">${t('mode_visualization')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function startSession(mode) {
|
||||
relaxationState.mode = mode;
|
||||
relaxationState.step = 0;
|
||||
relaxationState.isActive = true;
|
||||
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (overlay) overlay.className = `guided-overlay mode-${mode}`;
|
||||
|
||||
if (mode === 'grounding') {
|
||||
relaxationState.steps = getGroundingSteps();
|
||||
renderGroundingStep();
|
||||
} else if (mode === 'body_scan') {
|
||||
relaxationState.steps = getBodyScanSteps();
|
||||
runAutoSession();
|
||||
} else if (mode === 'visualization') {
|
||||
relaxationState.steps = getVisualizationSteps();
|
||||
runAutoSession();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Grounding Logic (Interactive) ---
|
||||
function renderGroundingStep() {
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (!overlay || !relaxationState.isActive) return;
|
||||
|
||||
@@ -1312,24 +1359,20 @@ function renderRelaxationStep() {
|
||||
${progressDots}
|
||||
</div>
|
||||
|
||||
<button class="guided-action-btn" onclick="nextRelaxationSubStep()">
|
||||
<button class="guided-action-btn" onclick="nextGroundingSubStep()">
|
||||
${t('guided_found_btn')}
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Speak instruction
|
||||
speakText(`${currentStep.instruction} ${currentStep.sub}`);
|
||||
}
|
||||
|
||||
let currentDotIndex = 0;
|
||||
|
||||
function nextRelaxationSubStep() {
|
||||
function nextGroundingSubStep() {
|
||||
const dots = document.querySelectorAll('.progress-dot');
|
||||
if (currentDotIndex < dots.length) {
|
||||
dots[currentDotIndex].classList.add('active');
|
||||
// Haptic feedback
|
||||
if (navigator.vibrate) navigator.vibrate(50);
|
||||
// Sound feedback
|
||||
soundManager.playTone(400 + (currentDotIndex * 50), 'sine', 0.1, 0.1);
|
||||
|
||||
currentDotIndex++;
|
||||
@@ -1339,16 +1382,75 @@ function nextRelaxationSubStep() {
|
||||
currentDotIndex = 0;
|
||||
relaxationState.step++;
|
||||
if (relaxationState.step < relaxationState.steps.length) {
|
||||
renderRelaxationStep();
|
||||
renderGroundingStep();
|
||||
} else {
|
||||
finishGuidedRelaxation();
|
||||
finishSession();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishGuidedRelaxation() {
|
||||
// --- Auto Session Logic (Body Scan & Visualization) ---
|
||||
function runAutoSession() {
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (!overlay || !relaxationState.isActive) return;
|
||||
|
||||
const step = relaxationState.steps[relaxationState.step];
|
||||
const totalSteps = relaxationState.steps.length;
|
||||
const progress = ((relaxationState.step) / totalSteps) * 100;
|
||||
|
||||
// Visuals based on mode
|
||||
let visualHTML = '';
|
||||
if (relaxationState.mode === 'body_scan') {
|
||||
visualHTML = `<div class="body-scan-pulse"></div><div class="guided-step-icon">🧘</div>`;
|
||||
} else {
|
||||
visualHTML = `<div class="guided-step-icon">🌄</div>`;
|
||||
}
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="guided-controls">
|
||||
<button class="icon-btn" onclick="closeGuidedRelaxation()">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${visualHTML}
|
||||
|
||||
<div class="guided-instruction">${step.instruction}</div>
|
||||
|
||||
<div class="guided-progress-bar-container">
|
||||
<div class="guided-progress-bar" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="guided-bottom-controls">
|
||||
<!-- Could add Play/Pause here if needed -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Speak and advance
|
||||
speakText(step.instruction, () => {
|
||||
// On end of speech, wait remainder of duration
|
||||
if (!relaxationState.isActive) return;
|
||||
|
||||
relaxationState.timer = setTimeout(() => {
|
||||
relaxationState.step++;
|
||||
if (relaxationState.step < relaxationState.steps.length) {
|
||||
runAutoSession();
|
||||
} else {
|
||||
finishSession();
|
||||
}
|
||||
}, 2000); // Short pause after speech
|
||||
});
|
||||
|
||||
// Animate progress bar
|
||||
setTimeout(() => {
|
||||
const bar = document.querySelector('.guided-progress-bar');
|
||||
if (bar) bar.style.width = `${((relaxationState.step + 1) / totalSteps) * 100}%`;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function finishSession() {
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (overlay) {
|
||||
overlay.innerHTML = `
|
||||
@@ -1361,11 +1463,17 @@ function finishGuidedRelaxation() {
|
||||
`;
|
||||
speakText(`${t('guided_complete_title')} ${t('guided_complete_sub')}`);
|
||||
triggerSuccessPing();
|
||||
|
||||
// Log stats
|
||||
const duration = relaxationState.mode === 'grounding' ? 180 :
|
||||
relaxationState.mode === 'body_scan' ? 300 : 240;
|
||||
exerciseAPI.logSession(relaxationState.mode, duration).then(() => updateProgress());
|
||||
}
|
||||
}
|
||||
|
||||
function closeGuidedRelaxation() {
|
||||
relaxationState.isActive = false;
|
||||
if (relaxationState.timer) clearTimeout(relaxationState.timer);
|
||||
currentDotIndex = 0;
|
||||
const overlay = document.getElementById('guided-relaxation-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
@@ -1373,29 +1481,27 @@ function closeGuidedRelaxation() {
|
||||
if (window.speechSynthesis) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
|
||||
// Log session
|
||||
if (relaxationState.step >= relaxationState.steps.length) {
|
||||
exerciseAPI.logSession('grounding', 180).then(() => updateProgress());
|
||||
}
|
||||
}
|
||||
|
||||
function speakText(text) {
|
||||
// Enhanced TTS Wrapper
|
||||
function speakText(text, onEndCallback) {
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel(); // Stop previous
|
||||
window.speechSynthesis.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.rate = 0.9;
|
||||
utterance.rate = 0.85; // Slightly slower for relaxation
|
||||
utterance.pitch = 1.0;
|
||||
|
||||
// Map language codes to TTS locales
|
||||
const langMap = {
|
||||
'en': 'en-US',
|
||||
'ru': 'ru-RU',
|
||||
'he': 'he-IL'
|
||||
};
|
||||
const langMap = { 'en': 'en-US', 'ru': 'ru-RU', 'he': 'he-IL' };
|
||||
utterance.lang = langMap[currentLang] || 'en-US';
|
||||
|
||||
if (onEndCallback) {
|
||||
utterance.onend = onEndCallback;
|
||||
}
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
} else {
|
||||
// Fallback if no TTS
|
||||
if (onEndCallback) setTimeout(onEndCallback, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user