From 8f75ffc43bbe7e7bbc96dbfe179ba170bb0d2623 Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Wed, 24 Jun 2026 21:36:32 +0200 Subject: [PATCH] Split GUI files --- cmd/cbconvert-gui/dialogs.go | 80 ++ cmd/cbconvert-gui/handlers.go | 442 +++++++++ cmd/cbconvert-gui/main.go | 1593 --------------------------------- cmd/cbconvert-gui/options.go | 101 +++ cmd/cbconvert-gui/settings.go | 248 +++++ cmd/cbconvert-gui/state.go | 124 +++ cmd/cbconvert-gui/widgets.go | 653 ++++++++++++++ 7 files changed, 1648 insertions(+), 1593 deletions(-) create mode 100644 cmd/cbconvert-gui/dialogs.go create mode 100644 cmd/cbconvert-gui/handlers.go create mode 100644 cmd/cbconvert-gui/options.go create mode 100644 cmd/cbconvert-gui/settings.go create mode 100644 cmd/cbconvert-gui/state.go create mode 100644 cmd/cbconvert-gui/widgets.go diff --git a/cmd/cbconvert-gui/dialogs.go b/cmd/cbconvert-gui/dialogs.go new file mode 100644 index 0000000..ea8d236 --- /dev/null +++ b/cmd/cbconvert-gui/dialogs.go @@ -0,0 +1,80 @@ +package main + +import "github.com/gen2brain/iup-go/iup" + +func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) { + ret := make([]string, 0) + + dlg := iup.FileDlg() + defer dlg.Destroy() + + if !directory { + mf := "YES" + if !multiple { + mf = "NO" + } + + dlg.SetAttributes(map[string]string{ + "DIALOGTYPE": "OPEN", + "MULTIPLEFILES": mf, + "MULTIVALUEPATH": "YES", + "EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|", + "FILTER": "*.cb*", // for Motif + "TITLE": title, + }) + } else { + dlg.SetAttributes(map[string]string{ + "DIALOGTYPE": "DIR", + "TITLE": title, + }) + } + + setStartDir(dlg, dirKey) + + iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT) + + if dlg.GetInt("STATUS") == 0 { + switch { + case multiple: + // MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works. + count := dlg.GetInt("MULTIVALUECOUNT") + if count > 1 { + for i := 1; i < count; i++ { + ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i)) + } + } else if value := dlg.GetAttribute("VALUE"); value != "" { + ret = append(ret, value) + } + default: + ret = append(ret, dlg.GetAttribute("VALUE")) + } + + rememberDir(dlg, dirKey) + } + + return ret, nil +} + +func saveDlg(title, dirKey string) string { + dlg := iup.FileDlg() + defer dlg.Destroy() + + dlg.SetAttributes(map[string]string{ + "DIALOGTYPE": "SAVE", + "EXTFILTER": "Comic Files|*.cbz;*.cbt|", + "FILTER": "*.cb*", // for Motif + "TITLE": title, + }) + + setStartDir(dlg, dirKey) + + iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT) + + if dlg.GetInt("STATUS") == -1 { + return "" + } + + rememberDir(dlg, dirKey) + + return dlg.GetAttribute("VALUE") +} diff --git a/cmd/cbconvert-gui/handlers.go b/cmd/cbconvert-gui/handlers.go new file mode 100644 index 0000000..c41da59 --- /dev/null +++ b/cmd/cbconvert-gui/handlers.go @@ -0,0 +1,442 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/gen2brain/cbconvert" + "github.com/gen2brain/iup-go/iup" +) + +// selectRow focuses and selects the given 0-based row in the table. +func selectRow(i int) { + if i < 0 || i >= len(files) { + return + } + + index = i + iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1)) +} + +// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file. +func onSort(ih iup.Ihandle, col int) int { + n := len(files) + if n < 2 { + return iup.DEFAULT + } + + rowKey := func(name, size string) string { + return name + "\x00" + size + } + + buckets := make(map[string][]int, n) + for i, f := range files { + size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64) + k := rowKey(f.Name, size) + buckets[k] = append(buckets[k], i) + } + + var selPath string + if index >= 0 && index < len(files) { + selPath = files[index].Path + } + + reordered := make([]cbconvert.File, 0, n) + for lin := 1; lin <= n; lin++ { + k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3)) + idxs := buckets[k] + if len(idxs) == 0 { + return iup.DEFAULT + } + reordered = append(reordered, files[idxs[0]]) + buckets[k] = idxs[1:] + } + + files = reordered + + index = -1 + if selPath != "" { + for i, f := range files { + if f.Path == selPath { + selectRow(i) + break + } + } + } + + return iup.DEFAULT +} + +// appendFile adds a file as a new row to the table and the files slice. +func appendFile(file cbconvert.File) { + files = append(files, file) + + t := iup.GetHandle("Table") + lin := len(files) + t.SetAttribute("NUMLIN", strconv.Itoa(lin)) + iup.SetAttributeId2(t, "", lin, 1, file.Name) + iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path)) + iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64)) +} + +func previewPost() { + if index == -1 || len(files) == 0 { + return + } + + width, height := previewSize() + iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES") + if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" { + iup.GetHandle("Preview").SetAttribute("IMAGE", "") + } + + opts := options() + + go func(opts cbconvert.Options) { + conv := cbconvert.New(opts) + + var s string + file := files[index] + + img, err := conv.Preview(file.Path, file.Stat, width, height) + if err != nil { + s = err.Error() + fmt.Println(err) + } + + iup.PostMessage(iup.GetHandle("Preview"), s, 0, img) + }(opts) +} + +func onAddFiles(ih iup.Ihandle) int { + args, err := fileDlg("Add Files", true, false, inputDirKey) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + if len(args) > 0 { + conv := cbconvert.New(options()) + + fs, err := conv.Files(args) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + wasEmpty := len(files) == 0 + + for _, file := range fs { + appendFile(file) + } + + if wasEmpty && len(files) > 0 { + selectRow(0) + } + + setActive() + + if wasEmpty { + previewPost() + } + } + + return iup.DEFAULT +} + +func onAddDir(ih iup.Ihandle) int { + args, err := fileDlg("Add Directory", false, true, inputDirKey) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + if len(args) > 0 { + conv := cbconvert.New(options()) + + fs, err := conv.Files(args) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + wasEmpty := len(files) == 0 + + for _, file := range fs { + appendFile(file) + } + + if wasEmpty && len(files) > 0 { + selectRow(0) + } + + setActive() + + if wasEmpty { + previewPost() + } + } + + return iup.DEFAULT +} + +func onRemove(ih iup.Ihandle) int { + if index < 0 || index >= len(files) { + return iup.IGNORE + } + + iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1)) + files = slices.Delete(files, index, index+1) + + if index >= len(files) { + index = len(files) - 1 + } + + setActive() + previewPost() + + return iup.DEFAULT +} + +func onRemoveAll(ih iup.Ihandle) int { + index = -1 + files = make([]cbconvert.File, 0) + + iup.GetHandle("Table").SetAttribute("NUMLIN", "0") + setActive() + + return iup.DEFAULT +} + +func onThumbnail(ih iup.Ihandle) int { + conv := cbconvert.New(options()) + conv.Nfiles = len(files) + activeConv = conv + setBusy(true) + + conv.OnProgress = func() { + iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv) + } + + var canceled = false + conv.OnCancel = func() { + canceled = true + } + + iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { + if c == iup.K_ESC { + conv.Cancel() + } + + return iup.DEFAULT + })) + + iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv) + + go func(c *cbconvert.Converter) { + for _, file := range files { + if canceled { + break + } + + if err := c.Thumbnail(file); err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + continue + } + } + + iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) + }(conv) + + return iup.DEFAULT +} + +func onCover(ih iup.Ihandle) int { + conv := cbconvert.New(options()) + conv.Nfiles = len(files) + activeConv = conv + setBusy(true) + + conv.OnProgress = func() { + iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv) + } + + var canceled = false + conv.OnCancel = func() { + canceled = true + } + + iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { + if c == iup.K_ESC { + conv.Cancel() + } + + return iup.DEFAULT + })) + + iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv) + + go func(c *cbconvert.Converter) { + for _, file := range files { + if canceled { + break + } + + if err := c.Cover(file); err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + continue + } + } + + iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) + }(conv) + + return iup.DEFAULT +} + +func onConvert(ih iup.Ihandle) int { + if busy { + if activeConv != nil { + activeConv.Cancel() + } + + return iup.DEFAULT + } + + conv := cbconvert.New(options()) + conv.Nfiles = len(files) + activeConv = conv + setBusy(true) + + conv.OnStart = func() { + iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv) + } + + conv.OnProgress = func() { + iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv) + } + + iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { + if c == iup.K_ESC { + conv.Cancel() + } + + return iup.DEFAULT + })).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int { + if err := os.RemoveAll(conv.Workdir); err != nil { + fmt.Println(err) + } + + return iup.DEFAULT + })) + + convertErr := func(err error) { + if errors.Is(err, context.Canceled) { + if err := os.RemoveAll(conv.Workdir); err != nil { + fmt.Println(err) + } + + return + } + + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + if err := os.RemoveAll(conv.Workdir); err != nil { + fmt.Println(err) + } + } + + go func(c *cbconvert.Converter) { + if c.Opts.Combine { + if err := c.Combine(files); err != nil { + convertErr(err) + } + } else { + for _, file := range files { + if err := c.Convert(file); err != nil { + convertErr(err) + if errors.Is(err, context.Canceled) { + break + } + + continue + } + } + } + + iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) + }(conv) + + return iup.DEFAULT +} + +func onOutputDirectory(ih iup.Ihandle) int { + args, err := fileDlg("Output Directory", false, true, outputDirKey) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + if len(args) == 1 { + iup.GetHandle("OutDir").SetAttribute("VALUE", args[0]) + } + + setActive() + + return iup.DEFAULT +} + +func onOutputFile(ih iup.Ihandle) int { + name := saveDlg("Output File", outputDirKey) + if name != "" { + iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name)) + iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name)) + setActive() + } + + return iup.DEFAULT +} + +func onFilterChanged(ih iup.Ihandle) int { + switch ih.GetInt("VALUE") { + case 1: + ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing") + case 2: + ih.SetAttribute("TIP", "Box filter (averaging pixels)") + case 3: + ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast") + case 4: + ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter") + case 5: + ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter") + case 6: + ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal") + case 7: + ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters") + } + + previewPost() + + return iup.DEFAULT +} diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index ecc0f14..214801e 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -2,19 +2,13 @@ package main import ( "bytes" - "context" _ "embed" - "errors" "flag" "fmt" - "image/gif" "image/png" - "net/url" "os" "path/filepath" "runtime/debug" - "slices" - "strconv" "strings" "github.com/gen2brain/cbconvert" @@ -41,55 +35,6 @@ var ( busy bool ) -const ( - pathsGroup = "Paths" - profilesGroup = "Profiles" - - inputDirKey = "InputDir" - outputDirKey = "OutputDir" -) - -type settingKind int - -const ( - kindBool settingKind = iota - kindInt - kindStr -) - -type setting struct { - handle string - kind settingKind - def string -} - -var settings = []setting{ - {"Recursive", kindBool, "OFF"}, - {"NoRGB", kindBool, "OFF"}, - {"NoCover", kindBool, "OFF"}, - {"NoConvert", kindBool, "OFF"}, - {"NoNonImage", kindBool, "OFF"}, - {"Combine", kindBool, "OFF"}, - {"Fit", kindBool, "OFF"}, - {"Lossless", kindBool, "OFF"}, - {"Grayscale", kindBool, "OFF"}, - {"OutDir", kindStr, ""}, - {"Suffix", kindStr, ""}, - {"Width", kindStr, ""}, - {"Height", kindStr, ""}, - {"DPI", kindStr, "Default"}, - {"Size", kindInt, "0"}, - {"Quality", kindInt, "75"}, - {"Effort", kindInt, "0"}, - {"Brightness", kindInt, "0"}, - {"Contrast", kindInt, "0"}, - {"Format", kindInt, "1"}, - {"Archive", kindInt, "1"}, - {"ZipLevel", kindInt, "1"}, - {"Filter", kindInt, "3"}, - {"Rotate", kindInt, "1"}, -} - func init() { if appVersion != "" { return @@ -189,406 +134,6 @@ func parseFlags() { } } -func options() cbconvert.Options { - var opts cbconvert.Options - opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON" - opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON" - opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON" - opts.Size = iup.GetHandle("Size").GetInt("VALUE") - opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE") - opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE") - opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON" - opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON" - opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING")) - opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING")) - opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) - opts.Width = iup.GetHandle("Width").GetInt("VALUE") - opts.Height = iup.GetHandle("Height").GetInt("VALUE") - opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE")) - opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON" - opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1 - opts.Quality = iup.GetHandle("Quality").GetInt("VALUE") - switch opts.Format { - case "webp", "avif", "jxl": - opts.Effort = iup.GetHandle("Effort").GetInt("VALUE") - default: - opts.Effort = -1 - } - opts.Lossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON" - opts.Combine = iup.GetHandle("Combine").GetAttribute("VALUE") == "ON" - if opts.Combine { - opts.OutFile = iup.GetHandle("OutFile").GetAttribute("VALUE") - } - opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON" - opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE") - opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE") - opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING") - - return opts -} - -func setActive() { - if busy { - return - } - - opts := options() - count := iup.GetHandle("Table").GetInt("NUMLIN") - - if count == 0 { - iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO") - iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO") - - iup.GetHandle("Preview").SetAttribute("IMAGE", "logo") - iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "") - } else { - if index != -1 { - iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES") - } - iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES") - } - - active := "YES" - var tip string - switch { - case count == 0 && opts.OutDir == "": - active, tip = "NO", "Add files and set output directory" - case count == 0: - active, tip = "NO", "Add files" - case opts.OutDir == "": - active, tip = "NO", "Set output directory" - } - - enabledTip := map[string]string{ - "Thumbnail": "Extract cover thumbnails", - "Cover": "Extract covers", - "Convert": "Convert files to the selected format", - } - - for _, h := range []string{"Thumbnail", "Cover", "Convert"} { - b := iup.GetHandle(h) - b.SetAttribute("ACTIVE", active) - if active == "YES" { - b.SetAttribute("TIP", enabledTip[h]) - } else { - b.SetAttribute("TIP", tip) - } - } - - if opts.NoConvert { - iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO") - iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO") - } else { - iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES") - iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES") - } - - canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl" - losslessOn := canLossless && opts.Lossless - - if (opts.Format == "jpeg" || canLossless) && !opts.NoConvert && !losslessOn { - iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES") - } else { - iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO") - } - - if canLossless && !opts.NoConvert { - iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES") - iup.GetHandle("Lossless").SetAttribute("ACTIVE", "YES") - } else { - iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO") - iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO") - } - - if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert { - iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES") - } else { - iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO") - } - - if opts.Combine { - iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "YES") - } else { - iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO") - } - - if opts.Archive == "zip" { - iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES") - } else { - iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO") - } -} - -// shellArg quotes a command-line argument that contains whitespace. -func shellArg(s string) string { - if strings.ContainsAny(s, " \t") { - return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` - } - - return s -} - -func commandLine() string { - parts := append([]string{"cbconvert", "convert"}, options().Args()...) - for _, file := range files { - parts = append(parts, file.Path) - } - - for i, p := range parts { - parts[i] = shellArg(p) - } - - return strings.Join(parts, " ") -} - -func onCommand(iup.Ihandle) int { - iup.GetText("Command Line", commandLine(), -1) - - return iup.DEFAULT -} - -// zipLevel maps the compression dropdown selection to Options.ZipLevel. -func zipLevel(value string) int { - switch value { - case "Default": - return -1 - case "Store (none)": - return 0 - default: - level, err := strconv.Atoi(value) - if err != nil { - return -1 - } - - return level - } -} - -func dpiValue(value string) int { - dpi, err := strconv.Atoi(strings.TrimSpace(value)) - if err != nil { - return 0 - } - - return dpi -} - -func profileGroup(name string) string { - return "Profile:" + name -} - -func profileNames() []string { - s := iup.ConfigGetVariableStr(config, profilesGroup, "Names") - if s == "" { - return nil - } - - return strings.Split(s, ";") -} - -func currentProfile() string { - return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default") -} - -func setStartDir(dlg iup.Ihandle, key string) { - if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" { - dlg.SetAttribute("DIRECTORY", dir) - } -} - -func rememberDir(dlg iup.Ihandle, key string) { - dir := dlg.GetAttribute("DIRECTORY") - if dir == "" { - return - } - - iup.ConfigSetVariableStr(config, pathsGroup, key, dir) - iup.ConfigSave(config) -} - -func settingsSave(group string) { - for _, s := range settings { - h := iup.GetHandle(s.handle) - switch s.kind { - case kindBool: - v := 0 - if h.GetAttribute("VALUE") == "ON" { - v = 1 - } - iup.ConfigSetVariableInt(config, group, s.handle, v) - case kindInt: - iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE")) - case kindStr: - iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE")) - } - } - - iup.ConfigSave(config) -} - -// settingsApply sets every control from the given profile group, or from defaults when a group is empty. -func settingsApply(group string) { - for _, s := range settings { - h := iup.GetHandle(s.handle) - switch s.kind { - case kindBool: - def := 0 - if s.def == "ON" { - def = 1 - } - v := def - if group != "" { - v = iup.ConfigGetVariableIntDef(config, group, s.handle, def) - } - if v != 0 { - h.SetAttribute("VALUE", "ON") - } else { - h.SetAttribute("VALUE", "OFF") - } - case kindInt: - def, _ := strconv.Atoi(s.def) - v := def - if group != "" { - v = iup.ConfigGetVariableIntDef(config, group, s.handle, def) - } - h.SetAttribute("VALUE", strconv.Itoa(v)) - case kindStr: - v := s.def - if group != "" { - v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def) - } - h.SetAttribute("VALUE", v) - } - } - - syncLabels() - setActive() - previewPost() -} - -// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format. -func syncLabels() { - iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE")) - iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE")) - iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE")) - - format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) - eff := iup.GetHandle("Effort").GetInt("VALUE") - setEffort(format) - switch format { - case "webp", "avif", "jxl": - val := iup.GetHandle("Effort") - val.SetAttribute("VALUE", strconv.Itoa(eff)) - iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff)) - } - - iup.Refresh(iup.GetHandle("Tabs")) -} - -func fillProfileList() { - list := iup.GetHandle("Profile") - list.SetAttribute("REMOVEITEM", "ALL") - - cur := currentProfile() - sel := 1 - for i, n := range profileNames() { - list.SetAttribute(strconv.Itoa(i+1), n) - if n == cur { - sel = i + 1 - } - } - - list.SetAttribute("VALUE", strconv.Itoa(sel)) -} - -// profilesInit loads the current profile on startup, creating a default one on the first run. -func profilesInit() { - if len(profileNames()) == 0 { - iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default") - iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default") - settingsSave(profileGroup("Default")) - } - - fillProfileList() - settingsApply(profileGroup(currentProfile())) -} - -func onProfileSelect(ih iup.Ihandle) int { - name := ih.GetAttribute("VALUESTRING") - if name == "" { - return iup.DEFAULT - } - - iup.ConfigSetVariableStr(config, profilesGroup, "Current", name) - iup.ConfigSave(config) - - settingsApply(profileGroup(name)) - - return iup.DEFAULT -} - -func onSave(iup.Ihandle) int { - name := currentProfile() - if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 { - return iup.DEFAULT - } - - name = strings.TrimSpace(name) - if name == "" || strings.ContainsAny(name, ".;") { - iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.") - - return iup.DEFAULT - } - - settingsSave(profileGroup(name)) - - names := profileNames() - if !slices.Contains(names, name) { - names = append(names, name) - iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";")) - } - - iup.ConfigSetVariableStr(config, profilesGroup, "Current", name) - iup.ConfigSave(config) - - fillProfileList() - - return iup.DEFAULT -} - -func onReset(iup.Ihandle) int { - settingsApply("") - - return iup.DEFAULT -} - -func setEffort(format string) { - val := iup.GetHandle("Effort") - - var name string - - switch format { - case "webp": - val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4") - val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)") - name = "Method" - case "avif": - val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10") - val.SetAttribute("TIP", "AVIF speed, higher is faster/worse (0-10, default 10)") - name = "Speed" - case "jxl": - val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7") - val.SetAttribute("TIP", "JXL effort, higher is better/slower (1-10, default 7)") - name = "Effort" - default: - return - } - - val.SetAttribute("EFFORTNAME", name) - iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", name, val.GetInt("VALUE"))) - iup.Refresh(iup.GetHandle("LabelEffort")) -} - func layout() iup.Ihandle { return iup.Vbox( iup.Hbox( @@ -606,1141 +151,3 @@ func layout() iup.Ihandle { status(), ) } - -// tableRowColors sets the alternating row colors for dark or light mode. -func tableRowColors(t iup.Ihandle, dark bool) { - even, odd := "#F0F0F0", "#FFFFFF" - if dark { - even, odd = "#3A3A3A", "#2D2D2D" - } - t.SetAttribute("EVENROWCOLOR", even) - t.SetAttribute("ODDROWCOLOR", odd) -} - -func list() iup.Ihandle { - t := iup.Table().SetHandle("Table") - t.SetAttributes(map[string]string{ - "EXPAND": "YES", - "NUMCOL": "3", - "NUMLIN": "0", - "TITLE1": "Title", - "TITLE2": "Type", - "TITLE3": "Size (MiB)", - "WIDTH1": "150", - "WIDTH2": "50", - "WIDTH3": "100", - "ALIGNMENT2": "ACENTER", - "ALIGNMENT3": "ARIGHT", - "SELECTIONMODE": "SINGLE", - "USERRESIZE": "YES", - "STRETCHLAST": "NO", - "FOCUSRECT": "NO", - "SORTABLE": "YES", - "ALTERNATECOLOR": "YES", - }) - - tableRowColors(t, iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES") - - t.SetCallback("ENTERITEM_CB", iup.EnterItemFunc(func(ih iup.Ihandle, lin, col int) int { - index = lin - 1 - setActive() - previewPost() - - return iup.DEFAULT - })) - - t.SetCallback("SORT_CB", iup.TableSortFunc(onSort)) - - t.SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int { - dec, err := url.QueryUnescape(fileName) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - conv := cbconvert.New(options()) - - fs, err := conv.Files([]string{dec}) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } - - return iup.DEFAULT - })) - - return iup.Vbox(t) -} - -// selectRow focuses and selects the given 0-based row in the table. -func selectRow(i int) { - if i < 0 || i >= len(files) { - return - } - - index = i - iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1)) -} - -// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file. -func onSort(ih iup.Ihandle, col int) int { - n := len(files) - if n < 2 { - return iup.DEFAULT - } - - rowKey := func(name, size string) string { - return name + "\x00" + size - } - - buckets := make(map[string][]int, n) - for i, f := range files { - size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64) - k := rowKey(f.Name, size) - buckets[k] = append(buckets[k], i) - } - - var selPath string - if index >= 0 && index < len(files) { - selPath = files[index].Path - } - - reordered := make([]cbconvert.File, 0, n) - for lin := 1; lin <= n; lin++ { - k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3)) - idxs := buckets[k] - if len(idxs) == 0 { - return iup.DEFAULT - } - reordered = append(reordered, files[idxs[0]]) - buckets[k] = idxs[1:] - } - - files = reordered - - index = -1 - if selPath != "" { - for i, f := range files { - if f.Path == selPath { - selectRow(i) - break - } - } - } - - return iup.DEFAULT -} - -// appendFile adds a file as a new row to the table and the files slice. -func appendFile(file cbconvert.File) { - files = append(files, file) - - t := iup.GetHandle("Table") - lin := len(files) - t.SetAttribute("NUMLIN", strconv.Itoa(lin)) - iup.SetAttributeId2(t, "", lin, 1, file.Name) - iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path)) - iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64)) -} - -func previewPost() { - if index == -1 || len(files) == 0 { - return - } - - width, height := previewSize() - iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES") - if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" { - iup.GetHandle("Preview").SetAttribute("IMAGE", "") - } - - opts := options() - - go func(opts cbconvert.Options) { - conv := cbconvert.New(opts) - - var s string - file := files[index] - - img, err := conv.Preview(file.Path, file.Stat, width, height) - if err != nil { - s = err.Error() - fmt.Println(err) - } - - iup.PostMessage(iup.GetHandle("Preview"), s, 0, img) - }(opts) -} - -func previewSize() (int, int) { - var width, height int - sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x") - if len(sp) == 2 { - width, _ = strconv.Atoi(sp[0]) - height, _ = strconv.Atoi(sp[1]) - } - - return width, height -} - -func preview() iup.Ihandle { - return iup.Frame( - iup.Vbox( - iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview"). - SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { - img := p.(cbconvert.Image) - - iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES") - - if img.Image != nil && len(s) == 0 { - iup.Destroy(iup.GetHandle("cover")) - iup.ImageFromImage(img.Image).SetHandle("cover") - - ih.SetAttribute("IMAGE", "cover") - iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height)) - } else { - ih.SetAttribute("IMAGE", "logo") - iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "") - - sp := strings.Split(s, ": ") - if len(sp) > 1 { - iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": "))) - } - } - - return iup.DEFAULT - })), - iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"), - ), - ) -} - -func tabs() iup.Ihandle { - vboxInput := iup.Hbox( - iup.Vbox( - iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive"). - SetAttributes(`TIP="Process subdirectories recursively"`), - iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB"). - SetAttributes(`TIP="Do not convert images that have RGB colorspace"`), - iup.Toggle(" Exclude Cover").SetHandle("NoCover"). - SetAttributes(`TIP="Do not convert the cover image"`), - iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage"). - SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"), - iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert"). - SetAttributes(`TIP="Copy images from archive or directory without modifications"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - - return iup.DEFAULT - })), - ).SetAttributes("NGAP=10"), - iup.Space().SetAttribute("SIZE", "15"), - iup.Vbox( - iup.Vbox( - iup.Label("Minimum Size (MiB):"), - iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size"). - SetAttributes(`TIP="Process only files larger than minimum size"`), - ), - iup.Vbox( - iup.Label("Document DPI:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "EDITBOX": "YES", - "VISIBLECOLUMNS": "6", - "VALUE": "Default", - "1": "Default", - "2": "150", - "3": "300", - "4": "600", - "5": "1200", - }).SetHandle("DPI"). - SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"), - ), - ).SetAttributes("NGAP=10"), - ).SetHandle("VboxInput") - - vboxOutput := iup.Hbox( - iup.Vbox( - iup.Vbox( - iup.Label("Output Directory:"), - iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir"). - SetAttribute("TIP", "Directory where converted files are written (required)"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - - return iup.DEFAULT - })), - iup.Space().SetAttribute("SIZE", "5x0"), - iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)), - ), - iup.Vbox( - iup.Label("Add Suffix to Output File:"), - iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix"). - SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"), - ), - iup.Vbox( - iup.Label("Archive Format:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "VALUE": "1", - "1": "ZIP", - "2": "TAR", - }).SetHandle("Archive"). - SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Label("Compression:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "VALUE": "1", - "1": "Default", - "2": "Store (none)", - "3": "1", - "4": "2", - "5": "3", - "6": "4", - "7": "5", - "8": "6", - "9": "7", - "10": "8", - "11": "9", - }).SetHandle("ZipLevel"). - SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"), - ).SetHandle("VboxZipLevel"), - ).SetAttributes("NGAP=10"), - iup.Space().SetAttribute("SIZE", "15"), - iup.Vbox( - iup.Vbox( - iup.Toggle(" Combine into single file").SetHandle("Combine"). - SetAttributes(`TIP="Merge all listed files into one archive"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Label("Output File:"), - iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile"). - SetAttribute("TIP", "Combined file name (default: first input + -combined)"), - iup.Space().SetAttribute("SIZE", "5x0"), - iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onOutputFile)), - ).SetHandle("VboxOutFile"), - ).SetAttributes("NGAP=10"), - ).SetHandle("VboxOutput") - - vboxImage := iup.Hbox( - iup.Vbox( - iup.Vbox( - iup.Label("Format:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "VALUE": "1", - "1": "JPEG", - "2": "PNG", - "3": "TIFF", - "4": "BMP", - "5": "WEBP", - "6": "AVIF", - "7": "JXL", - }).SetHandle("Format"). - SetAttribute("TIP", "Output image format for the converted pages"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING"))) - setActive() - previewPost() - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Label("Size:"), - iup.Hbox( - iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width"). - SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - iup.Space().SetAttribute("SIZE", "2"), - iup.Label("x"), - iup.Space().SetAttribute("SIZE", "2"), - iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height"). - SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), - ), - iup.Vbox( - iup.Toggle(" Best Fit").SetHandle("Fit"). - SetAttributes(`TIP="Best fit for required width and height"`), - ), - iup.Vbox( - iup.Label("Resize Filter:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "VALUE": "3", - "TIP": "Linear is the bilinear filter, smooth and reasonably fast", - "1": "NearestNeighbor", - "2": "Box", - "3": "Linear", - "4": "MitchellNetravali", - "5": "CatmullRom", - "6": "Gaussian", - "7": "Lanczos", - }).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)), - ), - ).SetAttributes("NGAP=10"), - iup.Space().SetAttribute("SIZE", "15"), - iup.Vbox( - iup.Vbox( - iup.Hbox( - iup.Label("Quality: "), - iup.Label("75").SetHandle("LabelQuality"), - ).SetAttributes("NMARGIN=0"), - iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality"). - SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE")) - iup.Refresh(iup.GetHandle("LabelQuality")) - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - ).SetHandle("VboxQuality"), - iup.Vbox( - iup.Label("Effort:").SetHandle("LabelEffort"), - iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort"). - SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", ih.GetAttribute("EFFORTNAME"), ih.GetInt("VALUE"))) - iup.Refresh(iup.GetHandle("LabelEffort")) - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - ).SetHandle("VboxEffort"), - iup.Vbox( - iup.Toggle(" Lossless").SetHandle("Lossless"). - SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - setActive() - previewPost() - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Toggle(" Grayscale").SetHandle("Grayscale"). - SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - previewPost() - - return iup.DEFAULT - })), - ), - ).SetAttributes("NGAP=10"), - ).SetHandle("VboxImage") - - vboxTransform := iup.Vbox( - iup.Vbox( - iup.Hbox( - iup.Label("Brightness: "), - iup.Label("0").SetHandle("LabelBrightness"), - ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), - iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness"). - SetAttributes(`TIP="Adjust the brightness of the images"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE")) - iup.Refresh(iup.GetHandle("LabelBrightness")) - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Hbox( - iup.Label("Contrast: "), - iup.Label("0").SetHandle("LabelContrast"), - ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), - iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast"). - SetAttributes(`TIP="Adjust the contrast of the images"`). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE")) - iup.Refresh(iup.GetHandle("LabelContrast")) - ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) - - return iup.DEFAULT - })). - SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { - if ih.GetAttribute("MYVALUE") != "" { - previewPost() - } - ih.SetAttribute("MYVALUE", "") - - return iup.DEFAULT - })), - ), - iup.Vbox( - iup.Label("Rotate:"), - iup.List().SetAttributes(map[string]string{ - "DROPDOWN": "YES", - "VALUE": "1", - "1": "0", - "2": "90", - "3": "180", - "4": "270", - }).SetHandle("Rotate"). - SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { - previewPost() - - return iup.DEFAULT - })), - ), - ).SetHandle("VboxTransform").SetAttributes("NGAP=10") - - return iup.Tabs( - vboxInput.SetAttributes("TABTITLE=Input, NMARGIN=10x10"), - vboxOutput.SetAttributes("TABTITLE=Output, NMARGIN=10x10"), - vboxImage.SetAttributes("TABTITLE=Image, NMARGIN=10x10"), - vboxTransform.SetAttributes("TABTITLE=Transform, NMARGIN=10x10"), - ).SetHandle("Tabs") -} - -func buttons() iup.Ihandle { - addFiles := iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onAddFiles)) - addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onAddDir)) - remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onRemove)) - removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onRemoveAll)) - thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onThumbnail)) - cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onCover)) - convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetCallback("ACTION", iup.ActionFunc(onConvert)) - reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetAttribute("TIP", "Restore all settings to their defaults"). - SetCallback("ACTION", iup.ActionFunc(onReset)) - save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetAttribute("TIP", "Save current settings to a profile"). - SetCallback("ACTION", iup.ActionFunc(onSave)) - - command := iup.Button("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). - SetAttribute("TIP", "Show the equivalent command line"). - SetCallback("ACTION", iup.ActionFunc(onCommand)) - - profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile"). - SetAttribute("TIP", "Select a settings profile"). - SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect)) - - iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH") - iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL") - - return iup.Vbox( - iup.Vbox( - addFiles, - addDir, - remove, - removeAll, - ).SetAttribute("NGAP", "2"), - iup.Space().SetAttribute("SIZE", "x5"), - iup.Vbox( - thumbnail, - cover, - ).SetAttribute("NGAP", "2"), - iup.Space().SetAttribute("SIZE", "x5"), - iup.Vbox( - convert, - ), - iup.Fill(), - iup.Vbox( - iup.Label("Profile:"), - profile, - reset, - save, - command, - ).SetAttribute("NGAP", "2"), - ).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER") -} - -func status() iup.Ihandle { - return iup.Hbox( - loading(), - iup.Fill(), - iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"), - iup.Space().SetAttribute("SIZE", "5"), - iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"), - iup.Space().SetAttribute("SIZE", "5"), - iup.ProgressBar().SetAttributes("VISIBLE=NO").SetHandle("ProgressBar"). - SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { - switch s { - case "convert": - conv := p.(*cbconvert.Converter) - ih.SetAttributes("VALUE=0, VISIBLE=YES") - ih.SetAttribute("MAX", conv.Ncontents) - - iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles)) - iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES") - iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES") - - iup.Refresh(iup.GetHandle("StatusBar")) - case "start": - conv := p.(*cbconvert.Converter) - ih.SetAttributes("VALUE=0, VISIBLE=YES") - ih.SetAttribute("MAX", conv.Nfiles) - - iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES") - case "progress": - conv := p.(*cbconvert.Converter) - ih.SetAttribute("VALUE", conv.CurrContent) - iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents)) - - iup.Refresh(iup.GetHandle("StatusBar")) - case "progress2": - conv := p.(*cbconvert.Converter) - ih.SetAttribute("VALUE", conv.CurrFile) - iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles)) - - iup.Refresh(iup.GetHandle("StatusBar")) - case "finish": - setBusy(false) - - iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`) - iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`) - ih.SetAttributes("VALUE=0, VISIBLE=NO") - - iup.Refresh(iup.GetHandle("StatusBar")) - - iup.GetHandle("dlg").SetCallback("K_ANY", nil) - iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil) - } - - return iup.DEFAULT - })), - iup.Space().SetAttribute("SIZE", "5x0"), - ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar") -} - -func loading() iup.Ihandle { - img, _ := gif.DecodeAll(bytes.NewReader(appLoading)) - animation := iup.User() - - for idx, i := range img.Image { - name := fmt.Sprintf("Loading%d", idx) - iup.ImageFromImage(i).SetHandle(name) - iup.Append(animation, iup.GetHandle(name)) - } - - return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading") -} - -func onAddFiles(ih iup.Ihandle) int { - args, err := fileDlg("Add Files", true, false, inputDirKey) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - if len(args) > 0 { - conv := cbconvert.New(options()) - - fs, err := conv.Files(args) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } - } - - return iup.DEFAULT -} - -func onAddDir(ih iup.Ihandle) int { - args, err := fileDlg("Add Directory", false, true, inputDirKey) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - if len(args) > 0 { - conv := cbconvert.New(options()) - - fs, err := conv.Files(args) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } - } - - return iup.DEFAULT -} - -func onRemove(ih iup.Ihandle) int { - if index < 0 || index >= len(files) { - return iup.IGNORE - } - - iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1)) - files = slices.Delete(files, index, index+1) - - if index >= len(files) { - index = len(files) - 1 - } - - setActive() - previewPost() - - return iup.DEFAULT -} - -func onRemoveAll(ih iup.Ihandle) int { - index = -1 - files = make([]cbconvert.File, 0) - - iup.GetHandle("Table").SetAttribute("NUMLIN", "0") - setActive() - - return iup.DEFAULT -} - -func onThumbnail(ih iup.Ihandle) int { - conv := cbconvert.New(options()) - conv.Nfiles = len(files) - activeConv = conv - setBusy(true) - - conv.OnProgress = func() { - iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv) - } - - var canceled = false - conv.OnCancel = func() { - canceled = true - } - - iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { - if c == iup.K_ESC { - conv.Cancel() - } - - return iup.DEFAULT - })) - - iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv) - - go func(c *cbconvert.Converter) { - for _, file := range files { - if canceled { - break - } - - if err := c.Thumbnail(file); err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - continue - } - } - - iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) - }(conv) - - return iup.DEFAULT -} - -func onCover(ih iup.Ihandle) int { - conv := cbconvert.New(options()) - conv.Nfiles = len(files) - activeConv = conv - setBusy(true) - - conv.OnProgress = func() { - iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv) - } - - var canceled = false - conv.OnCancel = func() { - canceled = true - } - - iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { - if c == iup.K_ESC { - conv.Cancel() - } - - return iup.DEFAULT - })) - - iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv) - - go func(c *cbconvert.Converter) { - for _, file := range files { - if canceled { - break - } - - if err := c.Cover(file); err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - continue - } - } - - iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) - }(conv) - - return iup.DEFAULT -} - -// setBusy locks the UI while an operation runs and turns Convert into a Cancel button. -func setBusy(on bool) { - busy = on - - // Controls not governed by setActive; setActive owns the rest. - always := "YES" - if on { - always = "NO" - } - for _, h := range []string{"AddFiles", "AddDir", "Profile", "Reset", "Save", "Command", "Tabs", "Table"} { - iup.GetHandle(h).SetAttribute("ACTIVE", always) - } - - convert := iup.GetHandle("Convert") - if on { - for _, h := range []string{"Remove", "RemoveAll", "Thumbnail", "Cover"} { - iup.GetHandle(h).SetAttribute("ACTIVE", "NO") - } - convert.SetAttribute("ACTIVE", "YES") - convert.SetAttribute("TITLE", "Cancel") - convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)") - } else { - activeConv = nil - convert.SetAttribute("TITLE", "&Convert") - convert.SetAttribute("TIP", "Convert files to the selected format") - setActive() // restores the conditional buttons and option boxes - } -} - -func onConvert(ih iup.Ihandle) int { - if busy { - if activeConv != nil { - activeConv.Cancel() - } - - return iup.DEFAULT - } - - conv := cbconvert.New(options()) - conv.Nfiles = len(files) - activeConv = conv - setBusy(true) - - conv.OnStart = func() { - iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv) - } - - conv.OnProgress = func() { - iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv) - } - - iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int { - if c == iup.K_ESC { - conv.Cancel() - } - - return iup.DEFAULT - })).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int { - if err := os.RemoveAll(conv.Workdir); err != nil { - fmt.Println(err) - } - - return iup.DEFAULT - })) - - convertErr := func(err error) { - if errors.Is(err, context.Canceled) { - if err := os.RemoveAll(conv.Workdir); err != nil { - fmt.Println(err) - } - - return - } - - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - if err := os.RemoveAll(conv.Workdir); err != nil { - fmt.Println(err) - } - } - - go func(c *cbconvert.Converter) { - if c.Opts.Combine { - if err := c.Combine(files); err != nil { - convertErr(err) - } - } else { - for _, file := range files { - if err := c.Convert(file); err != nil { - convertErr(err) - if errors.Is(err, context.Canceled) { - break - } - - continue - } - } - } - - iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0) - }(conv) - - return iup.DEFAULT -} - -func onOutputDirectory(ih iup.Ihandle) int { - args, err := fileDlg("Output Directory", false, true, outputDirKey) - if err != nil { - iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) - fmt.Println(err) - - return iup.DEFAULT - } - - if len(args) == 1 { - iup.GetHandle("OutDir").SetAttribute("VALUE", args[0]) - } - - setActive() - - return iup.DEFAULT -} - -func onOutputFile(ih iup.Ihandle) int { - name := saveDlg("Output File", outputDirKey) - if name != "" { - iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name)) - iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name)) - setActive() - } - - return iup.DEFAULT -} - -func onFilterChanged(ih iup.Ihandle) int { - switch ih.GetInt("VALUE") { - case 1: - ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing") - case 2: - ih.SetAttribute("TIP", "Box filter (averaging pixels)") - case 3: - ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast") - case 4: - ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter") - case 5: - ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter") - case 6: - ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal") - case 7: - ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters") - } - - previewPost() - - return iup.DEFAULT -} - -func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) { - ret := make([]string, 0) - - dlg := iup.FileDlg() - defer dlg.Destroy() - - if !directory { - mf := "YES" - if !multiple { - mf = "NO" - } - - dlg.SetAttributes(map[string]string{ - "DIALOGTYPE": "OPEN", - "MULTIPLEFILES": mf, - "MULTIVALUEPATH": "YES", - "EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|", - "FILTER": "*.cb*", // for Motif - "TITLE": title, - }) - } else { - dlg.SetAttributes(map[string]string{ - "DIALOGTYPE": "DIR", - "TITLE": title, - }) - } - - setStartDir(dlg, dirKey) - - iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT) - - if dlg.GetInt("STATUS") == 0 { - switch { - case multiple: - // MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works. - count := dlg.GetInt("MULTIVALUECOUNT") - if count > 1 { - for i := 1; i < count; i++ { - ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i)) - } - } else if value := dlg.GetAttribute("VALUE"); value != "" { - ret = append(ret, value) - } - default: - ret = append(ret, dlg.GetAttribute("VALUE")) - } - - rememberDir(dlg, dirKey) - } - - return ret, nil -} - -func saveDlg(title, dirKey string) string { - dlg := iup.FileDlg() - defer dlg.Destroy() - - dlg.SetAttributes(map[string]string{ - "DIALOGTYPE": "SAVE", - "EXTFILTER": "Comic Files|*.cbz;*.cbt|", - "FILTER": "*.cb*", // for Motif - "TITLE": title, - }) - - setStartDir(dlg, dirKey) - - iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT) - - if dlg.GetInt("STATUS") == -1 { - return "" - } - - rememberDir(dlg, dirKey) - - return dlg.GetAttribute("VALUE") -} diff --git a/cmd/cbconvert-gui/options.go b/cmd/cbconvert-gui/options.go new file mode 100644 index 0000000..090d5bb --- /dev/null +++ b/cmd/cbconvert-gui/options.go @@ -0,0 +1,101 @@ +package main + +import ( + "strconv" + "strings" + + "github.com/gen2brain/cbconvert" + "github.com/gen2brain/iup-go/iup" +) + +func options() cbconvert.Options { + var opts cbconvert.Options + opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON" + opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON" + opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON" + opts.Size = iup.GetHandle("Size").GetInt("VALUE") + opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE") + opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE") + opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON" + opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON" + opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING")) + opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING")) + opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) + opts.Width = iup.GetHandle("Width").GetInt("VALUE") + opts.Height = iup.GetHandle("Height").GetInt("VALUE") + opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE")) + opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON" + opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1 + opts.Quality = iup.GetHandle("Quality").GetInt("VALUE") + switch opts.Format { + case "webp", "avif", "jxl": + opts.Effort = iup.GetHandle("Effort").GetInt("VALUE") + default: + opts.Effort = -1 + } + opts.Lossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON" + opts.Combine = iup.GetHandle("Combine").GetAttribute("VALUE") == "ON" + if opts.Combine { + opts.OutFile = iup.GetHandle("OutFile").GetAttribute("VALUE") + } + opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON" + opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE") + opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE") + opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING") + + return opts +} + +// shellArg quotes a command-line argument that contains whitespace. +func shellArg(s string) string { + if strings.ContainsAny(s, " \t") { + return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` + } + + return s +} + +func commandLine() string { + parts := append([]string{"cbconvert", "convert"}, options().Args()...) + for _, file := range files { + parts = append(parts, file.Path) + } + + for i, p := range parts { + parts[i] = shellArg(p) + } + + return strings.Join(parts, " ") +} + +func onCommand(iup.Ihandle) int { + iup.GetText("Command Line", commandLine(), -1) + + return iup.DEFAULT +} + +// zipLevel maps the compression dropdown selection to Options.ZipLevel. +func zipLevel(value string) int { + switch value { + case "Default": + return -1 + case "Store (none)": + return 0 + default: + level, err := strconv.Atoi(value) + if err != nil { + return -1 + } + + return level + } +} + +func dpiValue(value string) int { + dpi, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil { + return 0 + } + + return dpi +} diff --git a/cmd/cbconvert-gui/settings.go b/cmd/cbconvert-gui/settings.go new file mode 100644 index 0000000..4437995 --- /dev/null +++ b/cmd/cbconvert-gui/settings.go @@ -0,0 +1,248 @@ +package main + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "github.com/gen2brain/iup-go/iup" +) + +const ( + pathsGroup = "Paths" + profilesGroup = "Profiles" + + inputDirKey = "InputDir" + outputDirKey = "OutputDir" +) + +type settingKind int + +const ( + kindBool settingKind = iota + kindInt + kindStr +) + +type setting struct { + handle string + kind settingKind + def string +} + +var settings = []setting{ + {"Recursive", kindBool, "OFF"}, + {"NoRGB", kindBool, "OFF"}, + {"NoCover", kindBool, "OFF"}, + {"NoConvert", kindBool, "OFF"}, + {"NoNonImage", kindBool, "OFF"}, + {"Combine", kindBool, "OFF"}, + {"Fit", kindBool, "OFF"}, + {"Lossless", kindBool, "OFF"}, + {"Grayscale", kindBool, "OFF"}, + {"OutDir", kindStr, ""}, + {"Suffix", kindStr, ""}, + {"Width", kindStr, ""}, + {"Height", kindStr, ""}, + {"DPI", kindStr, "Default"}, + {"Size", kindInt, "0"}, + {"Quality", kindInt, "75"}, + {"Effort", kindInt, "0"}, + {"Brightness", kindInt, "0"}, + {"Contrast", kindInt, "0"}, + {"Format", kindInt, "1"}, + {"Archive", kindInt, "1"}, + {"ZipLevel", kindInt, "1"}, + {"Filter", kindInt, "3"}, + {"Rotate", kindInt, "1"}, +} + +func profileGroup(name string) string { + return "Profile:" + name +} + +func profileNames() []string { + s := iup.ConfigGetVariableStr(config, profilesGroup, "Names") + if s == "" { + return nil + } + + return strings.Split(s, ";") +} + +func currentProfile() string { + return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default") +} + +func setStartDir(dlg iup.Ihandle, key string) { + if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" { + dlg.SetAttribute("DIRECTORY", dir) + } +} + +func rememberDir(dlg iup.Ihandle, key string) { + dir := dlg.GetAttribute("DIRECTORY") + if dir == "" { + return + } + + iup.ConfigSetVariableStr(config, pathsGroup, key, dir) + iup.ConfigSave(config) +} + +func settingsSave(group string) { + for _, s := range settings { + h := iup.GetHandle(s.handle) + switch s.kind { + case kindBool: + v := 0 + if h.GetAttribute("VALUE") == "ON" { + v = 1 + } + iup.ConfigSetVariableInt(config, group, s.handle, v) + case kindInt: + iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE")) + case kindStr: + iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE")) + } + } + + iup.ConfigSave(config) +} + +// settingsApply sets every control from the given profile group, or from defaults when a group is empty. +func settingsApply(group string) { + for _, s := range settings { + h := iup.GetHandle(s.handle) + switch s.kind { + case kindBool: + def := 0 + if s.def == "ON" { + def = 1 + } + v := def + if group != "" { + v = iup.ConfigGetVariableIntDef(config, group, s.handle, def) + } + if v != 0 { + h.SetAttribute("VALUE", "ON") + } else { + h.SetAttribute("VALUE", "OFF") + } + case kindInt: + def, _ := strconv.Atoi(s.def) + v := def + if group != "" { + v = iup.ConfigGetVariableIntDef(config, group, s.handle, def) + } + h.SetAttribute("VALUE", strconv.Itoa(v)) + case kindStr: + v := s.def + if group != "" { + v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def) + } + h.SetAttribute("VALUE", v) + } + } + + syncLabels() + setActive() + previewPost() +} + +// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format. +func syncLabels() { + iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE")) + iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE")) + iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE")) + + format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING")) + eff := iup.GetHandle("Effort").GetInt("VALUE") + setEffort(format) + switch format { + case "webp", "avif", "jxl": + val := iup.GetHandle("Effort") + val.SetAttribute("VALUE", strconv.Itoa(eff)) + iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff)) + } + + iup.Refresh(iup.GetHandle("Tabs")) +} + +func fillProfileList() { + list := iup.GetHandle("Profile") + list.SetAttribute("REMOVEITEM", "ALL") + + cur := currentProfile() + sel := 1 + for i, n := range profileNames() { + list.SetAttribute(strconv.Itoa(i+1), n) + if n == cur { + sel = i + 1 + } + } + + list.SetAttribute("VALUE", strconv.Itoa(sel)) +} + +// profilesInit loads the current profile on startup, creating a default one on the first run. +func profilesInit() { + if len(profileNames()) == 0 { + iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default") + iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default") + settingsSave(profileGroup("Default")) + } + + fillProfileList() + settingsApply(profileGroup(currentProfile())) +} + +func onProfileSelect(ih iup.Ihandle) int { + name := ih.GetAttribute("VALUESTRING") + if name == "" { + return iup.DEFAULT + } + + iup.ConfigSetVariableStr(config, profilesGroup, "Current", name) + iup.ConfigSave(config) + + settingsApply(profileGroup(name)) + + return iup.DEFAULT +} + +func onSave(iup.Ihandle) int { + name := currentProfile() + if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 { + return iup.DEFAULT + } + + name = strings.TrimSpace(name) + if name == "" || strings.ContainsAny(name, ".;") { + iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.") + + return iup.DEFAULT + } + + settingsSave(profileGroup(name)) + + names := profileNames() + if !slices.Contains(names, name) { + names = append(names, name) + iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";")) + } + + iup.ConfigSetVariableStr(config, profilesGroup, "Current", name) + iup.ConfigSave(config) + + fillProfileList() + + return iup.DEFAULT +} + +func onReset(iup.Ihandle) int { + settingsApply("") + + return iup.DEFAULT +} diff --git a/cmd/cbconvert-gui/state.go b/cmd/cbconvert-gui/state.go new file mode 100644 index 0000000..f4e1341 --- /dev/null +++ b/cmd/cbconvert-gui/state.go @@ -0,0 +1,124 @@ +package main + +import "github.com/gen2brain/iup-go/iup" + +func setActive() { + if busy { + return + } + + opts := options() + count := iup.GetHandle("Table").GetInt("NUMLIN") + + if count == 0 { + iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO") + iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO") + + iup.GetHandle("Preview").SetAttribute("IMAGE", "logo") + iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "") + } else { + if index != -1 { + iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES") + } + iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES") + } + + active := "YES" + var tip string + switch { + case count == 0 && opts.OutDir == "": + active, tip = "NO", "Add files and set output directory" + case count == 0: + active, tip = "NO", "Add files" + case opts.OutDir == "": + active, tip = "NO", "Set output directory" + } + + enabledTip := map[string]string{ + "Thumbnail": "Extract cover thumbnails", + "Cover": "Extract covers", + "Convert": "Convert files to the selected format", + } + + for _, h := range []string{"Thumbnail", "Cover", "Convert"} { + b := iup.GetHandle(h) + b.SetAttribute("ACTIVE", active) + if active == "YES" { + b.SetAttribute("TIP", enabledTip[h]) + } else { + b.SetAttribute("TIP", tip) + } + } + + if opts.NoConvert { + iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO") + iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO") + } else { + iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES") + iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES") + } + + canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl" + losslessOn := canLossless && opts.Lossless + + if (opts.Format == "jpeg" || canLossless) && !opts.NoConvert && !losslessOn { + iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO") + } + + if canLossless && !opts.NoConvert { + iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES") + iup.GetHandle("Lossless").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO") + iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO") + } + + if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert { + iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO") + } + + if opts.Combine { + iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO") + } + + if opts.Archive == "zip" { + iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO") + } +} + +// setBusy locks the UI while an operation runs and turns Convert into a Cancel button. +func setBusy(on bool) { + busy = on + + // Controls not governed by setActive; setActive owns the rest. + always := "YES" + if on { + always = "NO" + } + for _, h := range []string{"AddFiles", "AddDir", "Profile", "Reset", "Save", "Command", "Tabs", "Table"} { + iup.GetHandle(h).SetAttribute("ACTIVE", always) + } + + convert := iup.GetHandle("Convert") + if on { + for _, h := range []string{"Remove", "RemoveAll", "Thumbnail", "Cover"} { + iup.GetHandle(h).SetAttribute("ACTIVE", "NO") + } + convert.SetAttribute("ACTIVE", "YES") + convert.SetAttribute("TITLE", "Cancel") + convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)") + } else { + activeConv = nil + convert.SetAttribute("TITLE", "&Convert") + convert.SetAttribute("TIP", "Convert files to the selected format") + setActive() // restores the conditional buttons and option boxes + } +} diff --git a/cmd/cbconvert-gui/widgets.go b/cmd/cbconvert-gui/widgets.go new file mode 100644 index 0000000..63ecde9 --- /dev/null +++ b/cmd/cbconvert-gui/widgets.go @@ -0,0 +1,653 @@ +package main + +import ( + "bytes" + "fmt" + "image/gif" + "net/url" + "strconv" + "strings" + + "github.com/gen2brain/cbconvert" + "github.com/gen2brain/iup-go/iup" +) + +func setEffort(format string) { + val := iup.GetHandle("Effort") + + var name string + + switch format { + case "webp": + val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4") + val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)") + name = "Method" + case "avif": + val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10") + val.SetAttribute("TIP", "AVIF speed, higher is faster/worse (0-10, default 10)") + name = "Speed" + case "jxl": + val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7") + val.SetAttribute("TIP", "JXL effort, higher is better/slower (1-10, default 7)") + name = "Effort" + default: + return + } + + val.SetAttribute("EFFORTNAME", name) + iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", name, val.GetInt("VALUE"))) + iup.Refresh(iup.GetHandle("LabelEffort")) +} + +// tableRowColors sets the alternating row colors for dark or light mode. +func tableRowColors(t iup.Ihandle, dark bool) { + even, odd := "#F0F0F0", "#FFFFFF" + if dark { + even, odd = "#3A3A3A", "#2D2D2D" + } + t.SetAttribute("EVENROWCOLOR", even) + t.SetAttribute("ODDROWCOLOR", odd) +} + +func list() iup.Ihandle { + t := iup.Table().SetHandle("Table") + t.SetAttributes(map[string]string{ + "EXPAND": "YES", + "NUMCOL": "3", + "NUMLIN": "0", + "TITLE1": "Title", + "TITLE2": "Type", + "TITLE3": "Size (MiB)", + "WIDTH1": "150", + "WIDTH2": "50", + "WIDTH3": "100", + "ALIGNMENT2": "ACENTER", + "ALIGNMENT3": "ARIGHT", + "SELECTIONMODE": "SINGLE", + "USERRESIZE": "YES", + "STRETCHLAST": "NO", + "FOCUSRECT": "NO", + "SORTABLE": "YES", + "ALTERNATECOLOR": "YES", + }) + + tableRowColors(t, iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES") + + t.SetCallback("ENTERITEM_CB", iup.EnterItemFunc(func(ih iup.Ihandle, lin, col int) int { + index = lin - 1 + setActive() + previewPost() + + return iup.DEFAULT + })) + + t.SetCallback("SORT_CB", iup.TableSortFunc(onSort)) + + t.SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int { + dec, err := url.QueryUnescape(fileName) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + conv := cbconvert.New(options()) + + fs, err := conv.Files([]string{dec}) + if err != nil { + iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) + fmt.Println(err) + + return iup.DEFAULT + } + + wasEmpty := len(files) == 0 + + for _, file := range fs { + appendFile(file) + } + + if wasEmpty && len(files) > 0 { + selectRow(0) + } + + setActive() + + if wasEmpty { + previewPost() + } + + return iup.DEFAULT + })) + + return iup.Vbox(t) +} + +func previewSize() (int, int) { + var width, height int + sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x") + if len(sp) == 2 { + width, _ = strconv.Atoi(sp[0]) + height, _ = strconv.Atoi(sp[1]) + } + + return width, height +} + +func preview() iup.Ihandle { + return iup.Frame( + iup.Vbox( + iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview"). + SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { + img := p.(cbconvert.Image) + + iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES") + + if img.Image != nil && len(s) == 0 { + iup.Destroy(iup.GetHandle("cover")) + iup.ImageFromImage(img.Image).SetHandle("cover") + + ih.SetAttribute("IMAGE", "cover") + iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height)) + } else { + ih.SetAttribute("IMAGE", "logo") + iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "") + + sp := strings.Split(s, ": ") + if len(sp) > 1 { + iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": "))) + } + } + + return iup.DEFAULT + })), + iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"), + ), + ) +} + +func tabInput() iup.Ihandle { + return iup.Hbox( + iup.Vbox( + iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive"). + SetAttributes(`TIP="Process subdirectories recursively"`), + iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB"). + SetAttributes(`TIP="Do not convert images that have RGB colorspace"`), + iup.Toggle(" Exclude Cover").SetHandle("NoCover"). + SetAttributes(`TIP="Do not convert the cover image"`), + iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage"). + SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"), + iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert"). + SetAttributes(`TIP="Copy images from archive or directory without modifications"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + + return iup.DEFAULT + })), + ).SetAttributes("NGAP=10"), + iup.Space().SetAttribute("SIZE", "15"), + iup.Vbox( + iup.Vbox( + iup.Label("Minimum Size (MiB):"), + iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size"). + SetAttributes(`TIP="Process only files larger than minimum size"`), + ), + iup.Vbox( + iup.Label("Document DPI:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "EDITBOX": "YES", + "VISIBLECOLUMNS": "6", + "VALUE": "Default", + "1": "Default", + "2": "150", + "3": "300", + "4": "600", + "5": "1200", + }).SetHandle("DPI"). + SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"), + ), + ).SetAttributes("NGAP=10"), + ).SetHandle("VboxInput") +} + +func tabOutput() iup.Ihandle { + return iup.Hbox( + iup.Vbox( + iup.Vbox( + iup.Label("Output Directory:"), + iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir"). + SetAttribute("TIP", "Directory where converted files are written (required)"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + + return iup.DEFAULT + })), + iup.Space().SetAttribute("SIZE", "5x0"), + iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)), + ), + iup.Vbox( + iup.Label("Add Suffix to Output File:"), + iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix"). + SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"), + ), + iup.Vbox( + iup.Label("Archive Format:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "VALUE": "1", + "1": "ZIP", + "2": "TAR", + }).SetHandle("Archive"). + SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Label("Compression:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "VALUE": "1", + "1": "Default", + "2": "Store (none)", + "3": "1", + "4": "2", + "5": "3", + "6": "4", + "7": "5", + "8": "6", + "9": "7", + "10": "8", + "11": "9", + }).SetHandle("ZipLevel"). + SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"), + ).SetHandle("VboxZipLevel"), + ).SetAttributes("NGAP=10"), + iup.Space().SetAttribute("SIZE", "15"), + iup.Vbox( + iup.Vbox( + iup.Toggle(" Combine into single file").SetHandle("Combine"). + SetAttributes(`TIP="Merge all listed files into one archive"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Label("Output File:"), + iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile"). + SetAttribute("TIP", "Combined file name (default: first input + -combined)"), + iup.Space().SetAttribute("SIZE", "5x0"), + iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onOutputFile)), + ).SetHandle("VboxOutFile"), + ).SetAttributes("NGAP=10"), + ).SetHandle("VboxOutput") +} + +func tabImage() iup.Ihandle { + return iup.Hbox( + iup.Vbox( + iup.Vbox( + iup.Label("Format:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "VALUE": "1", + "1": "JPEG", + "2": "PNG", + "3": "TIFF", + "4": "BMP", + "5": "WEBP", + "6": "AVIF", + "7": "JXL", + }).SetHandle("Format"). + SetAttribute("TIP", "Output image format for the converted pages"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING"))) + setActive() + previewPost() + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Label("Size:"), + iup.Hbox( + iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width"). + SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + iup.Space().SetAttribute("SIZE", "2"), + iup.Label("x"), + iup.Space().SetAttribute("SIZE", "2"), + iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height"). + SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), + ), + iup.Vbox( + iup.Toggle(" Best Fit").SetHandle("Fit"). + SetAttributes(`TIP="Best fit for required width and height"`), + ), + iup.Vbox( + iup.Label("Resize Filter:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "VALUE": "3", + "TIP": "Linear is the bilinear filter, smooth and reasonably fast", + "1": "NearestNeighbor", + "2": "Box", + "3": "Linear", + "4": "MitchellNetravali", + "5": "CatmullRom", + "6": "Gaussian", + "7": "Lanczos", + }).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)), + ), + ).SetAttributes("NGAP=10"), + iup.Space().SetAttribute("SIZE", "15"), + iup.Vbox( + iup.Vbox( + iup.Hbox( + iup.Label("Quality: "), + iup.Label("75").SetHandle("LabelQuality"), + ).SetAttributes("NMARGIN=0"), + iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality"). + SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE")) + iup.Refresh(iup.GetHandle("LabelQuality")) + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + ).SetHandle("VboxQuality"), + iup.Vbox( + iup.Label("Effort:").SetHandle("LabelEffort"), + iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort"). + SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", ih.GetAttribute("EFFORTNAME"), ih.GetInt("VALUE"))) + iup.Refresh(iup.GetHandle("LabelEffort")) + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + ).SetHandle("VboxEffort"), + iup.Vbox( + iup.Toggle(" Lossless").SetHandle("Lossless"). + SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setActive() + previewPost() + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Toggle(" Grayscale").SetHandle("Grayscale"). + SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + previewPost() + + return iup.DEFAULT + })), + ), + ).SetAttributes("NGAP=10"), + ).SetHandle("VboxImage") +} + +func tabTransform() iup.Ihandle { + return iup.Vbox( + iup.Vbox( + iup.Hbox( + iup.Label("Brightness: "), + iup.Label("0").SetHandle("LabelBrightness"), + ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), + iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness"). + SetAttributes(`TIP="Adjust the brightness of the images"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE")) + iup.Refresh(iup.GetHandle("LabelBrightness")) + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Hbox( + iup.Label("Contrast: "), + iup.Label("0").SetHandle("LabelContrast"), + ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"), + iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast"). + SetAttributes(`TIP="Adjust the contrast of the images"`). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE")) + iup.Refresh(iup.GetHandle("LabelContrast")) + ih.SetAttribute("MYVALUE", ih.GetInt("VALUE")) + + return iup.DEFAULT + })). + SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int { + if ih.GetAttribute("MYVALUE") != "" { + previewPost() + } + ih.SetAttribute("MYVALUE", "") + + return iup.DEFAULT + })), + ), + iup.Vbox( + iup.Label("Rotate:"), + iup.List().SetAttributes(map[string]string{ + "DROPDOWN": "YES", + "VALUE": "1", + "1": "0", + "2": "90", + "3": "180", + "4": "270", + }).SetHandle("Rotate"). + SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + previewPost() + + return iup.DEFAULT + })), + ), + ).SetHandle("VboxTransform").SetAttributes("NGAP=10") +} + +func tabs() iup.Ihandle { + return iup.Tabs( + tabInput().SetAttributes("TABTITLE=Input, NMARGIN=10x10"), + tabOutput().SetAttributes("TABTITLE=Output, NMARGIN=10x10"), + tabImage().SetAttributes("TABTITLE=Image, NMARGIN=10x10"), + tabTransform().SetAttributes("TABTITLE=Transform, NMARGIN=10x10"), + ).SetHandle("Tabs") +} + +func buttons() iup.Ihandle { + addFiles := iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onAddFiles)) + addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onAddDir)) + remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onRemove)) + removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onRemoveAll)) + thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onThumbnail)) + cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onCover)) + convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetCallback("ACTION", iup.ActionFunc(onConvert)) + reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetAttribute("TIP", "Restore all settings to their defaults"). + SetCallback("ACTION", iup.ActionFunc(onReset)) + save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetAttribute("TIP", "Save current settings to a profile"). + SetCallback("ACTION", iup.ActionFunc(onSave)) + + command := iup.Button("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING"). + SetAttribute("TIP", "Show the equivalent command line"). + SetCallback("ACTION", iup.ActionFunc(onCommand)) + + profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile"). + SetAttribute("TIP", "Select a settings profile"). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect)) + + iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH") + iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL") + + return iup.Vbox( + iup.Vbox( + addFiles, + addDir, + remove, + removeAll, + ).SetAttribute("NGAP", "2"), + iup.Space().SetAttribute("SIZE", "x5"), + iup.Vbox( + thumbnail, + cover, + ).SetAttribute("NGAP", "2"), + iup.Space().SetAttribute("SIZE", "x5"), + iup.Vbox( + convert, + ), + iup.Fill(), + iup.Vbox( + iup.Label("Profile:"), + profile, + reset, + save, + command, + ).SetAttribute("NGAP", "2"), + ).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER") +} + +func status() iup.Ihandle { + return iup.Hbox( + loading(), + iup.Fill(), + iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"), + iup.Space().SetAttribute("SIZE", "5"), + iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"), + iup.Space().SetAttribute("SIZE", "5"), + iup.ProgressBar().SetAttributes("VISIBLE=NO").SetHandle("ProgressBar"). + SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { + switch s { + case "convert": + conv := p.(*cbconvert.Converter) + ih.SetAttributes("VALUE=0, VISIBLE=YES") + ih.SetAttribute("MAX", conv.Ncontents) + + iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles)) + iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES") + iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES") + + iup.Refresh(iup.GetHandle("StatusBar")) + case "start": + conv := p.(*cbconvert.Converter) + ih.SetAttributes("VALUE=0, VISIBLE=YES") + ih.SetAttribute("MAX", conv.Nfiles) + + iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES") + case "progress": + conv := p.(*cbconvert.Converter) + ih.SetAttribute("VALUE", conv.CurrContent) + iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents)) + + iup.Refresh(iup.GetHandle("StatusBar")) + case "progress2": + conv := p.(*cbconvert.Converter) + ih.SetAttribute("VALUE", conv.CurrFile) + iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles)) + + iup.Refresh(iup.GetHandle("StatusBar")) + case "finish": + setBusy(false) + + iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`) + iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`) + ih.SetAttributes("VALUE=0, VISIBLE=NO") + + iup.Refresh(iup.GetHandle("StatusBar")) + + iup.GetHandle("dlg").SetCallback("K_ANY", nil) + iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil) + } + + return iup.DEFAULT + })), + iup.Space().SetAttribute("SIZE", "5x0"), + ).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar") +} + +func loading() iup.Ihandle { + img, _ := gif.DecodeAll(bytes.NewReader(appLoading)) + animation := iup.User() + + for idx, i := range img.Image { + name := fmt.Sprintf("Loading%d", idx) + iup.ImageFromImage(i).SetHandle(name) + iup.Append(animation, iup.GetHandle(name)) + } + + return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading") +}