From e1134cd9027519bd36a031671cce82bce5fbb49c Mon Sep 17 00:00:00 2001 From: Milan Nikolic Date: Thu, 25 Jun 2026 11:03:27 +0200 Subject: [PATCH] Add page spin to preview other pages --- cbconvert.go | 33 ++++--- cbconvert_cover.go | 126 ++++++++++++++++++++++++++ cbconvert_image.go | 13 ++- cbconvert_test.go | 34 +++++++ cmd/cbconvert-gui/handlers.go | 164 ++++++++++++++++++++++++---------- cmd/cbconvert-gui/main.go | 15 ++-- cmd/cbconvert-gui/state.go | 6 ++ cmd/cbconvert-gui/widgets.go | 156 +++++++++++++++++++++----------- 8 files changed, 426 insertions(+), 121 deletions(-) diff --git a/cbconvert.go b/cbconvert.go index ad729d8..a2d217c 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -43,7 +43,7 @@ type Options struct { NoUpscale bool // Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default DPI int - // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos + // 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos Filter int // Do not convert the cover image NoCover bool @@ -410,7 +410,7 @@ func (c *Converter) Thumbnail(file File) error { if c.Opts.Width > 0 || c.Opts.Height > 0 { cover = c.resizeFit(cover) } else { - cover = resize(cover, 256, 0, filters[c.Opts.Filter]) + cover = resize(cover, 256, 0, resampleFilter(c.Opts.Filter)) } var buf bytes.Buffer @@ -542,15 +542,30 @@ func (c *Converter) Meta(fileName string) (any, error) { return "", nil } -// Preview returns image preview. +// Preview returns the cover as an image preview. func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) { - var img Image - i, err := c.coverImage(fileName, fileInfo) if err != nil { - return img, fmt.Errorf("%s: %w", fileName, err) + return Image{}, fmt.Errorf("%s: %w", fileName, err) } + return c.previewImage(fileName, i, width, height) +} + +// PreviewPage returns the page-th image (0-based) as an image preview. +func (c *Converter) PreviewPage(fileName string, fileInfo os.FileInfo, page, width, height int) (Image, error) { + i, err := c.pageImage(fileName, fileInfo, page) + if err != nil { + return Image{}, fmt.Errorf("%s: %w", fileName, err) + } + + return c.previewImage(fileName, i, width, height) +} + +// previewImage applies the configured transforms and fits the result into width x height. +func (c *Converter) previewImage(fileName string, i image.Image, width, height int) (Image, error) { + var img Image + i = c.imageTransform(i) var w bytes.Buffer @@ -563,15 +578,13 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height img.Height = i.Bounds().Dy() img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes()))) - r := bytes.NewReader(w.Bytes()) - - dec, err := c.imageDecode(r) + dec, err := c.imageDecode(bytes.NewReader(w.Bytes())) if err != nil { return img, fmt.Errorf("%s: %w", fileName, err) } if width != 0 && height != 0 { - dec = fit(dec, width, height, filters[c.Opts.Filter]) + dec = fit(dec, width, height, resampleFilter(c.Opts.Filter)) } img.Image = dec diff --git a/cbconvert_cover.go b/cbconvert_cover.go index 00d9815..aa79355 100644 --- a/cbconvert_cover.go +++ b/cbconvert_cover.go @@ -85,6 +85,132 @@ func (c *Converter) coverDirectory(dir string) (image.Image, error) { return img, nil } +// pageArchive extracts the page-th image (natural reading order) from an archive. +func (c *Converter) pageArchive(fileName string, page int) (image.Image, error) { + contents, err := c.archiveList(fileName) + if err != nil { + return nil, fmt.Errorf("pageArchive: %w", err) + } + + images := imagesFromSlice(contents) + sort.Sort(sortorder.Natural(images)) + + if page < 0 || page >= len(images) { + return nil, fmt.Errorf("pageArchive: page %d out of range (%d pages)", page+1, len(images)) + } + + data, err := c.archiveFile(fileName, images[page]) + if err != nil { + return nil, fmt.Errorf("pageArchive: %w", err) + } + + img, err := c.imageDecode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("pageArchive: %w", err) + } + + return img, nil +} + +// pageDocument extracts the page-th rendered page from a document. +func (c *Converter) pageDocument(fileName string, page int) (image.Image, error) { + doc, err := fitz.New(fileName) + if err != nil { + return nil, fmt.Errorf("pageDocument: %w", err) + } + defer doc.Close() + + if page < 0 || page >= doc.NumPage() { + return nil, fmt.Errorf("pageDocument: page %d out of range (%d pages)", page+1, doc.NumPage()) + } + + img, err := doc.ImageDPI(page, c.renderDPI()) + if err != nil { + return nil, fmt.Errorf("pageDocument: %w", err) + } + + return img, nil +} + +// pageDirectory extracts the page-th image (natural reading order) from a directory. +func (c *Converter) pageDirectory(dir string, page int) (image.Image, error) { + contents, err := imagesFromPath(dir) + if err != nil { + return nil, fmt.Errorf("pageDirectory: %w", err) + } + + images := imagesFromSlice(contents) + sort.Sort(sortorder.Natural(images)) + + if page < 0 || page >= len(images) { + return nil, fmt.Errorf("pageDirectory: page %d out of range (%d pages)", page+1, len(images)) + } + + file, err := os.Open(images[page]) + if err != nil { + return nil, fmt.Errorf("pageDirectory: %w", err) + } + defer file.Close() + + img, err := c.imageDecode(file) + if err != nil { + return nil, fmt.Errorf("pageDirectory: %w", err) + } + + return img, nil +} + +// pageImage returns the page-th image of a comic file, document or directory. +func (c *Converter) pageImage(fileName string, fileInfo os.FileInfo, page int) (image.Image, error) { + var err error + var img image.Image + + switch { + case fileInfo.IsDir(): + img, err = c.pageDirectory(fileName, page) + case isDocument(fileName): + img, err = c.pageDocument(fileName, page) + case isArchive(fileName): + img, err = c.pageArchive(fileName, page) + } + + if err != nil { + return nil, fmt.Errorf("pageImage: %w", err) + } + + return img, nil +} + +// PageCount returns the number of pages (images) in a comic file, document or directory. +func (c *Converter) PageCount(fileName string, fileInfo os.FileInfo) (int, error) { + switch { + case fileInfo.IsDir(): + contents, err := imagesFromPath(fileName) + if err != nil { + return 0, fmt.Errorf("PageCount: %w", err) + } + + return len(imagesFromSlice(contents)), nil + case isDocument(fileName): + doc, err := fitz.New(fileName) + if err != nil { + return 0, fmt.Errorf("PageCount: %w", err) + } + defer doc.Close() + + return doc.NumPage(), nil + case isArchive(fileName): + contents, err := c.archiveList(fileName) + if err != nil { + return 0, fmt.Errorf("PageCount: %w", err) + } + + return len(imagesFromSlice(contents)), nil + } + + return 0, nil +} + // coverName returns the filename that is the most likely to be the cover. func (c *Converter) coverName(images []string) string { if len(images) == 0 { diff --git a/cbconvert_image.go b/cbconvert_image.go index c4b51be..c4b4d5b 100644 --- a/cbconvert_image.go +++ b/cbconvert_image.go @@ -38,6 +38,15 @@ var filters = map[int]transform.ResampleFilter{ lanczos: transform.Lanczos, } +// resampleFilter returns the resample filter for index i, falling back to Linear for an unknown index. +func resampleFilter(i int) transform.ResampleFilter { + if f, ok := filters[i]; ok { + return f + } + + return filters[linear] +} + func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA { dstW, dstH := width, height @@ -96,14 +105,14 @@ func withinBounds(img image.Image, width, height int) bool { // resizeFit resizes img to the configured width/height, honoring Fit and NoUpscale. func (c *Converter) resizeFit(img image.Image) image.Image { if c.Opts.Fit { - return fit(img, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) + return fit(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter)) } if c.Opts.NoUpscale && withinBounds(img, c.Opts.Width, c.Opts.Height) { return img } - return resize(img, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) + return resize(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter)) } func rotate(img image.Image, angle float64) *image.RGBA { diff --git a/cbconvert_test.go b/cbconvert_test.go index d40bcb6..17cdead 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -211,6 +211,40 @@ func TestConvertDPI(t *testing.T) { } } +func TestPreviewPage(t *testing.T) { + for _, name := range []string{"testdata/test.cbz", "testdata/test.pdf"} { + conv := New(NewOptions()) + + files, err := conv.Files([]string{name}) + if err != nil { + t.Fatal(err) + } + if len(files) != 1 { + t.Fatalf("%s: expected 1 file, got %d", name, len(files)) + } + file := files[0] + + n, err := conv.PageCount(file.Path, file.Stat) + if err != nil { + t.Fatal(err) + } + if n < 2 { + t.Fatalf("%s: expected >= 2 pages, got %d", name, n) + } + + for _, page := range []int{0, 1, n - 1} { + img, err := conv.PreviewPage(file.Path, file.Stat, page, 0, 0) + if err != nil || img.Image == nil { + t.Fatalf("%s: page %d: img=%v err=%v", name, page, img.Image, err) + } + } + + if _, err := conv.PreviewPage(file.Path, file.Stat, n, 0, 0); err == nil { + t.Errorf("%s: page %d (out of range) should error", name, n) + } + } +} + func TestConvertResize(t *testing.T) { tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc") if err != nil { diff --git a/cmd/cbconvert-gui/handlers.go b/cmd/cbconvert-gui/handlers.go index c41da59..8461de1 100644 --- a/cmd/cbconvert-gui/handlers.go +++ b/cmd/cbconvert-gui/handlers.go @@ -7,9 +7,10 @@ import ( "os" "path/filepath" "slices" + "sort" "strconv" - "strings" + "github.com/fvbommel/sortorder" "github.com/gen2brain/cbconvert" "github.com/gen2brain/iup-go/iup" ) @@ -24,7 +25,7 @@ func selectRow(i int) { 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. +// onSort re-syncs the files slice to the table's displayed order after a sort. func onSort(ih iup.Ihandle, col int) int { n := len(files) if n < 2 { @@ -73,16 +74,49 @@ func onSort(ih iup.Ihandle, col int) int { 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) +// addFiles appends files, natural-sorts the list, and rebuilds the table. +func addFiles(fs []cbconvert.File) { + if len(fs) == 0 { + return + } + + wasEmpty := len(files) == 0 + + var selPath string + if index >= 0 && index < len(files) { + selPath = files[index].Path + } + + files = append(files, fs...) + sort.Slice(files, func(i, j int) bool { + return sortorder.NaturalLess(files[i].Name, files[j].Name) + }) 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)) + t.SetAttribute("NUMLIN", strconv.Itoa(len(files))) + for i, f := range files { + lin := i + 1 + iup.SetAttributeId2(t, "", lin, 1, f.Name) + iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(f.Path)) + iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64)) + } + + if wasEmpty { + selectRow(0) + setActive() + previewPost() + + return + } + + index = -1 + for i, f := range files { + if f.Path == selPath { + selectRow(i) + break + } + } + setActive() } func previewPost() { @@ -90,30 +124,77 @@ func previewPost() { return } - width, height := previewSize() - iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES") - if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" { - iup.GetHandle("Preview").SetAttribute("IMAGE", "") + file := files[index] + + // On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders. + if file.Path != previewPath { + previewPath = file.Path + iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES") + go pageCountPost(file) + + return } + previewRender() +} + +// previewRenderSize is the size the cover is rendered at. +const previewRenderSize = 1200 + +// previewRender renders the current file at previewPage off the UI thread. +func previewRender() { + if index == -1 || len(files) == 0 { + return + } + + file := files[index] + + iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES") + opts := options() + page := previewPage 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) + img, err := conv.PreviewPage(file.Path, file.Stat, page, previewRenderSize, previewRenderSize) if err != nil { s = err.Error() fmt.Println(err) } - iup.PostMessage(iup.GetHandle("Preview"), s, 0, img) + iup.PostMessage(iup.GetHandle("Preview"), s, page, img) }(opts) } +// pageCountPost computes the file's page count off the UI thread and posts it, tagged with the path, to the Page spin. +func pageCountPost(file cbconvert.File) { + n, err := cbconvert.New(cbconvert.NewOptions()).PageCount(file.Path, file.Stat) + if err != nil || n < 1 { + n = 1 + } + + iup.PostMessage(iup.GetHandle("Page"), file.Path, n, nil) +} + +// onPageChanged re-renders the preview for the spin's page; dedupes so spin and typing don't both fire. +func onPageChanged() int { + page := iup.GetHandle("Page").GetInt("VALUE") - 1 + if page < 0 { + page = 0 + } + if page == previewPage { + return iup.DEFAULT + } + + previewPage = page + previewRender() + + return iup.DEFAULT +} + func onAddFiles(ih iup.Ihandle) int { args, err := fileDlg("Add Files", true, false, inputDirKey) if err != nil { @@ -134,21 +215,7 @@ func onAddFiles(ih iup.Ihandle) int { return iup.DEFAULT } - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } + addFiles(fs) } return iup.DEFAULT @@ -174,21 +241,7 @@ func onAddDir(ih iup.Ihandle) int { return iup.DEFAULT } - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } + addFiles(fs) } return iup.DEFAULT @@ -207,16 +260,31 @@ func onRemove(ih iup.Ihandle) int { } setActive() - previewPost() + if len(files) == 0 { + clearPreview() + } else { + previewPost() + } return iup.DEFAULT } +// clearPreview resets the preview state and repaints the canvas to its empty state. +func clearPreview() { + hasCover = false + previewPath = "" + previewPage = 0 + + iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "") + iup.Update(iup.GetHandle("Preview")) +} + func onRemoveAll(ih iup.Ihandle) int { index = -1 files = make([]cbconvert.File, 0) iup.GetHandle("Table").SetAttribute("NUMLIN", "0") + clearPreview() setActive() return iup.DEFAULT diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index 214801e..de346ff 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -33,6 +33,10 @@ var ( activeConv *cbconvert.Converter busy bool + + previewPage int // 0-based page shown in the preview + previewPath string // path of the file whose page range is loaded + hasCover bool // whether a cover image is loaded for the preview canvas ) func init() { @@ -78,7 +82,7 @@ func main() { img, _ := png.Decode(bytes.NewReader(appLogo)) iup.ImageFromImage(img).SetHandle("logo") - dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo, SHRINK=YES`, appVersion)).SetHandle("dlg") + dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo`, appVersion)).SetHandle("dlg") dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { sp := strings.Split(s, ": ") @@ -89,15 +93,6 @@ func main() { return iup.DEFAULT })) - dlg.SetCallback("RESIZE_CB", iup.ResizeFunc(func(ih iup.Ihandle, width, height int) int { - iup.GetHandle("Preview").SetAttribute("IMAGE", "logo") - iup.Refresh(ih) - - previewPost() - - return iup.DEFAULT - })) - dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int { t := iup.GetHandle("Table") tableRowColors(t, darkMode == 1) diff --git a/cmd/cbconvert-gui/state.go b/cmd/cbconvert-gui/state.go index f4e1341..a298e97 100644 --- a/cmd/cbconvert-gui/state.go +++ b/cmd/cbconvert-gui/state.go @@ -10,6 +10,12 @@ func setActive() { opts := options() count := iup.GetHandle("Table").GetInt("NUMLIN") + if count > 0 && index != -1 { + iup.GetHandle("PageBox").SetAttribute("VISIBLE", "YES") + } else { + iup.GetHandle("PageBox").SetAttribute("VISIBLE", "NO") + } + if count == 0 { iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO") iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO") diff --git a/cmd/cbconvert-gui/widgets.go b/cmd/cbconvert-gui/widgets.go index a0b4f08..f7c527d 100644 --- a/cmd/cbconvert-gui/widgets.go +++ b/cmd/cbconvert-gui/widgets.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "image/gif" + "math" "net/url" "strconv" "strings" @@ -58,7 +59,7 @@ func list() iup.Ihandle { "TITLE1": "Title", "TITLE2": "Type", "TITLE3": "Size (MiB)", - "WIDTH1": "150", + "WIDTH1": "300", "WIDTH2": "50", "WIDTH3": "100", "ALIGNMENT2": "ACENTER", @@ -102,21 +103,7 @@ func list() iup.Ihandle { return iup.DEFAULT } - wasEmpty := len(files) == 0 - - for _, file := range fs { - appendFile(file) - } - - if wasEmpty && len(files) > 0 { - selectRow(0) - } - - setActive() - - if wasEmpty { - previewPost() - } + addFiles(fs) return iup.DEFAULT })) @@ -124,49 +111,115 @@ func list() iup.Ihandle { 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.Canvas().SetAttributes("EXPAND=YES, MINSIZE=400x, BORDER=NO").SetHandle("Preview"). + SetCallback("ACTION", iup.ActionFunc(drawPreview)). + SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(previewMessage)), iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"), ), ) } +// drawPreview draws the cover scaled to fit, else the logo centered. +func drawPreview(ih iup.Ihandle) int { + iup.DrawBegin(ih) + defer iup.DrawEnd(ih) + + cw, ch := iup.DrawGetSize(ih) + iup.DrawParentBackground(ih) + + name := "logo" + if hasCover { + name = "cover" + } + + iw, ihh, _ := iup.DrawGetImageInfo(name) + if iw <= 0 || ihh <= 0 { + return iup.DEFAULT + } + + dw, dh := iw, ihh + if hasCover { + s := math.Min(float64(cw)/float64(iw), float64(ch)/float64(ihh)) + dw = int(float64(iw) * s) + dh = int(float64(ihh) * s) + } + + iup.DrawImage(ih, name, (cw-dw)/2, (ch-dh)/2, dw, dh) + + return iup.DEFAULT +} + +// previewMessage receives a rendered cover from previewRender and triggers a canvas redraw. +func previewMessage(ih iup.Ihandle, s string, i int, p any) int { + if i != previewPage { + return iup.DEFAULT + } + + 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") + hasCover = true + iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height)) + } else { + hasCover = false + 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:], ": "))) + } + } + + iup.Update(ih) + + return iup.DEFAULT +} + +// pageBox is the page-navigation spin shown in the status bar; hidden until a comic is selected. +func pageBox() iup.Ihandle { + return iup.Hbox( + iup.Space().SetAttribute("SIZE", "5"), + iup.Label("Page:"), + iup.Space().SetAttribute("SIZE", "3"), + iup.Text().SetAttributes(`SPIN=YES, SPINMIN=1, SPINMAX=1, VALUE=1, VISIBLECOLUMNS=3, MASK="/d*"`).SetHandle("Page"). + SetAttribute("TIP", "Preview a different page of the selected comic"). + SetCallback("SPIN_CB", iup.SpinFunc(func(ih iup.Ihandle, pos int) int { + return onPageChanged() + })). + SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + return onPageChanged() + })). + SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int { + if s != previewPath { + return iup.DEFAULT + } + + ih.SetAttribute("SPINMAX", strconv.Itoa(i)) + iup.GetHandle("PageCount").SetAttribute("TITLE", fmt.Sprintf("/ %d", i)) + + if previewPage > i-1 { + previewPage = i - 1 + } + if previewPage < 0 { + previewPage = 0 + } + ih.SetAttribute("VALUE", strconv.Itoa(previewPage+1)) + + previewRender() + + return iup.DEFAULT + })), + iup.Space().SetAttribute("SIZE", "3"), + iup.Label("").SetHandle("PageCount"), + ).SetAttributes("ALIGNMENT=ACENTER, VISIBLE=NO").SetHandle("PageBox") +} + func tabInput() iup.Ihandle { return iup.Hbox( iup.Vbox( @@ -586,6 +639,7 @@ func buttons() iup.Ihandle { func status() iup.Ihandle { return iup.Hbox( loading(), + pageBox(), iup.Fill(), iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"), iup.Space().SetAttribute("SIZE", "5"),