Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,78 @@ http://localhost:8000/?effect=stripes&stripeColors=1,0,0,1,1,0&suppressWarnings=

*The Matrix has you. Follow the white rabbit.* 🐰

## UI Components Architecture

### Mode Display Panel (`js/mode-display.js`)
The Matrix Mode panel in the top-right corner provides user controls for customizing the experience:

**Key Features**:
- **Version Dropdown**: Interactive select element populated from `getAvailableModes()` in config.js
- Lists all available Matrix versions (classic, resurrections, trinity, etc.)
- Changing version triggers page reload with new URL parameter
- Current selection is synchronized with URL params

- **Effect Dropdown**: Interactive select element populated from `getAvailableEffects()` in config.js
- Lists all available effects (palette, rainbow, mirror, etc.)
- Changing effect triggers page reload with new URL parameter
- Current selection is synchronized with URL params

- **Auto Mode Switching**: Checkbox to enable/disable screensaver-like mode rotation
- When enabled, automatically cycles through different version/effect combinations
- Interval configurable via dropdown (10-60 minutes)

- **Switch Mode Now**: Button to manually trigger a random mode change
- Calls `modeManager.switchToRandomMode(true)` with manual flag
- For manual switches, page reloads to ensure clean state
- For auto switches, attempts in-place config update

**Event System**:
```javascript
modeDisplay.on("versionChange", (version) => { /* handle version change */ });
modeDisplay.on("effectChange", (effect) => { /* handle effect change */ });
modeDisplay.on("toggleScreensaver", (enabled) => { /* handle screensaver toggle */ });
modeDisplay.on("changeSwitchInterval", (interval) => { /* handle interval change */ });
```

**Integration Points**:
- Imports `getAvailableModes()` and `getAvailableEffects()` from config.js
- Communicates with `ModeManager` for mode switching logic
- Events handled in `main.js` `setupModeManagementEvents()` function

### Page Title Updates
The page title dynamically updates to reflect the current version and effect:
- Format: `"Matrix - {Version Name} / {Effect Name}"`
- Examples:
- `"Matrix - Classic / Palette"`
- `"Matrix - Resurrections / Rainbow"`
- `"Matrix - Trinity / Mirror"`
- Updated via `updatePageTitle(config)` function in main.js
- Title updates occur on:
- Initial page load
- Version/effect dropdown changes (via page reload)
- Auto mode switching (via history.replaceState)
- Works correctly in both normal browser and PWA modes
- Name formatting uses camelCase-to-Title-Case conversion for readability

### Spotify UI Component
The Spotify integration UI (`js/spotify-ui.js`) is hidden by default:
- Located in top-left corner when visible
- Controlled via `spotifyControlsVisible` config parameter
- **Note**: "Show Spotify Controls" checkbox removed from Mode Display as of QOL improvements
- To enable Spotify UI: use URL parameter `?spotifyControls=true`
- Component still functional for users who explicitly enable it via URL

### Configuration System
All UI options are controlled via URL parameters:
- `version`: Matrix version (classic, resurrections, etc.)
- `effect`: Visual effect (palette, rainbow, mirror, etc.)
- **Note**: The "trans" effect was removed in a prior PR and is no longer available
- `screensaver`: Enable auto mode switching (true/false)
- `switchInterval`: Auto-switch interval in milliseconds
- `suppressWarnings`: Hide hardware acceleration warnings (true/false)

See `js/config.js` `paramMapping` object for complete list of supported parameters.

Available effects are defined in `getAvailableEffects()` function in config.js:
- none, plain, palette, customStripes, stripes, rainbow, spectrum, image, mirror, gallery

