diff options
author | 2021-05-30 18:30:40 +0200 | |
---|---|---|
committer | 2021-05-30 22:41:50 +0530 | |
commit | 19a224623d8b4b2302994ccab050b463d0ee817d (patch) | |
tree | e768910bbea56c21158f1a0ad3a0057f40decd10 | |
parent | Add Pleroma recipe (diff) | |
download | ferdium-recipes-19a224623d8b4b2302994ccab050b463d0ee817d.tar.gz ferdium-recipes-19a224623d8b4b2302994ccab050b463d0ee817d.tar.zst ferdium-recipes-19a224623d8b4b2302994ccab050b463d0ee817d.zip |
Instance logo for Pleroma
Fetch instance logo from the instance configuration and the instance
colors from the pleroma-fe css to draw the instance logo.
If the instance logo is in full color, it will be shown on the instance
top bar background color.
If it is configured to display with a single color, alpha blending on a
<canvas> is used to render the logo in the top bar text color on top of
the top bar background color, as it appears on the webpage.
-rw-r--r-- | recipes/pleroma/package.json | 2 | ||||
-rw-r--r-- | recipes/pleroma/webview.js | 158 |
2 files changed, 156 insertions, 4 deletions
diff --git a/recipes/pleroma/package.json b/recipes/pleroma/package.json index 31fa06f..2fedd64 100644 --- a/recipes/pleroma/package.json +++ b/recipes/pleroma/package.json | |||
@@ -1,7 +1,7 @@ | |||
1 | { | 1 | { |
2 | "id": "pleroma", | 2 | "id": "pleroma", |
3 | "name": "Pleroma", | 3 | "name": "Pleroma", |
4 | "version": "1.0.0", | 4 | "version": "1.1.0", |
5 | "description": "Pleroma — a lightweight fediverse server", | 5 | "description": "Pleroma — a lightweight fediverse server", |
6 | "main": "index.js", | 6 | "main": "index.js", |
7 | "author": "Ferdi <hello@getferdi.com>", | 7 | "author": "Ferdi <hello@getferdi.com>", |
diff --git a/recipes/pleroma/webview.js b/recipes/pleroma/webview.js index d08216d..d9fc9b1 100644 --- a/recipes/pleroma/webview.js +++ b/recipes/pleroma/webview.js | |||
@@ -1,5 +1,146 @@ | |||
1 | module.exports = (Ferdi) => { | 1 | const { ipcRenderer } = require('electron'); |
2 | const titleRegex = /^\((\d+)\)/; | 2 | |
3 | const titleRegex = /^\((\d+)\)/; | ||
4 | |||
5 | const getJson = async (relativeUri) => { | ||
6 | const req = await window.fetch(`${window.origin}${relativeUri}`, { | ||
7 | 'Accept': 'application/json' | ||
8 | }); | ||
9 | return req.json(); | ||
10 | } | ||
11 | |||
12 | const getInstanceConfig = async () => { | ||
13 | const origin = window.origin; | ||
14 | const staticConfig = await getJson('/static/config.json'); | ||
15 | try { | ||
16 | const frontendConfig = await getJson('/api/pleroma/frontend_configurations'); | ||
17 | const pleromaFeConfig = frontendConfig.pleroma_fe || {}; | ||
18 | return { ...staticConfig, ...pleromaFeConfig }; | ||
19 | } catch (e) { | ||
20 | console.log('Failed to load dynamic frontend configuration', e); | ||
21 | return staticConfig; | ||
22 | } | ||
23 | }; | ||
24 | |||
25 | const getInstanceLogo = async () => { | ||
26 | const config = await getInstanceConfig(); | ||
27 | if (!config.logo) { | ||
28 | throw new Error('Instance has no logo'); | ||
29 | } | ||
30 | return new Promise((resolve, reject) => { | ||
31 | const img = document.createElement('img'); | ||
32 | img.addEventListener('load', () => { | ||
33 | resolve({ | ||
34 | logo: img, | ||
35 | logoMask: config.logoMask, | ||
36 | }); | ||
37 | }); | ||
38 | img.addEventListener('error', (event) => { | ||
39 | reject(new Error(`${event.type} error loading ${config.logo}: ${event.message}`)); | ||
40 | }); | ||
41 | img.src = `${origin}${config.logo}`; | ||
42 | }); | ||
43 | }; | ||
44 | |||
45 | const getPropertyValue = (style, property) => { | ||
46 | const value = style.getPropertyValue(property) || ''; | ||
47 | return value.trim(); | ||
48 | }; | ||
49 | |||
50 | const R = 0; | ||
51 | const G = 1; | ||
52 | const B = 2; | ||
53 | const A = 3; | ||
54 | const SCALE = 255; | ||
55 | |||
56 | const clamp = b => Math.min(Math.max(Math.round(b), 0), SCALE); | ||
57 | const scalePixel = b => clamp(b * SCALE); | ||
58 | const unscalePixel = b => b / SCALE; | ||
59 | const blend = (bgValue, bgWeight, fgValue, fgWeight) => { | ||
60 | const sum = bgValue * bgWeight + fgValue * fgWeight; | ||
61 | return clamp(sum / (bgWeight + fgWeight)); | ||
62 | } | ||
63 | |||
64 | class LogoUpdater { | ||
65 | constructor(img, mask) { | ||
66 | this._img = img; | ||
67 | this._mask = mask; | ||
68 | this._size = Math.max(img.width, img.height); | ||
69 | this._canvas = document.createElement('canvas'); | ||
70 | this._canvas.width = this._size; | ||
71 | this._canvas.height = this._size; | ||
72 | this._ctx = this._canvas.getContext('2d'); | ||
73 | this._dx = Math.floor((this._size - img.width) / 2); | ||
74 | this._dy = Math.floor((this._size - img.height) / 2); | ||
75 | this._previousBg = ''; | ||
76 | this._previousFg = ''; | ||
77 | } | ||
78 | |||
79 | update() { | ||
80 | const style = window.getComputedStyle(document.body); | ||
81 | const bg = getPropertyValue(style, '--topBar'); | ||
82 | if (this._mask) { | ||
83 | const fg = getPropertyValue(style, '--topBarText'); | ||
84 | if (this._previousBg !== bg || this._previousFg !== fg) { | ||
85 | this._updateMask(bg, fg); | ||
86 | this._previousBg = bg; | ||
87 | this._previousFg = fg; | ||
88 | return true; | ||
89 | } | ||
90 | } else { | ||
91 | if (this._previousBg !== bg) { | ||
92 | this._updateNoMask(bg); | ||
93 | this._previousBg = bg; | ||
94 | return true; | ||
95 | } | ||
96 | } | ||
97 | return false; | ||
98 | } | ||
99 | |||
100 | toDataURL() { | ||
101 | return this._canvas.toDataURL(); | ||
102 | } | ||
103 | |||
104 | _updateNoMask(bg) { | ||
105 | this._ctx.fillStyle = bg; | ||
106 | this._ctx.fillRect(0, 0, this._size, this._size); | ||
107 | this._drawImage(); | ||
108 | } | ||
109 | |||
110 | _updateMask(bg, fg) { | ||
111 | const bgColor = this._getColorData(bg); | ||
112 | const fgColor = this._getColorData(fg); | ||
113 | const bgAlpha = unscalePixel(bgColor[A]); | ||
114 | const fgAlpha = unscalePixel(fgColor[A]); | ||
115 | this._ctx.clearRect(0, 0, this._size, this._size); | ||
116 | this._drawImage(); | ||
117 | const data = this._ctx.getImageData(0, 0, this._size, this._size); | ||
118 | const arr = data.data; | ||
119 | const length = data.width * data.height * 4; | ||
120 | for (let i = 0; i < length; i += 4) { | ||
121 | const logoAlpha = unscalePixel(arr[i + A]); | ||
122 | const fgWeight = logoAlpha * fgAlpha; | ||
123 | const bgWeight = bgAlpha * (1 - fgWeight); | ||
124 | arr[i + R] = blend(bgColor[R], bgWeight, fgColor[R], fgWeight); | ||
125 | arr[i + G] = blend(bgColor[G], bgWeight, fgColor[G], fgWeight); | ||
126 | arr[i + B] = blend(bgColor[B], bgWeight, fgColor[B], fgWeight); | ||
127 | arr[i + A] = scalePixel(bgWeight + fgWeight); | ||
128 | } | ||
129 | this._ctx.putImageData(data, 0, 0); | ||
130 | } | ||
131 | |||
132 | _getColorData(str) { | ||
133 | this._ctx.fillStyle = str; | ||
134 | this._ctx.fillRect(0, 0, 1, 1); | ||
135 | return this._ctx.getImageData(0, 0, 1, 1).data; | ||
136 | }; | ||
137 | |||
138 | _drawImage() { | ||
139 | this._ctx.drawImage(this._img, this._dx, this._dy); | ||
140 | } | ||
141 | } | ||
142 | |||
143 | module.exports = Ferdi => { | ||
3 | 144 | ||
4 | const getMessages = () => { | 145 | const getMessages = () => { |
5 | let directCount = 0; | 146 | let directCount = 0; |
@@ -10,5 +151,16 @@ module.exports = (Ferdi) => { | |||
10 | Ferdi.setBadge(directCount, 0); | 151 | Ferdi.setBadge(directCount, 0); |
11 | }; | 152 | }; |
12 | 153 | ||
13 | Ferdi.loop(getMessages); | 154 | getInstanceLogo().then(({ logo, logoMask }) => { |
155 | const updater = new LogoUpdater(logo, logoMask); | ||
156 | Ferdi.loop(() => { | ||
157 | getMessages(); | ||
158 | if (updater.update()) { | ||
159 | ipcRenderer.sendToHost('avatar', updater.toDataURL()); | ||
160 | } | ||
161 | }); | ||
162 | }, (e) => { | ||
163 | console.log('Failed to load instance logo', e); | ||
164 | Ferdi.loop(getMessages); | ||
165 | }) | ||
14 | }; | 166 | }; |