Skip to content

Instantly share code, notes, and snippets.

@htlin222
Created May 14, 2025 14:48
Show Gist options
  • Save htlin222/9861e4747313657598535f76ef4cb09b to your computer and use it in GitHub Desktop.
Save htlin222/9861e4747313657598535f76ef4cb09b to your computer and use it in GitHub Desktop.
Google Slides Custom Progress Bar Generator
function onOpen() {
const ui = SlidesApp.getUi();
ui.createMenu('自定功能')
.addItem('⏳ Reload ProgressBars', 'updateProgressBars')
.addToUi();
}
function updateProgressBars() {
const presentation = SlidesApp.getActivePresentation();
const slides = presentation.getSlides();
const totalSlides = slides.length;
const maxWidth = 720;
const yPosition = 402;
const height = 3;
for (let i = 1; i < totalSlides; i++) { // Start from 2nd slide
const slide = slides[i];
// 1. Remove old progress bar if exists
const shapes = slide.getShapes();
for (let shape of shapes) {
if (shape.getTitle && shape.getTitle() === 'PROGRESS') {
shape.remove();
}
}
// 2. Calculate new width
const progressRatio = i / ( totalSlides - 1);
const barWidth = maxWidth * progressRatio;
// 3. Insert new progress bar
const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 0, yPosition, barWidth, height);
bar.getFill().setSolidFill('#000000');
bar.getBorder().setTransparent();
// 4. Tag the shape for future identification
bar.setTitle('PROGRESS');
}
}
@raywentsai
Copy link

Brilliant work! This is a fantastic addition to the already excellent "NEJM模版2020新版" — thank you so much for sharing it.
I’ve put together a modified version that omits skipped slides in the progress bar and adds a slide number feature as well.
P.S. The Apps Script is almost entirely "vibe-coded," so the style might not be the cleanest 😄

function onOpen() {
  const ui = SlidesApp.getUi();
  ui.createMenu('自定功能')
    .addItem('⏳ Reload ProgressBars', 'updateProgressBars')
    .addItem('🔢 Update SlideNumbers', 'addSlideNumbersSkippingSkipped')
    .addItem('✅ Update Both', 'updateAll')
    .addToUi();
}

function updateProgressBars() {
  const presentation = SlidesApp.getActivePresentation();
  const slides = presentation.getSlides();

  // Build list of non-skipped slide indexes (excluding 1st slide)
  const nonSkippedIndexes = slides
    .map((slide, index) => ({ slide, index }))
    .filter(({ slide, index }) => index > 0 && !slide.isSkipped())
    .map(({ index }) => index);

  const totalNonSkipped = nonSkippedIndexes.length;

  const maxWidth = 720;
  const yPosition = 402;
  const height = 3;

  if (totalNonSkipped <= 1) return; // nothing to show progress for

  for (let i = 1; i < slides.length; i++) { // Start from 2nd slide
    const slide = slides[i];

    // Remove old progress bar if exists
    const shapes = slide.getShapes();
    for (let shape of shapes) {
      if (shape.getTitle && shape.getTitle() === 'PROGRESS') {
        shape.remove();
      }
    }

    // Skip if slide is marked as skipped
    if (slide.isSkipped()) continue;

    // Find progress position in the non-skipped slide sequence
    const progressPosition = nonSkippedIndexes.indexOf(i) + 1; // progressPosition starts from 0
    const progressRatio = progressPosition / (totalNonSkipped); // totalNonSkipped excluded 1st slide
    const barWidth = maxWidth * progressRatio;

    const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 0, yPosition, barWidth, height);
    bar.getFill().setSolidFill('#000000');
    bar.getBorder().setTransparent();
    
    bar.setTitle('PROGRESS');
  }
}

function addSlideNumbersSkippingSkipped() {
  const presentation = SlidesApp.getActivePresentation();
  const slides = presentation.getSlides();

  // Get indexes of non-skipped slides (excluding title slide)
  const nonSkippedIndexes = slides
    .map((slide, index) => ({ slide, index }))
    .filter(({ slide, index }) => index > 0 && !slide.isSkipped())
    .map(({ index }) => index);

  const fontSize = 18;
  const fontFamily = "Cambria";
  const x = 670;
  const y = 365;

  // Clear existing slide numbers
  slides.forEach(slide => {
    slide.getShapes().forEach(shape => {
      if (shape.getTitle?.() === 'SLIDE_NUMBER') {
        shape.remove();
      }
    });
  });

  // Insert new slide numbers based on non-skipped order
  nonSkippedIndexes.forEach((slideIndex, i) => {
    const slide = slides[slideIndex];
    const shape = slide.insertTextBox(`${i + 1}`, x, y, 40, 30);
    shape.getText().getTextStyle().setFontSize(fontSize);
    shape.getText().getTextStyle().setFontFamily(fontFamily);
    shape.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.END);
    shape.setTitle('SLIDE_NUMBER');
  });
}

function updateAll() {
  updateProgressBars();
  addSlideNumbersSkippingSkipped();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment