Created
July 30, 2026 00:37
-
-
Save wecsam/e3a05e2b23157863028181171fd7049d to your computer and use it in GitHub Desktop.
A Google Slides extension that adds numbers to a slideshow of pictures and shuffles the pictures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Creates a custom menu in Google Slides when the presentation is opened. | |
| * | |
| * @returns {void} | |
| */ | |
| function onOpen() { | |
| SlidesApp.getUi() | |
| .createMenu('Picture Tools') | |
| .addItem('Select and Shuffle Pictures', 'shufflePictureSlides') | |
| .addItem('Auto-Number Picture Slides', 'autoNumberSlides') | |
| .addToUi(); | |
| } | |
| /** | |
| * Starts from the current slide and adds speaker notes and a styled numbering shape | |
| * to every slide until the end of the presentation. | |
| * | |
| * @returns {void} | |
| */ | |
| function autoNumberSlides() { | |
| const presentation = SlidesApp.getActivePresentation(); | |
| const ui = SlidesApp.getUi(); | |
| const selection = presentation.getSelection(); | |
| // Ensure a slide is selected | |
| if (!selection) { | |
| ui.alert('Please select a slide to start from.'); | |
| return; | |
| } | |
| const currentPage = selection.getCurrentPage(); | |
| if (!currentPage) { | |
| ui.alert('Please select a specific slide in the left sidebar to start from.'); | |
| return; | |
| } | |
| const slides = presentation.getSlides(); | |
| let startingIndex = -1; | |
| // Find the array index of the currently selected slide | |
| for (let i = 0; i < slides.length; i++) { | |
| if (slides[i].getObjectId() === currentPage.getObjectId()) { | |
| startingIndex = i; | |
| break; | |
| } | |
| } | |
| if (startingIndex === -1) { | |
| ui.alert('Could not determine the current slide.'); | |
| return; | |
| } | |
| let picNumber = 101; | |
| for (let i = startingIndex; i < slides.length; i++) { | |
| const slide = slides[i]; | |
| updateSpeakerNotes(slide, picNumber); | |
| addPictureNumberShape(slide, picNumber); | |
| picNumber++; | |
| } | |
| } | |
| /** | |
| * Updates the speaker notes of a slide to include the picture number. | |
| * | |
| * @param {GoogleAppsScript.Slides.Slide} slide - The slide to update. | |
| * @param {number} picNumber - The picture number to append. | |
| * @returns {void} | |
| */ | |
| function updateSpeakerNotes(slide, picNumber) { | |
| const notesShape = slide.getNotesPage().getSpeakerNotesShape(); | |
| let currentNotes = notesShape.getText().asString().trim(); | |
| // Remove any existing "Pic: X" tags so we don't duplicate them | |
| currentNotes = currentNotes.replace(/Pic:\s*\d+/gi, '').trim(); | |
| const newNote = `Pic: ${picNumber}`; | |
| const finalNotes = currentNotes.length > 0 ? currentNotes + '\n\n' + newNote : newNote; | |
| notesShape.getText().setText(finalNotes); | |
| } | |
| /** | |
| * Inserts a styled rounded rectangle with the picture number onto a slide. | |
| * | |
| * @param {GoogleAppsScript.Slides.Slide} slide - The slide to update. | |
| * @param {number} picNumber - The number to display in the shape. | |
| * @returns {void} | |
| */ | |
| function addPictureNumberShape(slide, picNumber) { | |
| const presentation = SlidesApp.getActivePresentation(); | |
| const pageHeight = presentation.getPageHeight(); | |
| // Shape dimensions: 2.5 inches (180pt) by 1.5 inches (108pt) | |
| const shapeWidth = 180; | |
| const shapeHeight = 108; | |
| const margin = 15; | |
| const leftPos = margin; | |
| const topPos = pageHeight - shapeHeight - margin; | |
| // Insert Rounded Rectangle | |
| const shape = slide.insertShape( | |
| SlidesApp.ShapeType.ROUND_RECTANGLE, | |
| leftPos, | |
| topPos, | |
| shapeWidth, | |
| shapeHeight | |
| ); | |
| // Format the Shape | |
| // "Red berry" hex color is typically #980000 | |
| shape.getFill().setSolidFill('#980000'); | |
| shape.getBorder().getLineFill().setSolidFill('#000000'); // Black border | |
| shape.getBorder().setWeight(4); // 4px border | |
| // Add and Format Text | |
| const textRange = shape.getText(); | |
| textRange.setText(picNumber.toString()); | |
| const textStyle = textRange.getTextStyle(); | |
| textStyle.setForegroundColor('#FFFFFF'); // White text | |
| textStyle.setFontFamily('Arial'); | |
| textStyle.setFontSize(72); | |
| textStyle.setBold(false); | |
| // Center the text horizontally and vertically | |
| textRange.getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); | |
| shape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); | |
| } | |
| /** | |
| * Main orchestrator function: gets input, processes slides, and shuffles them. | |
| * | |
| * @returns {void} | |
| */ | |
| function shufflePictureSlides() { | |
| const presentation = SlidesApp.getActivePresentation(); | |
| const ui = SlidesApp.getUi(); | |
| // 1. Get user input directly as a Set for O(1) lookups | |
| const selectedNumbers = getUserInput(ui); | |
| if (!selectedNumbers || selectedNumbers.size === 0) { | |
| return; // Exit silently if canceled or empty | |
| } | |
| // 2. Build a map of picNumber -> original slide index | |
| const picSlideMap = getPictureSlideMap(presentation); | |
| if (picSlideMap.size === 0) { | |
| ui.alert('No picture slides found with "Pic: X" tags in the speaker notes.'); | |
| return; | |
| } | |
| // 3. Compute the starting index for the picture slides | |
| const indexOfFirstPictureSlide = Math.min(...Array.from(picSlideMap.values())); | |
| // 4. Segregate and toggle visibility | |
| const slides = presentation.getSlides(); | |
| const selectedSlides = []; | |
| const unselectedSlides = []; | |
| picSlideMap.forEach((slideIndex, picNumber) => { | |
| const slide = slides[slideIndex]; | |
| if (selectedNumbers.has(picNumber)) { | |
| slide.setSkipped(false); | |
| selectedSlides.push(slide); | |
| } else { | |
| slide.setSkipped(true); | |
| unselectedSlides.push(slide); | |
| } | |
| }); | |
| if (selectedSlides.length === 0) { | |
| ui.alert('None of the entered numbers matched the available picture slides.'); | |
| return; | |
| } | |
| // 5. Shuffle the selected slides only | |
| shuffleArray(selectedSlides); | |
| // 6. Combine arrays: selected (shuffled) come before unselected (hidden) | |
| const orderedSlides = [...selectedSlides, ...unselectedSlides]; | |
| // 7. Move all picture slides to their new sequential positions | |
| reorderSlides(orderedSlides, indexOfFirstPictureSlide); | |
| } | |
| /** | |
| * Prompts the user and returns a Set of parsed numbers. | |
| * | |
| * @param {GoogleAppsScript.Base.Ui} ui - The Google Slides UI object used to prompt the user. | |
| * @returns {Set<number>|null} A Set containing the parsed picture numbers, or null if the user canceled the prompt. | |
| */ | |
| function getUserInput(ui) { | |
| const response = ui.prompt( | |
| 'Select Pictures', | |
| 'Enter the picture numbers you want to show (separated by spaces, commas, or semicolons):', | |
| ui.ButtonSet.OK_CANCEL | |
| ); | |
| if (response.getSelectedButton() !== ui.Button.OK) { | |
| return null; | |
| } | |
| const parsedNumbers = response.getResponseText() | |
| .split(/[\s,;]+/g) | |
| .map(s => parseInt(s, 10)) | |
| .filter(n => !isNaN(n)); | |
| return new Set(parsedNumbers); | |
| } | |
| /** | |
| * Scans the presentation and returns a Map linking picture numbers to their slide indices. | |
| * | |
| * @param {GoogleAppsScript.Slides.Presentation} presentation - The active Google Slides presentation. | |
| * @returns {Map<number, number>} A Map where the key is the picture number (extracted from speaker notes) and the value is the slide's original index. | |
| */ | |
| function getPictureSlideMap(presentation) { | |
| const slides = presentation.getSlides(); | |
| const picMap = new Map(); | |
| for (let i = 0; i < slides.length; i++) { | |
| const notes = slides[i].getNotesPage().getSpeakerNotesShape().getText().asString().trim(); | |
| const match = notes.match(/^Pic:\s*(\d+)/i); | |
| if (match) { | |
| const picNumber = parseInt(match[1], 10); | |
| picMap.set(picNumber, i); | |
| } | |
| } | |
| return picMap; | |
| } | |
| /** | |
| * Moves an array of slides to a sequential block starting at a target index. | |
| * | |
| * @param {GoogleAppsScript.Slides.Slide[]} slidesToMove - An array of Slide objects to be moved. | |
| * @param {number} startIndex - The target index where the first slide in the array should be placed. | |
| * @returns {void} | |
| */ | |
| function reorderSlides(slidesToMove, startIndex) { | |
| for (let j = 0; j < slidesToMove.length; j++) { | |
| slidesToMove[j].move(startIndex + j); | |
| } | |
| } | |
| /** | |
| * Helper function to randomize an array in place (Fisher-Yates shuffle). | |
| * | |
| * @param {Array<any>} array - The array to shuffle. The array is modified in place. | |
| * @returns {void} | |
| */ | |
| function shuffleArray(array) { | |
| for (let i = array.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| const temp = array[i]; | |
| array[i] = array[j]; | |
| array[j] = temp; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment