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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ GitHub. The full reasoning and the measurements behind each change are in [ROADM

### Studio

- **New presets start in the app language [772].** In English, Studio → "+ Shader" opened a visualizer
named "Yeni Görselleştirici", and the discard-changes prompt showed that Turkish name. The name,
the default "Amount" slider label, the variation names and the starter shader's comment header
become the user's own editable text, so they are now written in the UI language at creation,
like the import fix in [759].
A new draft that was not edited no longer asks before it is discarded (it did in both languages
before), and the self-test's untranslated-text scan skips user content marked with
`data-user-text` / `data-user-attr`, so saved preset names are no longer flagged.
- **Imported presets follow the app language [759].** A `.milk`, Shadertoy or ISF file imported
through Studio got a Turkish description even in English, and the import notes stayed Turkish.
The description is the user's own editable text once saved, so it is now written in the UI
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ layers and **40 GPU effects**, an **OBS overlay**, **Spout / Syphon**, **project
[![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#download)
[![Tests](https://img.shields.io/badge/tests-3183%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3189%20passing-2ea043.svg)](#tests)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/Download-Windows-0a84ff?style=for-the-badge" alt="Download for Windows" /></a>
Expand Down Expand Up @@ -1741,7 +1741,7 @@ npm test
npm start -- --smoke
```

**3183 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3189 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 staying
Expand Down
4 changes: 2 additions & 2 deletions README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ destesi**, ve **MCP üzerinden yapay zekâ kontrolü**.
[![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases)
[![Lisans: MIT](https://img.shields.io/badge/Lisans-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#hızlı-başlangıç)
[![Test](https://img.shields.io/badge/test-3183%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3189%20geçiyor-2ea043.svg)](#testler)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/%C4%B0ndir-Windows-0a84ff?style=for-the-badge" alt="Windows için indir" /></a>
Expand Down Expand Up @@ -1744,7 +1744,7 @@ npm test
npm start -- --smoke
```

**3183 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**3189 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ı kalması ve görüş
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ npm test
npm start -- --smoke
```

- **3183 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **3189 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 — 1597 at
that tag — 1402 more with v3.1.5 — 2999 at that tag — and 27
Expand Down
46 changes: 29 additions & 17 deletions src/admin/studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@
let compileState = { ok: true, message: '', line: 0 };
let compileTimer = null;

const STARTER_SHADER = `// Kendi görselleştiricin.
// Kullanabileceklerin:
// sv_resolution, sv_time, sv_level, sv_bass, sv_mid, sv_treble, sv_beat
// sv_spec(x) -> 0..1 konumundaki spektrum değeri
// sv_waveAt(x) -> dalga formu (-1..1)
// sv_col(x) -> senin 5 renkli paletinden renk
// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video
// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
/* Şablonun başlık yorumu kullanıcının düzenleyeceği koda giriyor; içe aktarılan
açıklamalar gibi (#759) oluşturulurken arayüz dilinde yazılıyor. Satırlar
ayrı çevriliyor: birleşik metin sözlükte eşleşmez. */
const STARTER_HEADER = [
'// Kendi görselleştiricin.',
'// Kullanabileceklerin:',
'// sv_resolution, sv_time, sv_level, sv_bass, sv_mid, sv_treble, sv_beat',
'// sv_spec(x) -> 0..1 konumundaki spektrum değeri',
'// sv_waveAt(x) -> dalga formu (-1..1)',
'// sv_col(x) -> senin 5 renkli paletinden renk',
'// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video',
'// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).',
];
const STARTER_BODY = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * sv_resolution) / min(sv_resolution.x, sv_resolution.y);
float r = length(uv);
float a = atan(uv.y, uv.x);
Expand All @@ -54,6 +58,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {

fragColor = vec4(col, clamp(ring + sv_bass * 0.2, 0.0, 1.0));
}`;
const starterShader = () => STARTER_HEADER.map(trStudio).join('\n') + '\n\n' + STARTER_BODY;

// ==========================================================================
// GLSL renklendirme
Expand Down Expand Up @@ -224,12 +229,14 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// ==========================================================================
function newShader(kind) {
return IS().normalize({
name: kind === 'background' ? 'Yeni Arkaplan' : 'Yeni Görselleştirici',
/* Ad ve etiket kullanıcının kendi düzenlenebilir metni oluyor; editör onu
çevirmiyor. Bu yüzden oluşturulurken arayüz dilinde yazılıyor (#759 gibi). */
name: trStudio(kind === 'background' ? 'Yeni Arkaplan' : 'Yeni Görselleştirici'),
kind,
engine: 'shader',
shader: STARTER_SHADER,
shader: starterShader(),
controls: [
{ name: 'uAmount', label: 'Miktar', type: 'slider', min: 0, max: 2, step: 0.01, default: 1 },
{ name: 'uAmount', label: trStudio('Miktar'), type: 'slider', min: 0, max: 2, step: 0.01, default: 1 },
],
});
}
Expand All @@ -250,7 +257,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
function newVariation(kind) {
const look = currentLook(P().cfg(), kind);
return IS().normalize({
name: kind === 'background' ? 'Arkaplan Varyasyonum' : 'Görselleştiricim',
name: trStudio(kind === 'background' ? 'Arkaplan Varyasyonum' : 'Görselleştiricim'),
kind,
engine: 'variation',
base: kind === 'background' ? look.background.type : look.visualizer.type,
Expand Down Expand Up @@ -342,7 +349,10 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
if (!(await confirmDiscard())) return;
draft = make();
selectedId = null;
dirty = true;
/* Dokunulmamış yeni taslak "değişmiş" sayılmaz: atılırken sormaya gerek
yok. Eskiden burada true'ydu ve TR/EN fark etmeden onay çıkıyordu.
Kaydedilmemiş olduğu yine belli: Kaydet düğmesi !selectedId ile vurgulu. */
dirty = false;
compileNow();
P().rerender();
}
Expand Down Expand Up @@ -552,10 +562,12 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
class: 'studio-item' + (p.id === selectedId ? ' active' : ''),
type: 'button',
title: trStudio(p.description || p.name),
// Kendi presetinin adı/açıklaması kullanıcı içeriği: öz test çeviri aramaz
...(p.builtin ? {} : { 'data-user-attr': '' }),
onclick: () => guardedSelect(p.id),
},
[
el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name }),
el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name, ...(p.builtin ? {} : { 'data-user-text': '' }) }),
el('span', { class: 'si-tag', text: trStudio(p.builtin ? 'yerleşik' : p.engine === 'shader' ? 'shader' : 'varyasyon') }),
]
)
Expand Down Expand Up @@ -760,7 +772,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
}

const actions = el('div', { class: 'studio-actions' }, [
el('button', { id: 'studioSave', class: 'btn ' + (dirty ? 'primary' : ''), type: 'button', icon: 'save', text: 'Kaydet', onclick: saveDraft }),
el('button', { id: 'studioSave', class: 'btn ' + (dirty || !selectedId ? 'primary' : ''), type: 'button', icon: 'save', text: 'Kaydet', onclick: saveDraft }),
el('button', { class: 'btn', type: 'button', icon: 'play', text: 'Sahnede Kullan', onclick: applyToScene }),
el('button', {
class: 'btn ghost small', type: 'button', icon: 'copy', text: 'Çoğalt',
Expand Down
5 changes: 5 additions & 0 deletions src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7053,7 +7053,11 @@ async function runSmoke() {
// Yer tutucu ve başlık metinleri de taranır: gözle görünür oldukları
// halde metin düğümü olmadıkları için kolayca gözden kaçarlar.
var attrs = document.querySelectorAll('#sections [placeholder], #sections [title]');
/* Kullanıcı içeriği (preset, sahne, katman, klip adları) çevrilmez ve
taranmaz: onu çizen öğe data-user-text (metni ve altı) ya da
data-user-attr (yalnız title/placeholder) ile işaretlenir. */
for (var ai = 0; ai < attrs.length; ai++) {
if (attrs[ai].closest('[data-user-attr],[data-user-text]')) continue;
var av = (attrs[ai].getAttribute('placeholder') || '') + ' ' + (attrs[ai].getAttribute('title') || '');
if (/[çğıöşüÇĞİÖŞÜ]/.test(av)) out.push(av.trim().slice(0, 90));
}
Expand All @@ -7067,6 +7071,7 @@ async function runSmoke() {
while (el && el.id !== 'sections') {
for (var k = 0; k < skip.length; k++) if (el.classList && el.classList.contains(skip[k])) skipIt = true;
if (el.tagName === 'OPTION' || el.tagName === 'INPUT') skipIt = true;
if (el.hasAttribute && el.hasAttribute('data-user-text')) skipIt = true;
el = el.parentElement;
}
if (!skipIt) out.push(text.slice(0, 90));
Expand Down
8 changes: 8 additions & 0 deletions src/shared/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2870,6 +2870,14 @@
'Yeni Görselleştirici': 'New Visualizer',
'Arkaplan Varyasyonum': 'My Background Variation',
'Görselleştiricim': 'My Visualizer',
// Yeni shader şablonunun başlık yorumu (satır satır)
'// Kendi görselleştiricin.': '// Your own visualizer.',
'// Kullanabileceklerin:': '// What you can use:',
'// sv_spec(x) -> 0..1 konumundaki spektrum değeri': '// sv_spec(x) -> spectrum value at position 0..1',
'// sv_waveAt(x) -> dalga formu (-1..1)': '// sv_waveAt(x) -> waveform (-1..1)',
'// sv_col(x) -> senin 5 renkli paletinden renk': '// sv_col(x) -> a color from your 5-color palette',
'// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video': '// sv_prev -> the previous frame (feedback), sv_media -> camera/video',
'// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).': '// You can add your own sliders below too (Parameters section).',
'Dışa aktarılacak preset yok.': 'No presets to export.',
'CAYADEV Preset Paketi': 'CAYADEV Preset Pack',
'Dosyaya yazıldı.': 'Written to file.',
Expand Down
100 changes: 100 additions & 0 deletions tests/studio-new-preset-i18n.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';
/* STUDIO'DA YENİ PRESET VARSAYILANLARI ARAYÜZ DİLİNDE.
*
* İngilizce arayüzde "+ Shader" yeni görselleştiriciyi "Yeni Görselleştirici"
* adıyla açıyordu; değişiklikleri at onayı da bu adı gösteriyordu. Ad, etiket
* ve şablonun başlık yorumu kullanıcının kendi düzenlenebilir metni oluyor
* ve editör onu çevirmiyor; bu yüzden oluşturulurken çevriliyorlar (#759 gibi).
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');

const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8').replace(/\r\n/g, '\n');
const norm = (v) => String(v).replace(/\s+/g, ' ').trim();

function dictionary() {
const src = read('src/shared/i18n.js');
const body = src.slice(src.indexOf('const EN = {'), src.indexOf('const EN_NORMALIZED'));
const STR = "'((?:[^'\\\\]|\\\\.)*)'";
const re = new RegExp(STR + '\\s*:\\s*' + STR, 'g');
const unesc = (s) => s.replace(/\\(.)/g, '$1');
const map = new Map();
let m;
while ((m = re.exec(body))) map.set(norm(unesc(m[1])), unesc(m[2]));
return map;
}

const studio = read('src/admin/studio.js');
const fnBody = (name) => {
const i = studio.indexOf('function ' + name + '(');
assert.ok(i >= 0, name + ' bulunamadı');
return studio.slice(i, studio.indexOf('\n }\n', i));
};

test('yeni shader ve varyasyon adları ile etiketi çevrilerek yazılıyor', () => {
const d = dictionary();
const shader = fnBody('newShader');
const variation = fnBody('newVariation');
assert.match(shader, /name: trStudio\(kind === 'background' \? 'Yeni Arkaplan' : 'Yeni Görselleştirici'\)/);
assert.match(shader, /label: trStudio\('Miktar'\)/);
assert.match(shader, /shader: starterShader\(\)/);
assert.match(variation, /name: trStudio\(kind === 'background' \? 'Arkaplan Varyasyonum' : 'Görselleştiricim'\)/);
for (const k of ['Yeni Arkaplan', 'Yeni Görselleştirici', 'Miktar', 'Arkaplan Varyasyonum', 'Görselleştiricim']) {
assert.ok(d.has(k), 'çevirisi yok: ' + k);
}
});

test('şablon başlık yorumunun Türkçe her satırının İngilizcesi var', () => {
const d = dictionary();
const m = /const STARTER_HEADER = \[([\s\S]*?)\];/.exec(studio);
assert.ok(m, 'STARTER_HEADER bulunamadı');
const lines = [...m[1].matchAll(/'((?:[^'\\]|\\.)*)'/g)].map((x) => x[1]);
assert.ok(lines.length >= 6);
for (const l of lines) {
assert.ok(l.startsWith('//'), 'yorum değil: ' + l);
if (!/[^\x00-\x7f]|[a-z]+ [a-z]+ [a-z]/i.test(l.replace(/sv_\w+(\(x\))?,?/g, ''))) continue;
const en = d.get(norm(l));
assert.ok(en, 'çevirisi yok: ' + l);
assert.ok(en.startsWith('//'), 'çeviri yorum olarak kalmalı: ' + en);
// Hizalı kolonlar korunsun: "->" aynı sütunda
if (l.includes('->')) assert.strictEqual(en.indexOf('->'), l.indexOf('->'), en);
}
assert.ok(!/STARTER_SHADER/.test(studio), 'eski sabit şablon kalmamalı');
});

test('i18n sözlüğünde yinelenen anahtar yok (yeni satırlar)', () => {
const src = read('src/shared/i18n.js');
for (const k of ['// Kendi görselleştiricin.', '// Kullanabileceklerin:', 'Yeni Görselleştirici']) {
const n = src.split("'" + k + "':").length - 1;
assert.strictEqual(n, 1, k + ' ' + n + ' kez');
}
});

/* Öz testin "çevrilmemiş metin" taraması kullanıcının kendi adlarını
(ör. kaydedilmiş "A Yeni Görselleştirici") işaretliyordu. Kullanıcı içeriği
genel bir öznitelikle hariç tutulur; tek tek metin istisnası yok. */
test('öz test çeviri taraması kullanıcı içeriği özniteliklerini atlıyor', () => {
const main = read('src/main/main.js');
const i = main.indexOf("untranslated string(s) in English UI");
const scan = main.slice(main.lastIndexOf('createTreeWalker(document.getElementById', i) - 1500, i);
assert.match(scan, /closest\('\[data-user-attr\],\[data-user-text\]'\)\) continue;/);
assert.match(scan, /hasAttribute\('data-user-text'\)\) skipIt = true;/);
assert.doesNotMatch(scan, /Yeni Görselleştirici/, 'metne özel istisna olmamalı');
});

test('Studio listesi kendi preset adlarını kullanıcı içeriği olarak işaretliyor', () => {
assert.match(studio, /class: 'si-name', text: p\.builtin \? trStudio\(p\.name\) : p\.name, \.\.\.\(p\.builtin \? \{\} : \{ 'data-user-text': '' \}\)/);
assert.match(studio, /\.\.\.\(p\.builtin \? \{\} : \{ 'data-user-attr': '' \}\)/);
});

test('dokunulmamış yeni taslak atılırken onay sorulmuyor', () => {
const body = fnBody('startDraft');
assert.match(body, /dirty = false;/);
assert.doesNotMatch(body, /dirty = true/);
const confirm = fnBody('confirmDiscard');
assert.match(confirm, /if \(!dirty \|\| !draft\) return true;/);
// Kaydedilmemiş yeni taslakta Kaydet yine vurgulu
assert.match(studio, /class: 'btn ' \+ \(dirty \|\| !selectedId \? 'primary' : ''\)/);
});
Loading