FEAT: menu Help (About modal + hướng dẫn sử dụng) + Tools Preferences (theme/language/button font size)
This commit is contained in:
+220
-2
@@ -6469,6 +6469,155 @@ const SystemManagerModal = ({
|
||||
}, "Xóa")))))))));
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
ABOUT / HELP / PREFERENCES MODALS (menu Help + Tools → Preferences)
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
const APP_VERSION = '1.0.0'; // khớp src-tauri/tauri.conf.json
|
||||
|
||||
const AboutModal = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-cyan-500 to-violet-600 flex items-center justify-center text-white text-xl font-black shadow-lg">SF</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">SonicForge Studio</h2>
|
||||
<p className="text-xs text-zinc-400">Professional DAW — v{APP_VERSION}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p className="text-zinc-300 leading-relaxed">Phần mềm sản xuất âm nhạc (DAW) — soạn nhạc, ghi âm, chỉnh sửa MIDI/Audio, SoundFont & VST, trộn và master.</p>
|
||||
<div className="pt-2 border-t border-zinc-800 space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Developer</span><span className="text-cyan-400 font-semibold">Lộc Phạm</span></div>
|
||||
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Email</span><a href="mailto:tranloclqd@gmail.com" className="text-cyan-400 hover:underline">tranloclqd@gmail.com</a></div>
|
||||
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Version</span><span className="text-zinc-300">{APP_VERSION}</span></div>
|
||||
<div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Build</span><span className="text-zinc-300">Standalone (Tauri v2 + PyInstaller)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">Đóng</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HelpModal = ({ isOpen, onClose, lang }) => {
|
||||
if (!isOpen) return null;
|
||||
const vi = lang !== 'en';
|
||||
const sections = vi ? [
|
||||
{ title: '🚀 Bắt đầu nhanh', body: 'Nhấn phím Cách (Space) để Play/Pause. Nhấn phím / để dừng. Dùng nút Record (●) để ghi âm/MIDI. Tạo track mới từ menu Track hoặc nút "+ Add Track".' },
|
||||
{ title: '🎹 MIDI & ARM', body: 'Bật nút ARM đỏ trên track để nhận phím từ MIDI keyboard. Vào menu Tools → MIDI Devices để chọn thiết bị. Khi ARM + bấm phím, âm preview phát và VU meter nhảy theo trường độ.' },
|
||||
{ title: '🎼 Piano Roll', body: 'Nhấp đúp vào MIDI item để mở Piano Roll. Dùng Ctrl+scroll hoặc nút +/− để zoom. Nhấp để vẽ note, kéo để di chuyển, Ctrl+kéo để copy. Phím tắt: C (vẽ), E (tẩy), Space (nghe).' },
|
||||
{ title: '🎛️ FX & Master', body: 'Chọn track → nút FX để mở FX Rack (thêm EQ, compressor...). Bấm nút PWR ở Master để mở Mastering Panel: EQ 4 băng, compressor, limiter. Mọi thay đổi áp dụng realtime.' },
|
||||
{ title: '🧪 SoundFont & VST', body: 'Vào Tools → Plugin Manager để quét SoundFont (.sf2/.sf3) và VSTi. Track MIDI dùng SoundFont làm nhạc cụ — chọn instrument từ nút Synth trên track.' },
|
||||
{ title: '🤖 AI', body: 'Tools → Config AI Providers để cấu hình API (OpenAI, Gemini, Ollama...). Dùng AI Prompt Generator (nút ✨) để sinh MIDI/ý tưởng; AI MIDI Preset Manager để lưu preset.' },
|
||||
{ title: '💾 Lưu & Xuất', body: 'Ctrl+S lưu project (cloud/temp), Ctrl+Shift+S Save As. Export WAV qua nút Export — chọn vùng, format, bitrate rồi Render.' },
|
||||
{ title: '⌨️ Phím tắt', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
|
||||
] : [
|
||||
{ title: '🚀 Quick start', body: 'Press Space to Play/Pause. Press / to stop. Use Record (●) to capture audio/MIDI. Add tracks from the Track menu or the "+ Add Track" button.' },
|
||||
{ title: '🎹 MIDI & ARM', body: 'Enable the red ARM button on a track to receive keys from a MIDI keyboard. Go to Tools → MIDI Devices to pick your device. When ARM + key press, preview audio plays and the VU meter animates for the note length.' },
|
||||
{ title: '🎼 Piano Roll', body: 'Double-click a MIDI item to open the Piano Roll. Use Ctrl+scroll or +/− buttons to zoom. Click to draw notes, drag to move, Ctrl+drag to copy. Shortcuts: C (draw), E (erase), Space (listen).' },
|
||||
{ title: '🎛️ FX & Master', body: 'Select a track → FX button to open the FX Rack (EQ, compressor...). Press the PWR button on the Master to open the Mastering Panel: 4-band EQ, compressor, limiter — all realtime.' },
|
||||
{ title: '🧪 SoundFont & VST', body: 'Tools → Plugin Manager scans SoundFonts (.sf2/.sf3) and VSTi. MIDI tracks use SoundFonts as instruments — pick one from the Synth button on the track.' },
|
||||
{ title: '🤖 AI', body: 'Tools → Config AI Providers to set up APIs (OpenAI, Gemini, Ollama...). Use the AI Prompt Generator (✨) to create MIDI/ideas; AI MIDI Preset Manager stores presets.' },
|
||||
{ title: '💾 Save & Export', body: 'Ctrl+S saves the project (cloud/temp), Ctrl+Shift+S Save As. Export WAV via the Export button — pick range, format, bitrate, then Render.' },
|
||||
{ title: '⌨️ Shortcuts', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
|
||||
];
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col text-slate-200" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0">
|
||||
<h2 className="text-base font-bold text-white">{vi ? 'Hướng dẫn sử dụng SonicForge Studio' : 'SonicForge Studio User Guide'}</h2>
|
||||
<button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm">✕</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto px-5 py-4 space-y-3">
|
||||
{sections.map((s, i) => (
|
||||
<div key={i} className="bg-zinc-900/60 border border-zinc-800 rounded-lg p-3">
|
||||
<h3 className="text-sm font-bold text-cyan-400 mb-1">{s.title}</h3>
|
||||
<p className="text-xs text-zinc-300 leading-relaxed">{s.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-5 py-3 border-t border-zinc-800 flex justify-end shrink-0">
|
||||
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Đóng' : 'Close'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PreferencesModal = ({ isOpen, onClose, prefs, onPrefsChange }) => {
|
||||
if (!isOpen) return null;
|
||||
const vi = prefs.language !== 'en';
|
||||
const themes = [
|
||||
{ id: 'dark', name: vi ? 'Tối (mặc định)' : 'Dark (default)', swatch: 'from-zinc-700 to-zinc-900', ring: 'ring-cyan-400' },
|
||||
{ id: 'midnight', name: vi ? 'Đêm xanh' : 'Midnight', swatch: 'from-sky-800 to-slate-950', ring: 'ring-sky-400' },
|
||||
{ id: 'forest', name: vi ? 'Rừng xanh' : 'Forest', swatch: 'from-emerald-700 to-green-950', ring: 'ring-emerald-400' },
|
||||
{ id: 'violet', name: vi ? 'Tím' : 'Violet', swatch: 'from-violet-700 to-purple-950', ring: 'ring-violet-400' },
|
||||
{ id: 'graphite', name: vi ? 'Than chì' : 'Graphite', swatch: 'from-zinc-600 to-neutral-900', ring: 'ring-zinc-300' },
|
||||
];
|
||||
const set = (k, v) => onPrefsChange({ ...prefs, [k]: v });
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto p-5 text-slate-200" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-base font-bold text-white">{vi ? 'Tùy chọn (Preferences)' : 'Preferences'}</h2>
|
||||
<button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm">✕</button>
|
||||
</div>
|
||||
|
||||
{/* Theme */}
|
||||
<div className="mb-5">
|
||||
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🎨 Chủ đề màu' : '🎨 Theme'}</h3>
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{themes.map(th => (
|
||||
<button key={th.id} onClick={() => set('theme', th.id)}
|
||||
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-lg border text-xs text-left transition ${prefs.theme === th.id ? 'border-cyan-500 bg-zinc-800' : 'border-zinc-700 bg-zinc-900 hover:bg-zinc-800'}`}>
|
||||
<span className={`w-6 h-6 rounded-md bg-gradient-to-br ${th.swatch} ring-1 ring-black/40 shrink-0 ${prefs.theme === th.id ? `ring-2 ${th.ring}` : ''}`}></span>
|
||||
<span className="text-zinc-200">{th.name}</span>
|
||||
{prefs.theme === th.id && <span className="ml-auto text-cyan-400 text-[10px] font-bold">✓</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language */}
|
||||
<div className="mb-5">
|
||||
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🌐 Ngôn ngữ' : '🌐 Language'}</h3>
|
||||
<div className="flex gap-1.5">
|
||||
{[{ id: 'vi', label: 'Tiếng Việt' }, { id: 'en', label: 'English' }].map(l => (
|
||||
<button key={l.id} onClick={() => set('language', l.id)}
|
||||
className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.language === l.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-zinc-500 mt-1">{vi ? 'Áp dụng ngay cho menu & hướng dẫn.' : 'Applies immediately to menus & guide.'}</p>
|
||||
</div>
|
||||
|
||||
{/* Button font size */}
|
||||
<div className="mb-5">
|
||||
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🔠 Cỡ chữ nút bấm' : '🔠 Button font size'}</h3>
|
||||
<div className="flex gap-1.5">
|
||||
{[{ id: 'sm', label: vi ? 'Nhỏ' : 'Small' }, { id: 'md', label: vi ? 'Vừa' : 'Medium' }, { id: 'lg', label: vi ? 'Lớn' : 'Large' }].map(f => (
|
||||
<button key={f.id} onClick={() => set('buttonFontSize', f.id)}
|
||||
className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.buttonFontSize === f.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-zinc-800">
|
||||
<button onClick={onClose} className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded text-xs font-bold text-white transition">{vi ? 'Hủy' : 'Cancel'}</button>
|
||||
<button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Lưu & Đóng' : 'Save & Close'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -15562,6 +15711,51 @@ const App = () => {
|
||||
const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
|
||||
const [aiPresetVersion, setAiPresetVersion] = useState(0);
|
||||
const [showMasteringModal, setShowMasteringModal] = useState(false);
|
||||
// ── Help / About / Preferences (menu Help + Tools) ──
|
||||
const [aboutModalOpen, setAboutModalOpen] = useState(false);
|
||||
const [helpModalOpen, setHelpModalOpen] = useState(false);
|
||||
const [preferencesModalOpen, setPreferencesModalOpen] = useState(false);
|
||||
const [prefs, setPrefs] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('sf_prefs');
|
||||
if (saved) return JSON.parse(saved);
|
||||
} catch (e) { }
|
||||
return { theme: 'dark', language: 'vi', buttonFontSize: 'md' };
|
||||
});
|
||||
// Áp dụng theme + font size lên <html> ngay khi đổi
|
||||
useEffect(() => {
|
||||
try {
|
||||
document.documentElement.setAttribute('data-theme', prefs.theme || 'dark');
|
||||
document.documentElement.setAttribute('data-btnfont', prefs.buttonFontSize || 'md');
|
||||
localStorage.setItem('sf_prefs', JSON.stringify(prefs));
|
||||
} catch (e) { }
|
||||
}, [prefs]);
|
||||
// Load preferences từ server (nếu có tài khoản) khi khởi động
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (window.SonicAPI && window.SonicAPI.getPreferences) {
|
||||
window.SonicAPI.getPreferences().then(res => {
|
||||
if (cancelled || !res || !res.success || !res.preferences) return;
|
||||
const p = res.preferences;
|
||||
if (p.theme || p.language || p.buttonFontSize) {
|
||||
setPrefs(prev => ({
|
||||
theme: p.theme || prev.theme,
|
||||
language: p.language || prev.language,
|
||||
buttonFontSize: p.buttonFontSize || prev.buttonFontSize
|
||||
}));
|
||||
}
|
||||
}).catch(() => { });
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
// Lưu preferences lên server khi đổi
|
||||
const handlePrefsChange = (next) => {
|
||||
setPrefs(next);
|
||||
try { localStorage.setItem('sf_prefs', JSON.stringify(next)); } catch (e) { }
|
||||
if (window.SonicAPI && window.SonicAPI.savePreferences) {
|
||||
window.SonicAPI.savePreferences(next).catch(() => { });
|
||||
}
|
||||
};
|
||||
// Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null
|
||||
const [fxRackTarget, setFxRackTarget] = useState(null);
|
||||
window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });
|
||||
@@ -26282,13 +26476,25 @@ STRICT CONSTRAINTS:
|
||||
setPluginManagerModalOpen(true);
|
||||
window.SonicAPI.listPlugins().then(data => setPluginsData(data)).catch(() => {});
|
||||
}
|
||||
}, {
|
||||
sep: true
|
||||
}, {
|
||||
label: 'Preferences...',
|
||||
icon: 'settings-2',
|
||||
action: () => setPreferencesModalOpen(true)
|
||||
}]
|
||||
}, {
|
||||
label: 'Help',
|
||||
items: [{
|
||||
label: 'About SonicForge',
|
||||
label: 'Hướng dẫn sử dụng...',
|
||||
icon: 'book-open',
|
||||
action: () => setHelpModalOpen(true)
|
||||
}, {
|
||||
sep: true
|
||||
}, {
|
||||
label: 'About SonicForge Studio...',
|
||||
icon: 'info',
|
||||
action: () => showToast('SonicForge Studio v1.0 - Professional DAW', 'info')
|
||||
action: () => setAboutModalOpen(true)
|
||||
}]
|
||||
}].map(menu => /*#__PURE__*/React.createElement("div", {
|
||||
key: menu.label,
|
||||
@@ -29264,6 +29470,18 @@ STRICT CONSTRAINTS:
|
||||
const active = providers.find(p => p.is_active) || providers[0];
|
||||
if (active) setSelectedProviderId(active.id);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(AboutModal, {
|
||||
isOpen: aboutModalOpen,
|
||||
onClose: () => setAboutModalOpen(false)
|
||||
}), /*#__PURE__*/React.createElement(HelpModal, {
|
||||
isOpen: helpModalOpen,
|
||||
onClose: () => setHelpModalOpen(false),
|
||||
lang: prefs.language
|
||||
}), /*#__PURE__*/React.createElement(PreferencesModal, {
|
||||
isOpen: preferencesModalOpen,
|
||||
onClose: () => setPreferencesModalOpen(false),
|
||||
prefs: prefs,
|
||||
onPrefsChange: handlePrefsChange
|
||||
}), /*#__PURE__*/React.createElement(SystemManagerModal, {
|
||||
isOpen: systemManagerModalOpen,
|
||||
onClose: () => setSystemManagerModalOpen(false)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -43,7 +43,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608081400" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608081500" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
@@ -52,8 +52,43 @@
|
||||
--top-bar-height: 80px;
|
||||
--status-bar-height: 25px;
|
||||
--panel-border-color: #2a2a2a;
|
||||
/* Theme (Preferences) — có thể override bằng data-theme trên <html> */
|
||||
--sf-bg: #1e1e1e;
|
||||
--sf-panel: #262626;
|
||||
--sf-header: #2e2e2e;
|
||||
--sf-border: #181818;
|
||||
--sf-accent: #00ffcc;
|
||||
}
|
||||
|
||||
/* ── THEME presets (Tools → Preferences) ── */
|
||||
html[data-theme="dark"] {
|
||||
--sf-bg: #1e1e1e; --sf-panel: #262626; --sf-header: #2e2e2e; --sf-border: #181818; --sf-accent: #00ffcc;
|
||||
}
|
||||
html[data-theme="midnight"] {
|
||||
--sf-bg: #0f172a; --sf-panel: #1e293b; --sf-header: #1e293b; --sf-border: #0f172a; --sf-accent: #38bdf8;
|
||||
}
|
||||
html[data-theme="forest"] {
|
||||
--sf-bg: #111c15; --sf-panel: #1b2a1e; --sf-header: #1e2d21; --sf-border: #0c1510; --sf-accent: #34d399;
|
||||
}
|
||||
html[data-theme="violet"] {
|
||||
--sf-bg: #170f26; --sf-panel: #221537; --sf-header: #251640; --sf-border: #110a1c; --sf-accent: #a78bfa;
|
||||
}
|
||||
html[data-theme="graphite"] {
|
||||
--sf-bg: #18181b; --sf-panel: #232327; --sf-header: #27272a; --sf-border: #101012; --sf-accent: #e4e4e7;
|
||||
}
|
||||
|
||||
/* Áp theme lên các vùng chính của app shell */
|
||||
.daw-app-shell { background-color: var(--sf-bg) !important; }
|
||||
.daw-panel { background-color: var(--sf-panel) !important; }
|
||||
.daw-header { background-color: var(--sf-header) !important; }
|
||||
body { background-color: var(--sf-bg) !important; }
|
||||
::-webkit-scrollbar-thumb { background: var(--sf-accent); }
|
||||
|
||||
/* ── BUTTON FONT SIZE (Tools → Preferences) ── */
|
||||
html[data-btnfont="sm"] button { font-size: 10px !important; }
|
||||
html[data-btnfont="md"] button { font-size: 12px !important; }
|
||||
html[data-btnfont="lg"] button { font-size: 14px !important; }
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #c0c0c0;
|
||||
|
||||
Reference in New Issue
Block a user