2 changes: 1 addition & 1 deletion js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function getAvailableModes() {
* Returns a list of all available effect names
*/
export function getAvailableEffects() {
return ["none", "plain", "palette", "customStripes", "stripes", "rainbow", "spectrum", "trans", "image", "mirror", "gallery"];
return ["none", "plain", "palette", "customStripes", "stripes", "rainbow", "spectrum", "image", "mirror", "gallery"];
}

/*
Expand Down
54 changes: 42 additions & 12 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ function initializeModeManagement(config) {
modeDisplay.setModeManager(modeManager);

// Set initial toggle states
modeDisplay.setToggleStates(config.screensaverMode || false, config.spotifyControlsVisible || false, config.modeSwitchInterval || 600000);
modeDisplay.setToggleStates(config.screensaverMode || false, config.modeSwitchInterval || 600000);

// Set up event listeners
setupModeManagementEvents(config);
Expand All @@ -266,6 +266,9 @@ function initializeModeManagement(config) {
if (config.screensaverMode) {
modeManager.start();
}

// Update page title with initial mode
updatePageTitle(config);
}

/**
Expand All @@ -282,15 +285,18 @@ function setupModeManagementEvents(config) {
}
});

modeDisplay.on("toggleSpotifyControls", (visible) => {
config.spotifyControlsVisible = visible;
if (spotifyUI) {
if (visible) {
spotifyUI.show();
} else {
spotifyUI.hide();
}
}
modeDisplay.on("versionChange", (version) => {
// Update URL and reload with new version
const urlParams = new URLSearchParams(window.location.search);
urlParams.set("version", version);
window.location.search = urlParams.toString();
});

modeDisplay.on("effectChange", (effect) => {
// Update URL and reload with new effect
const urlParams = new URLSearchParams(window.location.search);
urlParams.set("effect", effect);
window.location.search = urlParams.toString();
});

modeDisplay.on("changeSwitchInterval", (interval) => {
Expand Down Expand Up @@ -318,10 +324,34 @@ function setupModeManagementEvents(config) {
// Update the configuration and restart the renderer
const newConfig = makeConfig(Object.fromEntries(urlParams.entries()));
restartMatrixWithNewConfig(newConfig);

// Update page title
updatePageTitle(newConfig);
}
});
}

/**
* Update page title with current version and effect
*/
function updatePageTitle(config) {
const version = config.version || "classic";
const effect = config.effect || "palette";

// Format names for display
const formatName = (name) => {
return name
.split(/(?=[A-Z])|[_-]/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
};
Comment on lines +342 to +347
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatName function duplicates the same logic as formatModeName in mode-manager.js and mode-display.js. This code duplication creates maintenance overhead. Consider importing a shared utility function instead of defining this inline.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback


const versionName = formatName(version);
const effectName = formatName(effect);

document.title = `Matrix - ${versionName} / ${effectName}`;
}

/**
* Restart the Matrix renderer with new configuration
*/
Expand Down Expand Up @@ -353,10 +383,10 @@ function initializeSpotifyIntegration(config) {
// Create Spotify integration instance
spotifyIntegration = new SpotifyIntegration();

// Create UI controls
// Create UI controls - hidden by default
spotifyUI = new SpotifyUI({
clientId: config.spotifyClientId,
visible: config.spotifyControlsVisible,
visible: false, // Always hide Spotify controls
});
spotifyUI.setSpotifyIntegration(spotifyIntegration);

Expand Down
75 changes: 51 additions & 24 deletions js/mode-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* along with controls for the screensaver mode functionality.
*/

import { getAvailableModes, getAvailableEffects } from "./config.js";

export default class ModeDisplay {
constructor(config = {}) {
this.config = {
Expand All @@ -21,8 +23,9 @@ export default class ModeDisplay {
this.modeManager = null;
this.callbacks = {
toggleScreensaver: [],
toggleSpotifyControls: [],
changeSwitchInterval: [],
versionChange: [],
effectChange: [],
};

this.init();
Expand Down Expand Up @@ -65,15 +68,31 @@ export default class ModeDisplay {
this.element.className = "mode-display";
this.element.style.cssText = this.getBaseStyles();

const availableVersions = getAvailableModes();
const availableEffects = getAvailableEffects();

const versionOptions = availableVersions.map((v) => `<option value="${v}">${this.formatModeName(v)}</option>`).join("");
const effectOptions = availableEffects.map((e) => `<option value="${e}">${this.formatModeName(e)}</option>`).join("");

this.element.innerHTML = `
<div class="mode-header" style="cursor: pointer; padding: 8px; border-bottom: 1px solid rgba(0, 255, 0, 0.2); margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
<span class="mode-title">Matrix Mode</span>
<span class="toggle-icon">◐</span>
</div>
<div class="mode-content" style="display: none;">
<div class="current-mode" style="margin-bottom: 10px; padding: 5px 0;">
<div class="mode-version" style="font-weight: bold; margin-bottom: 2px;">Version: <span class="version-name">Classic</span></div>
<div class="mode-effect" style="font-size: 10px; opacity: 0.8;">Effect: <span class="effect-name">Mirror</span></div>
<div class="mode-version" style="font-weight: bold; margin-bottom: 5px;">
<label style="display: block; font-size: 10px; margin-bottom: 2px;">Version:</label>
<select class="version-select" style="width: 100%; background: rgba(0, 255, 0, 0.1); border: 1px solid rgba(0, 255, 0, 0.3); color: #00ff00; font-family: monospace; font-size: 10px; padding: 3px; border-radius: 3px;">
${versionOptions}
</select>
</div>
<div class="mode-effect" style="margin-bottom: 5px;">
<label style="display: block; font-size: 10px; margin-bottom: 2px;">Effect:</label>
<select class="effect-select" style="width: 100%; background: rgba(0, 255, 0, 0.1); border: 1px solid rgba(0, 255, 0, 0.3); color: #00ff00; font-family: monospace; font-size: 10px; padding: 3px; border-radius: 3px;">
${effectOptions}
</select>
</div>
</div>

<div class="mode-controls" style="border-top: 1px solid rgba(0, 255, 0, 0.2); padding-top: 8px;">
Expand All @@ -94,10 +113,6 @@ export default class ModeDisplay {
</select>
</label>
</div>
<label style="display: block; margin-bottom: 5px; font-size: 10px;">
<input type="checkbox" class="spotify-controls-toggle" style="margin-right: 5px;" />
Show Spotify Controls
</label>
<button class="switch-mode-btn" style="width: 100%; padding: 4px; background: rgba(0, 255, 0, 0.2); border: 1px solid rgba(0, 255, 0, 0.3); color: #00ff00; font-family: monospace; font-size: 9px; border-radius: 3px; cursor: pointer; margin-top: 5px;">
Switch Mode Now
</button>
Expand Down Expand Up @@ -184,6 +199,18 @@ export default class ModeDisplay {
this.toggleExpanded();
});

// Version dropdown change
const versionSelect = this.element.querySelector(".version-select");
versionSelect.addEventListener("change", (e) => {
this.emit("versionChange", e.target.value);
});

// Effect dropdown change
const effectSelect = this.element.querySelector(".effect-select");
effectSelect.addEventListener("change", (e) => {
this.emit("effectChange", e.target.value);
});

// Screensaver toggle
const screensaverToggle = this.element.querySelector(".screensaver-toggle");
screensaverToggle.addEventListener("change", (e) => {
Expand All @@ -198,12 +225,6 @@ export default class ModeDisplay {
this.updateNextSwitchTime();
});

// Spotify controls toggle
const spotifyToggle = this.element.querySelector(".spotify-controls-toggle");
spotifyToggle.addEventListener("change", (e) => {
this.emit("toggleSpotifyControls", e.target.checked);
});

// Interval selection
const intervalSelect = this.element.querySelector(".interval-select");
intervalSelect.addEventListener("change", (e) => {
Expand Down Expand Up @@ -320,14 +341,14 @@ export default class ModeDisplay {
if (!this.modeManager) return;

const modeInfo = this.modeManager.getModeInfo();
const versionElement = this.element.querySelector(".version-name");
const effectElement = this.element.querySelector(".effect-name");
const versionSelect = this.element.querySelector(".version-select");
const effectSelect = this.element.querySelector(".effect-select");

if (versionElement) {
versionElement.textContent = modeInfo.versionName;
if (versionSelect) {
versionSelect.value = modeInfo.version;
}
if (effectElement) {
effectElement.textContent = modeInfo.effectName;
if (effectSelect) {
effectSelect.value = modeInfo.effect;
}

this.updateNextSwitchTime();
Expand Down Expand Up @@ -355,24 +376,30 @@ export default class ModeDisplay {
/**
* Set toggle states
*/
setToggleStates(screensaverEnabled, spotifyControlsVisible, switchInterval = 600000) {
setToggleStates(screensaverEnabled, switchInterval = 600000) {
const screensaverToggle = this.element.querySelector(".screensaver-toggle");
const spotifyToggle = this.element.querySelector(".spotify-controls-toggle");
const intervalSelect = this.element.querySelector(".interval-select");

if (screensaverToggle) {
screensaverToggle.checked = screensaverEnabled;
}
if (spotifyToggle) {
spotifyToggle.checked = spotifyControlsVisible;
}
if (intervalSelect) {
intervalSelect.value = switchInterval.toString();
}

this.updateNextSwitchTime();
}

/**
* Format mode name for display
*/
formatModeName(name) {
return name
.split(/(?=[A-Z])|[_-]/) // Split on camelCase, underscores, and hyphens
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}
Comment on lines +396 to +401
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatModeName method is duplicated across three files (mode-display.js, main.js, and mode-manager.js). This violates the DRY (Don't Repeat Yourself) principle and creates maintenance overhead. Consider extracting this to a shared utility module (e.g., js/utils.js) and importing it where needed, or reusing the existing implementation from ModeManager.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback


/**
* Add event listener
*/
Expand Down
Loading