# Expanded Playback & Background Features — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add timeline scrubber bar, keyboard shortcuts, blur/dim overlay, and theme-derived backgrounds to PulseJT v3.4.

**Architecture:** Timeline scrubber is a new DOM element between `#drop` and `.bottom`. Keyboard shortcuts hook into the existing global keydown listener. Blur/dim and theme backgrounds extend the existing `applyBackground()` function and Background tab UI. All JS is ES5 inside the existing IIFE.

**Tech Stack:** HTML5, CSS3 (custom properties, filters, gradients), vanilla ES5 JavaScript, localStorage.

---

## Important Context

- CEP extension — no test framework, no build system. Verify visually.
- ALL JS must be ES5 (no let/const/arrow/template literals).
- IIFE wraps all code: `(function() { 'use strict'; ... })()`.
- Storage keys in `STORAGE_KEYS` object at `JS/bundle.js:~1114`.
- Settings logic in `initSettingsModal()` at `JS/bundle.js:~4396`.
- `applyBackground()` at `JS/bundle.js:~4607`.
- Video element created in `loadVideo()` at `JS/bundle.js:~2394`, class `gif`.
- Global keydown listener at `JS/bundle.js:~3952`.
- `#drop` is the preview container. `.bottom > .dock` is the toolbar below it.
- Mirror `index.html` -> `assets/index.html` and `style.css` -> `assets/style.css` at the end.
- Copy updated files to `C:\Users\tyson.DESKTOP-GI9SJ3L\AppData\Roaming\Adobe\CEP\extensions\com.pulsejt.extension\` at the end.

---

### Task 1: Add New Storage Keys

**Files:**
- Modify: `JS/bundle.js` — STORAGE_KEYS object

Add after the existing BG_IMAGE_OPACITY key:

```javascript
    TIMELINE_ENABLED: 'gifPreview.timelineEnabled',
    SHORTCUTS_ENABLED: 'gifPreview.shortcutsEnabled',
    BG_DIM: 'gifPreview.bgDim',
    BG_BLUR: 'gifPreview.bgBlur',
    BG_THEME_VARIANT: 'gifPreview.bgThemeVariant'
```

Commit: "feat: add storage keys for timeline, shortcuts, blur/dim, theme bg"

---

### Task 2: Timeline Scrubber — HTML

**Files:**
- Modify: `index.html`

Add a new div between the `#drop` closing tag and the fullscreen button (`#btnMinus`). Find the line `</div>` that closes `#drop` (after the file input), and add after it:

```html
    <!-- Timeline Scrubber Bar -->
    <div class="timeline-bar" id="timelineBar" style="display: none;">
      <button class="timeline-play-btn" id="timelinePlayBtn" title="Play/Pause">
        <span class="timeline-play-icon" id="timelinePlayIcon">&#9654;</span>
      </button>
      <div class="timeline-seek" id="timelineSeek">
        <div class="timeline-track">
          <div class="timeline-progress" id="timelineProgress"></div>
          <div class="timeline-thumb" id="timelineThumb"></div>
        </div>
      </div>
      <div class="timeline-time" id="timelineTime">0:00 / 0:00</div>
    </div>
```

Commit: "feat: add timeline scrubber bar HTML"

---

### Task 3: Timeline Scrubber — CSS

**Files:**
- Modify: `style.css`

Add after the existing background component styles (after the `.drop.bg-custom-image` rules):

