feat: STM32 Blue Pill 에뮬레이터 UI — 보드 SVG + GPIO LED 시각화
- Blue Pill SVG: 핀 서클(r=3), 폰트 4px bold, 온보드 LED/PWR/RST 버튼 확대 - GPIO 텔레메트리: _RN_PNAME()+Serial.write(STX/ETX)로 digitalWrite() 후 핀 상태 전송 - PA0 등 아날로그 핀 번호(0xC0=192) → digitalPinToPinName()으로 런타임 복원 - PC13 toggle 시 핀 서클 + 온보드 LED 동시 업데이트 (active LOW) - PA9(TX)/PA10(RX) 시리얼 신호 깜빡임 - 코드 에디터 AI에 선택된 보드 이름(boardContext) 전달 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -456,6 +456,45 @@ input.tb-num{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8c
|
||||
}
|
||||
.drum-back-btn:hover{background:rgba(147,51,234,.3);border-color:var(--studio-accent2);color:#e9d5ff;}
|
||||
|
||||
/* ── Folder popup ── */
|
||||
#fp-overlay{display:none;position:fixed;inset:0;z-index:950;background:rgba(0,0,0,.6);backdrop-filter:blur(3px);}
|
||||
#fp-overlay.open{display:block;}
|
||||
#fp-sheet{
|
||||
position:absolute;bottom:0;left:0;right:0;
|
||||
max-height:72vh;background:var(--studio-panel);
|
||||
border-radius:16px 16px 0 0;
|
||||
border-top:1px solid var(--studio-border2);
|
||||
display:flex;flex-direction:column;overflow:hidden;
|
||||
box-shadow:0 -8px 40px rgba(0,0,0,.5);
|
||||
transform:translateY(100%);transition:transform .25s ease;
|
||||
}
|
||||
#fp-overlay.open #fp-sheet{transform:translateY(0);}
|
||||
@media(min-width:1025px){
|
||||
#fp-sheet{
|
||||
position:absolute;top:50%;left:50%;bottom:auto;right:auto;
|
||||
width:400px;max-height:65vh;
|
||||
border-radius:14px;border:1px solid var(--studio-border2);
|
||||
box-shadow:0 20px 60px rgba(0,0,0,.6);
|
||||
transform:translate(-50%,-46%);transition:none;
|
||||
}
|
||||
#fp-overlay.open #fp-sheet{transform:translate(-50%,-50%);}
|
||||
}
|
||||
.fp-hdr{display:flex;align-items:center;padding:13px 16px 10px;flex-shrink:0;border-bottom:1px solid var(--studio-border);gap:6px;}
|
||||
.fp-title{font-size:13px;font-weight:700;color:var(--studio-text);flex:1;}
|
||||
.fp-icon-btn{background:none;border:none;color:var(--studio-muted);cursor:pointer;font-size:14px;padding:3px 6px;border-radius:6px;transition:.15s;font-family:inherit;}
|
||||
.fp-icon-btn:hover{color:var(--studio-accent2);background:var(--studio-glow2);}
|
||||
.fp-tabs{display:flex;gap:4px;padding:8px 12px 6px;flex-shrink:0;}
|
||||
.fp-tab{flex:1;padding:6px;font-size:11px;font-weight:700;border:1px solid var(--studio-border2);border-radius:8px;background:transparent;color:var(--studio-muted);cursor:pointer;font-family:inherit;transition:.2s;}
|
||||
.fp-tab.active{background:var(--studio-accent);border-color:var(--studio-accent);color:#fff;box-shadow:0 0 10px var(--studio-glow);}
|
||||
.fp-body{flex:1;overflow-y:auto;padding:4px 8px 14px;min-height:0;}
|
||||
.fp-item{display:flex;align-items:center;gap:6px;padding:7px 8px;border-radius:8px;transition:.15s;}
|
||||
.fp-item:hover{background:var(--studio-glow2);}
|
||||
.fp-item-icon{font-size:15px;flex-shrink:0;}
|
||||
.fp-item-name{flex:1;font-size:12px;color:var(--studio-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;}
|
||||
.fp-item-del{background:none;border:none;color:var(--studio-muted);cursor:pointer;font-size:13px;padding:2px 5px;border-radius:4px;flex-shrink:0;transition:.15s;line-height:1;}
|
||||
.fp-item-del:hover{color:var(--studio-red);}
|
||||
.fp-empty{font-size:12px;color:var(--studio-muted);padding:24px 12px;text-align:center;}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media(max-width:1024px){
|
||||
.ms-hdr{padding:0 10px;gap:6px;}
|
||||
@@ -564,6 +603,7 @@ input.tb-num{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8c
|
||||
<div class="ms-divider"></div>
|
||||
<div class="ms-file-hdr">
|
||||
<span>MIDI 다운로드</span>
|
||||
<button onclick="openFolderPopup('midi')" title="저장된 파일 보기" style="font-size:13px;padding:1px 6px;border:1px solid var(--studio-border2);border-radius:6px;background:var(--studio-panel2);color:var(--studio-muted);cursor:pointer;transition:.15s;" onmouseover="this.style.borderColor='var(--studio-accent2)';this.style.color='var(--studio-accent2)'" onmouseout="this.style.borderColor='var(--studio-border2)';this.style.color='var(--studio-muted)'">📂</button>
|
||||
</div>
|
||||
<!-- MIDI download site picker -->
|
||||
<div style="padding:4px 6px 2px;display:flex;gap:3px;">
|
||||
@@ -581,6 +621,7 @@ input.tb-num{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8c
|
||||
<div class="ms-divider"></div>
|
||||
<div class="ms-file-hdr">
|
||||
<span>Guitar Pro</span>
|
||||
<button onclick="openFolderPopup('gp')" title="저장된 파일 보기" style="font-size:13px;padding:1px 6px;border:1px solid var(--studio-border2);border-radius:6px;background:var(--studio-panel2);color:var(--studio-muted);cursor:pointer;transition:.15s;" onmouseover="this.style.borderColor='var(--studio-accent2)';this.style.color='var(--studio-accent2)'" onmouseout="this.style.borderColor='var(--studio-border2)';this.style.color='var(--studio-muted)'">📂</button>
|
||||
</div>
|
||||
<!-- GP 검색 -->
|
||||
<div style="padding:4px 6px 2px;display:flex;gap:3px;">
|
||||
@@ -4378,7 +4419,106 @@ function meStatus(msg){const el=document.getElementById('status-txt');if(!el)ret
|
||||
document.getElementById('chat-input').focus();
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Folder popup ──────────────────────────────────────────────────────────────
|
||||
let _fpTab = 'midi';
|
||||
|
||||
function openFolderPopup(tab = 'midi') {
|
||||
_fpTab = tab;
|
||||
document.getElementById('fp-overlay').classList.add('open');
|
||||
fpSetTab(tab);
|
||||
fpLoadMidi();
|
||||
fpLoadGp();
|
||||
}
|
||||
|
||||
function closeFolderPopup() {
|
||||
document.getElementById('fp-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
function fpSetTab(tab) {
|
||||
_fpTab = tab;
|
||||
document.getElementById('fp-tab-midi').classList.toggle('active', tab === 'midi');
|
||||
document.getElementById('fp-tab-gp').classList.toggle('active', tab === 'gp');
|
||||
document.getElementById('fp-midi-body').style.display = tab === 'midi' ? '' : 'none';
|
||||
document.getElementById('fp-gp-body').style.display = tab === 'gp' ? '' : 'none';
|
||||
}
|
||||
|
||||
function fpReload() { fpLoadMidi(); fpLoadGp(); }
|
||||
|
||||
async function fpLoadMidi() {
|
||||
const el = document.getElementById('fp-midi-body');
|
||||
el.innerHTML = '<div class="fp-empty">로딩 중...</div>';
|
||||
try {
|
||||
const r = await fetch('/api/music/list', { headers: authH() });
|
||||
const data = await r.json();
|
||||
if (!data.files?.length) { el.innerHTML = '<div class="fp-empty">저장된 MIDI 파일 없음</div>'; return; }
|
||||
el.innerHTML = data.files.map(f => `
|
||||
<div class="fp-item">
|
||||
<span class="fp-item-icon">🎵</span>
|
||||
<span class="fp-item-name" onclick="fpOpenMidi('${escAttr(f.path)}','${escAttr(f.name)}')" title="${escHtml(f.name)}">${escHtml(f.name)}</span>
|
||||
<button class="fp-item-del" onclick="fpDeleteMidi('${escAttr(f.name)}',this)" title="삭제">✕</button>
|
||||
</div>`).join('');
|
||||
} catch { el.innerHTML = '<div class="fp-empty" style="color:var(--studio-red)">로드 실패</div>'; }
|
||||
}
|
||||
|
||||
async function fpLoadGp() {
|
||||
const el = document.getElementById('fp-gp-body');
|
||||
el.innerHTML = '<div class="fp-empty">로딩 중...</div>';
|
||||
try {
|
||||
const r = await fetch('/api/music/gp/list', { headers: authH() });
|
||||
const data = await r.json();
|
||||
if (!data.files?.length) { el.innerHTML = '<div class="fp-empty">저장된 GP 파일 없음</div>'; return; }
|
||||
el.innerHTML = data.files.map(f => `
|
||||
<div class="fp-item">
|
||||
<span class="fp-item-icon">🎸</span>
|
||||
<span class="fp-item-name" onclick="fpOpenGp('${escAttr(f.path)}','${escAttr(f.name)}')" title="${escHtml(f.name)}">${escHtml(f.name)}</span>
|
||||
<button class="fp-item-del" onclick="fpDeleteGp('${escAttr(f.name)}',this)" title="삭제">✕</button>
|
||||
</div>`).join('');
|
||||
} catch { el.innerHTML = '<div class="fp-empty" style="color:var(--studio-red)">로드 실패</div>'; }
|
||||
}
|
||||
|
||||
async function fpOpenMidi(path, name) { closeFolderPopup(); await loadMidiToStage(path, name); }
|
||||
async function fpOpenGp(path, name) { closeFolderPopup(); await loadGpFromServer(path, name); }
|
||||
|
||||
async function fpDeleteMidi(name, btn) {
|
||||
if (!confirm(`"${name}" 삭제할까요?`)) return;
|
||||
try {
|
||||
const r = await fetch('/api/music/workspace-file/' + encodeURIComponent(name), { method: 'DELETE', headers: authH() });
|
||||
if (!r.ok) throw new Error((await r.json()).error || '삭제 실패');
|
||||
btn.closest('.fp-item').remove();
|
||||
loadMidiFiles();
|
||||
} catch(e) { alert('삭제 실패: ' + e.message); }
|
||||
}
|
||||
|
||||
async function fpDeleteGp(name, btn) {
|
||||
if (!confirm(`"${name}" 삭제할까요?`)) return;
|
||||
try {
|
||||
const r = await fetch(`/api/music/gp-file/${encodeURIComponent(name)}`, { method: 'DELETE', headers: authH() });
|
||||
if (!r.ok) throw new Error((await r.json()).error || '삭제 실패');
|
||||
btn.closest('.fp-item').remove();
|
||||
loadGpFileList();
|
||||
} catch(e) { alert('삭제 실패: ' + e.message); }
|
||||
}
|
||||
</script>
|
||||
<!-- ── Folder popup ── -->
|
||||
<div id="fp-overlay" onclick="if(event.target===this)closeFolderPopup()">
|
||||
<div id="fp-sheet">
|
||||
<div class="fp-hdr">
|
||||
<span class="fp-title">📂 저장된 파일</span>
|
||||
<button class="fp-icon-btn" onclick="fpReload()" title="새로고침">↺</button>
|
||||
<button class="fp-icon-btn" onclick="closeFolderPopup()" style="font-size:16px;">✕</button>
|
||||
</div>
|
||||
<div class="fp-tabs">
|
||||
<button class="fp-tab active" id="fp-tab-midi" onclick="fpSetTab('midi')">🎵 MIDI</button>
|
||||
<button class="fp-tab" id="fp-tab-gp" onclick="fpSetTab('gp')">🎸 Guitar Pro</button>
|
||||
</div>
|
||||
<div class="fp-body">
|
||||
<div id="fp-midi-body"><div class="fp-empty">로딩 중...</div></div>
|
||||
<div id="fp-gp-body" style="display:none"><div class="fp-empty">로딩 중...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AudioMass scripts (loaded but not initialized until panel opens) -->
|
||||
<script src="/audiomass/dist/wavesurfer.js"></script>
|
||||
<script src="/audiomass/dist/plugin/wavesurfer.regions.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user