the images could be generated from the dolly svg but that would probably require bringing in a large dependency like inkscape and wouldn't bring huge benifits imo (&& just sticking it in tree was mentioned as best in the discord)
+1
appview/state/router.go
+1
appview/state/router.go
+110
-15
appview/state/state.go
+110
-15
appview/state/state.go
···
1
1
package state
2
2
3
3
import (
4
+
"bytes"
4
5
"context"
5
6
"database/sql"
6
7
"errors"
7
8
"fmt"
9
+
"image"
10
+
"image/color"
11
+
"image/draw"
12
+
"image/png"
8
13
"log/slog"
9
14
"net/http"
15
+
"strconv"
10
16
"strings"
11
17
"time"
12
18
···
20
26
dbnotify "tangled.org/core/appview/notify/db"
21
27
phnotify "tangled.org/core/appview/notify/posthog"
22
28
"tangled.org/core/appview/oauth"
29
+
"tangled.org/core/appview/ogcard"
23
30
"tangled.org/core/appview/pages"
24
31
"tangled.org/core/appview/reporesolver"
25
32
"tangled.org/core/appview/validator"
···
39
46
securejoin "github.com/cyphar/filepath-securejoin"
40
47
"github.com/go-chi/chi/v5"
41
48
"github.com/posthog/posthog-go"
49
+
"github.com/srwiley/rasterx"
42
50
)
43
51
44
52
type State struct {
···
221
229
222
230
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
223
231
const manifestJson = `{
224
-
"name": "tangled",
225
-
"description": "tightly-knit social coding.",
226
-
"icons": [
227
-
{
228
-
"src": "/favicon.svg",
229
-
"sizes": "144x144"
230
-
}
231
-
],
232
-
"start_url": "/",
233
-
"id": "org.tangled",
234
-
235
-
"display": "standalone",
236
-
"background_color": "#111827",
237
-
"theme_color": "#111827"
232
+
"name": "tangled",
233
+
"description": "tightly-knit social coding.",
234
+
"start_url": "/",
235
+
"id": "org.tangled",
236
+
"display": "standalone",
237
+
"background_color": "#111827",
238
+
"theme_color": "#111827",
239
+
"icons": [
240
+
{
241
+
"src": "/pwa-icon.png?res=512&transparent=true",
242
+
"type": "image/png",
243
+
"sizes": "512x512",
244
+
"purpose": "any monchrome"
245
+
},
246
+
{
247
+
"src": "/pwa-icon.png?res=512",
248
+
"type": "image/png",
249
+
"sizes": "512x512",
250
+
"purpose": "maskable"
251
+
},
252
+
{
253
+
"src": "/pwa-icon.png?res=192",
254
+
"type": "image/png",
255
+
"sizes": "192x192",
256
+
"purpose": "maskable"
257
+
},
258
+
{
259
+
"src": "/pwa-icon.png?res=144",
260
+
"type": "image/png",
261
+
"sizes": "144x144",
262
+
"purpose": "maskable"
263
+
},
264
+
{
265
+
"src": "/pwa-icon.png?res=96",
266
+
"type": "image/png",
267
+
"sizes": "96x96",
268
+
"purpose": "maskable"
269
+
},
270
+
{
271
+
"src": "/pwa-icon.png?res=72",
272
+
"type": "image/png",
273
+
"sizes": "72x72",
274
+
"purpose": "maskable"
275
+
},
276
+
{
277
+
"src": "/pwa-icon.png?res=48",
278
+
"type": "image/png",
279
+
"sizes": "48x48",
280
+
"purpose": "maskable"
281
+
}
282
+
]
238
283
}`
239
284
240
-
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
285
+
func (s *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
241
286
w.Header().Set("Content-Type", "application/json")
242
287
w.Write([]byte(manifestJson))
243
288
}
244
289
290
+
291
+
func (s *State) PWAIcon(w http.ResponseWriter, r *http.Request) {
292
+
tangledBgColour := color.RGBA{0x11, 0x18, 0x27, 0xff}
293
+
etag := "W/\"dolly-logo-v1 " + r.URL.Query().Get("res") + r.URL.Query().Get("transparent") + "\""
294
+
295
+
w.Header().Set("Content-Type", "image/png")
296
+
w.Header().Set("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400, stale-if-error=86400")
297
+
w.Header().Set("Etag", etag)
298
+
// if the client already has the same logo dont bother recreating it
299
+
if len(r.Header["If-None-Match"]) == 1 && r.Header["If-None-Match"][0] == etag {
300
+
w.WriteHeader(304)
301
+
return
302
+
}
303
+
304
+
icon, err := ogcard.BuildSVGIconFromPath("templates/fragments/dolly/logo.svg", tangledBgColour)
305
+
// ignore error as icon is default icon on error
306
+
// also this should not fail
307
+
308
+
transparent := r.URL.Query().Get("transparent") == "true";
309
+
size, err := strconv.Atoi(r.URL.Query().Get("res"))
310
+
if err != nil {
311
+
size = 512
312
+
}
313
+
314
+
// maskable safe area is centered circle with radius 40%
315
+
// area outside may be cropped so make sure the icon fills it
316
+
// bc trig, safe square is just over 70% of screen
317
+
icon.SetTarget(float64(size) * 0.15, float64(size) * 0.15, float64(size) * 0.7, float64(size) * 0.7)
318
+
rgba := image.NewRGBA(image.Rect(0, 0, size, size))
319
+
320
+
// set image bg
321
+
var bgColour color.Color = tangledBgColour
322
+
if transparent {
323
+
bgColour = color.Transparent
324
+
}
325
+
draw.Draw(rgba, rgba.Bounds(), &image.Uniform{ bgColour }, image.Point{}, draw.Src)
326
+
327
+
// Create a scanner and rasterize the SVG
328
+
scanner := rasterx.NewScannerGV(size, size, rgba, rgba.Bounds())
329
+
raster := rasterx.NewDasher(size, size, scanner)
330
+
331
+
icon.Draw(raster, 1.0)
332
+
333
+
var img_buff bytes.Buffer
334
+
err = png.Encode(&img_buff, rgba)
335
+
// ignore error as encoding shouldnt fail
336
+
// if it fails itll return no bytes which is fine
337
+
w.Write(img_buff.Bytes())
338
+
}
339
+
245
340
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
246
341
user := s.oauth.GetUser(r)
247
342
s.pages.TermsOfService(w, pages.TermsOfServiceParams{