File size: 6,344 Bytes
25d51c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0fcd39
 
 
 
 
 
 
25d51c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0fcd39
 
25d51c9
 
 
 
a0fcd39
 
 
 
 
25d51c9
a0fcd39
25d51c9
 
a0fcd39
 
25d51c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0fcd39
 
25d51c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0fcd39
25d51c9
 
 
 
 
 
 
 
 
a0fcd39
25d51c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0fcd39
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import React, { useState, useEffect, useMemo, useCallback } from 'react'

const ALL_KEYS_WITH_MODES = [
  'C major', 'C minor', 'C# major', 'C# minor',
  'D major', 'D minor', 'D# major', 'D# minor',
  'E major', 'E minor', 'F major', 'F minor',
  'F# major', 'F# minor', 'G major', 'G minor',
  'G# major', 'G# minor', 'A major', 'A minor',
  'A# major', 'A# minor', 'B major', 'B minor'
]

const KEY_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

function AnalysisDisplay({
  detection,
  loading,
  onReady,
  appliedSemitones = 0,
  onProcess,
  isProcessing,
  hasRegion
}) {
  const [targetKeyWithMode, setTargetKeyWithMode] = useState('C major')
  const [targetBpm, setTargetBpm] = useState(120)

  useEffect(() => {
    if (detection && onReady) {
      onReady()
    }
  }, [detection, onReady])

  useEffect(() => {
    if (detection) {
      setTargetKeyWithMode(`${detection.key} ${detection.mode}`)
      setTargetBpm(detection.bpm)
    }
  }, [detection])

  const targetKey = targetKeyWithMode.split(' ')[0]

  const semitones = useMemo(() => {
    if (!detection || !targetKey) return 0
    const fromIdx = KEY_NAMES.indexOf(detection.key)
    const toIdx = KEY_NAMES.indexOf(targetKey)
    let diff = toIdx - fromIdx
    if (diff > 6) diff -= 12
    if (diff < -6) diff += 12
    return diff
  }, [detection, targetKey])

  const handleKeyShift = useCallback((shift) => {
    const currentIdx = KEY_NAMES.indexOf(targetKey)
    const newIdx = (currentIdx + shift + 12) % 12
    const mode = targetKeyWithMode.split(' ')[1]
    setTargetKeyWithMode(`${KEY_NAMES[newIdx]} ${mode}`)
  }, [targetKey, targetKeyWithMode])

  const handleApply = useCallback(() => {
    if (!detection || !onProcess) return
    const newBpm = Math.abs(targetBpm - detection.bpm) > 0.1 ? targetBpm : null
    onProcess(semitones, newBpm)
  }, [onProcess, semitones, targetBpm, detection])

  const hasChanges = detection && (semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1)

  if (loading) {
    return (
      <div className="card rounded-xl p-5 animate-pulse">
        <div className="flex gap-6">
          <div className="h-12 bg-white/5 rounded-lg w-40"></div>
          <div className="h-12 bg-white/5 rounded-lg w-40"></div>
        </div>
      </div>
    )
  }

  if (!detection) return null

  const btnClass = 'w-8 h-8 rounded-lg font-bold text-sm transition-all bg-white/[0.06] hover:bg-white/[0.12] text-gray-300 border border-white/[0.08] flex items-center justify-center flex-shrink-0'
  const inputClass = 'bg-surface-elevated border border-white/[0.08] rounded-lg text-white text-sm text-center focus:outline-none focus:border-accent-500 transition-colors'

  return (
    <div className="card rounded-xl p-5 animate-fade-in">
      <div className="flex items-center gap-6 flex-wrap">

        {/* BPM Control: - [editable input] + */}
        <div className="flex items-center gap-1.5">
          <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">BPM</span>
          <button onClick={() => setTargetBpm(v => Math.max(20, v - 1))} className={btnClass}>-</button>
          <input
            type="number"
            value={Math.round(targetBpm)}
            onChange={(e) => {
              const val = parseFloat(e.target.value)
              if (!isNaN(val) && val >= 20 && val <= 400) setTargetBpm(val)
            }}
            min={20}
            max={400}
            className={`${inputClass} w-20 px-2 py-1.5 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`}
          />
          <button onClick={() => setTargetBpm(v => Math.min(400, v + 1))} className={btnClass}>+</button>
          {Math.abs(targetBpm - detection.bpm) > 0.1 && (
            <button
              onClick={() => setTargetBpm(detection.bpm)}
              className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1"
            >
              Reset
            </button>
          )}
        </div>

        {/* Divider */}
        <div className="w-px h-10 bg-white/[0.08]"></div>

        {/* Key Control: - [dropdown] + */}
        <div className="flex items-center gap-1.5">
          <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">Key</span>
          <button onClick={() => handleKeyShift(-1)} className={btnClass}>-</button>
          <select
            value={targetKeyWithMode}
            onChange={(e) => setTargetKeyWithMode(e.target.value)}
            className={`${inputClass} w-28 px-2 py-1.5 cursor-pointer`}
          >
            {ALL_KEYS_WITH_MODES.map(k => (
              <option key={k} value={k}>{k}</option>
            ))}
          </select>
          <button onClick={() => handleKeyShift(1)} className={btnClass}>+</button>
          {semitones !== 0 && (
            <span className="text-xs text-gray-500 font-mono ml-1">
              {semitones > 0 ? '+' : ''}{semitones}st
            </span>
          )}
          {semitones !== 0 && (
            <button
              onClick={() => setTargetKeyWithMode(`${detection.key} ${detection.mode}`)}
              className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1"
            >
              Reset
            </button>
          )}
        </div>

        {/* Divider */}
        <div className="w-px h-10 bg-white/[0.08]"></div>

        {/* Apply Button */}
        <button
          onClick={handleApply}
          disabled={!hasChanges || isProcessing}
          className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${
            !hasChanges || isProcessing
              ? 'bg-white/[0.04] text-gray-600 cursor-not-allowed'
              : 'bg-accent-500 hover:bg-accent-400 text-white'
          }`}
        >
          {isProcessing ? 'Processing...' : hasChanges ? (hasRegion ? 'Apply to Selection' : 'Apply') : 'No Changes'}
        </button>

        {/* Original values */}
        {(semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) && (
          <span className="text-[10px] text-gray-600 ml-auto">
            Original: {detection.key} {detection.mode} / {detection.bpm.toFixed(1)} BPM
          </span>
        )}
      </div>
    </div>
  )
}

export default AnalysisDisplay