2026-01-26 14:29:05 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-11 13:45:38 -05:00
|
|
|
"embed"
|
2026-03-11 13:08:40 -05:00
|
|
|
"encoding/json"
|
2026-01-27 15:16:23 -06:00
|
|
|
"image"
|
2026-02-26 13:21:04 -06:00
|
|
|
"image/draw"
|
2026-03-11 13:45:38 -05:00
|
|
|
"io/fs"
|
2026-02-02 13:26:28 -06:00
|
|
|
"log"
|
2026-01-27 15:16:23 -06:00
|
|
|
"os"
|
2026-03-11 15:00:29 -05:00
|
|
|
"path"
|
2026-01-27 15:16:23 -06:00
|
|
|
"path/filepath"
|
2026-03-11 13:45:38 -05:00
|
|
|
"sort"
|
2026-02-02 12:24:14 -06:00
|
|
|
"strings"
|
2026-02-02 13:26:28 -06:00
|
|
|
"time"
|
2026-01-26 14:29:05 -06:00
|
|
|
)
|
|
|
|
|
|
2026-03-11 13:45:38 -05:00
|
|
|
//go:embed Presets/*.json
|
|
|
|
|
var embeddedPresets embed.FS
|
|
|
|
|
|
2026-02-26 13:21:04 -06:00
|
|
|
func runWithTimeout[T any](timeout time.Duration, fn func() T) (T, bool) {
|
|
|
|
|
ch := make(chan T, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
ch <- fn()
|
|
|
|
|
}()
|
|
|
|
|
select {
|
|
|
|
|
case out := <-ch:
|
|
|
|
|
return out, true
|
|
|
|
|
case <-time.After(timeout):
|
|
|
|
|
var zero T
|
|
|
|
|
return zero, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneToRGBA(src image.Image, width, height int) *image.RGBA {
|
|
|
|
|
if src == nil {
|
|
|
|
|
return image.NewRGBA(image.Rect(0, 0, width, height))
|
|
|
|
|
}
|
|
|
|
|
if rgba, ok := src.(*image.RGBA); ok {
|
|
|
|
|
out := image.NewRGBA(rgba.Bounds())
|
|
|
|
|
copy(out.Pix, rgba.Pix)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
out := image.NewRGBA(src.Bounds())
|
|
|
|
|
draw.Draw(out, out.Bounds(), src, image.Point{}, draw.Src)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 13:45:38 -05:00
|
|
|
type presetEntry struct {
|
|
|
|
|
Name string
|
|
|
|
|
Path string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 14:24:18 -05:00
|
|
|
type presetInfo struct {
|
|
|
|
|
Name string
|
|
|
|
|
Path string
|
|
|
|
|
Source string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 13:45:38 -05:00
|
|
|
func listEmbeddedPresets() ([]presetEntry, error) {
|
|
|
|
|
entries, err := fs.ReadDir(embeddedPresets, "Presets")
|
|
|
|
|
if err != 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,
|
2026-03-11 15:00:29 -05:00
|
|
|
Path: path.Join("Presets", name),
|
2026-03-11 13:45:38 -05:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(presets, func(i, j int) bool {
|
|
|
|
|
return presets[i].Name < presets[j].Name
|
|
|
|
|
})
|
|
|
|
|
return presets, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadEmbeddedPreset(path string) (*Settings, error) {
|
|
|
|
|
raw, err := embeddedPresets.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return decodeSettingsJSON(raw)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 14:24:18 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 13:08:40 -05:00
|
|
|
func decodeSettingsJSON(raw []byte) (*Settings, error) {
|
|
|
|
|
var imported Settings
|
|
|
|
|
if err := json.Unmarshal(raw, &imported); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var rawKeys map[string]json.RawMessage
|
|
|
|
|
if err := json.Unmarshal(raw, &rawKeys); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
normalizeSettings(&imported, rawKeys)
|
|
|
|
|
return &imported, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveSettingsToFile(path string, settings *Settings) error {
|
|
|
|
|
exportSettings := *settings
|
|
|
|
|
exportSettings.LastExportPath = ""
|
|
|
|
|
exportSettings.ImageViewState = 0
|
|
|
|
|
|
|
|
|
|
data, err := json.MarshalIndent(&exportSettings, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return os.WriteFile(path, data, 0644)
|
|
|
|
|
}
|