Просмотр исходного кода

Split Service.state into named helpers

state() used p, v, q, f, m and st across one long function (R3). It now
calls limits, missingSources, padState and sequenceStates, with
descriptive names. Service.edit becomes apply, so "edit" no longer means
two things (R4). The State built is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude 1 час назад
Родитель
Сommit
5c499b8365
1 измененных файлов с 98 добавлено и 62 удалено
  1. 98 62
      internal/app/app.go

+ 98 - 62
internal/app/app.go

@@ -217,63 +217,99 @@ func (s *Service) About() About {
 	}
 	}
 }
 }
 
 
+// state builds what the frontend shows about the project. Callers hold s.mu.
 func (s *Service) state() State {
 func (s *Service) state() State {
-	p := s.p
+	proj := s.p
+	missing := missingSources(proj)
 	st := State{
 	st := State{
-		Name:        p.Name,
-		Dirty:       p.Dirty(),
-		Pads:        make([]Pad, 0, project.Pads),
-		Sequences:   []Sequence{},
-		SampleCount: p.SampleCount(),
-		Limits: Limits{
-			Pads:         project.Pads,
-			MaxSamples:   project.MaxSamples,
-			MaxSequences: project.MaxSequences,
-			MinCoarse:    project.MinCoarse,
-			MaxCoarse:    project.MaxCoarse,
-			MinFine:      project.MinFine,
-			MaxFine:      project.MaxFine,
-			MaxMuteGroup: project.MaxMuteGroup,
-			MinBPM:       project.MinBPM,
-			MaxBPM:       project.MaxBPM,
-			MaxBars:      project.MaxBars,
-		},
-	}
-	missing := map[string]bool{} // pool file -> source missing, as export sees it
-	for _, f := range p.PoolFiles() {
-		src, _ := p.Source(f)
-		missing[f] = src == "" || !export.Usable(src)
+		Name:         proj.Name,
+		Dirty:        proj.Dirty(),
+		Pads:         make([]Pad, 0, project.Pads),
+		Sequences:    sequenceStates(proj),
+		SampleCount:  proj.SampleCount(),
+		MissingCount: len(missing),
+		Limits:       limits(),
 	}
 	}
 	for slot := 0; slot < project.Pads; slot++ {
 	for slot := 0; slot < project.Pads; slot++ {
-		v, err := p.Pad(slot)
-		if err != nil {
-			// A loaded file with an odd pad; show it as empty.
-			st.Pads = append(st.Pads, Pad{Slot: slot, Label: project.SlotName(slot)})
-			continue
-		}
-		pad := Pad{Slot: slot, Label: project.SlotName(slot), Name: v.SampleName, File: v.File,
-			Coarse: v.Coarse, Fine: v.Fine, MuteGroup: v.MuteGroup, Start: v.Start, End: v.End}
-		if !v.Empty() {
-			pad.HasNotes = p.PadHasNotes(slot)
-			pad.Source, _ = p.Source(v.File)
-			pad.Missing = missing[v.File]
-			if !pad.Missing {
-				pad.Audio = audioURL(slot, pad.Source)
-			}
-		}
-		st.Pads = append(st.Pads, pad)
-	}
-	for _, q := range p.Sequences() {
-		st.Sequences = append(st.Sequences, Sequence{Slot: q.Slot, Label: project.SlotName(q.Slot),
-			Name: q.Name, BPM: q.BPM, Bars: q.Bars, HasNotes: q.HasNotes})
+		st.Pads = append(st.Pads, padState(proj, slot, missing))
 	}
 	}
 	st.SequenceCount = len(st.Sequences)
 	st.SequenceCount = len(st.Sequences)
-	for _, m := range missing {
-		if m {
-			st.MissingCount++
+	return st
+}
+
+// limits returns the device and format limits the frontend enforces.
+func limits() Limits {
+	return Limits{
+		Pads:         project.Pads,
+		MaxSamples:   project.MaxSamples,
+		MaxSequences: project.MaxSequences,
+		MinCoarse:    project.MinCoarse,
+		MaxCoarse:    project.MaxCoarse,
+		MinFine:      project.MinFine,
+		MaxFine:      project.MaxFine,
+		MaxMuteGroup: project.MaxMuteGroup,
+		MinBPM:       project.MinBPM,
+		MaxBPM:       project.MaxBPM,
+		MaxBars:      project.MaxBars,
+	}
+}
+
+// missingSources returns the pool files that export could not copy: no
+// source recorded, or one export refuses (see export.Usable).
+func missingSources(proj *project.Project) map[string]bool {
+	missing := map[string]bool{}
+	for _, file := range proj.PoolFiles() {
+		if src, _ := proj.Source(file); src == "" || !export.Usable(src) {
+			missing[file] = true
 		}
 		}
 	}
 	}
-	return st
+	return missing
+}
+
+// padState describes the pad in slot. missing is from missingSources.
+func padState(proj *project.Project, slot int, missing map[string]bool) Pad {
+	view, err := proj.Pad(slot)
+	if err != nil {
+		// A loaded file with an odd pad; show it as empty.
+		return Pad{Slot: slot, Label: project.SlotName(slot)}
+	}
+	pad := Pad{
+		Slot:      slot,
+		Label:     project.SlotName(slot),
+		Name:      view.SampleName,
+		File:      view.File,
+		Coarse:    view.Coarse,
+		Fine:      view.Fine,
+		MuteGroup: view.MuteGroup,
+		Start:     view.Start,
+		End:       view.End,
+	}
+	if view.Empty() {
+		return pad
+	}
+	pad.HasNotes = proj.PadHasNotes(slot)
+	pad.Source, _ = proj.Source(view.File)
+	pad.Missing = missing[view.File]
+	if !pad.Missing {
+		pad.Audio = audioURL(slot, pad.Source)
+	}
+	return pad
+}
+
+// sequenceStates describes the project's sequences, in slot order.
+func sequenceStates(proj *project.Project) []Sequence {
+	seqs := []Sequence{}
+	for _, seq := range proj.Sequences() {
+		seqs = append(seqs, Sequence{
+			Slot:     seq.Slot,
+			Label:    project.SlotName(seq.Slot),
+			Name:     seq.Name,
+			BPM:      seq.BPM,
+			Bars:     seq.Bars,
+			HasNotes: seq.HasNotes,
+		})
+	}
+	return seqs
 }
 }
 
 
 // audioURL is the preview URL for a pad. The key in the query changes with
 // audioURL is the preview URL for a pad. The key in the query changes with
