Timer extension for Google Slides - Presentation Timer

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})();
Timer extension for Google Slides - Presentation Timer | Robomonkey