```css
/* ===== TIMELINE SCRUBBER BAR ===== */
.timeline-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 10px;
  margin: 0 8px;
  flex-shrink: 0;
  z-index: 5;
  transition: opacity 0.2s ease;
}

.timeline-play-btn {
  width: 28px;
  height: 28px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  color: var(--theme-primary);
  font-size: 12px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: all 0.2s ease;
  padding: 0;
}

.timeline-play-btn:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: var(--theme-primary-soft);
  box-shadow: 0 0 8px var(--theme-glow);
}

.timeline-seek {
  flex: 1;
  height: 28px;
  display: flex;
  align-items: center;
  cursor: pointer;
  position: relative;
  padding: 0 4px;
}

.timeline-track {
  width: 100%;
  height: 4px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 2px;
  position: relative;
  overflow: visible;
}

.timeline-progress {
  height: 100%;
  background: linear-gradient(90deg, var(--theme-primary), var(--theme-primary-light));
  border-radius: 2px;
  width: 0%;
  transition: width 0.1s linear;
  position: relative;
  z-index: 1;
}

.timeline-thumb {
  width: 12px;
  height: 12px;
  background: var(--theme-primary);
  border: 2px solid rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  position: absolute;
  top: 50%;
  left: 0%;
  transform: translate(-50%, -50%);
  z-index: 2;
  cursor: grab;
  transition: box-shadow 0.2s ease;
  box-shadow: 0 0 6px var(--theme-glow);
}

.timeline-thumb:hover,
.timeline-thumb:active {
  box-shadow: 0 0 12px var(--theme-glow);
  transform: translate(-50%, -50%) scale(1.2);
}

.timeline-thumb:active {
  cursor: grabbing;
}

.timeline-time {
  font-size: 11px;
  color: var(--theme-text-secondary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  min-width: 70px;
  text-align: right;
  flex-shrink: 0;
}

/* Hide timeline in fullscreen/uiHidden */
.app.uiHidden .timeline-bar {
  display: none !important;
}
```

Also add responsive rules inside the existing `@media (max-width: 399px)` block:

```css
  .timeline-bar {
    padding: 4px 8px;
    gap: 6px;
  }

  .timeline-time {
    font-size: 10px;
    min-width: 58px;
  }
```

Commit: "feat: add timeline scrubber bar CSS"

---

### Task 4: Timeline Scrubber — JS Logic

**Files:**
- Modify: `JS/bundle.js`

Add a new function `initTimeline()` and call it from the main init flow (near where `initSettingsModal()` is called).

The function should:

1. Get DOM references: `timelineBar`, `timelinePlayBtn`, `timelinePlayIcon`, `timelineSeek`, `timelineProgress`, `timelineThumb`, `timelineTime`

2. Create a `formatTime(seconds)` helper that returns `M:SS` format

3. On video `timeupdate` event (use MutationObserver or attach when video loads):
   - Update progress bar width as percentage of `currentTime / duration`
   - Update thumb position to match
   - Update time display text

4. On `timelinePlayBtn` click:
   - Toggle video play/pause
   - Update icon (▶ when paused, ❚❚ when playing)

5. On `timelineSeek` click/drag:
   - Calculate seek position from mouse X relative to track
   - Set `video.currentTime`
   - Support click-to-seek AND drag-to-scrub

6. Show/hide the timeline bar:
   - Show when a video element exists AND `STORAGE_KEYS.TIMELINE_ENABLED !== 'false'`
   - Hide when no video (image/GIF loaded instead)
   - Hook into `loadVideo()` to show bar, and `loadImage()` to hide it

7. In the Playback tab settings HTML (`index.html`), add a "Show Timeline" toggle after the Autoplay toggle:
   ```html
   <div class="settingRow">
     <label class="toggle-label" style="justify-content: space-between; width: 100%;">
       <span class="toggle-text">Show Timeline</span>
       <div class="toggle-container">
         <input type="checkbox" id="timelineToggle" checked>
         <div class="toggle-slider">
           <div class="toggle-thumb"></div>
         </div>
       </div>
     </label>
     <div class="settingHelp">Show seek bar below the preview when playing video</div>
   </div>
   ```

8. In `initSettingsModal()`, add the toggle logic:
   ```javascript
   var timelineToggle = document.getElementById('timelineToggle');
   if (timelineToggle) {
     var timelineOn = localStorage.getItem(STORAGE_KEYS.TIMELINE_ENABLED) !== 'false';
     timelineToggle.checked = timelineOn;
     timelineToggle.addEventListener('change', function(e) {
       localStorage.setItem(STORAGE_KEYS.TIMELINE_ENABLED, e.target.checked.toString());
       // Show/hide timeline based on toggle
       var bar = document.getElementById('timelineBar');
       var video = document.querySelector('video.gif');
       if (bar) {
         bar.style.display = (e.target.checked && video) ? 'flex' : 'none';
       }
     });
   }
   ```

