Skip to content

Commit f7c0f0e

Browse files
committed
feat: PWA favorite modes, dark select styling, random from favorites
Made-with: Cursor
1 parent f20bf50 commit f7c0f0e

4 files changed

Lines changed: 187 additions & 10 deletions

File tree

js/main.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import makeConfig from "./config.js";
1+
import makeConfig, { getAvailableModes } from "./config.js";
2+
import { loadFavoriteVersions } from "./mode-favorites.js";
23
import SpotifyIntegration from "./spotify.js";
34
import SpotifyUI from "./spotify-ui.js";
45
import ModeManager from "./mode-manager.js";
@@ -299,15 +300,18 @@ document.body.onload = async () => {
299300
* Initialize mode management and display components
300301
*/
301302
function initializeModeManagement(config) {
303+
const favoriteVersions = loadFavoriteVersions(getAvailableModes());
304+
302305
// Create mode manager
303-
modeManager = new ModeManager(config);
306+
modeManager = new ModeManager({ ...config, favoriteVersions });
304307

305308
// Create mode display
306309
modeDisplay = new ModeDisplay({
307310
position: "top-right",
308311
autoHide: true,
309312
autoHideDelay: 5000,
310313
showControls: true,
314+
favoriteVersions,
311315
});
312316

313317
// Connect mode display to mode manager

js/mode-display.js

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { getAvailableModes, getAvailableEffects } from "./config.js";
9+
import { loadFavoriteVersions, toggleFavoriteVersion } from "./mode-favorites.js";
910
import { formatModeName } from "./utils.js";
1011

1112
export default class ModeDisplay {
@@ -18,6 +19,9 @@ export default class ModeDisplay {
1819
...config,
1920
};
2021

22+
/** @type {string[]} */
23+
this.favoriteVersions = [...(this.config.favoriteVersions ?? loadFavoriteVersions(getAvailableModes()))];
24+
2125
this.element = null;
2226
this.isVisible = false;
2327
this.isMouseOverPanel = false;
@@ -57,6 +61,7 @@ export default class ModeDisplay {
5761
*/
5862
setModeManager(modeManager) {
5963
this.modeManager = modeManager;
64+
this.modeManager.updateConfig({ favoriteVersions: [...this.favoriteVersions] });
6065
this.updateModeInfo();
6166

6267
// Listen for mode changes
@@ -73,14 +78,14 @@ export default class ModeDisplay {
7378
* Create the UI element
7479
*/
7580
createElement() {
81+
this.injectModeDisplayStyles();
7682
this.element = document.createElement("div");
7783
this.element.className = "mode-display";
7884
this.element.style.cssText = this.getBaseStyles();
7985

80-
const availableVersions = getAvailableModes();
8186
const availableEffects = getAvailableEffects();
8287

83-
const versionOptions = availableVersions.map((v) => `<option value="${v}">${formatModeName(v)}</option>`).join("");
88+
const versionOptions = this.buildVersionSelectInnerHtml();
8489
const effectOptions = availableEffects.map((e) => `<option value="${e}">${formatModeName(e)}</option>`).join("");
8590

8691
this.element.innerHTML = `
@@ -91,14 +96,17 @@ export default class ModeDisplay {
9196
<div class="mode-content" style="display: none;">
9297
<div class="current-mode" style="margin-bottom: 10px; padding: 5px 0;">
9398
<div class="mode-version" style="font-weight: bold; margin-bottom: 5px;">
94-
<label style="display: block; font-size: 10px; margin-bottom: 2px;">Version:</label>
95-
<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;">
99+
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 2px;">
100+
<label style="display: block; font-size: 10px; margin: 0; flex: 1;">Version:</label>
101+
<button type="button" class="version-favorite-btn" title="Add to favorites" aria-label="Toggle favorite for selected version" aria-pressed="false">☆</button>
102+
</div>
103+
<select class="version-select matrix-select" style="width: 100%; font-size: 10px; padding: 3px;">
96104
${versionOptions}
97105
</select>
98106
</div>
99107
<div class="mode-effect" style="margin-bottom: 5px;">
100108
<label style="display: block; font-size: 10px; margin-bottom: 2px;">Effect:</label>
101-
<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;">
109+
<select class="effect-select matrix-select" style="width: 100%; font-size: 10px; padding: 3px;">
102110
${effectOptions}
103111
</select>
104112
</div>
@@ -112,7 +120,7 @@ export default class ModeDisplay {
112120
<div class="auto-switch-interval" style="margin-left: 20px; margin-bottom: 5px; font-size: 10px;">
113121
<label style="display: block;">
114122
Interval:
115-
<select class="interval-select" style="margin-left: 5px; background: rgba(0, 255, 0, 0.1); border: 1px solid rgba(0, 255, 0, 0.3); color: #00ff00; font-family: monospace; font-size: 9px; padding: 2px;">
123+
<select class="interval-select matrix-select" style="margin-left: 5px; font-size: 9px; padding: 2px;">
116124
<option value="600000">10 min</option>
117125
<option value="1200000">20 min</option>
118126
<option value="1800000">30 min</option>
@@ -149,6 +157,97 @@ export default class ModeDisplay {
149157
`;
150158

151159
document.body.appendChild(this.element);
160+
this.updateFavoriteButton();
161+
}
162+
163+
/**
164+
* Dark dropdown styling so native option lists are readable (esp. Windows/Chrome).
165+
*/
166+
injectModeDisplayStyles() {
167+
if (document.getElementById("mode-display-styles")) return;
168+
const style = document.createElement("style");
169+
style.id = "mode-display-styles";
170+
style.textContent = `
171+
.mode-display select.matrix-select {
172+
color-scheme: dark;
173+
background-color: #0a140a;
174+
color: #b6f7b6;
175+
border: 1px solid rgba(0, 255, 0, 0.45);
176+
font-family: monospace;
177+
border-radius: 3px;
178+
}
179+
.mode-display select.matrix-select option {
180+
background-color: #050805;
181+
color: #d4ffd4;
182+
}
183+
.mode-display select.matrix-select option:checked,
184+
.mode-display select.matrix-select option:hover {
185+
background-color: #145a20;
186+
color: #ffffff;
187+
}
188+
.mode-display .version-favorite-btn {
189+
flex-shrink: 0;
190+
min-width: 28px;
191+
padding: 2px 6px;
192+
font-size: 14px;
193+
line-height: 1;
194+
cursor: pointer;
195+
background: rgba(0, 255, 0, 0.12);
196+
border: 1px solid rgba(0, 255, 0, 0.35);
197+
border-radius: 3px;
198+
color: #9ff59f;
199+
}
200+
.mode-display .version-favorite-btn:hover {
201+
background: rgba(0, 255, 0, 0.22);
202+
}
203+
`;
204+
document.head.appendChild(style);
205+
}
206+
207+
/**
208+
* Build version <select> HTML: favorites group first, then remaining modes.
209+
*/
210+
buildVersionSelectInnerHtml() {
211+
const availableVersions = getAvailableModes();
212+
const favSet = new Set(this.favoriteVersions);
213+
const favList = this.favoriteVersions.filter((f) => availableVersions.includes(f));
214+
const rest = availableVersions.filter((v) => !favSet.has(v));
215+
let html = "";
216+
if (favList.length > 0) {
217+
html += `<optgroup label="★ Favorites">`;
218+
for (const v of favList) {
219+
html += `<option value="${v}">${formatModeName(v)}</option>`;
220+
}
221+
html += `</optgroup>`;
222+
}
223+
html += `<optgroup label="All modes">`;
224+
for (const v of rest) {
225+
html += `<option value="${v}">${formatModeName(v)}</option>`;
226+
}
227+
html += `</optgroup>`;
228+
return html;
229+
}
230+
231+
rebuildVersionSelect() {
232+
const versionSelect = this.element.querySelector(".version-select");
233+
if (!versionSelect) return;
234+
const current = versionSelect.value;
235+
versionSelect.innerHTML = this.buildVersionSelectInnerHtml();
236+
versionSelect.value = current;
237+
if (versionSelect.value !== current && versionSelect.options.length) {
238+
versionSelect.selectedIndex = 0;
239+
}
240+
}
241+
242+
updateFavoriteButton() {
243+
const btn = this.element?.querySelector(".version-favorite-btn");
244+
const versionSelect = this.element?.querySelector(".version-select");
245+
if (!btn || !versionSelect) return;
246+
const v = versionSelect.value;
247+
const isFav = this.favoriteVersions.includes(v);
248+
btn.textContent = isFav ? "★" : "☆";
249+
btn.setAttribute("aria-pressed", isFav ? "true" : "false");
250+
btn.title = isFav ? "Remove from favorites" : "Add to favorites";
152251
}
153252

154253
/**
@@ -227,9 +326,23 @@ export default class ModeDisplay {
227326
// Version dropdown change
228327
const versionSelect = this.element.querySelector(".version-select");
229328
versionSelect.addEventListener("change", (e) => {
329+
this.updateFavoriteButton();
230330
this.emit("versionChange", e.target.value);
231331
});
232332

333+
const favoriteBtn = this.element.querySelector(".version-favorite-btn");
334+
favoriteBtn.addEventListener("click", (e) => {
335+
e.preventDefault();
336+
e.stopPropagation();
337+
const v = versionSelect.value;
338+
this.favoriteVersions = toggleFavoriteVersion(v, getAvailableModes());
339+
if (this.modeManager) {
340+
this.modeManager.updateConfig({ favoriteVersions: [...this.favoriteVersions] });
341+
}
342+
this.rebuildVersionSelect();
343+
this.updateFavoriteButton();
344+
});
345+
233346
// Effect dropdown change
234347
const effectSelect = this.element.querySelector(".effect-select");
235348
effectSelect.addEventListener("change", (e) => {
@@ -416,6 +529,7 @@ export default class ModeDisplay {
416529
effectSelect.value = modeInfo.effect;
417530
}
418531

532+
this.updateFavoriteButton();
419533
this.updateNextSwitchTime();
420534
}
421535

js/mode-favorites.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Persist favorite Matrix versions in localStorage (PWA-friendly).
3+
*/
4+
5+
const STORAGE_KEY = "matrix.pwa.favoriteVersions";
6+
7+
/**
8+
* @param {string[]} validModes
9+
* @returns {string[]}
10+
*/
11+
export function loadFavoriteVersions(validModes) {
12+
if (!validModes?.length) return [];
13+
const allowed = new Set(validModes);
14+
try {
15+
const raw = localStorage.getItem(STORAGE_KEY);
16+
if (!raw) return [];
17+
const parsed = JSON.parse(raw);
18+
if (!Array.isArray(parsed)) return [];
19+
return parsed.filter((v) => typeof v === "string" && allowed.has(v));
20+
} catch {
21+
return [];
22+
}
23+
}
24+
25+
/**
26+
* @param {string[]} favorites
27+
*/
28+
export function saveFavoriteVersions(favorites) {
29+
try {
30+
localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites));
31+
} catch (e) {
32+
console.warn("[Matrix] Could not save favorite modes:", e);
33+
}
34+
}
35+
36+
/**
37+
* @param {string} version
38+
* @param {string[]} validModes
39+
* @returns {string[]} new list (sorted for stable storage)
40+
*/
41+
export function toggleFavoriteVersion(version, validModes) {
42+
const allowed = new Set(validModes);
43+
if (!allowed.has(version)) return loadFavoriteVersions(validModes);
44+
const current = new Set(loadFavoriteVersions(validModes));
45+
if (current.has(version)) {
46+
current.delete(version);
47+
} else {
48+
current.add(version);
49+
}
50+
const next = validModes.filter((v) => current.has(v));
51+
saveFavoriteVersions(next);
52+
return next;
53+
}

js/mode-manager.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* to create a screensaver-like experience with rotating visual modes.
66
*/
77

8-
import { getRandomVersion, getAvailableModes, getAvailableEffects, versions } from "./config.js";
8+
import { getAvailableModes, getAvailableEffects, versions } from "./config.js";
99
import { formatModeName } from "./utils.js";
1010

1111
export default class ModeManager {
@@ -103,7 +103,13 @@ export default class ModeManager {
103103
*/
104104
getRandomVersionName() {
105105
const availableModes = this.config.availableModes || this.availableModes;
106-
return availableModes[Math.floor(Math.random() * availableModes.length)];
106+
const favs = this.config.favoriteVersions;
107+
const pool =
108+
Array.isArray(favs) && favs.length > 0
109+
? favs.filter((v) => availableModes.includes(v))
110+
: null;
111+
const pickFrom = pool?.length ? pool : availableModes;
112+
return pickFrom[Math.floor(Math.random() * pickFrom.length)];
107113
}
108114

109115
/**

0 commit comments

Comments
 (0)