~gioverse/chat

ref: 84203ce5a460 chat/example/kitchen/util.go -rw-r--r-- 4.5 KiB
84203ce5Jack Mordaunt widget/material: maintain size for empty images 4 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package main

import (
	"errors"
	"fmt"
	"image"
	_ "image/jpeg"
	_ "image/png"
	"io"
	"io/fs"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"path/filepath"
	"strconv"

	"gioui.org/app"
	"git.sr.ht/~gioverse/chat/list"
)

// randomImage returns a random image at the given size.
// Downloads some number of random images from unplash and caches them on disk.
//
// TODO(jfm) [performance]: download images concurrently (parallel downloads,
// async to the gui event loop).
func randomImage(sz image.Point) (image.Image, error) {
	mkCacheDir := func(base string) string {
		return filepath.Join(base, "chat", fmt.Sprintf("%dx%d", sz.X, sz.Y))
	}
	cache := mkCacheDir(os.TempDir())
	if err := os.MkdirAll(cache, 0755); err != nil {
		if !errors.Is(err, os.ErrPermission) {
			return nil, fmt.Errorf("preparing cache directory: %w", err)
		}
		dir, err := app.DataDir()
		if err != nil {
			return nil, fmt.Errorf("failed finding application data dir: %w", err)
		}
		cache = mkCacheDir(dir)
		if err := os.MkdirAll(cache, 0755); err != nil {
			return nil, fmt.Errorf("preparing fallback cache directory: %w", err)
		}
	}
	entries, err := ioutil.ReadDir(cache)
	if err != nil {
		return nil, fmt.Errorf("reading cache entries: %w", err)
	}
	entries = filter(entries, isFile)
	if len(entries) == 0 {
		for ii := 0; ii < 10; ii++ {
			ii := ii
			if err := func() error {
				r, err := http.Get(fmt.Sprintf("https://source.unsplash.com/random/%dx%d?nature", sz.X, sz.Y))
				if err != nil {
					return fmt.Errorf("fetching image data: %w", err)
				}
				defer r.Body.Close()
				imgf, err := os.Create(filepath.Join(cache, strconv.Itoa(ii)))
				if err != nil {
					return fmt.Errorf("creating image file on disk: %w", err)
				}
				defer imgf.Close()
				if _, err := io.Copy(imgf, r.Body); err != nil {
					return fmt.Errorf("downloading image: %w", err)
				}
				return nil
			}(); err != nil {
				return nil, fmt.Errorf("populating image cache: %w", err)
			}
		}
		return randomImage(sz)
	}
	selection := entries[rand.Intn(len(entries))]
	imgf, err := os.Open(filepath.Join(cache, selection.Name()))
	if err != nil {
		return nil, fmt.Errorf("opening image file: %w", err)
	}
	defer imgf.Close()
	img, _, err := image.Decode(imgf)
	if err != nil {
		return nil, fmt.Errorf("decoding image: %w", err)
	}
	return img, nil
}

// isFile filters out non-file entries.
func isFile(info fs.FileInfo) bool {
	return !info.IsDir()
}

func filter(list []fs.FileInfo, predicate func(fs.FileInfo) bool) (filtered []fs.FileInfo) {
	for _, item := range list {
		if predicate(item) {
			filtered = append(filtered, item)
		}
	}
	return filtered
}

// dupSlice returns a slice composed of the same elements in the same order,
// but backed by a different array.
func dupSlice(in []list.Element) []list.Element {
	out := make([]list.Element, len(in))
	for i := range in {
		out[i] = in[i]
	}
	return out
}

// sliceRemove takes the given index of a slice and swaps it with the final
// index in the slice, then shortens the slice by one element. This hides
// the element at index from the slice, though it does not erase its data.
func sliceRemove(s *[]list.Element, index int) {
	lastIndex := len(*s) - 1
	(*s)[index], (*s)[lastIndex] = (*s)[lastIndex], (*s)[index]
	*s = (*s)[:lastIndex]
}

func maximum(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// fetch image for the given id.
// Image is initially downloaded from the provided url and stored on disk.
func fetch(id, u string) (image.Image, error) {
	path := filepath.Join(os.TempDir(), "chat", "resources", id)
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return nil, fmt.Errorf("preparing resource directory: %w", err)
	}
	if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
		if err := func() error {
			f, err := os.Create(path)
			if err != nil {
				return fmt.Errorf("creating resource file: %w", err)
			}
			defer f.Close()
			r, err := http.Get(u)
			if err != nil {
				return fmt.Errorf("GET: %w", err)
			}
			defer r.Body.Close()
			if r.StatusCode != http.StatusOK {
				return fmt.Errorf("GET: %s", r.Status)
			}
			if _, err := io.Copy(f, r.Body); err != nil {
				return fmt.Errorf("downloading resource to disk: %w", err)
			}
			return nil
		}(); err != nil {
			return nil, err
		}
	}
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("opening resource file: %w", err)
	}
	defer f.Close()
	img, _, err := image.Decode(f)
	if err != nil {
		return nil, fmt.Errorf("decoding image: %w", err)
	}
	return img, nil
}