diff --git a/README.md b/README.md index 92030b8d..583500bf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Tests](https://img.shields.io/badge/tests-2548%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2552%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1341,7 +1341,7 @@ npm test npm start -- --smoke ``` -**2548 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2552 unit tests, all passing.** They are written to check answers, not to exercise lines: - **Formulas** are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor diff --git a/README.tr.md b/README.tr.md index b16f4339..65af8939 100644 --- a/README.tr.md +++ b/README.tr.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Test](https://img.shields.io/badge/test-2548%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2552%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1341,7 +1341,7 @@ npm test npm start -- --smoke ``` -**2548 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2552 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: - **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı diff --git a/ROADMAP.md b/ROADMAP.md index fef8aad8..2c567621 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2548 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2552 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; 105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 878 on `main` since. diff --git a/src/admin/scene-panels.js b/src/admin/scene-panels.js index b8fd3cb4..125916ca 100644 --- a/src/admin/scene-panels.js +++ b/src/admin/scene-panels.js @@ -301,7 +301,7 @@ try { localStorage.setItem('sv-layers-open', JSON.stringify([...set].slice(-64))); } catch { /* yok say */ } } - const LAYER_ICONS = { background: 'background', visualizer: 'bars', text: 'text', media: 'film', sprites: 'sparkles', logo: 'tag' }; + const LAYER_ICONS = { background: 'background', visualizer: 'bars', text: 'text', media: 'film', sprites: 'sparkles', logo: 'tag', nowplaying: 'music' }; /* Katman başlığı: sıra okları, tür simgesi, ad ve özet, bayraklar, aç/kapa ve kaldır. Liste ekranda ters sırada: "yukarı" dizide İLERİ demek (bkz. @@ -309,7 +309,7 @@ hedeflemek zorunda kalınmasın. */ function layerHead(list, i, l, name, summary, flags, open, toggle, onChange) { const el = P().el; - const icon = LAYER_ICONS[l.kind === 'visualizer' && l.type === 'text' ? 'text' : l.kind] || 'grid'; + const icon = LAYER_ICONS[(l.kind === 'visualizer' && l.type === 'text') ? 'text' : (l.kind === 'visualizer' && l.type === 'nowplaying') ? 'nowplaying' : l.kind] || 'grid'; return el('div', { class: 'layer-head' + (open ? ' open' : '') }, [ el('div', { class: 'layer-ord' }, [ el('button', { @@ -383,6 +383,7 @@ ['media', 'Medya'], ['sprites', 'Görsel Nesneler'], ['logo', 'Logo'], + ['nowplaying', 'Çalan Parça'], ]; /* Metin bir kind değil; görselleştirici türünün alt tipi. Ayrı düğmeler @@ -428,6 +429,7 @@ if (kind === 'background') return MC().layerPairs('background'); if (kind === 'visualizer') return MC().layerPairs('visualizer'); if (kind === 'sprites') return [['back', 'Arka Katman'], ['front', 'Ön Katman']]; + if (kind === 'nowplaying') return []; return []; } @@ -960,10 +962,11 @@ return out; } - if (l.kind === 'visualizer' && l.type !== 'none' && l.type !== 'custom') { + if (l.kind === 'nowplaying' || (l.kind === 'visualizer' && l.type !== 'none' && l.type !== 'custom')) { l.settings = l.settings || {}; const defVis = def.visualizer || {}; const vs = (l.settings.visualizer = l.settings.visualizer || {}); + if (l.kind === 'nowplaying') l.type = 'nowplaying'; const getV = (k, fallback) => vs[k] !== undefined ? vs[k] : (defVis[k] !== undefined ? defVis[k] : fallback); const setV = (k, val) => { vs[k] = val; }; @@ -1585,12 +1588,17 @@ el('button', { class: 'btn ghost small', type: 'button', icon: 'plus', text: label, onclick: () => { - const opts = typeOptionsFor(kind); - const made = window.SVLayers.normalizeLayer({ - kind, - name: label, - type: opts.length ? opts[0][0] : 'back', - }); + let made; + if (kind === 'nowplaying' && window.SVLayers.makeNowPlayingLayer) { + made = window.SVLayers.makeNowPlayingLayer({ name: label }); + } else { + const opts = typeOptionsFor(kind); + made = window.SVLayers.normalizeLayer({ + kind, + name: label, + type: opts.length ? opts[0][0] : 'back', + }); + } list.push(made); openNew(made); rerender(); diff --git a/src/visualizer/layers.js b/src/visualizer/layers.js index 12a9f7e5..40104c4f 100644 --- a/src/visualizer/layers.js +++ b/src/visualizer/layers.js @@ -34,7 +34,7 @@ const CSS_BLEND = (b) => (b === 'add' ? 'screen' : BLEND_MODES.indexOf(b) >= 0 ? b : 'normal'); const CANVAS_BLEND = (b) => (b === 'add' ? 'lighter' : BLEND_MODES.indexOf(b) >= 0 ? b : 'source-over'); - const KINDS = ['background', 'visualizer', 'media', 'sprites', 'logo']; + const KINDS = ['background', 'visualizer', 'media', 'sprites', 'logo', 'nowplaying']; /* Bağlamı kaybolan bir yüzey en çok bu aralıkla yeniden kuruluyor (#594). */ const REVIVE_MS = 2000; @@ -93,6 +93,7 @@ out.postfx = Array.isArray(l && l.postfx) ? l.postfx : []; out.settings = (l && l.settings) || {}; if (KINDS.indexOf(out.kind) < 0) out.kind = 'visualizer'; + if (out.kind === 'nowplaying') out.type = 'nowplaying'; if (!out.id) out.id = newLayerId(); return out; } @@ -141,6 +142,23 @@ }); } + /* Now Playing layer factory — first-class kind (alongside media/logo). + Reuses SVModes.nowplaying cover/title rendering + settings.nowplaying. */ + function makeNowPlayingLayer(spec) { + const s = spec || {}; + const np = Object.assign({ + enabled: true, + source: s.source || 'system', + coverOverlay: s.coverOverlay != null ? !!s.coverOverlay : false, + }, s.nowplaying || {}); + return normalizeLayer({ + name: s.name || 'Çalan Parça', + kind: 'nowplaying', + type: 'nowplaying', + settings: { nowplaying: np }, + }); + } + /* Eski (katmansız) yapılandırmadan katman listesi üretir. Sıra, v2.0'daki z-index yığınının birebir aynısıdır: arkaplan → (medya arkada) → sprite arka → görselleştirici → @@ -368,6 +386,7 @@ const list = Array.isArray(cfg.layers) ? cfg.layers : []; const vis = list.find((l) => l && l.kind === 'visualizer' && l.type !== 'text' && l.type !== 'nowplaying'); + /* kind nowplaying is a dedicated overlay, not a classic visualizer type. */ if (cfg.visualizer && cfg.visualizer.type === 'none') { cfg.visualizer.type = vis ? vis.type : 'bars'; } @@ -502,6 +521,16 @@ }); } + if (layer.kind === 'nowplaying') { + const npSettings = (layer.settings && layer.settings.nowplaying) || {}; + const defNp = (cfg && cfg.nowplaying) || (def ? def.nowplaying : {}); + const baseVis = (cfg && cfg.visualizer) || (def ? def.visualizer : {}) || {}; + return Object.assign({}, base, { + visualizer: Object.assign({}, baseVis, { type: 'nowplaying' }), + nowplaying: Object.assign({}, defNp, base.nowplaying, npSettings, { enabled: layer.enabled !== false }), + }); + } + if (layer.kind === 'visualizer') { const defVis = def ? def.visualizer : {}; const baseVis = (cfg && cfg.visualizer) || defVis; @@ -613,7 +642,7 @@ trackArtwork = t.nowPlaying.artwork; } } - } else if (l.type === 'nowplaying') { + } else if (l.type === 'nowplaying' || l.kind === 'nowplaying') { lyricsOrTextActive = true; const np = (l.settings && l.settings.nowplaying) || cfg.nowplaying; if (np && np.showArtwork === false) showArtworkAllowed = false; @@ -1071,8 +1100,9 @@ e.ctx = e.canvas.getContext('2d'); e.solid = true; // 'solid' ya da bilinmeyen tür: düz renk } - } else if (layer.kind === 'visualizer') { - if (window.SVModes[layer.type]) e.mode = new window.SVModes[layer.type](e.canvas); + } else if (layer.kind === 'visualizer' || layer.kind === 'nowplaying') { + const modeType = layer.kind === 'nowplaying' ? 'nowplaying' : layer.type; + if (window.SVModes[modeType]) e.mode = new window.SVModes[modeType](e.canvas); e.ctx = e.canvas.getContext('2d'); } else { e.ctx = e.canvas.getContext('2d'); @@ -1593,7 +1623,7 @@ return; } - if (l.kind === 'visualizer') { + if (l.kind === 'visualizer' || l.kind === 'nowplaying') { if (!e.mode) { e.ctx.clearRect(0, 0, W, H); return; } if (!audio || !audio.ready) { e.ctx.clearRect(0, 0, W, H); return; } e.mode.draw(audio, lcfg, t, dt); @@ -1836,6 +1866,7 @@ KINDS, normalizeLayer, makeTextLayer, + makeNowPlayingLayer, newLayerId, synthesize, resolve, diff --git a/tests/layers-panel-ui.test.js b/tests/layers-panel-ui.test.js index cbd33085..2d4445dc 100644 --- a/tests/layers-panel-ui.test.js +++ b/tests/layers-panel-ui.test.js @@ -102,6 +102,18 @@ test('aç/kapa durumu kaydediliyor, yeni eklenen katman açık geliyor', () => { assert.ok(JSON.parse(store['sv-layers-open']).includes(made.id), 'yeni katman açık'); }); + +test('nowplaying add button creates nowplaying kind layer', () => { + const { panel, cfg } = setup(LAYERS); + const add = all(panel, (n) => n.tag === 'button' && /Çalan Parça$/.test(n.attrs.text || ''))[0]; + assert.ok(add, 'Çalan Parça add button present'); + add.attrs.onclick(); + const made = cfg.layers[cfg.layers.length - 1]; + assert.strictEqual(made.kind, 'nowplaying'); + assert.strictEqual(made.type, 'nowplaying'); + assert.ok(made.settings && made.settings.nowplaying); +}); + test('kilitli katman açılamıyor', () => { const { panel } = setup([{ id: 'k', kind: 'visualizer', type: 'bars', locked: true }], ['k']); assert.strictEqual(all(panel, cls('layer-tabs')).length, 0); diff --git a/tests/layers.test.js b/tests/layers.test.js index 59e4297a..e1db5f0b 100644 --- a/tests/layers.test.js +++ b/tests/layers.test.js @@ -129,6 +129,28 @@ test('makeTextLayer sanatçı adı katmanı üretir', () => { assert.strictEqual(t.align, 'left'); }); + +test('makeNowPlayingLayer first-class kind', () => { + const l = L.makeNowPlayingLayer({ name: 'Çalan Parça' }); + assert.strictEqual(l.kind, 'nowplaying'); + assert.strictEqual(l.type, 'nowplaying'); + assert.strictEqual(l.name, 'Çalan Parça'); + assert.ok(l.id); + assert.strictEqual(l.settings.nowplaying.enabled, true); + assert.strictEqual(l.settings.nowplaying.source, 'system'); + assert.strictEqual(l.settings.nowplaying.coverOverlay, false); +}); + +test('normalizeLayer forces nowplaying type for nowplaying kind', () => { + const l = L.normalizeLayer({ kind: 'nowplaying', name: 'NP', type: 'bars' }); + assert.strictEqual(l.kind, 'nowplaying'); + assert.strictEqual(l.type, 'nowplaying'); +}); + +test('KINDS includes nowplaying', () => { + assert.ok(L.KINDS.indexOf('nowplaying') >= 0); +}); + test('yığın açıkken kullanıcı listesi kullanılır', () => { const cfg = baseCfg(); cfg.layers = [{ id: 'a', kind: 'visualizer', type: 'wave' }];