diff --git a/main.go b/main.go index 2f635ba..b612a23 100644 --- a/main.go +++ b/main.go @@ -72,6 +72,12 @@ type presetEntry struct { Path string } +type presetInfo struct { + Name string + Path string + Source string +} + func listEmbeddedPresets() ([]presetEntry, error) { entries, err := fs.ReadDir(embeddedPresets, "Presets") if err != nil { @@ -106,6 +112,102 @@ func loadEmbeddedPreset(path string) (*Settings, error) { return decodeSettingsJSON(raw) } +func listUserPresets(dir string) ([]presetEntry, error) { + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return []presetEntry{}, nil + } + return nil, err + } + var presets []presetEntry + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(strings.ToLower(name), ".json") { + continue + } + displayName := strings.TrimSuffix(name, filepath.Ext(name)) + presets = append(presets, presetEntry{ + Name: displayName, + Path: filepath.Join(dir, name), + }) + } + sort.Slice(presets, func(i, j int) bool { + return presets[i].Name < presets[j].Name + }) + return presets, nil +} + +func buildPresetOptions(settings *Settings, userPresetDir string) ([]string, map[string]presetInfo) { + hidden := map[string]bool{} + for _, name := range settings.HiddenPresets { + hidden[name] = true + } + + info := map[string]presetInfo{} + var names []string + + embedded, err := listEmbeddedPresets() + if err != nil { + log.Println("Error loading embedded presets:", err) + } else { + for _, preset := range embedded { + if hidden[preset.Name] { + continue + } + info[preset.Name] = presetInfo{ + Name: preset.Name, + Path: preset.Path, + Source: "embedded", + } + names = append(names, preset.Name) + } + } + + userPresets, err := listUserPresets(userPresetDir) + if err != nil { + log.Println("Error loading user presets:", err) + } else { + for _, preset := range userPresets { + if _, exists := info[preset.Name]; exists { + continue + } + info[preset.Name] = presetInfo{ + Name: preset.Name, + Path: preset.Path, + Source: "user", + } + names = append(names, preset.Name) + } + } + + sort.Strings(names) + return append([]string{"No Preset"}, names...), info +} + +func savePresetToFile(path string, settings *Settings) error { + raw, err := json.Marshal(settings) + if err != nil { + return err + } + var data map[string]any + if err := json.Unmarshal(raw, &data); err != nil { + return err + } + delete(data, "seed") + delete(data, "last_export_path") + delete(data, "image_view_state") + + pretty, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, pretty, 0644) +} + func main() { // Initialize the Fyne application and window a := app.NewWithID("com.example.rpgcitymaker") @@ -1651,26 +1753,38 @@ func main() { updateGenerateBtnState() } - presetEntries, err := listEmbeddedPresets() - if err != nil { - log.Println("Error loading embedded presets:", err) - } - presetNames := []string{"No Preset"} - presetPathByName := map[string]string{} - for _, preset := range presetEntries { - presetNames = append(presetNames, preset.Name) - presetPathByName[preset.Name] = preset.Path - } - presetSelect := widget.NewSelect(presetNames, func(name string) { - if name == "" || name == "No Preset" { + presetDir := filepath.Join(appConfigDir, "presets") + var presetInfoByName map[string]presetInfo + var suppressPresetApply bool + var removePresetBtn *widget.Button + + presetSelect := widget.NewSelect([]string{"No Preset"}, func(name string) { + if suppressPresetApply { return } - path, ok := presetPathByName[name] + if name == "" || name == "No Preset" { + if removePresetBtn != nil { + removePresetBtn.Disable() + } + return + } + info, ok := presetInfoByName[name] if !ok { return } applyingPreset = true - imported, err := loadEmbeddedPreset(path) + var imported *Settings + var err error + if info.Source == "embedded" { + imported, err = loadEmbeddedPreset(info.Path) + } else { + raw, readErr := os.ReadFile(info.Path) + if readErr != nil { + err = readErr + } else { + imported, err = decodeSettingsJSON(raw) + } + } if err != nil { applyingPreset = false dialog.ShowError(err, w) @@ -1683,6 +1797,9 @@ func main() { applyingPreset = false }) }) + if removePresetBtn != nil { + removePresetBtn.Enable() + } }) presetSelect.SetSelected("No Preset") clearPresetSelection = func() { @@ -1691,6 +1808,121 @@ func main() { } } + refreshPresetOptions := func(selectName string) { + names, info := buildPresetOptions(settings, presetDir) + presetInfoByName = info + suppressPresetApply = true + presetSelect.Options = names + presetSelect.Refresh() + + if selectName == "" { + selectName = presetSelect.Selected + } + if _, ok := presetInfoByName[selectName]; !ok { + selectName = "No Preset" + } + presetSelect.SetSelected(selectName) + if removePresetBtn != nil { + if selectName == "" || selectName == "No Preset" { + removePresetBtn.Disable() + } else { + removePresetBtn.Enable() + } + } + suppressPresetApply = false + } + + addPresetBtn := widget.NewButton("Add Preset", func() { + nameEntry := widget.NewEntry() + nameEntry.SetPlaceHolder("Preset Name") + + content := container.NewVBox( + widget.NewLabel("Preset Name:"), + nameEntry, + ) + + var addDialog *dialog.CustomDialog + saveBtn := widget.NewButton("Save", func() { + name := strings.TrimSpace(nameEntry.Text) + if name == "" || name == "No Preset" { + return + } + if strings.ContainsAny(name, "/\\") { + dialog.ShowError(fmt.Errorf("preset name cannot contain path separators"), w) + return + } + if _, exists := presetInfoByName[name]; exists { + dialog.ShowError(fmt.Errorf("preset name already exists"), w) + return + } + if err := os.MkdirAll(presetDir, 0755); err != nil { + dialog.ShowError(err, w) + return + } + filePath := filepath.Join(presetDir, name+".json") + if err := savePresetToFile(filePath, settings); err != nil { + dialog.ShowError(err, w) + return + } + refreshPresetOptions(name) + if addDialog != nil { + addDialog.Hide() + } + }) + saveBtn.Disable() + + nameEntry.OnChanged = func(text string) { + if strings.TrimSpace(text) == "" { + saveBtn.Disable() + } else { + saveBtn.Enable() + } + } + + addDialog = dialog.NewCustom("Add Preset", "Cancel", content, w) + addDialog.SetButtons([]fyne.CanvasObject{ + widget.NewButton("Cancel", func() { + addDialog.Hide() + }), + saveBtn, + }) + addDialog.Show() + }) + + removePresetBtn = widget.NewButton("Remove Preset", func() { + name := presetSelect.Selected + if name == "" || name == "No Preset" { + return + } + info, ok := presetInfoByName[name] + if !ok { + return + } + if info.Source == "embedded" { + found := false + for _, existing := range settings.HiddenPresets { + if existing == name { + found = true + break + } + } + if !found { + settings.HiddenPresets = append(settings.HiddenPresets, name) + if err := settings.Save(); err != nil { + log.Println("Error saving hidden preset list:", err) + } + } + } else { + if err := os.Remove(info.Path); err != nil { + dialog.ShowError(err, w) + return + } + } + refreshPresetOptions("No Preset") + }) + removePresetBtn.Disable() + refreshPresetOptions("No Preset") + buildingsTab := container.NewTabItem("Buildings", container.NewVBox( numBuildingsSlider, minBuildingSizeSlider, @@ -1705,6 +1937,7 @@ func main() { imageTab := container.NewTabItem("Image", container.NewVBox( widget.NewLabel("Preset:"), presetSelect, + container.NewGridWithColumns(2, addPresetBtn, removePresetBtn), widget.NewSeparator(), widget.NewLabel("Width:"), widthEntry, widget.NewLabel("Height:"), diff --git a/settings.go b/settings.go index 07d7565..5af8233 100644 --- a/settings.go +++ b/settings.go @@ -72,9 +72,10 @@ type Settings struct { BuildingComplexityRatio float64 `json:"building_complexity_ratio"` // General settings - Seed int64 `json:"seed"` - LastExportPath string `json:"last_export_path"` - ImageViewState int `json:"image_view_state"` + Seed int64 `json:"seed"` + LastExportPath string `json:"last_export_path"` + ImageViewState int `json:"image_view_state"` + HiddenPresets []string `json:"hidden_presets"` } // Save saves the current settings to a JSON file in the user's config directory. @@ -176,6 +177,7 @@ func LoadSettings() (*Settings, error) { BuildingComplexityRatio: 50, LastExportPath: homeDir, ImageViewState: 0, + HiddenPresets: []string{}, }, nil } return nil, err @@ -332,4 +334,8 @@ func normalizeSettings(settings *Settings, rawKeys map[string]json.RawMessage) { } settings.LastExportPath = homeDir } + + if settings.HiddenPresets == nil { + settings.HiddenPresets = []string{} + } }