@@ -283,9 +319,9 @@ func audioURL(slot int, src string) string {
 	return fmt.Sprintf("%s%d?k=%s", AudioPrefix, slot, hex.EncodeToString(sum[:6]))
 	return fmt.Sprintf("%s%d?k=%s", AudioPrefix, slot, hex.EncodeToString(sum[:6]))
 }
 }
 
 
-// edit runs fn on the project under the lock and returns the new state.
+// apply runs fn on the project under the lock and returns the new state.
 // The state is returned even on error, so the frontend stays in step.
 // The state is returned even on error, so the frontend stays in step.
-func (s *Service) edit(fn func(p *project.Project) error) (State, error) {
+func (s *Service) apply(fn func(p *project.Project) error) (State, error) {
 	s.mu.Lock()
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	defer s.mu.Unlock()
 	err := fn(s.p)
 	err := fn(s.p)
@@ -294,7 +330,7 @@ func (s *Service) edit(fn func(p *project.Project) error) (State, error) {
 
 
 // SetName renames the project.
 // SetName renames the project.
 func (s *Service) SetName(name string) (State, error) {
 func (s *Service) SetName(name string) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.SetName(strings.TrimSpace(name)) })
+	return s.apply(func(p *project.Project) error { return p.SetName(strings.TrimSpace(name)) })
 }
 }
 
 
 // AssignSampleDialog asks for a WAV file and puts it on the pad in slot.
 // AssignSampleDialog asks for a WAV file and puts it on the pad in slot.
@@ -308,22 +344,22 @@ func (s *Service) AssignSampleDialog(slot int) (State, error) {
 }
 }
 
 
 func (s *Service) assignSample(slot int, path string) (State, error) {
 func (s *Service) assignSample(slot int, path string) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.AssignSample(slot, path) })
+	return s.apply(func(p *project.Project) error { return p.AssignSample(slot, path) })
 }
 }
 
 
 // ClearPad empties the pad in slot.
 // ClearPad empties the pad in slot.
 func (s *Service) ClearPad(slot int) (State, error) {
 func (s *Service) ClearPad(slot int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.ClearPad(slot) })
