Displays an avatar that reminds students to stay focused with customizable prompts at adjustable intervals
Size
30.1 KB
Version
1.1.30
Created
Oct 28, 2025
Updated
17 days ago
1// ==UserScript==
2// @name Student Focus Reminder Avatar
3// @description Displays an avatar that reminds students to stay focused with customizable prompts at adjustable intervals
4// @version 1.1.30
5// @match https://*.google.com/*
6// @icon https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // Prevent multiple instances - check if already running
12 if (window.focusReminderRunning) {
13 console.log('Focus Reminder already running, exiting...');
14 return;
15 }
16 window.focusReminderRunning = true;
17
18 // Array of on-task prompts
19 const prompts = [
20 'Keep your eyes on the teacher! π',
21 'Stay focused on the lesson! π',
22 'Are you paying attention? π―',
23 'Listen carefully to your teacher! π',
24 'Focus on what\'s being taught! π‘',
25 'Eyes up front, please! ποΈ',
26 'Stay engaged with the lesson! β¨',
27 'Remember to concentrate! π§ ',
28 'Keep your attention on the teacher! π',
29 'Stay on task! β'
30 ];
31
32 let reminderInterval;
33 let currentIntervalMinutes = 2; // Default 2 minutes
34 let customAvatarImage = null; // Store custom avatar
35 let textToSpeechEnabled = true; // Enable text-to-speech by default
36 let avatarVerticalPosition = 15; // Default vertical position in pixels
37 let isShowingPrompt = false; // Prevent overlapping prompts
38 let isInitialized = false; // Prevent double initialization
39 let customPrompts = []; // Store custom prompts
40 let selectedFemaleVoice = null; // Cache the selected female voice
41 let selectedVoiceName = null; // Store the user's selected voice name
42 let isPaused = false; // Track if extension is paused
43
44 // Create the avatar container
45 function createAvatar() {
46 const avatarContainer = document.createElement('div');
47 avatarContainer.id = 'focus-reminder-avatar';
48 avatarContainer.style.cssText = `
49 position: fixed;
50 bottom: 20px;
51 right: 20px;
52 z-index: 999999;
53 font-family: Arial, sans-serif;
54 `;
55
56 // Avatar circle
57 const avatar = document.createElement('div');
58 avatar.id = 'avatar-circle';
59 avatar.style.cssText = `
60 width: 60px;
61 height: 60px;
62 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
63 border-radius: 50%;
64 display: flex;
65 align-items: center;
66 justify-content: center;
67 font-size: 30px;
68 cursor: pointer;
69 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
70 transition: transform 0.3s ease;
71 overflow: hidden;
72 `;
73
74 updateAvatarDisplay(avatar);
75
76 avatar.addEventListener('mouseenter', () => {
77 avatar.style.transform = 'scale(1.1)';
78 });
79
80 avatar.addEventListener('mouseleave', () => {
81 avatar.style.transform = 'scale(1)';
82 });
83
84 avatar.addEventListener('click', () => {
85 showSettingsPanel();
86 });
87
88 // Pause/Play button
89 const pauseButton = document.createElement('div');
90 pauseButton.id = 'pause-play-button';
91 pauseButton.style.cssText = `
92 position: absolute;
93 top: -10px;
94 right: -10px;
95 width: 30px;
96 height: 30px;
97 background: ${isPaused ? '#28a745' : '#ffc107'};
98 border-radius: 50%;
99 display: flex;
100 align-items: center;
101 justify-content: center;
102 font-size: 16px;
103 cursor: pointer;
104 box-shadow: 0 2px 8px rgba(0,0,0,0.3);
105 transition: all 0.3s ease;
106 z-index: 1000000;
107 `;
108 pauseButton.textContent = isPaused ? 'βΆοΈ' : 'βΈοΈ';
109 pauseButton.title = isPaused ? 'Resume reminders' : 'Pause reminders';
110
111 pauseButton.addEventListener('click', async (e) => {
112 e.stopPropagation(); // Prevent opening settings panel
113 e.preventDefault();
114 console.log('Pause button clicked! Current state:', isPaused ? 'paused' : 'running');
115
116 isPaused = !isPaused;
117 await GM.setValue('isPaused', isPaused);
118
119 if (isPaused) {
120 clearInterval(reminderInterval);
121 pauseButton.style.background = '#28a745';
122 pauseButton.textContent = 'βΆοΈ';
123 pauseButton.title = 'Resume reminders';
124 console.log('Extension paused');
125 } else {
126 startReminderInterval();
127 pauseButton.style.background = '#ffc107';
128 pauseButton.textContent = 'βΈοΈ';
129 pauseButton.title = 'Pause reminders';
130 console.log('Extension resumed');
131 }
132 }, true); // Use capture phase
133
134 pauseButton.addEventListener('mouseenter', () => {
135 pauseButton.style.transform = 'scale(1.2)';
136 });
137
138 pauseButton.addEventListener('mouseleave', () => {
139 pauseButton.style.transform = 'scale(1)';
140 });
141
142 // Message bubble (hidden by default)
143 const messageBubble = document.createElement('div');
144 messageBubble.id = 'message-bubble';
145 messageBubble.style.cssText = `
146 position: absolute;
147 bottom: 70px;
148 right: 0;
149 background: white;
150 color: #333;
151 padding: 15px 20px;
152 border-radius: 15px;
153 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
154 max-width: 250px;
155 display: none;
156 font-size: 16px;
157 font-weight: 600;
158 text-align: center;
159 border: 3px solid #667eea;
160 `;
161
162 avatarContainer.appendChild(messageBubble);
163 avatarContainer.appendChild(avatar);
164 avatarContainer.appendChild(pauseButton);
165 document.body.appendChild(avatarContainer);
166
167 console.log('Focus reminder avatar created');
168 }
169
170 // Update avatar display with custom image or emoji
171 function updateAvatarDisplay(avatar) {
172 if (!avatar) avatar = document.getElementById('avatar-circle');
173 if (!avatar) return;
174
175 if (customAvatarImage) {
176 const img = document.createElement('img');
177 img.src = customAvatarImage;
178 img.style.width = '100%';
179 img.style.height = '100%';
180 img.style.objectFit = 'cover';
181 img.style.borderRadius = '50%';
182 img.style.transform = `translateY(${avatarVerticalPosition}px)`;
183 avatar.innerHTML = '';
184 avatar.appendChild(img);
185 console.log('Custom avatar displayed with position:', avatarVerticalPosition);
186 } else {
187 // Use Google Drive image with proper positioning
188 const img = document.createElement('img');
189 img.src = 'https://drive.google.com/uc?export=view&id=1yVtZLFNhkWHfaay5nDEJg60iEjhooJ-h';
190 img.style.width = '100%';
191 img.style.height = '100%';
192 img.style.objectFit = 'cover';
193 img.style.borderRadius = '50%';
194 img.style.transform = `translateY(${avatarVerticalPosition}px)`;
195 img.onerror = function() {
196 console.error('Failed to load avatar image from Google Drive');
197 avatar.innerHTML = 'π¨βπ«';
198 };
199 img.onload = function() {
200 console.log('Avatar image loaded successfully with position:', avatarVerticalPosition);
201 };
202 avatar.innerHTML = '';
203 avatar.appendChild(img);
204 }
205 }
206
207 // Show a random prompt
208 function showPrompt() {
209 if (isPaused) {
210 console.log('Extension is paused, skipping prompt');
211 return;
212 }
213
214 if (isShowingPrompt) {
215 console.log('Prompt blocked - already showing a prompt');
216 return; // Prevent overlapping prompts
217 }
218
219 const messageBubble = document.getElementById('message-bubble');
220 if (!messageBubble) {
221 console.log('Message bubble not found');
222 return;
223 }
224
225 isShowingPrompt = true;
226
227 // Use custom prompts if available, otherwise use default prompts
228 const promptsToUse = customPrompts.length > 0 ? customPrompts : prompts;
229 const randomPrompt = promptsToUse[Math.floor(Math.random() * promptsToUse.length)];
230
231 messageBubble.textContent = randomPrompt;
232 messageBubble.style.display = 'block';
233
234 // Animate the avatar
235 const avatar = document.getElementById('avatar-circle');
236 avatar.style.animation = 'bounce 0.5s ease';
237
238 console.log('Showing and speaking prompt:', randomPrompt);
239
240 // Text-to-speech
241 if (textToSpeechEnabled) {
242 speakPrompt(randomPrompt);
243 }
244
245 // Hide the message after 10 seconds
246 setTimeout(() => {
247 messageBubble.style.display = 'none';
248 avatar.style.animation = '';
249 isShowingPrompt = false;
250 console.log('Prompt hidden, ready for next prompt');
251 }, 10000);
252 }
253
254 // Text-to-speech function
255 function speakPrompt(text) {
256 try {
257 // Cancel any ongoing speech
258 window.speechSynthesis.cancel();
259
260 // Remove all emojis and special characters for better speech
261 const cleanText = text.replace(/[\u{1F000}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim();
262
263 const utterance = new SpeechSynthesisUtterance(cleanText);
264
265 // Get voices every time to ensure they're loaded
266 const voices = window.speechSynthesis.getVoices();
267
268 // Use user-selected voice if available
269 if (selectedVoiceName) {
270 const userVoice = voices.find(voice => voice.name === selectedVoiceName);
271 if (userVoice) {
272 utterance.voice = userVoice;
273 console.log('Using user-selected voice:', userVoice.name);
274 } else {
275 console.log('User-selected voice not found, falling back to female voice');
276 }
277 }
278
279 // If no user voice or user voice not found, use female voice
280 if (!utterance.voice) {
281 if (!selectedFemaleVoice || !voices.includes(selectedFemaleVoice)) {
282 // Re-find female voice if not cached or not in current voices list
283 selectedFemaleVoice = voices.find(voice =>
284 voice.name.includes('Google US English Female') ||
285 voice.name.includes('Microsoft Zira') ||
286 voice.name.includes('Samantha') ||
287 voice.name.includes('Victoria') ||
288 voice.name.includes('Karen') ||
289 voice.name.includes('Moira') ||
290 voice.name.includes('Fiona') ||
291 voice.name.includes('Female') ||
292 voice.name.includes('female')
293 );
294 }
295
296 if (selectedFemaleVoice) {
297 utterance.voice = selectedFemaleVoice;
298 console.log('Using female voice:', selectedFemaleVoice.name);
299 } else {
300 console.log('No female voice found, using system default');
301 }
302 }
303
304 utterance.rate = 0.9; // Slightly slower for clarity
305 utterance.pitch = 1.0;
306 utterance.volume = 1.0;
307
308 // Add event listeners for debugging
309 utterance.onstart = () => {
310 console.log('Speech started');
311 };
312
313 utterance.onend = () => {
314 console.log('Speech ended');
315 };
316
317 utterance.onerror = (event) => {
318 console.error('Speech error:', event.error, event);
319 };
320
321 window.speechSynthesis.speak(utterance);
322 console.log('TTS speaking:', cleanText);
323 } catch (error) {
324 console.error('Text-to-speech error:', error);
325 }
326 }
327
328 // Create settings panel
329 function showSettingsPanel() {
330 // Remove existing panel if any
331 const existingPanel = document.getElementById('focus-settings-panel');
332 if (existingPanel) {
333 existingPanel.remove();
334 return;
335 }
336
337 const panel = document.createElement('div');
338 panel.id = 'focus-settings-panel';
339 panel.style.cssText = `
340 position: fixed;
341 bottom: 90px;
342 right: 20px;
343 background: white;
344 color: #333;
345 padding: 20px;
346 border-radius: 10px;
347 box-shadow: 0 4px 20px rgba(0,0,0,0.3);
348 z-index: 999998;
349 min-width: 280px;
350 max-height: 500px;
351 overflow-y: auto;
352 border: 2px solid #667eea;
353 `;
354
355 // Build custom prompts list HTML
356 const promptsToDisplay = customPrompts.length > 0 ? customPrompts : prompts;
357 const promptsListHTML = promptsToDisplay.map((prompt, index) => `
358 <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; padding: 8px; background: #f5f5f5; border-radius: 5px;">
359 <span style="flex: 1; font-size: 14px;">${prompt}</span>
360 ${customPrompts.length > 0 ? `<button class="delete-prompt-btn" data-index="${index}" style="
361 background: #dc3545;
362 color: white;
363 border: none;
364 border-radius: 3px;
365 padding: 4px 8px;
366 font-size: 12px;
367 cursor: pointer;
368 ">Delete</button>` : ''}
369 </div>
370 `).join('');
371
372 // Build voice options
373 const voices = window.speechSynthesis.getVoices();
374 const voiceOptionsHTML = voices.map(voice =>
375 `<option value="${voice.name}" ${selectedVoiceName === voice.name ? 'selected' : ''}>${voice.name} (${voice.lang})</option>`
376 ).join('');
377
378 panel.innerHTML = `
379 <h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 18px;">βοΈ Reminder Settings</h3>
380
381 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
382 Custom Avatar Image:
383 </label>
384 <input type="file" id="avatar-upload" accept="image/*"
385 style="width: 100%; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 14px; margin-bottom: 10px;">
386 <button id="reset-avatar-btn" style="
387 width: 100%;
388 padding: 8px;
389 background: #dc3545;
390 color: white;
391 border: none;
392 border-radius: 5px;
393 font-size: 14px;
394 font-weight: 600;
395 cursor: pointer;
396 margin-bottom: 15px;
397 ">Reset to Default Avatar</button>
398
399 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
400 Avatar Vertical Position:
401 </label>
402 <input type="range" id="avatar-position-slider" min="-20" max="40" step="1" value="${avatarVerticalPosition}"
403 style="width: 100%; margin-bottom: 5px;">
404 <div style="text-align: center; font-size: 12px; color: #666; margin-bottom: 15px;">
405 Position: <span id="position-value">${avatarVerticalPosition}</span>px
406 </div>
407
408 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
409 <input type="checkbox" id="tts-toggle" ${textToSpeechEnabled ? 'checked' : ''}
410 style="margin-right: 8px; width: 18px; height: 18px; vertical-align: middle; cursor: pointer;">
411 Enable Text-to-Speech
412 </label>
413
414 <label style="display: block; margin-bottom: 10px; font-weight: 600; color: #333;">
415 Voice Selection:
416 </label>
417 <select id="voice-select" style="width: 100%; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 14px; margin-bottom: 15px;">
418 <option value="">Default (Auto-select Female)</option>
419 ${voiceOptionsHTML}
420 </select>
421
422 <label style="display: block; margin-bottom: 10px; margin-top: 15px; font-weight: 600; color: #333;">
423 Reminder Interval (minutes):
424 </label>
425 <input type="number" id="interval-input" min="0.5" max="60" step="0.5" value="${currentIntervalMinutes}"
426 style="width: 100%; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 16px; margin-bottom: 15px;">
427
428 <label style="display: block; margin-bottom: 10px; margin-top: 15px; font-weight: 600; color: #333;">
429 Custom Reminder Messages:
430 </label>
431 <div id="prompts-list" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto;">
432 ${promptsListHTML}
433 </div>
434 <div style="display: flex; gap: 8px; margin-bottom: 15px;">
435 <input type="text" id="new-prompt-input" placeholder="Add new reminder message..."
436 style="flex: 1; padding: 8px; border: 2px solid #667eea; border-radius: 5px; font-size: 14px;">
437 <button id="add-prompt-btn" style="
438 padding: 8px 15px;
439 background: #28a745;
440 color: white;
441 border: none;
442 border-radius: 5px;
443 font-size: 14px;
444 font-weight: 600;
445 cursor: pointer;
446 ">Add</button>
447 </div>
448 ${customPrompts.length > 0 ? `<button id="reset-prompts-btn" style="
449 width: 100%;
450 padding: 8px;
451 background: #ffc107;
452 color: #333;
453 border: none;
454 border-radius: 5px;
455 font-size: 14px;
456 font-weight: 600;
457 cursor: pointer;
458 margin-bottom: 15px;
459 ">Reset to Default Messages</button>` : ''}
460
461 <button id="save-interval-btn" style="
462 width: 100%;
463 padding: 10px;
464 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
465 color: white;
466 border: none;
467 border-radius: 5px;
468 font-size: 16px;
469 font-weight: 600;
470 cursor: pointer;
471 margin-bottom: 10px;
472 ">Save Changes</button>
473 <button id="test-prompt-btn" style="
474 width: 100%;
475 padding: 10px;
476 background: #28a745;
477 color: white;
478 border: none;
479 border-radius: 5px;
480 font-size: 16px;
481 font-weight: 600;
482 cursor: pointer;
483 ">Test Prompt Now</button>
484 <p style="margin: 15px 0 0 0; font-size: 12px; color: #666;">
485 Current: Every ${currentIntervalMinutes} minute(s)
486 </p>
487 `;
488
489 document.body.appendChild(panel);
490
491 // Voice selection handler
492 document.getElementById('voice-select').addEventListener('change', async (e) => {
493 selectedVoiceName = e.target.value || null;
494 await GM.setValue('selectedVoiceName', selectedVoiceName);
495 console.log('Voice changed to:', selectedVoiceName || 'Default (Auto-select Female)');
496 });
497
498 // Add prompt button handler
499 document.getElementById('add-prompt-btn').addEventListener('click', async () => {
500 const input = document.getElementById('new-prompt-input');
501 const newPrompt = input.value.trim();
502 if (newPrompt) {
503 customPrompts.push(newPrompt);
504 await GM.setValue('customPrompts', customPrompts);
505 input.value = '';
506 console.log('Added new prompt:', newPrompt);
507 // Refresh the panel
508 panel.remove();
509 showSettingsPanel();
510 }
511 });
512
513 // Delete prompt button handlers
514 document.querySelectorAll('.delete-prompt-btn').forEach(btn => {
515 btn.addEventListener('click', async (e) => {
516 const index = parseInt(e.target.getAttribute('data-index'));
517 customPrompts.splice(index, 1);
518 await GM.setValue('customPrompts', customPrompts);
519 console.log('Deleted prompt at index:', index);
520 // Refresh the panel
521 panel.remove();
522 showSettingsPanel();
523 });
524 });
525
526 // Reset prompts button handler
527 const resetPromptsBtn = document.getElementById('reset-prompts-btn');
528 if (resetPromptsBtn) {
529 resetPromptsBtn.addEventListener('click', async () => {
530 customPrompts = [];
531 await GM.deleteValue('customPrompts');
532 console.log('Reset to default prompts');
533 // Refresh the panel
534 panel.remove();
535 showSettingsPanel();
536 });
537 }
538
539 // Avatar position slider handler
540 const positionSlider = document.getElementById('avatar-position-slider');
541 const positionValue = document.getElementById('position-value');
542 positionSlider.addEventListener('input', (e) => {
543 avatarVerticalPosition = parseInt(e.target.value);
544 positionValue.textContent = avatarVerticalPosition;
545 updateAvatarDisplay();
546 });
547
548 // Avatar upload handler
549 const avatarUpload = document.getElementById('avatar-upload');
550 avatarUpload.addEventListener('change', async (e) => {
551 const file = e.target.files[0];
552 if (file && file.type.startsWith('image/')) {
553 const reader = new FileReader();
554 reader.onload = async (event) => {
555 customAvatarImage = event.target.result;
556 await GM.setValue('customAvatarImage', customAvatarImage);
557 updateAvatarDisplay();
558 console.log('Custom avatar uploaded successfully');
559 alert('Avatar uploaded successfully! Now adjust the position and click Save Changes.');
560 };
561 reader.onerror = (error) => {
562 console.error('Error reading file:', error);
563 alert('Error uploading avatar. Please try again.');
564 };
565 reader.readAsDataURL(file);
566 } else {
567 alert('Please select a valid image file.');
568 }
569 });
570
571 // Reset avatar handler
572 document.getElementById('reset-avatar-btn').addEventListener('click', async () => {
573 customAvatarImage = null;
574 await GM.deleteValue('customAvatarImage');
575 updateAvatarDisplay();
576 console.log('Avatar reset to default');
577 alert('Avatar reset to default!');
578 });
579
580 // Text-to-speech toggle handler
581 document.getElementById('tts-toggle').addEventListener('change', async (e) => {
582 textToSpeechEnabled = e.target.checked;
583 await GM.setValue('textToSpeechEnabled', textToSpeechEnabled);
584 console.log('Text-to-speech:', textToSpeechEnabled ? 'enabled' : 'disabled');
585 });
586
587 // Save button handler
588 document.getElementById('save-interval-btn').addEventListener('click', async () => {
589 const newInterval = parseFloat(document.getElementById('interval-input').value);
590 if (newInterval > 0) {
591 currentIntervalMinutes = newInterval;
592 await GM.setValue('focusReminderInterval', currentIntervalMinutes);
593 await GM.setValue('avatarVerticalPosition', avatarVerticalPosition);
594 await GM.setValue('selectedVoiceName', selectedVoiceName);
595 startReminderInterval();
596 panel.remove();
597 console.log('Interval updated to:', currentIntervalMinutes, 'minutes');
598 console.log('Avatar position saved:', avatarVerticalPosition, 'px');
599 console.log('Voice saved:', selectedVoiceName || 'Default');
600 }
601 });
602
603 // Test button handler
604 document.getElementById('test-prompt-btn').addEventListener('click', () => {
605 showPrompt();
606 panel.remove();
607 });
608
609 // Close panel when clicking outside
610 setTimeout(() => {
611 document.addEventListener('click', function closePanel(e) {
612 if (!panel.contains(e.target) && !document.getElementById('avatar-circle').contains(e.target)) {
613 panel.remove();
614 document.removeEventListener('click', closePanel);
615 }
616 });
617 }, 100);
618 }
619
620 // Start the reminder interval
621 function startReminderInterval() {
622 // Clear existing interval
623 if (reminderInterval) {
624 clearInterval(reminderInterval);
625 }
626
627 // Convert minutes to milliseconds
628 const intervalMs = currentIntervalMinutes * 60 * 1000;
629
630 // Set new interval
631 reminderInterval = setInterval(() => {
632 showPrompt();
633 }, intervalMs);
634
635 console.log('Reminder interval started:', currentIntervalMinutes, 'minutes');
636 }
637
638 // Add CSS animations
639 function addStyles() {
640 const style = document.createElement('style');
641 style.textContent = `
642 @keyframes bounce {
643 0%, 100% { transform: translateY(0); }
644 50% { transform: translateY(-10px); }
645 }
646 `;
647 document.head.appendChild(style);
648 }
649
650 // Initialize the extension
651 async function init() {
652 if (isInitialized) {
653 console.log('Extension already initialized, skipping...');
654 return;
655 }
656 isInitialized = true;
657
658 console.log('Student Focus Reminder Avatar initializing...');
659
660 // Check if avatar already exists and remove it
661 const existingAvatar = document.getElementById('focus-reminder-avatar');
662 if (existingAvatar) {
663 console.log('Removing existing avatar');
664 existingAvatar.remove();
665 }
666
667 // Load saved interval
668 const savedInterval = await GM.getValue('focusReminderInterval', 2);
669 currentIntervalMinutes = savedInterval;
670
671 // Load saved custom avatar
672 const savedAvatar = await GM.getValue('customAvatarImage', null);
673 customAvatarImage = savedAvatar;
674
675 // Load text-to-speech preference
676 const savedTTS = await GM.getValue('textToSpeechEnabled', true);
677 textToSpeechEnabled = savedTTS;
678
679 // Load saved avatar position
680 const savedPosition = await GM.getValue('avatarVerticalPosition', 15);
681 avatarVerticalPosition = savedPosition;
682
683 // Load custom prompts
684 const savedPrompts = await GM.getValue('customPrompts', []);
685 customPrompts = savedPrompts;
686
687 // Load selected voice name
688 const savedVoiceName = await GM.getValue('selectedVoiceName', null);
689 selectedVoiceName = savedVoiceName;
690
691 // Load paused state
692 const savedPausedState = await GM.getValue('isPaused', false);
693 isPaused = savedPausedState;
694
695 // Initialize voices for text-to-speech
696 if ('speechSynthesis' in window) {
697 // Load voices immediately if available
698 if (window.speechSynthesis.getVoices().length > 0) {
699 const voices = window.speechSynthesis.getVoices();
700 selectedFemaleVoice = voices.find(voice =>
701 voice.name.includes('Google US English Female') ||
702 voice.name.includes('Microsoft Zira') ||
703 voice.name.includes('Samantha') ||
704 voice.name.includes('Victoria') ||
705 voice.name.includes('Karen') ||
706 voice.name.includes('Moira') ||
707 voice.name.includes('Fiona') ||
708 voice.name.includes('Female') ||
709 voice.name.includes('female')
710 );
711 if (selectedFemaleVoice) {
712 console.log('Female voice selected:', selectedFemaleVoice.name);
713 }
714 }
715
716 // Also listen for voiceschanged event
717 window.speechSynthesis.addEventListener('voiceschanged', () => {
718 const voices = window.speechSynthesis.getVoices();
719 selectedFemaleVoice = voices.find(voice =>
720 voice.name.includes('Google US English Female') ||
721 voice.name.includes('Microsoft Zira') ||
722 voice.name.includes('Samantha') ||
723 voice.name.includes('Victoria') ||
724 voice.name.includes('Karen') ||
725 voice.name.includes('Moira') ||
726 voice.name.includes('Fiona') ||
727 voice.name.includes('Female') ||
728 voice.name.includes('female')
729 );
730 if (selectedFemaleVoice) {
731 console.log('Female voice selected after voiceschanged:', selectedFemaleVoice.name);
732 }
733 });
734 }
735
736 addStyles();
737 createAvatar();
738
739 // Only start interval if not paused
740 if (!isPaused) {
741 startReminderInterval();
742
743 // Show initial prompt after 5 seconds
744 setTimeout(() => {
745 showPrompt();
746 }, 5000);
747 } else {
748 console.log('Extension is paused, not starting interval');
749 }
750
751 console.log('Student Focus Reminder Avatar initialized successfully');
752 }
753
754 // Wait for page to load
755 if (document.readyState === 'loading') {
756 document.addEventListener('DOMContentLoaded', init);
757 } else {
758 init();
759 }
760})();