summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRandomChars <random@chars.jp>2021-10-14 15:53:17 +0900
committerRandomChars <random@chars.jp>2021-10-14 15:53:17 +0900
commit47b377628b0c89f1cbe9fc015c6b2f213f27d009 (patch)
treecd39abdd4491c4b16cdc181df58a51869d0c251f
parente224f24ad6902022d96ebbc500c719681098034a (diff)
bump image board dependency, deferred loading with options of firing callbacks at content set or immediately, lock image cache for concurrent access, time image fetching in fetch method, get image by flake with graphical must-fetch error handling, return ungrouped images without fetching from remote, do grouping locally when making gallery view, fetch gallery images concurrently deferred, disable grouping im tag search result views, fetch full image, tags and owner deferred concurrently in image view, fetch group and its previews concurrently on demand in image view, use metadata information setting group forward/backwards buttonsHEADmaster
-rw-r--r--defer_load.go60
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--image.go448
-rw-r--r--main.go13
-rw-r--r--tag.go2
6 files changed, 394 insertions, 135 deletions
diff --git a/defer_load.go b/defer_load.go
new file mode 100644
index 0000000..7619c62
--- /dev/null
+++ b/defer_load.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "context"
+ "log"
+)
+
+var (
+ deferredLoadCancel context.CancelFunc
+ deferredLoadCallback func()
+)
+
+func registerDeferredLoad(callbacks []func(), immediate bool) {
+ deferredLoadCallback = func() {
+ deferredLoadCallback = nil
+
+ func() {
+ giantLock.Lock()
+ defer giantLock.Unlock()
+ if deferredLoadCancel != nil {
+ deferredLoadCancel()
+ deferredLoadCancel = nil
+ }
+ }()
+
+ ctx, cancel := context.WithCancel(context.TODO())
+ deferredLoadCancel = cancel
+ proceed := true
+ done := make(chan bool)
+
+ go func() {
+ for i, callback := range callbacks {
+ if !proceed {
+ log.Printf("Deferred load cancelled.")
+ return
+ }
+
+ if callback == nil {
+ log.Printf("Callback %v is nil.", i)
+ continue
+ }
+ log.Printf("Firing deferred loading callback %v.", i)
+ callback()
+ }
+ done <- true
+ }()
+
+ select {
+ case <-ctx.Done():
+ deferredLoadCancel = nil
+ proceed = false
+ case <-done:
+ deferredLoadCancel = nil
+ log.Printf("Deferred load done.")
+ }
+ }
+ if immediate {
+ go deferredLoadCallback()
+ }
+}
diff --git a/go.mod b/go.mod
index 5a9b602..8a4b0ab 100644
--- a/go.mod
+++ b/go.mod
@@ -8,5 +8,5 @@ require (
github.com/bwmarrin/snowflake v0.3.0
github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a // indirect
github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91
- random.chars.jp/git/image-board v1.4.0
+ random.chars.jp/git/image-board v1.4.2
)
diff --git a/go.sum b/go.sum
index 47bb4a3..8f8a82f 100644
--- a/go.sum
+++ b/go.sum
@@ -470,4 +470,8 @@ random.chars.jp/git/image-board v1.3.10 h1:aoN5qsHzIuwxcQnWCl56LJQz8pHzi3Stcs9wO
random.chars.jp/git/image-board v1.3.10/go.mod h1:fKoOuCTZ+1F4DCciH+fywbzD+go1GXgStZ+OF7hdr8c=
random.chars.jp/git/image-board v1.4.0 h1:JxRJNczJ5ojIusXAz12mAJD3QF/cjsL0Pxrqs2pI2+Q=
random.chars.jp/git/image-board v1.4.0/go.mod h1:fKoOuCTZ+1F4DCciH+fywbzD+go1GXgStZ+OF7hdr8c=
+random.chars.jp/git/image-board v1.4.1 h1:HsodNrCL76h6HXvCCVGG6qd+n7tvZN4CyWqfxr44a+c=
+random.chars.jp/git/image-board v1.4.1/go.mod h1:fKoOuCTZ+1F4DCciH+fywbzD+go1GXgStZ+OF7hdr8c=
+random.chars.jp/git/image-board v1.4.2 h1:mCdm8K2fzzxWlNRf8NSurgoMXyEW5+2C2htigqyPK9c=
+random.chars.jp/git/image-board v1.4.2/go.mod h1:fKoOuCTZ+1F4DCciH+fywbzD+go1GXgStZ+OF7hdr8c=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/image.go b/image.go
index d3867bb..b45abd1 100644
--- a/image.go
+++ b/image.go
@@ -21,6 +21,7 @@ import (
"random.chars.jp/git/image-board/api"
"random.chars.jp/git/image-board/store"
"strings"
+ "sync"
"time"
)
@@ -67,9 +68,10 @@ var (
tp *int
)
var (
- imageCache = make(map[string]*imageEntry)
- groupCache = make(map[string][]store.Image)
- pageCache = make(map[int][]store.Image)
+ imageCache = make(map[string]*imageEntry)
+ imageCacheLock = sync.RWMutex{}
+ groupCache = make(map[string][]store.Image)
+ pageCache = make(map[int][]store.Image)
)
// imageMeta represents image metadata not included in the API payload.
@@ -91,6 +93,7 @@ func (i *imageEntry) resource(preview bool) (*fyne.StaticResource, error) {
if i == nil {
return nil, errors.New("did not get image entry")
}
+
switch preview {
case true:
if i.preview != nil {
@@ -116,14 +119,17 @@ func (i *imageEntry) resource(preview bool) (*fyne.StaticResource, error) {
}
}
+ s := time.Now()
if res, err := remote.ImageFile(i.image.Snowflake, preview); err != nil {
return nil, err
} else {
switch preview {
case true:
+ log.Printf("Fetched image %s preview in %s.", i.image.Snowflake, time.Now().Sub(s))
i.preview = fyne.NewStaticResource(i.image.Snowflake+"-preview.jpg", res)
return i.preview, nil
case false:
+ log.Printf("Fetched image %s full in %s.", i.image.Snowflake, time.Now().Sub(s))
i.file = fyne.NewStaticResource(i.image.Snowflake+"."+i.image.Type, res)
return i.file, nil
}
@@ -223,15 +229,58 @@ func totalPages() (int, error) {
}
func getImage(image store.Image) *imageEntry {
+ imageCacheLock.RLock()
+ defer imageCacheLock.RUnlock()
if entry, ok := imageCache[image.Snowflake]; !ok {
+ imageCacheLock.RUnlock()
+ imageCacheLock.Lock()
imageCache[image.Snowflake] = &imageEntry{image: image}
+ imageCacheLock.Unlock()
+ imageCacheLock.RLock()
return imageCache[image.Snowflake]
} else {
return entry
}
}
+func getImageFlake(flake string) *imageEntry {
+ imageCacheLock.RLock()
+ defer imageCacheLock.RUnlock()
+ if entry, ok := imageCache[flake]; !ok {
+ imageCacheLock.RUnlock()
+ imageCacheLock.Lock()
+ fetch:
+ s := time.Now()
+ if img, err := remote.Image(flake); err != nil {
+ retry := make(chan bool)
+ diag := dialog.NewError(err, window)
+ diag.SetDismissText("Retry")
+ diag.SetOnClosed(func() {
+ retry <- true
+ })
+ <-retry
+ goto fetch
+ } else {
+ log.Printf("Fetched image %s in %s.", flake, time.Now().Sub(s))
+ imageCache[flake] = &imageEntry{image: img}
+ }
+ imageCacheLock.Unlock()
+ imageCacheLock.RLock()
+ return imageCache[flake]
+ } else {
+ return entry
+ }
+}
+
func getImageGroup(image store.Image) (entry *imageEntry, group []*imageEntry) {
+ entry = getImage(image)
+
+ if image.Parent == image.Child {
+ log.Printf("Ungrouped image %s returned as-is.", image.Snowflake)
+ group = []*imageEntry{entry}
+ return
+ }
+
if g, ok := groupCache[image.Snowflake]; !ok {
s := time.Now()
if gr, err := remote.ImageGroup(image.Snowflake); err != nil {
@@ -240,7 +289,6 @@ func getImageGroup(image store.Image) (entry *imageEntry, group []*imageEntry) {
} else {
e := time.Now().Sub(s)
grFlakes := make([]string, len(gr))
- entry = getImage(image)
group = make([]*imageEntry, len(gr))
for i, img := range gr {
group[i] = getImage(img)
@@ -248,15 +296,15 @@ func getImageGroup(image store.Image) (entry *imageEntry, group []*imageEntry) {
groupCache[img.Snowflake] = gr
}
log.Printf("Fetched group %s in %s.", strings.Join(grFlakes, ", "), e)
+ return
}
} else {
- entry = getImage(image)
group = make([]*imageEntry, len(g))
for i, img := range g {
group[i] = getImage(img)
}
+ return
}
- return
}
func makeImageBorder(data []byte, c color.Color, thickness float64) (image.Image, error) {
@@ -294,19 +342,40 @@ func makeImageBorder(data []byte, c color.Color, thickness float64) (image.Image
return newImg, nil
}
-func makeGallery(images []store.Image) (*fyne.Container, error) {
- var loadImage func(gallery *fyne.Container, m store.Image) error
- switch getCollapseCategory() {
+func makeGallery(images []store.Image, noGroup bool) (*fyne.Container, error) {
+ var callbacks = make([]func(), len(images))
+ defer func() { go registerDeferredLoad(callbacks, false) }()
+ var (
+ flakeImage map[string]*store.Image
+ imageGroups map[string]*[]string
+ groupLocks map[*[]string]*sync.Mutex
+ )
+
+ cc := getCollapseCategory()
+ if noGroup {
+ cc = false
+ }
+
+ var loadImage func(gallery *fyne.Container, m store.Image, i int) error
+ switch cc {
case false:
- loadImage = func(gallery *fyne.Container, m store.Image) error {
+ loadImage = func(gallery *fyne.Container, m store.Image, i int) error {
img := &canvas.Image{
FillMode: canvas.ImageFillContain,
ScaleMode: canvas.ImageScaleFastest,
}
- if res, err := getImage(m).resource(true); err != nil {
- return err
- } else {
- img.Resource = res
+
+ callbacks[i] = func() {
+ go func(m store.Image, img *canvas.Image) {
+ defer img.Refresh()
+
+ if res, err := getImage(m).resource(true); err != nil {
+ go dialog.ShowError(err, window)
+ } else {
+ log.Printf("Populated image %s.", m.Snowflake)
+ img.Resource = res
+ }
+ }(m, img)
}
gallery.Add(newImageButton(img, m))
@@ -314,34 +383,54 @@ func makeGallery(images []store.Image) (*fyne.Container, error) {
return nil
}
case true:
- loadImage = func(gallery *fyne.Container, m store.Image) error {
- entry, group := getImageGroup(m)
- if len(group) == 0 || m.Snowflake != group[0].image.Snowflake {
- log.Printf("Skipped secondary image %s.", m.Snowflake)
- return nil
+ loadImage = func(gallery *fyne.Container, m store.Image, i int) error {
+ var (
+ entry = getImage(m)
+ group []string
+ )
+ if imageGroups[m.Snowflake] != nil {
+ group = *imageGroups[m.Snowflake]
+ if len(group) == 0 || m.Snowflake != group[0] {
+ log.Printf("Skipped secondary image %s.", m.Snowflake)
+ return nil
+ }
+ log.Printf("Adding primary image %s to view.", m.Snowflake)
+ } else {
+ group = []string{entry.image.Snowflake}
+ log.Printf("Adding ungrouped image %s to view.", m.Snowflake)
}
img := &canvas.Image{
FillMode: canvas.ImageFillContain,
ScaleMode: canvas.ImageScaleFastest,
}
- if res, err := entry.resource(true); err != nil {
- return err
- } else {
- if len(group) == 1 {
- img.Resource = res
- } else {
- var i image.Image
- if i, err = makeImageBorder(res.Content(), color.RGBA{R: 0x00, G: 0x00, B: 0xff, A: 0xff}, 0.03); err != nil {
- return err
+
+ callbacks[i] = func() {
+ go func(m store.Image, img *canvas.Image) {
+ defer img.Refresh()
+
+ if res, err := entry.resource(true); err != nil {
+ go dialog.ShowError(err, window)
} else {
- img.Image = i
+ if len(group) == 1 {
+ img.Resource = res
+ log.Printf("Populated ungrouped image %s.", m.Snowflake)
+ } else {
+ var newImg image.Image
+ if newImg, err = makeImageBorder(res.Content(),
+ color.RGBA{R: 0x00, G: 0x00, B: 0xff, A: 0xff}, 0.03); err != nil {
+ go dialog.ShowError(err, window)
+ } else {
+ img.Image = newImg
+ log.Printf("Populated primary image %s with group size %v.",
+ m.Snowflake, len(group))
+ }
+ }
}
- }
+ }(m, img)
}
gallery.Add(newImageButton(img, m))
- log.Printf("Added primary image %s to view.", m.Snowflake)
return nil
}
}
@@ -352,8 +441,53 @@ func makeGallery(images []store.Image) (*fyne.Container, error) {
Width: 128,
Height: 128,
})
+ if cc {
+ log.Printf("Starting local group discovery.")
+
+ flakeImage = make(map[string]*store.Image)
+ imageGroups = make(map[string]*[]string)
+ groupLocks = make(map[*[]string]*sync.Mutex)
+
+ for _, img := range images {
+ a := img
+ flakeImage[img.Snowflake] = &a
+ }
+
+ for flake, img := range flakeImage {
+ if imageGroups[flake] != nil {
+ log.Printf("Skipping local group discovery for image %s since it already belongs to one.", flake)
+ continue
+ }
+ if img.Child == img.Parent {
+ log.Printf("Skipping local group discovery for image %s since it is not grouped.", flake)
+ continue
+ }
+
+ group := []string{img.Snowflake}
+ // Iterate backwards
+ if flakeImage[img.Parent] != nil {
+ for parent := flakeImage[img.Parent]; parent != nil; parent = flakeImage[parent.Parent] {
+ group = append([]string{parent.Snowflake}, group...)
+ }
+ }
+ // Iterate forwards
+ if flakeImage[img.Child] != nil {
+ for child := flakeImage[img.Child]; child != nil; child = flakeImage[child.Child] {
+ group = append(group, child.Snowflake)
+ }
+ }
+
+ for _, f := range group {
+ imageGroups[f] = &group
+ }
+ groupLocks[&group] = &sync.Mutex{}
+ log.Printf("Discovered local group %s.", strings.Join(group, ", "))
+ }
+ log.Printf("Local group discovery done.")
+ }
+
for i, img := range images {
- if err := loadImage(gallery, img); err != nil {
+ if err := loadImage(gallery, img, i); err != nil {
return nil, err
}
go value(float64(i+1) / float64(len(images)))
@@ -380,7 +514,7 @@ func makePageView(page int) (*fyne.Container, error) {
return nil, err
}
var gallery *fyne.Container
- if g, err := makeGallery(images); err != nil {
+ if g, err := makeGallery(images, false); err != nil {
return nil, err
} else {
gallery = g
@@ -447,44 +581,100 @@ func makeImageView(image store.Image) (*fyne.Container, error) {
if remote == nil {
return nil, errors.New("attempting to make image with nil remote")
}
+ var br *fyne.Container
+ loadingLabelImage := widget.NewLabel("Loading...")
+
+ var callbacks []func()
+ defer func() { registerDeferredLoad(callbacks, false) }()
- entry, group := getImageGroup(image)
+ entry := getImage(image)
img := &canvas.Image{
FillMode: canvas.ImageFillContain,
ScaleMode: canvas.ImageScaleSmooth,
}
+ download := widget.NewButton(strings.ToUpper(image.Type), func() {
+ go fileSave(image.Snowflake, image.Type, img.Resource.Content())
+ })
- s := time.Now()
- if res, err := entry.resource(false); err != nil {
- return nil, err
- } else {
- img.Resource = res
- }
- log.Printf("Fetched image %s in %s.", image.Snowflake, time.Now().Sub(s).String())
+ callbacks = append(callbacks, func() {
+ go func() {
+ log.Printf("Starting fetch image %s.", image.Snowflake)
+ if res, err := entry.resource(false); err != nil {
+ go dialog.ShowError(err, window)
+ } else {
+ img.Resource = res
+ }
+ log.Printf("Finish fetch image %s.", image.Snowflake)
+ br.Remove(loadingLabelImage)
+ br.Add(img)
+ download.Enable()
+ }()
+ })
var metadata *imageMeta
- s = time.Now()
- if meta, err := entry.metadata(); err != nil {
- return nil, err
- } else {
- metadata = meta
- }
- log.Printf("Fetched image %s metadata in %s.", image.Snowflake, time.Now().Sub(s).String())
+ loadingLabel := widget.NewLabel("Loading...")
+ ts := widget.NewForm()
+ tv := container.NewBorder(widget.NewSeparator(), nil, nil, nil, loadingLabel)
+ info := widget.NewForm()
+
+ callbacks = append(callbacks, func() {
+ go func() {
+ log.Printf("Starting metadata fetch for image %s.", image.Snowflake)
+ s := time.Now()
+ if meta, err := entry.metadata(); err != nil {
+ diag := dialog.NewError(err, window)
+ diag.SetOnClosed(func() { setPage(currentPage) })
+ diag.SetDismissText("Return to previous view")
+ } else {
+ metadata = meta
+ log.Printf("Fetched image %s metadata in %s.", image.Snowflake, time.Now().Sub(s).String())
+ }
+ info.Items[1].Widget.(*link).Text = metadata.owner.Username
+ info.Items[1].Widget.(*link).onTap = func() { displayUserInfo(metadata.owner) }
+ info.Refresh()
+ log.Printf("Uploader field of image %s populated with %s.",
+ image.Snowflake, userString(metadata.owner))
+
+ // Make tag list
+ makeTags := func(tagType string) *fyne.Container {
+ a := container.NewVBox()
+ for _, tag := range metadata.tags[tagType] {
+ func(tag string) { a.Add(newLink(tag, func() { tagSearch([]string{tag}, func() { setImage(image) }) })) }(tag)
+ }
+ return a
+ }
+ appendTags := func(tagType string) {
+ a := makeTags(tagType)
+ if len(a.Objects) > 0 {
+ ts.Append(strings.Title(tagType), a)
+ }
+ }
+ appendTags(store.ArtistType)
+ appendTags(store.CharacterType)
+ appendTags(store.CopyrightType)
+ appendTags(store.MetaType)
+ appendTags(store.GenericType)
+
+ log.Printf("Finish metadata fetch for image %s.", image.Snowflake)
+ tv.Remove(loadingLabel)
+ tv.Add(ts)
+ }()
+ })
- info := widget.NewForm(
- widget.NewFormItem("ID", newLink(image.Snowflake, func() { window.Clipboard().SetContent(image.Snowflake) })),
- widget.NewFormItem("Uploader", newLink(metadata.owner.Username, func() { displayUserInfo(metadata.owner) })),
- )
+ // Populate info section
+ info.Append("ID", newLink(image.Snowflake, func() { window.Clipboard().SetContent(image.Snowflake) }))
+ info.Append("Uploader", newLink("Loading...", func() {}))
if image.Source != "" {
- u, err := url.Parse(image.Source)
- if err != nil {
- return nil, err
+ if u, err := url.Parse(image.Source); err != nil {
+ log.Printf("Invalid source URL on image %s.", image.Snowflake)
+ go dialog.ShowInformation("Warning", "This image has an invalid source URL.\n"+
+ "If this dialog reoccurs, contact your system administrator.", window)
+ } else {
+ src := widget.NewHyperlink(u.Host, u)
+ src.Wrapping = fyne.TextWrapWord
+ info.Append("Source", src)
}
- src := widget.NewHyperlink(u.Host, u)
- src.Wrapping = fyne.TextWrapWord
- info.Append("Source", src)
}
-
cL := widget.NewLabel(image.Commentary)
cL.Wrapping = fyne.TextWrapBreak
ctL := widget.NewLabel(image.CommentaryTranslation)
@@ -499,37 +689,14 @@ func makeImageView(image store.Image) (*fyne.Container, error) {
info.Append("Translated Commentary", ctL)
}
}
-
- info.Append("Download", widget.NewButton(strings.ToUpper(image.Type), func() {
- go fileSave(image.Snowflake, image.Type, img.Resource.Content())
- }))
-
- // Make tag list
- makeTags := func(tagType string) *fyne.Container {
- a := container.NewVBox()
- for _, tag := range metadata.tags[tagType] {
- func(tag string) { a.Add(newLink(tag, func() { tagSearch([]string{tag}, func() { setImage(image) }) })) }(tag)
- }
- return a
- }
- ts := widget.NewForm()
- appendTags := func(tagType string) {
- a := makeTags(tagType)
- if len(a.Objects) > 0 {
- ts.Append(strings.Title(tagType), a)
- }
- }
- appendTags(store.ArtistType)
- appendTags(store.CharacterType)
- appendTags(store.CopyrightType)
- appendTags(store.MetaType)
- appendTags(store.GenericType)
+ info.Append("Download", download)
+ download.Disable()
sc := container.NewVScroll(container.NewBorder(nil, nil, nil, widget.NewSeparator(),
- container.NewVBox(info, container.NewBorder(widget.NewSeparator(), nil, nil, nil, ts))))
+ container.NewVBox(info, tv)))
- br := container.NewBorder(nil, nil, sc, nil,
- img, sc)
+ br = container.NewBorder(nil, nil, sc, nil,
+ loadingLabelImage)
const (
retract = "<<"
@@ -630,7 +797,7 @@ func makeImageView(image store.Image) (*fyne.Container, error) {
if submit {
defer loading("Submitting", "Waiting for server...")()
- s = time.Now()
+ s := time.Now()
if err := remote.ImageUpdate(image.Snowflake, source, parent, commentary, commentaryTranslation); err != nil {
go dialog.ShowError(err, window)
}
@@ -706,7 +873,7 @@ func makeImageView(image store.Image) (*fyne.Container, error) {
del := widget.NewFormItem("Delete", widget.NewButton("Delete image", func() {
go dialog.ShowConfirm("Confirmation", "Are you sure you want to destroy this image?", func(okay bool) {
if okay {
- s = time.Now()
+ s := time.Now()
if err := remote.ImageDestroy(image.Snowflake); err != nil {
go dialog.ShowError(err, window)
return
@@ -736,68 +903,85 @@ func makeImageView(image store.Image) (*fyne.Container, error) {
ops := container.NewBorder(nil, nil, fwd, cnf)
var border *fyne.Container
- switch len(group) == 1 {
+ switch image.Child == image.Parent {
case true:
border = container.NewBorder(ops, nil, nil, nil,
container.NewBorder(widget.NewSeparator(), nil, nil, nil, lf))
case false:
- var ent int
- for i, im := range group {
- if im.image.Snowflake == image.Snowflake {
- ent = i
- }
- }
b := widget.NewButton("Previous", func() {
- setImage(group[ent-1].image)
+ setImage(getImageFlake(image.Parent).image)
})
r := widget.NewButton("Next", func() {
- setImage(group[ent+1].image)
+ setImage(getImageFlake(image.Child).image)
})
- if ent == 0 {
+ if image.Parent == "" {
b.Disable()
}
- if ent == len(group)-1 {
+ if image.Child == "" {
r.Disable()
}
var (
- groupView *container.Scroll
- groupViewBox = container.NewGridWrap(fyne.Size{Width: 128, Height: 128})
+ groupView *container.Scroll
+ groupViewBox = container.NewGridWrap(fyne.Size{Width: 128, Height: 128})
+ groupViewDiag dialog.Dialog
)
- groupView = container.NewHScroll(groupViewBox)
- for _, e := range group {
- eImg := &canvas.Image{
- FillMode: canvas.ImageFillContain,
- ScaleMode: canvas.ImageScaleFastest,
- }
- if res, err := e.resource(true); err != nil {
- return nil, err
- } else {
- if e.image.Snowflake == entry.image.Snowflake {
- if i, err := makeImageBorder(res.Content(), color.RGBA{R: 0x00, G: 0x00, B: 0xff, A: 0xff}, 0.03); err != nil {
- return nil, err
- } else {
- eImg.Image = i
- }
- } else {
- eImg.Resource = res
- }
- }
- groupViewBox.Add(newImageButton(eImg, e.image))
- log.Printf("Added image %s to group view of %s.", e.image.Snowflake, entry.image.Snowflake)
- }
- groupViewDiag := dialog.NewCustom("Image group", "Close", groupView, window)
- groupViewDiag.Resize(fyne.Size{Width: window.Content().Size().Width - 16})
- groupView.SetMinSize(fyne.Size{Height: 128})
+ ops.Add(widget.NewButton("Show group", func() {
+ if groupViewDiag == nil {
+ func() {
+ defer loading("Loading", "Fetching group...")()
+ log.Printf("Making group view for image %s.", image.Snowflake)
+ _, group := getImageGroup(image)
+ groupView = container.NewHScroll(groupViewBox)
+
+ callback := make([]func(), len(group))
+ defer registerDeferredLoad(callback, true)
+ for i, e := range group {
+ eImg := &canvas.Image{
+ FillMode: canvas.ImageFillContain,
+ ScaleMode: canvas.ImageScaleFastest,
+ }
+ e := e
+
+ callback[i] = func() {
+ go func(eImg *canvas.Image) {
+ defer eImg.Refresh()
+ if res, err := e.resource(true); err != nil {
+ go dialog.ShowError(err, window)
+ return
+ } else {
+ if e.image.Snowflake == entry.image.Snowflake {
+ if i, err := makeImageBorder(res.Content(), color.RGBA{R: 0x00, G: 0x00, B: 0xff, A: 0xff}, 0.03); err != nil {
+ go dialog.ShowError(err, window)
+ return
+ } else {
+ eImg.Image = i
+ }
+ } else {
+ eImg.Resource = res
+ }
+ }
+ }(eImg)
+ }
- var groupViewToggle *widget.Button
- groupViewToggle = widget.NewButton("Show group", func() {
+ groupViewBox.Add(newImageButton(eImg, e.image))
+ log.Printf("Added image %s to group view of %s.", e.image.Snowflake, entry.image.Snowflake)
+ }
+
+ groupViewDiag = dialog.NewCustom("Image group", "Close", groupView, window)
+ groupViewDiag.Resize(fyne.Size{Width: window.Content().Size().Width - 16})
+ groupView.SetMinSize(fyne.Size{Height: 128})
+ log.Printf("Done group view make for image %s.", image.Snowflake)
+ }()
+ }
groupViewDiag.Show()
- })
- ops.Add(groupViewToggle)
+ }))
+
registerImageSwitchCallback(func() {
- groupViewDiag.Hide()
+ if groupViewDiag != nil {
+ groupViewDiag.Hide()
+ }
})
border = container.NewBorder(ops, container.NewBorder(nil, nil, b, r), nil, nil,
diff --git a/main.go b/main.go
index 93d190d..67c78f8 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@ import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
+ "log"
"random.chars.jp/git/image-board/client"
"sync"
)
@@ -15,7 +16,7 @@ var (
remote *client.Remote
application = app.NewWithID("jp.chars.rand.imageboard")
window = application.NewWindow("Image Board")
- giantLock = sync.Mutex{}
+ giantLock = sync.RWMutex{}
menu *fyne.MainMenu
inPageView = false
)
@@ -61,7 +62,17 @@ func mainMenuRefresh() {
}
func setContent(o fyne.CanvasObject) {
+ if deferredLoadCancel != nil {
+ log.Printf("Cancelling deferred load due to content set.")
+ deferredLoadCancel()
+ }
+
inPageView = false
window.SetContent(container.NewBorder(tagSearchBar, nil, nil, nil,
tagSearchBar, o))
+
+ if deferredLoadCallback != nil {
+ log.Printf("Initiating deferred load after content set.")
+ go deferredLoadCallback()
+ }
}
diff --git a/tag.go b/tag.go
index 0d03b9d..835bf1e 100644
--- a/tag.go
+++ b/tag.go
@@ -406,7 +406,7 @@ func tagSearchPageSet(page int, back func()) error {
}
var gallery *fyne.Container
- if g, err := makeGallery(images); err != nil {
+ if g, err := makeGallery(images, true); err != nil {
return err
} else {
gallery = g