+	return s.apply(func(p *project.Project) error { return p.ClearPad(slot) })
 }
 }
 
 
 // SetTune sets a pad's tuning in semitones and cents.
 // SetTune sets a pad's tuning in semitones and cents.
 func (s *Service) SetTune(slot, coarse, fine int) (State, error) {
 func (s *Service) SetTune(slot, coarse, fine int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.SetTune(slot, coarse, fine) })
+	return s.apply(func(p *project.Project) error { return p.SetTune(slot, coarse, fine) })
 }
 }
 
 
 // SetMuteGroup sets a pad's mute group; 0 is off.
 // SetMuteGroup sets a pad's mute group; 0 is off.
 func (s *Service) SetMuteGroup(slot, group int) (State, error) {
 func (s *Service) SetMuteGroup(slot, group int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.SetMuteGroup(slot, group) })
+	return s.apply(func(p *project.Project) error { return p.SetMuteGroup(slot, group) })
 }
 }
 
 
 // ChooseExportFolder asks for the folder to export into and remembers it
 // ChooseExportFolder asks for the folder to export into and remembers it
@@ -412,17 +448,17 @@ func (s *Service) exported(path string, err error) (ExportResult, error) {
 // MovePad moves the pad in from to slot to, replacing what was there.
 // MovePad moves the pad in from to slot to, replacing what was there.
 // Recorded notes stay on their slots; the frontend warns first.
 // Recorded notes stay on their slots; the frontend warns first.
 func (s *Service) MovePad(from, to int) (State, error) {
 func (s *Service) MovePad(from, to int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.MovePad(from, to) })
+	return s.apply(func(p *project.Project) error { return p.MovePad(from, to) })
 }
 }
 
 
 // CopyPad copies the pad in from to slot to, replacing what was there.
 // CopyPad copies the pad in from to slot to, replacing what was there.
 func (s *Service) CopyPad(from, to int) (State, error) {
 func (s *Service) CopyPad(from, to int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.CopyPad(from, to) })
+	return s.apply(func(p *project.Project) error { return p.CopyPad(from, to) })
 }
 }
 
 
 // SwapPads exchanges two pads.
 // SwapPads exchanges two pads.
 func (s *Service) SwapPads(a, b int) (State, error) {
 func (s *Service) SwapPads(a, b int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.SwapPads(a, b) })
+	return s.apply(func(p *project.Project) error { return p.SwapPads(a, b) })
 }
 }
 
 
 // Load asks for a project file and opens it in place of the current one.
 // Load asks for a project file and opens it in place of the current one.
@@ -453,19 +489,19 @@ func (s *Service) loadPath(path string) (LoadResult, error) {
 // AddSequence adds a sequence in an empty slot: "Sequence NN", 2 bars, at
 // AddSequence adds a sequence in an empty slot: "Sequence NN", 2 bars, at
 // the BPM of the most recently edited sequence (else 120).
 // the BPM of the most recently edited sequence (else 120).
 func (s *Service) AddSequence(slot int) (State, error) {
 func (s *Service) AddSequence(slot int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.AddSequence(slot) })
+	return s.apply(func(p *project.Project) error { return p.AddSequence(slot) })
 }
 }
 
 
 // RemoveSequence deletes a sequence and its recorded notes. The last
 // RemoveSequence deletes a sequence and its recorded notes. The last
 // sequence cannot be removed.
 // sequence cannot be removed.
 func (s *Service) RemoveSequence(slot int) (State, error) {
 func (s *Service) RemoveSequence(slot int) (State, error) {
-	return s.edit(func(p *project.Project) error { return p.RemoveSequence(slot) })
+	return s.apply(func(p *project.Project) error { return p.RemoveSequence(slot) })
 }
 }
 
 
 // SetSequence sets a sequence's name, BPM and length in bars. Notes past a
 // SetSequence sets a sequence's name, BPM and length in bars. Notes past a
 // new, shorter end are left in the file.
 // new, shorter end are left in the file.
 func (s *Service) SetSequence(slot int, name string, bpm float64, bars int) (State, error) {
 func (s *Service) SetSequence(slot int, name string, bpm float64, bars int) (State, error) {
-	return s.edit(func(p *project.Project) error {
+	return s.apply(func(p *project.Project) error {
 		return p.SetSequence(slot, strings.TrimSpace(name), bpm, bars)
 		return p.SetSequence(slot, strings.TrimSpace(name), bpm, bars)
 	})
 	})
 }
 }