mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
Improve mobile UI (resizing)
This commit is contained in:
103
src/App.css
103
src/App.css
@@ -1,9 +1,11 @@
|
|||||||
/* App.css */
|
/* App.css */
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
/* Changed from flex to allow natural document flow */
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
@@ -38,13 +40,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-stage {
|
.main-stage {
|
||||||
flex: 1;
|
/* Natural height - not flex to prevent shrinking when footer is present */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -58,6 +63,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
transition: max-width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.2s ease;
|
transition: max-width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.has-hint {
|
.card.has-hint {
|
||||||
@@ -357,7 +364,8 @@ button {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid var(--color-primary);
|
border: 2px solid var(--color-primary);
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.4rem 0.8rem;
|
||||||
|
/* Compact padding by default */
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -365,7 +373,8 @@ button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
|
/* Compact font size by default */
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint-button:hover {
|
.hint-button:hover {
|
||||||
@@ -387,7 +396,8 @@ button {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid var(--color-text-muted);
|
border: 2px solid var(--color-text-muted);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.4rem 0.8rem;
|
||||||
|
/* Compact padding by default */
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -395,7 +405,8 @@ button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
|
/* Compact font size by default */
|
||||||
}
|
}
|
||||||
|
|
||||||
.zen-button:hover {
|
.zen-button:hover {
|
||||||
@@ -608,6 +619,20 @@ button {
|
|||||||
.header-controls {
|
.header-controls {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fix action row overflow on narrow screens */
|
||||||
|
.action-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px !important;
|
||||||
|
padding: 0 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce card padding on mobile */
|
||||||
|
.card {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-option.active {
|
.toggle-option.active {
|
||||||
@@ -653,26 +678,29 @@ button {
|
|||||||
|
|
||||||
.main-stage {
|
.main-stage {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
/* Center vertically */
|
|
||||||
gap: 0;
|
gap: 0;
|
||||||
/* Allow scrolling */
|
height: 100vh;
|
||||||
height: auto;
|
overflow: hidden;
|
||||||
min-height: 100vh;
|
/* Prevent scroll - fit everything on one screen */
|
||||||
overflow-y: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.sheet-music-card {
|
.card.sheet-music-card {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
/* Fit within viewport */
|
||||||
|
max-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Staff Row: Horizontal Layout for Staff + Note Name */
|
/* Staff Row: Horizontal Layout for Staff + Note Name */
|
||||||
@@ -701,10 +729,11 @@ button {
|
|||||||
.sheet-music-container {
|
.sheet-music-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-height: 180px;
|
min-height: 120px;
|
||||||
/* Increased from 150px */
|
/* Reduced to allow more space for fretboard */
|
||||||
transform: scale(1.1);
|
max-height: 180px;
|
||||||
/* Scale UP */
|
transform: scale(1.0);
|
||||||
|
/* No scaling to prevent overflow */
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -747,14 +776,17 @@ button {
|
|||||||
transform: none;
|
transform: none;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
|
/* Take remaining space */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
/* distinct bg for full width feel */
|
min-height: 100px;
|
||||||
min-height: 200px;
|
/* Reduced to fit on screen */
|
||||||
padding: 10px 0;
|
max-height: 50vh;
|
||||||
|
/* Limit height */
|
||||||
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide Instruction Text ("Play note above") */
|
/* Hide Instruction Text ("Play note above") */
|
||||||
@@ -784,32 +816,33 @@ button {
|
|||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Push settings way down so they don't interfere with main view */
|
/* Hide settings footer in landscape - accessible via settings modal */
|
||||||
.settings-footer {
|
.settings-footer {
|
||||||
margin-top: 50px;
|
display: none;
|
||||||
padding-bottom: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make main stage scrollable to reach settings */
|
/* App container in landscape */
|
||||||
.app-container {
|
.app-container {
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: block;
|
|
||||||
/* Break flex height lock */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row {
|
.action-row {
|
||||||
margin-top: 20px !important;
|
margin-top: 5px !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
max-width: 100vw;
|
||||||
|
padding: 5px;
|
||||||
position: static;
|
position: static;
|
||||||
/* Flow naturally below */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Don't shrink */
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row button {
|
.action-row button {
|
||||||
|
|||||||
12
src/App.tsx
12
src/App.tsx
@@ -75,6 +75,15 @@ function App() {
|
|||||||
const [hoveredMidi, setHoveredMidi] = useState<number | null>(null);
|
const [hoveredMidi, setHoveredMidi] = useState<number | null>(null);
|
||||||
const feedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const feedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowWidth(window.innerWidth);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
@@ -485,7 +494,7 @@ function App() {
|
|||||||
keySignature={settings.keySignature}
|
keySignature={settings.keySignature}
|
||||||
clef={activeClef}
|
clef={activeClef}
|
||||||
transpose={activeTranspose}
|
transpose={activeTranspose}
|
||||||
width={Math.min(window.innerWidth - 40, 500)}
|
width={Math.min(windowWidth - 40, 500)}
|
||||||
height={activeClef === 'grand' ? 260 : 180}
|
height={activeClef === 'grand' ? 260 : 180}
|
||||||
hideTargetNote={settings.gameMode === 'ear_training' && !revealed}
|
hideTargetNote={settings.gameMode === 'ear_training' && !revealed}
|
||||||
hoverMidi={settings.showHint ? hoveredMidi : null}
|
hoverMidi={settings.showHint ? hoveredMidi : null}
|
||||||
@@ -708,6 +717,7 @@ function App() {
|
|||||||
audioLevel={audioLevel}
|
audioLevel={audioLevel}
|
||||||
debugInfo={debugInfo}
|
debugInfo={debugInfo}
|
||||||
isListening={isListening}
|
isListening={isListening}
|
||||||
|
onMicToggle={() => setListening(l => !l)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<OpenSourceModal
|
<OpenSourceModal
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, Volume2, VolumeX, ChevronDown, ChevronUp } from 'lucide-react';
|
import { X, Volume2, VolumeX, ChevronDown, ChevronUp, Mic, MicOff } 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';
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ interface SettingsModalProps {
|
|||||||
audioLevel?: number;
|
audioLevel?: number;
|
||||||
debugInfo?: MicrophoneDebugInfo | null;
|
debugInfo?: MicrophoneDebugInfo | null;
|
||||||
isListening?: boolean;
|
isListening?: boolean;
|
||||||
|
onMicToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level meter component
|
// Level meter component
|
||||||
@@ -58,7 +59,8 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|||||||
onUpdateSettings,
|
onUpdateSettings,
|
||||||
audioLevel = 0,
|
audioLevel = 0,
|
||||||
debugInfo,
|
debugInfo,
|
||||||
isListening = false
|
isListening = false,
|
||||||
|
onMicToggle
|
||||||
}) => {
|
}) => {
|
||||||
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
||||||
|
|
||||||
@@ -166,6 +168,26 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|||||||
|
|
||||||
<hr style={{ borderColor: 'rgba(255,255,255,0.1)', margin: '24px 0' }} />
|
<hr style={{ borderColor: 'rgba(255,255,255,0.1)', margin: '24px 0' }} />
|
||||||
|
|
||||||
|
{/* Microphone Toggle */}
|
||||||
|
<div className="control-group" style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<label className="control-label" style={{ marginBottom: 0, fontSize: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
{isListening ? <Mic size={18} /> : <MicOff size={18} />}
|
||||||
|
<span>Microphone</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className={`switch-button ${isListening ? 'active' : ''}`}
|
||||||
|
onClick={onMicToggle}
|
||||||
|
title={isListening ? "Turn Off Microphone" : "Turn On Microphone"}
|
||||||
|
>
|
||||||
|
<div className="switch-thumb" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '12px', opacity: 0.7, margin: 0 }}>
|
||||||
|
{isListening ? 'Microphone is active and listening for notes.' : 'Enable microphone to detect your playing.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mic Sensitivity */}
|
{/* Mic Sensitivity */}
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
<label className="control-label" style={{ marginBottom: '12px', fontSize: '16px' }}>
|
<label className="control-label" style={{ marginBottom: '12px', fontSize: '16px' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user