Parcourir la source

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 il y a 1 heure
Parent
commit
5c499b8365
1 fichiers modifiés avec 98 ajouts et 62 suppressions
  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 {
-	p := s.p
+	proj := s.p
+	missing := missingSources(proj)
 	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++ {
-		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)
-	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
@@ -283,9 +319,9 @@ func audioURL(slot int, src string) string {
 	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.
-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()
 	defer s.mu.Unlock()
 	err := fn(s.p)
@@ -294,7 +330,7 @@ func (s *Service) edit(fn func(p *project.Project) error) (State, error) {
 
 // SetName renames the project.
 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.
@@ -308,22 +344,22 @@ func (s *Service) AssignSampleDialog(slot int) (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.
 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.
 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.
 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
@@ -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.
 // Recorded notes stay on their slots; the frontend warns first.
 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.
 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.
 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.
@@ -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
 // the BPM of the most recently edited sequence (else 120).
 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
 // sequence cannot be removed.
 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
 // new, shorter end are left in the file.
 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)
 	})
 }