Commit: "feat: add timeline scrubber bar with seek and play/pause"

---

### Task 5: Keyboard Shortcuts — JS Logic

**Files:**
- Modify: `JS/bundle.js` — extend the global keydown listener at ~line 3952
- Modify: `index.html` — add toggle + reference card in Playback tab

In the existing `document.addEventListener('keydown', ...)` handler, AFTER the ESC handling and modal checks, add:

```javascript
      // Keyboard shortcuts (only when no modal open and shortcuts enabled)
      if (localStorage.getItem(STORAGE_KEYS.SHORTCUTS_ENABLED) !== 'false') {
        var video = document.querySelector('video.gif');
        if (video) {
          var speedPresets = [0.25, 0.5, 0.75, 1, 1.5, 2];

          if (e.code === 'Space') {
            e.preventDefault();
            if (video.paused) { video.play(); } else { video.pause(); }
          } else if (e.code === 'ArrowLeft') {
            e.preventDefault();
            video.currentTime = Math.max(0, video.currentTime - 5);
          } else if (e.code === 'ArrowRight') {
            e.preventDefault();
            video.currentTime = Math.min(video.duration, video.currentTime + 5);
          } else if (e.code === 'ArrowUp') {
            e.preventDefault();
            var currentSpeed = video.playbackRate;
            var idx = speedPresets.indexOf(currentSpeed);
            if (idx < speedPresets.length - 1) {
              var newSpeed = speedPresets[idx + 1];
              video.playbackRate = newSpeed;
              localStorage.setItem(STORAGE_KEYS.PLAYBACK_SPEED, newSpeed.toString());
              showToast('Speed: ' + newSpeed + 'x', 1500, 'info');
              // Update speed selector buttons if settings is open
              var speedBtns = document.querySelectorAll('#speedSelector .speed-btn');
              speedBtns.forEach(function(b) {
                b.classList.toggle('active', parseFloat(b.dataset.speed) === newSpeed);
              });
            }
          } else if (e.code === 'ArrowDown') {
            e.preventDefault();
            var currentSpd = video.playbackRate;
            var i = speedPresets.indexOf(currentSpd);
            if (i > 0) {
              var ns = speedPresets[i - 1];
              video.playbackRate = ns;
              localStorage.setItem(STORAGE_KEYS.PLAYBACK_SPEED, ns.toString());
              showToast('Speed: ' + ns + 'x', 1500, 'info');
              var sBtns = document.querySelectorAll('#speedSelector .speed-btn');
              sBtns.forEach(function(b) {
                b.classList.toggle('active', parseFloat(b.dataset.speed) === ns);
              });
            }
          }
        }
      }
```

In `index.html` Playback tab, add after the "Show Timeline" toggle:

```html
              <!-- Keyboard Shortcuts Toggle -->
              <div class="settingRow">
                <label class="toggle-label" style="justify-content: space-between; width: 100%;">
                  <span class="toggle-text">Keyboard Shortcuts</span>
                  <div class="toggle-container">
                    <input type="checkbox" id="shortcutsToggle" checked>
                    <div class="toggle-slider">
                      <div class="toggle-thumb"></div>
                    </div>
                  </div>
                </label>
                <div class="settingHelp">Enable keyboard controls for video playback</div>
              </div>

              <!-- Shortcuts Reference -->
              <div class="settingRow shortcuts-ref">
                <label>Shortcut Reference</label>
                <div class="shortcuts-grid">
                  <div class="shortcut-item"><kbd>Space</kbd><span>Play / Pause</span></div>
                  <div class="shortcut-item"><kbd>&#8592;</kbd><span>Seek back 5s</span></div>
                  <div class="shortcut-item"><kbd>&#8594;</kbd><span>Seek forward 5s</span></div>
                  <div class="shortcut-item"><kbd>&#8593;</kbd><span>Speed up</span></div>
                  <div class="shortcut-item"><kbd>&#8595;</kbd><span>Speed down</span></div>
                </div>
              </div>
```

