feat: unified navigation
fix: install script copying build binary chore: updated readme
This commit is contained in:
@@ -18,14 +18,12 @@ type SeekToCueMsg struct {
|
||||
|
||||
// Model represents the transcript view component
|
||||
type Model struct {
|
||||
viewport viewport.Model
|
||||
transcript *srt.Transcript
|
||||
currentCue int // Cue currently playing (from playback position)
|
||||
selectedCue int // Cue selected by user navigation
|
||||
cueLines []int // Starting line number (in rendered view) for each cue
|
||||
Width int
|
||||
Height int
|
||||
Focused bool
|
||||
viewport viewport.Model
|
||||
transcript *srt.Transcript
|
||||
activeCue int // Cue currently active (playback position + navigation cursor)
|
||||
cueLines []int // Starting line number (in rendered view) for each cue
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// New creates a new transcript model
|
||||
@@ -34,16 +32,15 @@ func New() Model {
|
||||
vp.Style = lipgloss.NewStyle()
|
||||
|
||||
return Model{
|
||||
viewport: vp,
|
||||
currentCue: -1,
|
||||
selectedCue: 0,
|
||||
viewport: vp,
|
||||
activeCue: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTranscript sets the transcript to display
|
||||
func (m *Model) SetTranscript(t *srt.Transcript) {
|
||||
m.transcript = t
|
||||
m.selectedCue = 0
|
||||
m.activeCue = 0
|
||||
m.updateContent()
|
||||
m.scrollToCue(0)
|
||||
}
|
||||
@@ -53,12 +50,12 @@ func (m *Model) Transcript() *srt.Transcript {
|
||||
return m.transcript
|
||||
}
|
||||
|
||||
// SelectedCueLineNumber returns the line number of the selected cue for vim
|
||||
func (m *Model) SelectedCueLineNumber() int {
|
||||
if m.transcript == nil || m.selectedCue < 0 || m.selectedCue >= len(m.transcript.Cues) {
|
||||
// ActiveCueLineNumber returns the line number of the active cue for vim
|
||||
func (m *Model) ActiveCueLineNumber() int {
|
||||
if m.transcript == nil || m.activeCue < 0 || m.activeCue >= len(m.transcript.Cues) {
|
||||
return 1
|
||||
}
|
||||
return m.transcript.Cues[m.selectedCue].LineNumber
|
||||
return m.transcript.Cues[m.activeCue].LineNumber
|
||||
}
|
||||
|
||||
// SetPosition updates which cue is highlighted based on playback position
|
||||
@@ -68,11 +65,11 @@ func (m *Model) SetPosition(pos time.Duration) {
|
||||
}
|
||||
|
||||
newCue := m.transcript.CueIndexAt(pos)
|
||||
if newCue != m.currentCue {
|
||||
m.currentCue = newCue
|
||||
if newCue != m.activeCue {
|
||||
m.activeCue = newCue
|
||||
m.updateContent()
|
||||
// Only auto-scroll if not focused (let user navigate freely when focused)
|
||||
if !m.Focused && newCue >= 0 {
|
||||
// Always auto-scroll during playback
|
||||
if newCue >= 0 {
|
||||
m.scrollToCue(newCue)
|
||||
}
|
||||
}
|
||||
@@ -87,22 +84,21 @@ func (m *Model) SetSize(width, height int) {
|
||||
m.updateContent()
|
||||
}
|
||||
|
||||
// SetFocused sets the focus state
|
||||
func (m *Model) SetFocused(focused bool) {
|
||||
m.Focused = focused
|
||||
// When focusing, sync selected cue to current playback position if valid
|
||||
if focused && m.currentCue >= 0 {
|
||||
m.selectedCue = m.currentCue
|
||||
m.updateContent()
|
||||
m.scrollToCue(m.selectedCue)
|
||||
}
|
||||
}
|
||||
|
||||
// ModeString returns the mode as a string
|
||||
func (m *Model) ModeString() string {
|
||||
return "VIEW"
|
||||
}
|
||||
|
||||
// seekToActiveCue returns a command to seek audio to the active cue
|
||||
func (m *Model) seekToActiveCue() tea.Cmd {
|
||||
if m.transcript == nil || m.activeCue < 0 || m.activeCue >= len(m.transcript.Cues) {
|
||||
return nil
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return SeekToCueMsg{Position: m.transcript.Cues[m.activeCue].Start}
|
||||
}
|
||||
}
|
||||
|
||||
// Update handles messages
|
||||
func (m *Model) Update(msg tea.Msg) tea.Cmd {
|
||||
if m.transcript == nil {
|
||||
@@ -113,63 +109,57 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "j", "down":
|
||||
// Move to next cue
|
||||
if m.selectedCue < len(m.transcript.Cues)-1 {
|
||||
m.selectedCue++
|
||||
// Move to next cue and seek
|
||||
if m.activeCue < len(m.transcript.Cues)-1 {
|
||||
m.activeCue++
|
||||
m.refreshAndScroll()
|
||||
return m.seekToActiveCue()
|
||||
}
|
||||
return nil
|
||||
case "k", "up":
|
||||
// Move to previous cue
|
||||
if m.selectedCue > 0 {
|
||||
m.selectedCue--
|
||||
// Move to previous cue and seek
|
||||
if m.activeCue > 0 {
|
||||
m.activeCue--
|
||||
m.refreshAndScroll()
|
||||
return m.seekToActiveCue()
|
||||
}
|
||||
return nil
|
||||
case "ctrl+d":
|
||||
// Jump 5 cues down
|
||||
m.selectedCue += 5
|
||||
if m.selectedCue >= len(m.transcript.Cues) {
|
||||
m.selectedCue = len(m.transcript.Cues) - 1
|
||||
// Jump 5 cues down and seek
|
||||
m.activeCue += 5
|
||||
if m.activeCue >= len(m.transcript.Cues) {
|
||||
m.activeCue = len(m.transcript.Cues) - 1
|
||||
}
|
||||
m.refreshAndScroll()
|
||||
return nil
|
||||
return m.seekToActiveCue()
|
||||
case "ctrl+u":
|
||||
// Jump 5 cues up
|
||||
m.selectedCue -= 5
|
||||
if m.selectedCue < 0 {
|
||||
m.selectedCue = 0
|
||||
// Jump 5 cues up and seek
|
||||
m.activeCue -= 5
|
||||
if m.activeCue < 0 {
|
||||
m.activeCue = 0
|
||||
}
|
||||
m.refreshAndScroll()
|
||||
return nil
|
||||
return m.seekToActiveCue()
|
||||
case "g":
|
||||
// Go to first cue
|
||||
m.selectedCue = 0
|
||||
// Go to first cue and seek
|
||||
m.activeCue = 0
|
||||
m.refreshAndScroll()
|
||||
return nil
|
||||
return m.seekToActiveCue()
|
||||
case "G":
|
||||
// Go to last cue
|
||||
m.selectedCue = len(m.transcript.Cues) - 1
|
||||
// Go to last cue and seek
|
||||
m.activeCue = len(m.transcript.Cues) - 1
|
||||
m.refreshAndScroll()
|
||||
return nil
|
||||
case "enter":
|
||||
// Seek to selected cue
|
||||
if m.selectedCue >= 0 && m.selectedCue < len(m.transcript.Cues) {
|
||||
return func() tea.Msg {
|
||||
return SeekToCueMsg{Position: m.transcript.Cues[m.selectedCue].Start}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return m.seekToActiveCue()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshAndScroll updates content and scrolls to selected cue
|
||||
// refreshAndScroll updates content and scrolls to active cue
|
||||
func (m *Model) refreshAndScroll() {
|
||||
m.updateContent()
|
||||
m.scrollToCue(m.selectedCue)
|
||||
m.scrollToCue(m.activeCue)
|
||||
}
|
||||
|
||||
func (m *Model) updateContent() {
|
||||
@@ -186,9 +176,8 @@ func (m *Model) updateContent() {
|
||||
for i, cue := range m.transcript.Cues {
|
||||
m.cueLines[i] = currentLine
|
||||
|
||||
isCurrent := i == m.currentCue
|
||||
isSelected := i == m.selectedCue
|
||||
rendered := RenderCue(&cue, isCurrent, isSelected, m.Width-4)
|
||||
isActive := i == m.activeCue
|
||||
rendered := RenderCue(&cue, isActive, m.Width-4)
|
||||
sb.WriteString(rendered)
|
||||
|
||||
// Count lines in this cue's rendering
|
||||
@@ -224,10 +213,7 @@ func (m *Model) scrollToCue(cueIndex int) {
|
||||
func (m Model) View() string {
|
||||
content := m.viewport.View()
|
||||
|
||||
style := ui.TranscriptStyle
|
||||
if m.Focused {
|
||||
style = ui.TranscriptFocusedStyle
|
||||
}
|
||||
style := ui.TranscriptFocusedStyle
|
||||
|
||||
return style.Width(m.Width - 2).Height(m.Height - 2).Render(content)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user