mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
mobile UI
This commit is contained in:
@@ -465,6 +465,8 @@ button {
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid rgba(128, 128, 128, 0.1);
|
border: 1px solid rgba(128, 128, 128, 0.1);
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-item {
|
.help-item {
|
||||||
@@ -781,7 +783,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #2a2a2a;
|
/* background: #2a2a2a; Removed hardcoded background to respect theme */
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
/* Reduced to fit on screen */
|
/* Reduced to fit on screen */
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
|
|||||||
14
src/App.tsx
14
src/App.tsx
@@ -61,7 +61,8 @@ function App() {
|
|||||||
virtualGuitarVolume: 0.5,
|
virtualGuitarVolume: 0.5,
|
||||||
virtualGuitarMute: false,
|
virtualGuitarMute: false,
|
||||||
micSensitivity: 0.5,
|
micSensitivity: 0.5,
|
||||||
disableAnimation: false
|
disableAnimation: false,
|
||||||
|
theme: 'auto'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { pitchData, error, audioLevel, debugInfo, isListening } = usePitchDetector(listening, settings.micSensitivity);
|
const { pitchData, error, audioLevel, debugInfo, isListening } = usePitchDetector(listening, settings.micSensitivity);
|
||||||
@@ -85,6 +86,17 @@ function App() {
|
|||||||
return () => window.removeEventListener('resize', handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Theme support
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const theme = settings.theme || 'auto';
|
||||||
|
if (theme === 'auto') {
|
||||||
|
root.removeAttribute('data-theme');
|
||||||
|
} else {
|
||||||
|
root.setAttribute('data-theme', theme);
|
||||||
|
}
|
||||||
|
}, [settings.theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
setIsFullscreen(!!document.fullscreenElement);
|
setIsFullscreen(!!document.fullscreenElement);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface AppSettings {
|
|||||||
micSensitivity?: number; // 0.0 to 1.0 (0=least sensitive, 1=most)
|
micSensitivity?: number; // 0.0 to 1.0 (0=least sensitive, 1=most)
|
||||||
virtualGuitarMute?: boolean;
|
virtualGuitarMute?: boolean;
|
||||||
disableAnimation?: boolean;
|
disableAnimation?: boolean;
|
||||||
|
theme?: 'light' | 'dark' | 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ export function Fretboard({
|
|||||||
y={paddingY}
|
y={paddingY}
|
||||||
width={width - (paddingX * 2) - nutWidth}
|
width={width - (paddingX * 2) - nutWidth}
|
||||||
height={height - (paddingY * 2)}
|
height={height - (paddingY * 2)}
|
||||||
fill="#999999"
|
fill="var(--color-text-muted)"
|
||||||
|
fillOpacity={0.3}
|
||||||
stroke="none"
|
stroke="none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ export function Fretboard({
|
|||||||
y={paddingY}
|
y={paddingY}
|
||||||
width={nutWidth}
|
width={nutWidth}
|
||||||
height={height - (paddingY * 2)}
|
height={height - (paddingY * 2)}
|
||||||
fill="#333"
|
fill="var(--color-primary)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Fret Markers (Dots) */}
|
{/* Fret Markers (Dots) */}
|
||||||
@@ -120,13 +121,13 @@ export function Fretboard({
|
|||||||
if (isDouble) {
|
if (isDouble) {
|
||||||
return (
|
return (
|
||||||
<g key={`marker-${m}`}>
|
<g key={`marker-${m}`}>
|
||||||
<circle cx={cx} cy={height / 2 - stringSpacing} r={6} fill="#777" />
|
<circle cx={cx} cy={height / 2 - stringSpacing} r={6} fill="var(--color-bg)" />
|
||||||
<circle cx={cx} cy={height / 2 + stringSpacing} r={6} fill="#777" />
|
<circle cx={cx} cy={height / 2 + stringSpacing} r={6} fill="var(--color-bg)" />
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<circle key={`marker-${m}`} cx={cx} cy={height / 2} r={6} fill="#777" />
|
<circle key={`marker-${m}`} cx={cx} cy={height / 2} r={6} fill="var(--color-bg)" />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ export function Fretboard({
|
|||||||
y1={paddingY}
|
y1={paddingY}
|
||||||
x2={x}
|
x2={x}
|
||||||
y2={height - paddingY}
|
y2={height - paddingY}
|
||||||
stroke="#CCCCCC"
|
stroke="var(--color-text-muted)"
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -158,7 +159,7 @@ export function Fretboard({
|
|||||||
y1={y}
|
y1={y}
|
||||||
x2={width - paddingX}
|
x2={width - paddingX}
|
||||||
y2={y}
|
y2={y}
|
||||||
stroke="#EEEEEE"
|
stroke="var(--color-text-main)"
|
||||||
strokeWidth={Math.max(1.5, visualThickness)}
|
strokeWidth={Math.max(1.5, visualThickness)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,12 +157,12 @@ export function PianoKeys({
|
|||||||
Range: {keys.list[0]?.midi} - {keys.list[keys.list.length - 1]?.midi}
|
Range: {keys.list[0]?.midi} - {keys.list[keys.list.length - 1]?.midi}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="view-toggles" style={{ display: 'flex', gap: '8px', background: '#333', padding: '2px', borderRadius: '4px' }}>
|
<div className="view-toggles" style={{ display: 'flex', gap: '8px', background: 'var(--color-surface)', padding: '2px', borderRadius: '4px', border: '1px solid var(--color-text-muted)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('zoomed')}
|
onClick={() => setViewMode('zoomed')}
|
||||||
style={{
|
style={{
|
||||||
background: viewMode === 'zoomed' ? '#666' : 'transparent',
|
background: viewMode === 'zoomed' ? 'var(--color-primary)' : 'transparent',
|
||||||
border: 'none', color: 'white', padding: '2px 8px', borderRadius: '2px', cursor: 'pointer', fontSize: '10px'
|
border: 'none', color: viewMode === 'zoomed' ? 'var(--color-bg)' : 'var(--color-text-main)', padding: '2px 8px', borderRadius: '2px', cursor: 'pointer', fontSize: '10px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Zoom
|
Zoom
|
||||||
@@ -170,8 +170,8 @@ export function PianoKeys({
|
|||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('full')}
|
onClick={() => setViewMode('full')}
|
||||||
style={{
|
style={{
|
||||||
background: viewMode === 'full' ? '#666' : 'transparent',
|
background: viewMode === 'full' ? 'var(--color-primary)' : 'transparent',
|
||||||
border: 'none', color: 'white', padding: '2px 8px', borderRadius: '2px', cursor: 'pointer', fontSize: '10px'
|
border: 'none', color: viewMode === 'full' ? 'var(--color-bg)' : 'var(--color-text-main)', padding: '2px 8px', borderRadius: '2px', cursor: 'pointer', fontSize: '10px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Full
|
Full
|
||||||
@@ -187,8 +187,8 @@ export function PianoKeys({
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflowX: viewMode === 'full' ? 'hidden' : 'auto',
|
overflowX: viewMode === 'full' ? 'hidden' : 'auto',
|
||||||
borderTop: '1px solid #444',
|
borderTop: '1px solid var(--color-text-muted)',
|
||||||
background: '#222',
|
background: 'var(--color-bg)',
|
||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -208,14 +208,14 @@ export function PianoKeys({
|
|||||||
const isMarked = markedNotes.includes(k.midi);
|
const isMarked = markedNotes.includes(k.midi);
|
||||||
|
|
||||||
// Styling
|
// Styling
|
||||||
let fill = k.isBlack ? '#222' : '#fff';
|
let fill = k.isBlack ? 'var(--color-primary)' : 'var(--color-surface)';
|
||||||
if (isMarked) {
|
if (isMarked) {
|
||||||
fill = k.isBlack ? '#d32f2f' : '#ffcdd2'; // Red-ish for marked
|
fill = k.isBlack ? 'var(--color-error)' : 'var(--color-success)'; // Use theme vars
|
||||||
} else if (interactive && hoverMidi === k.midi) {
|
} else if (interactive && hoverMidi === k.midi) {
|
||||||
fill = k.isBlack ? '#444' : '#eee'; // Subtle hover
|
fill = k.isBlack ? 'var(--color-text-muted)' : 'var(--color-bg)'; // Subtle hover
|
||||||
}
|
}
|
||||||
|
|
||||||
const stroke = '#000';
|
const stroke = 'var(--color-text-main)';
|
||||||
const rectHeight = k.isBlack ? BLACK_KEY_HEIGHT_PERCENT * 100 : 100;
|
const rectHeight = k.isBlack ? BLACK_KEY_HEIGHT_PERCENT * 100 : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff } from 'lucide-react';
|
import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff, Sun, Moon, Monitor } from 'lucide-react';
|
||||||
import type { AppSettings } from './Controls';
|
import type { AppSettings } from './Controls';
|
||||||
import type { MicrophoneDebugInfo } from '../hooks/usePitchDetector';
|
import type { MicrophoneDebugInfo } from '../hooks/usePitchDetector';
|
||||||
|
|
||||||
@@ -72,6 +72,47 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|||||||
<button className="help-close" onClick={onClose}><X size={20} /></button>
|
<button className="help-close" onClick={onClose}><X size={20} /></button>
|
||||||
<h2 style={{ marginTop: 0, marginBottom: '24px' }}>Settings</h2>
|
<h2 style={{ marginTop: 0, marginBottom: '24px' }}>Settings</h2>
|
||||||
|
|
||||||
|
{/* Theme Settings */}
|
||||||
|
<div className="control-group" style={{ marginBottom: '24px' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: '8px', fontSize: '16px' }}>
|
||||||
|
<span>Theme</span>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', background: 'rgba(128,128,128,0.1)', padding: '4px', borderRadius: '8px', gap: '4px' }}>
|
||||||
|
{[
|
||||||
|
{ id: 'light', icon: Sun, label: 'Light' },
|
||||||
|
{ id: 'dark', icon: Moon, label: 'Dark' },
|
||||||
|
{ id: 'auto', icon: Monitor, label: 'Auto' }
|
||||||
|
].map(mode => (
|
||||||
|
<button
|
||||||
|
key={mode.id}
|
||||||
|
onClick={() => onUpdateSettings({ ...settings, theme: mode.id as any })}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '8px',
|
||||||
|
border: 'none',
|
||||||
|
background: (settings.theme || 'auto') === mode.id ? 'var(--color-surface)' : 'transparent',
|
||||||
|
color: (settings.theme || 'auto') === mode.id ? 'var(--color-primary)' : 'var(--color-text-muted)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: (settings.theme || 'auto') === mode.id ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '13px',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<mode.icon size={16} />
|
||||||
|
{mode.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style={{ borderColor: 'rgba(255,255,255,0.1)', margin: '24px 0' }} />
|
||||||
|
|
||||||
{/* Animation Settings */}
|
{/* Animation Settings */}
|
||||||
<div className="control-group" style={{ marginBottom: '24px' }}>
|
<div className="control-group" style={{ marginBottom: '24px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
|||||||
@@ -9,17 +9,14 @@
|
|||||||
--hue-text: 30;
|
--hue-text: 30;
|
||||||
/* Warmer text */
|
/* Warmer text */
|
||||||
|
|
||||||
|
/* Default Light Theme */
|
||||||
--color-bg: hsl(30, 20%, 97%);
|
--color-bg: hsl(30, 20%, 97%);
|
||||||
/* Warm paper-like bg */
|
/* Warm paper-like bg */
|
||||||
--color-surface: hsl(0, 0%, 100%);
|
--color-surface: hsl(0, 0%, 100%);
|
||||||
|
|
||||||
/* Neutral / Black Accent */
|
|
||||||
--color-primary: hsl(0, 0%, 20%);
|
--color-primary: hsl(0, 0%, 20%);
|
||||||
--color-primary-dark: hsl(0, 0%, 0%);
|
--color-primary-dark: hsl(0, 0%, 0%);
|
||||||
|
|
||||||
--color-text-main: hsl(var(--hue-text), 10%, 20%);
|
--color-text-main: hsl(var(--hue-text), 10%, 20%);
|
||||||
--color-text-muted: hsl(var(--hue-text), 5%, 60%);
|
--color-text-muted: hsl(var(--hue-text), 5%, 60%);
|
||||||
|
|
||||||
--color-success: hsl(var(--hue-success), 60%, 45%);
|
--color-success: hsl(var(--hue-success), 60%, 45%);
|
||||||
--color-error: hsl(var(--hue-error), 70%, 55%);
|
--color-error: hsl(var(--hue-error), 70%, 55%);
|
||||||
--color-warning: hsl(35, 90%, 50%);
|
--color-warning: hsl(35, 90%, 50%);
|
||||||
@@ -52,18 +49,40 @@
|
|||||||
--radius-lg: 24px;
|
--radius-lg: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Auto Dark Mode (System Preference) */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--color-bg: hsl(30, 20%, 10%);
|
--color-bg: hsl(30, 20%, 10%);
|
||||||
--color-surface: hsl(30, 20%, 15%);
|
--color-surface: hsl(30, 20%, 15%);
|
||||||
--color-text-main: hsl(30, 10%, 90%);
|
--color-text-main: hsl(30, 10%, 90%);
|
||||||
--color-text-muted: hsl(30, 10%, 60%);
|
--color-text-muted: hsl(30, 10%, 60%);
|
||||||
/* In dark mode, 'primary' (black) matches background too much, so we invert or use white */
|
|
||||||
--color-primary: hsl(0, 0%, 90%);
|
--color-primary: hsl(0, 0%, 90%);
|
||||||
--color-primary-dark: hsl(0, 0%, 100%);
|
--color-primary-dark: hsl(0, 0%, 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Manual Dark Mode Override */
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--color-bg: hsl(30, 20%, 10%);
|
||||||
|
--color-surface: hsl(30, 20%, 15%);
|
||||||
|
--color-text-main: hsl(30, 10%, 90%);
|
||||||
|
--color-text-muted: hsl(30, 10%, 60%);
|
||||||
|
--color-primary: hsl(0, 0%, 90%);
|
||||||
|
--color-primary-dark: hsl(0, 0%, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manual Light Mode Override */
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--color-bg: hsl(30, 20%, 97%);
|
||||||
|
--color-surface: hsl(0, 0%, 100%);
|
||||||
|
--color-primary: hsl(0, 0%, 20%);
|
||||||
|
--color-primary-dark: hsl(0, 0%, 0%);
|
||||||
|
--color-text-main: hsl(var(--hue-text), 10%, 20%);
|
||||||
|
--color-text-muted: hsl(var(--hue-text), 5%, 60%);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
|||||||
Reference in New Issue
Block a user