In `initSettingsModal()`, add the shortcuts toggle:

```javascript
    var shortcutsToggle = document.getElementById('shortcutsToggle');
    if (shortcutsToggle) {
      var shortcutsOn = localStorage.getItem(STORAGE_KEYS.SHORTCUTS_ENABLED) !== 'false';
      shortcutsToggle.checked = shortcutsOn;
      shortcutsToggle.addEventListener('change', function(e) {
        localStorage.setItem(STORAGE_KEYS.SHORTCUTS_ENABLED, e.target.checked.toString());
      });
    }
```

Add CSS for the shortcuts reference card:

```css
/* ===== SHORTCUTS REFERENCE ===== */
.shortcuts-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}

.shortcut-item {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 12px;
  color: var(--theme-text-secondary);
}

.shortcut-item kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  padding: 4px 8px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 6px;
  font-size: 11px;
  font-family: inherit;
  color: var(--theme-primary);
  font-weight: 600;
}
```

Commit: "feat: add keyboard shortcuts for video playback"

---

### Task 6: Blur/Dim Overlay — HTML + CSS + JS

**Files:**
- Modify: `index.html` — add sliders to Background tab
- Modify: `style.css` — add overlay styles
- Modify: `JS/bundle.js` — extend applyBackground()

**HTML:** In the Background tab, add TWO new setting rows AFTER the Background Type selector row and BEFORE the conditional bg-options divs. These should always be visible (they apply to all background types):

```html
              <!-- Dim Overlay -->
              <div class="settingRow">
                <label>Dim Background</label>
                <div class="settingHelp">Darken the background for better media contrast</div>
                <div class="bg-opacity-row">
                  <input type="range" id="bgDimSlider" min="0" max="100" step="1" value="0" class="volume-slider">
                  <span class="bg-opacity-value" id="bgDimValue">0%</span>
                </div>
              </div>

              <!-- Blur Background -->
              <div class="settingRow">
                <label>Blur Background</label>
                <div class="settingHelp">Apply blur effect to the background</div>
                <div class="bg-opacity-row">
                  <input type="range" id="bgBlurSlider" min="0" max="20" step="1" value="0" class="volume-slider">
                  <span class="bg-opacity-value" id="bgBlurValue">0px</span>
                </div>
              </div>
```

**CSS:** Add to the background styles section:

```css
/* Background dim/blur overlay */
.drop.bg-has-overlay::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, var(--bg-dim, 0));
  backdrop-filter: blur(var(--bg-blur, 0px));
  -webkit-backdrop-filter: blur(var(--bg-blur, 0px));
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
}

.drop.bg-has-overlay .gif,
.drop.bg-has-overlay video.gif,
.drop.bg-has-overlay .placeholder {
  position: relative;
  z-index: 1;
}
```

**JS:** In `applyBackground()`, after the existing class removal, also handle the overlay:

```javascript
      drop.classList.remove('bg-has-overlay');
      drop.style.removeProperty('--bg-dim');
      drop.style.removeProperty('--bg-blur');

      // ... existing background type logic ...

      // Apply dim/blur overlay (works with all background types)
      var bgDim = parseFloat(localStorage.getItem(STORAGE_KEYS.BG_DIM) || '0') / 100;
      var bgBlur = parseInt(localStorage.getItem(STORAGE_KEYS.BG_BLUR) || '0', 10);
      if (bgDim > 0 || bgBlur > 0) {
        drop.classList.add('bg-has-overlay');
        drop.style.setProperty('--bg-dim', bgDim.toString());
        drop.style.setProperty('--bg-blur', bgBlur + 'px');
      }
```

