// Package app is the Wails service the frontend calls. It owns the current // project and returns a fresh State after every call; the frontend keeps no // project data of its own (build plan 4.6 and 5). package app import ( "fmt" "sync" "git.monoexp.com/bajdev/mpckit/internal/project" ) // Version is set at build time with // -ldflags "-X git.monoexp.com/bajdev/mpckit/internal/app.Version=v1.0.0". var Version = "dev" // DefaultName is the name of a new project. const DefaultName = "NewKit" // Pad is one pad as the frontend shows it. type Pad struct { Slot int `json:"slot"` Label string `json:"label"` // "A01" .. "H16" Name string `json:"name"` // sample name; empty for an empty pad File string `json:"file"` // pool file name Coarse int `json:"coarse"` Fine int `json:"fine"` MuteGroup int `json:"muteGroup"` HasNotes bool `json:"hasNotes"` } // Sequence is one existing sequence. type Sequence struct { Slot int `json:"slot"` Label string `json:"label"` // slot label, "A01" .. "H16" Name string `json:"name"` BPM float64 `json:"bpm"` Bars int `json:"bars"` HasNotes bool `json:"hasNotes"` } // Limits are the ranges the UI offers, from the device checks. type Limits struct { Pads int `json:"pads"` MaxSamples int `json:"maxSamples"` MaxSequences int `json:"maxSequences"` MinCoarse int `json:"minCoarse"` MaxCoarse int `json:"maxCoarse"` MinFine int `json:"minFine"` MaxFine int `json:"maxFine"` MaxMuteGroup int `json:"maxMuteGroup"` MinBPM float64 `json:"minBPM"` MaxBPM float64 `json:"maxBPM"` MaxBars int `json:"maxBars"` } // State is everything the frontend shows about the project. type State struct { Name string `json:"name"` Dirty bool `json:"dirty"` Pads []Pad `json:"pads"` // all 128, in slot order Sequences []Sequence `json:"sequences"` // existing ones, in slot order SampleCount int `json:"sampleCount"` SequenceCount int `json:"sequenceCount"` Limits Limits `json:"limits"` } // About is the text for the About dialog. type About struct { Version string `json:"version"` License string `json:"license"` Repo string `json:"repo"` } // Service is registered with Wails; its exported methods become the // frontend bindings. type Service struct { mu sync.Mutex p *project.Project } // NewService returns a service holding a new, empty project. func NewService() *Service { s := &Service{} s.reset() return s } func (s *Service) reset() { p, err := project.New(DefaultName) if err != nil { panic(err) // the embedded template and a constant name } s.p = p } // New replaces the current project with a new, empty one. func (s *Service) New() State { s.mu.Lock() defer s.mu.Unlock() s.reset() return s.state() } // State returns the current project's state. func (s *Service) State() State { s.mu.Lock() defer s.mu.Unlock() return s.state() } // Version returns the app version. func (s *Service) Version() string { return Version } // About returns the About dialog text. func (s *Service) About() About { return About{ Version: Version, License: "MIT License, copyright (c) 2026 bajdev", Repo: "https://git.monoexp.com/bajdev/mpckit", } } // Label returns the label the UI and the device use for a pad or sequence // slot: bank letter and two-digit pad number, e.g. 0 -> "A01", 17 -> "B02". func Label(slot int) string { return fmt.Sprintf("%c%02d", 'A'+slot/16, slot%16+1) } func (s *Service) state() State { p := s.p 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, }, } 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: Label(slot)}) continue } pad := Pad{Slot: slot, Label: Label(slot), Name: v.SampleName, File: v.File, Coarse: v.Coarse, Fine: v.Fine, MuteGroup: v.MuteGroup} if !v.Empty() { pad.HasNotes = p.PadHasNotes(slot) } st.Pads = append(st.Pads, pad) } for _, q := range p.Sequences() { st.Sequences = append(st.Sequences, Sequence{Slot: q.Slot, Label: Label(q.Slot), Name: q.Name, BPM: q.BPM, Bars: q.Bars, HasNotes: q.HasNotes}) } st.SequenceCount = len(st.Sequences) return st }