Add a countdown timer to your Google Slides presentations
Size
14.8 KB
Version
1.1.2
Created
Oct 22, 2025
Updated
1 day ago
1// ==UserScript==
2// @name Timer extension for Google Slides - Presentation Timer
3// @description Add a countdown timer to your Google Slides presentations
4// @version 1.1.2
5// @match *://docs.google.com/presentation/*
6// @icon https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Google Slides Presentation Timer loaded');
12
13 // Add styles for the timer
14 TM_addStyle(`
15 #slides-timer-container {
16 position: fixed;
17 top: 60px;
18 right: 20px;
19 background: white;
20 border: 1px solid #dadce0;
21 border-radius: 8px;
22 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
23 z-index: 999999;
24 font-family: "Google Sans", Roboto, sans-serif;
25 color: #202124;
26 padding: 20px;
27 min-width: 280px;
28 }
29
30 #slides-timer-header {
31 display: flex;
32 justify-content: space-between;
33 align-items: center;
34 margin-bottom: 15px;
35 padding-bottom: 12px;
36 border-bottom: 1px solid #e8eaed;
37 }
38
39 #slides-timer-header h3 {
40 margin: 0;
41 font-size: 16px;
42 font-weight: 500;
43 color: #202124;
44 }
45
46 #slides-timer-close {
47 background: transparent;
48 color: #5f6368;
49 border: none;
50 padding: 4px 8px;
51 border-radius: 4px;
52 cursor: pointer;
53 font-size: 18px;
54 transition: background 0.2s;
55 }
56
57 #slides-timer-close:hover {
58 background: #f1f3f4;
59 }
60
61 #slides-timer-display {
62 font-size: 48px;
63 font-weight: 400;
64 text-align: center;
65 margin: 20px 0;
66 font-family: 'Roboto Mono', monospace;
67 color: #202124;
68 }
69
70 .timer-input-group {
71 margin-bottom: 15px;
72 }
73
74 .timer-input-group label {
75 display: block;
76 margin-bottom: 8px;
77 font-size: 13px;
78 font-weight: 500;
79 color: #5f6368;
80 }
81
82 .timer-inputs {
83 display: flex;
84 gap: 10px;
85 justify-content: center;
86 }
87
88 .timer-input-wrapper {
89 display: flex;
90 flex-direction: column;
91 align-items: center;
92 }
93
94 .timer-input-wrapper input {
95 width: 60px;
96 padding: 8px;
97 border: 1px solid #dadce0;
98 border-radius: 4px;
99 font-size: 16px;
100 text-align: center;
101 background: white;
102 color: #202124;
103 font-weight: 500;
104 }
105
106 .timer-input-wrapper input:focus {
107 outline: none;
108 border-color: #1a73e8;
109 box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1);
110 }
111
112 .timer-input-wrapper span {
113 font-size: 11px;
114 margin-top: 4px;
115 color: #5f6368;
116 }
117
118 .timer-buttons {
119 display: flex;
120 gap: 8px;
121 margin-top: 15px;
122 }
123
124 .timer-btn {
125 flex: 1;
126 padding: 8px 16px;
127 border: 1px solid #dadce0;
128 border-radius: 4px;
129 cursor: pointer;
130 font-size: 14px;
131 font-weight: 500;
132 transition: all 0.2s;
133 background: white;
134 color: #202124;
135 }
136
137 #slides-timer-start {
138 background: #1a73e8;
139 color: white;
140 border-color: #1a73e8;
141 }
142
143 #slides-timer-start:hover {
144 background: #1765cc;
145 border-color: #1765cc;
146 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
147 }
148
149 #slides-timer-pause:hover {
150 background: #f8f9fa;
151 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
152 }
153
154 #slides-timer-reset:hover {
155 background: #f8f9fa;
156 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
157 }
158
159 #slides-timer-toggle-btn {
160 display: inline-flex;
161 align-items: center;
162 gap: 6px;
163 background: transparent;
164 color: #5f6368;
165 border: none;
166 padding: 6px 12px;
167 border-radius: 4px;
168 cursor: pointer;
169 font-size: 14px;
170 font-weight: 500;
171 font-family: "Google Sans", Roboto, sans-serif;
172 transition: background 0.2s;
173 height: 28px;
174 margin-left: 8px;
175 }
176
177 #slides-timer-toggle-btn:hover {
178 background: #f1f3f4;
179 }
180
181 .timer-expired {
182 animation: pulse 1s infinite;
183 }
184
185 @keyframes pulse {
186 0%, 100% {
187 opacity: 1;
188 }
189 50% {
190 opacity: 0.5;
191 }
192 }
193
194 .timer-warning {
195 color: #ea8600 !important;
196 }
197
198 .timer-danger {
199 color: #d93025 !important;
200 }
201 `);
202
203 let timerInterval = null;
204 let remainingSeconds = 0;
205 let isPaused = true;
206 let initialSeconds = 0;
207
208 async function init() {
209 createToggleButton();
210 createTimerPanel();
211 await loadTimerState();
212 setupEventListeners();
213 }
214
215 function createToggleButton() {
216 // Wait for toolbar to be available
217 const waitForToolbar = setInterval(() => {
218 const toolbar = document.querySelector('#docs-toolbar');
219 if (toolbar) {
220 clearInterval(waitForToolbar);
221
222 const toggleBtn = document.createElement('button');
223 toggleBtn.id = 'slides-timer-toggle-btn';
224 toggleBtn.innerHTML = '<span style="font-size: 16px;">⏱️</span> Timer';
225 toggleBtn.addEventListener('click', togglePanel);
226
227 // Insert button into the toolbar
228 toolbar.appendChild(toggleBtn);
229 console.log('Timer toggle button created in toolbar');
230 }
231 }, 100);
232 }
233
234 function createTimerPanel() {
235 const panel = document.createElement('div');
236 panel.id = 'slides-timer-container';
237 panel.style.display = 'none';
238
239 panel.innerHTML = `
240 <div id="slides-timer-header">
241 <h3>⏱️ Presentation Timer</h3>
242 <button id="slides-timer-close">✕</button>
243 </div>
244
245 <div id="slides-timer-display">00:00</div>
246
247 <div class="timer-input-group">
248 <label>Set Timer Duration:</label>
249 <div class="timer-inputs">
250 <div class="timer-input-wrapper">
251 <input type="number" id="timer-minutes" min="0" max="999" value="5" />
252 <span>Minutes</span>
253 </div>
254 <div class="timer-input-wrapper">
255 <input type="number" id="timer-seconds" min="0" max="59" value="0" />
256 <span>Seconds</span>
257 </div>
258 </div>
259 </div>
260
261 <div class="timer-buttons">
262 <button class="timer-btn" id="slides-timer-start">Start</button>
263 <button class="timer-btn" id="slides-timer-pause">Pause</button>
264 <button class="timer-btn" id="slides-timer-reset">Reset</button>
265 </div>
266 `;
267
268 document.body.appendChild(panel);
269 makeDraggable(panel);
270 console.log('Timer panel created');
271 }
272
273 function setupEventListeners() {
274 document.getElementById('slides-timer-close').addEventListener('click', togglePanel);
275 document.getElementById('slides-timer-start').addEventListener('click', startTimer);
276 document.getElementById('slides-timer-pause').addEventListener('click', pauseTimer);
277 document.getElementById('slides-timer-reset').addEventListener('click', resetTimer);
278
279 // Update timer when inputs change
280 document.getElementById('timer-minutes').addEventListener('change', updateTimerFromInputs);
281 document.getElementById('timer-seconds').addEventListener('change', updateTimerFromInputs);
282 }
283
284 function togglePanel() {
285 const panel = document.getElementById('slides-timer-container');
286 if (panel.style.display === 'none') {
287 panel.style.display = 'block';
288 console.log('Timer panel opened');
289 } else {
290 panel.style.display = 'none';
291 console.log('Timer panel closed');
292 }
293 }
294
295 function updateTimerFromInputs() {
296 if (isPaused) {
297 const minutes = parseInt(document.getElementById('timer-minutes').value) || 0;
298 const seconds = parseInt(document.getElementById('timer-seconds').value) || 0;
299 remainingSeconds = (minutes * 60) + seconds;
300 initialSeconds = remainingSeconds;
301 updateDisplay();
302 saveTimerState();
303 }
304 }
305
306 function startTimer() {
307 if (remainingSeconds === 0) {
308 updateTimerFromInputs();
309 }
310
311 if (remainingSeconds === 0) {
312 console.log('Timer is at 0, cannot start');
313 return;
314 }
315
316 isPaused = false;
317 console.log('Timer started');
318
319 if (timerInterval) {
320 clearInterval(timerInterval);
321 }
322
323 timerInterval = setInterval(() => {
324 if (!isPaused && remainingSeconds > 0) {
325 remainingSeconds--;
326 updateDisplay();
327 saveTimerState();
328
329 if (remainingSeconds === 0) {
330 onTimerExpired();
331 }
332 }
333 }, 1000);
334 }
335
336 function pauseTimer() {
337 isPaused = true;
338 if (timerInterval) {
339 clearInterval(timerInterval);
340 timerInterval = null;
341 }
342 saveTimerState();
343 console.log('Timer paused');
344 }
345
346 function resetTimer() {
347 isPaused = true;
348 if (timerInterval) {
349 clearInterval(timerInterval);
350 timerInterval = null;
351 }
352
353 const minutes = parseInt(document.getElementById('timer-minutes').value) || 0;
354 const seconds = parseInt(document.getElementById('timer-seconds').value) || 0;
355 remainingSeconds = (minutes * 60) + seconds;
356 initialSeconds = remainingSeconds;
357
358 updateDisplay();
359 saveTimerState();
360 console.log('Timer reset');
361 }
362
363 function updateDisplay() {
364 const display = document.getElementById('slides-timer-display');
365 const minutes = Math.floor(remainingSeconds / 60);
366 const seconds = remainingSeconds % 60;
367
368 const timeString = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
369 display.textContent = timeString;
370
371 // Remove all color classes
372 display.classList.remove('timer-warning', 'timer-danger', 'timer-expired');
373
374 // Add appropriate color based on time remaining
375 if (remainingSeconds === 0) {
376 display.classList.add('timer-expired');
377 } else if (remainingSeconds <= 60) {
378 display.classList.add('timer-danger');
379 } else if (remainingSeconds <= 300) {
380 display.classList.add('timer-warning');
381 }
382 }
383
384 function onTimerExpired() {
385 pauseTimer();
386 console.log('Timer expired!');
387
388 // Play notification sound (browser notification)
389 if (Notification.permission === 'granted') {
390 new Notification('⏱️ Timer Expired!', {
391 body: 'Your presentation timer has finished.',
392 icon: 'https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico'
393 });
394 } else if (Notification.permission !== 'denied') {
395 Notification.requestPermission().then(permission => {
396 if (permission === 'granted') {
397 new Notification('⏱️ Timer Expired!', {
398 body: 'Your presentation timer has finished.',
399 icon: 'https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico'
400 });
401 }
402 });
403 }
404
405 // Visual alert
406 alert('⏱️ Timer Expired! Your presentation time is up.');
407 }
408
409 async function saveTimerState() {
410 await GM.setValue('timer_remaining', remainingSeconds);
411 await GM.setValue('timer_initial', initialSeconds);
412 await GM.setValue('timer_paused', isPaused);
413 await GM.setValue('timer_minutes', document.getElementById('timer-minutes').value);
414 await GM.setValue('timer_seconds', document.getElementById('timer-seconds').value);
415 }
416
417 async function loadTimerState() {
418 remainingSeconds = await GM.getValue('timer_remaining', 300);
419 initialSeconds = await GM.getValue('timer_initial', 300);
420 isPaused = await GM.getValue('timer_paused', true);
421
422 const savedMinutes = await GM.getValue('timer_minutes', '5');
423 const savedSeconds = await GM.getValue('timer_seconds', '0');
424
425 document.getElementById('timer-minutes').value = savedMinutes;
426 document.getElementById('timer-seconds').value = savedSeconds;
427
428 updateDisplay();
429 console.log('Timer state loaded');
430 }
431
432 function makeDraggable(element) {
433 const header = element.querySelector('#slides-timer-header');
434 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
435
436 header.style.cursor = 'move';
437 header.onmousedown = dragMouseDown;
438
439 function dragMouseDown(e) {
440 e.preventDefault();
441 pos3 = e.clientX;
442 pos4 = e.clientY;
443 document.onmouseup = closeDragElement;
444 document.onmousemove = elementDrag;
445 }
446
447 function elementDrag(e) {
448 e.preventDefault();
449 pos1 = pos3 - e.clientX;
450 pos2 = pos4 - e.clientY;
451 pos3 = e.clientX;
452 pos4 = e.clientY;
453 element.style.top = (element.offsetTop - pos2) + 'px';
454 element.style.left = (element.offsetLeft - pos1) + 'px';
455 element.style.right = 'auto';
456 }
457
458 function closeDragElement() {
459 document.onmouseup = null;
460 document.onmousemove = null;
461 }
462 }
463
464 // Request notification permission on load
465 if (Notification.permission === 'default') {
466 Notification.requestPermission();
467 }
468
469 // Initialize when DOM is ready
470 if (document.body) {
471 init();
472 } else {
473 TM_runBody(init);
474 }
475
476})();