In `initSettingsModal()`, add slider logic:

```javascript
    // Dim slider
    var bgDimSlider = document.getElementById('bgDimSlider');
    var bgDimValue = document.getElementById('bgDimValue');
    var savedDim = localStorage.getItem(STORAGE_KEYS.BG_DIM) || '0';
    if (bgDimSlider) bgDimSlider.value = savedDim;
    if (bgDimValue) bgDimValue.textContent = savedDim + '%';

    if (bgDimSlider) {
      bgDimSlider.addEventListener('input', function() {
        var val = bgDimSlider.value;
        if (bgDimValue) bgDimValue.textContent = val + '%';
        localStorage.setItem(STORAGE_KEYS.BG_DIM, val);
        applyBackground();
      });
    }

    // Blur slider
    var bgBlurSlider = document.getElementById('bgBlurSlider');
    var bgBlurValue = document.getElementById('bgBlurValue');
    var savedBlur = localStorage.getItem(STORAGE_KEYS.BG_BLUR) || '0';
    if (bgBlurSlider) bgBlurSlider.value = savedBlur;
    if (bgBlurValue) bgBlurValue.textContent = savedBlur + 'px';

    if (bgBlurSlider) {
      bgBlurSlider.addEventListener('input', function() {
        var val = bgBlurSlider.value;
        if (bgBlurValue) bgBlurValue.textContent = val + 'px';
        localStorage.setItem(STORAGE_KEYS.BG_BLUR, val);
        applyBackground();
      });
    }
```

Commit: "feat: add blur and dim overlay for background"

---

### Task 7: Theme-Derived Backgrounds — HTML + CSS + JS

**Files:**
- Modify: `index.html` — add "Theme" option to bg type selector, add variant picker
- Modify: `style.css` — theme background variant styles
- Modify: `JS/bundle.js` — extend applyBackground() and bg type logic

**HTML:** In the Background tab's `bgTypeSelector`, add a "Theme" button as the second option:

```html
                  <button class="bg-type-btn" data-bg="default">Default</button>
                  <button class="bg-type-btn" data-bg="theme">Theme</button>
                  <button class="bg-type-btn" data-bg="color">Color</button>
                  <button class="bg-type-btn" data-bg="checker">Checker</button>
                  <button class="bg-type-btn" data-bg="image">Image</button>
```

Add a new options div after the type selector for theme variants:

```html
              <!-- Theme Background Options (hidden by default) -->
              <div class="settingRow bg-options" id="bgThemeOptions" style="display: none;">
                <label>Theme Variant</label>
                <div class="settingHelp">Auto-generated backgrounds from your active theme</div>
                <div class="theme-bg-variants" id="themeBgVariants">
                  <button class="theme-bg-card active" data-variant="gradient" title="Gradient">
                    <div class="theme-bg-preview" id="themePreviewGradient"></div>
                    <span>Gradient</span>
                  </button>
                  <button class="theme-bg-card" data-variant="radial" title="Radial Glow">
                    <div class="theme-bg-preview" id="themePreviewRadial"></div>
                    <span>Glow</span>
                  </button>
                  <button class="theme-bg-card" data-variant="mesh" title="Mesh">
                    <div class="theme-bg-preview" id="themePreviewMesh"></div>
                    <span>Mesh</span>
                  </button>
                  <button class="theme-bg-card" data-variant="solid" title="Solid Accent">
                    <div class="theme-bg-preview" id="themePreviewSolid"></div>
                    <span>Solid</span>
                  </button>
                </div>
              </div>
```

**CSS:**

```css
/* ===== THEME BACKGROUND VARIANTS ===== */
.theme-bg-variants {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 8px;
}

.theme-bg-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 6px;
  border: 1px solid var(--theme-border);
  border-radius: 8px;
  background: transparent;
  cursor: pointer;
  transition: all 0.2s ease;
  color: var(--theme-text-secondary);
  font-size: 10px;
}

.theme-bg-card:hover {
  border-color: var(--theme-primary-soft);
  background: rgba(255, 255, 255, 0.04);
}

.theme-bg-card.active {
  border-color: var(--theme-primary);
  box-shadow: 0 0 10px var(--theme-glow);
  color: var(--theme-primary);
}

.theme-bg-preview {
  width: 100%;
  aspect-ratio: 16/9;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.06);
}

/* Drop theme backgrounds applied via JS inline styles */
.drop.bg-theme-gradient,
.drop.bg-theme-radial,
.drop.bg-theme-mesh,
.drop.bg-theme-solid {
  /* Background set by JS using computed theme colors */
}
```

**JS:** In `applyBackground()`, add a `'theme'` case. Add to class removal list: `'bg-theme-gradient'`, `'bg-theme-radial'`, `'bg-theme-mesh'`, `'bg-theme-solid'`.

```javascript
      } else if (bgType === 'theme') {
        var variant = localStorage.getItem(STORAGE_KEYS.BG_THEME_VARIANT) || 'gradient';
        var cs = getComputedStyle(document.documentElement);
        var primary = cs.getPropertyValue('--theme-primary').trim();
        var primaryDark = cs.getPropertyValue('--theme-primary-dark').trim();
        var bgPrimary = cs.getPropertyValue('--theme-bg-primary').trim();
        var accent = cs.getPropertyValue('--theme-accent').trim();

        drop.classList.add('bg-theme-' + variant);
        if (variant === 'gradient') {
          drop.style.background = 'linear-gradient(145deg, ' + primaryDark + ', ' + bgPrimary + ')';
        } else if (variant === 'radial') {
          drop.style.background = 'radial-gradient(circle at center, ' + primary + ' 0%, ' + bgPrimary + ' 70%)';
        } else if (variant === 'mesh') {
          drop.style.background = 'radial-gradient(circle at 30% 30%, ' + primary + ' 0%, transparent 50%), radial-gradient(circle at 70% 70%, ' + accent + ' 0%, ' + bgPrimary + ' 60%)';
        } else if (variant === 'solid') {
          drop.style.background = primaryDark;
        }
      }
```

In `showBgOptions()`, add the theme case:

```javascript
      } else if (type === 'theme') {
        var themeOpts = document.getElementById('bgThemeOptions');
        if (themeOpts) themeOpts.style.display = '';
      }
```

Add variant selector logic + live preview updates in `initSettingsModal()`.

Also hook into `applyTheme()` to re-run `applyBackground()` when the user switches themes, so theme-derived backgrounds update automatically.

Commit: "feat: add theme-derived background variants"

---

### Task 8: Mirror, Install, and Final Verification

**Files:**
- Copy: `index.html` -> `assets/index.html`
- Copy: `style.css` -> `assets/style.css`
- Copy all 3 files to CEP extensions folder

```bash
cp index.html assets/index.html
cp style.css assets/style.css
cp index.html "/c/Users/tyson.DESKTOP-GI9SJ3L/AppData/Roaming/Adobe/CEP/extensions/com.pulsejt.extension/index.html"
cp style.css "/c/Users/tyson.DESKTOP-GI9SJ3L/AppData/Roaming/Adobe/CEP/extensions/com.pulsejt.extension/style.css"
cp JS/bundle.js "/c/Users/tyson.DESKTOP-GI9SJ3L/AppData/Roaming/Adobe/CEP/extensions/com.pulsejt.extension/JS/bundle.js"
```

**Verification checklist:**
1. Timeline bar appears when video loads, hides for images
2. Play/pause button toggles correctly
3. Click-to-seek and drag-to-scrub work
4. Time display updates in real-time
5. Space bar toggles play/pause
6. Left/Right arrows seek 5 seconds
7. Up/Down arrows change speed (with toast)
8. Dim slider darkens background
9. Blur slider blurs background
10. Theme background variants match active theme
11. Switching themes updates theme backgrounds
12. All toggles persist across reload
13. Settings modal responsive at narrow widths

Commit: "chore: mirror and install expanded features"
