diff options
Diffstat (limited to 'src/features/appearance/index.ts')
-rw-r--r-- | src/features/appearance/index.ts | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/src/features/appearance/index.ts b/src/features/appearance/index.ts new file mode 100644 index 000000000..b00b9df3d --- /dev/null +++ b/src/features/appearance/index.ts | |||
@@ -0,0 +1,250 @@ | |||
1 | import color from 'color'; | ||
2 | import { reaction } from 'mobx'; | ||
3 | import { iconSizeBias } from '../../config'; | ||
4 | import { DEFAULT_APP_SETTINGS } from '../../environment'; | ||
5 | |||
6 | const STYLE_ELEMENT_ID = 'custom-appearance-style'; | ||
7 | |||
8 | function createStyleElement() { | ||
9 | const styles = document.createElement('style'); | ||
10 | styles.id = STYLE_ELEMENT_ID; | ||
11 | |||
12 | document.querySelector('head')?.appendChild(styles); | ||
13 | } | ||
14 | |||
15 | function setAppearance(style) { | ||
16 | const styleElement = document.querySelector(`#${STYLE_ELEMENT_ID}`); | ||
17 | |||
18 | if (styleElement) { | ||
19 | styleElement.innerHTML = style; | ||
20 | } | ||
21 | } | ||
22 | |||
23 | // See https://github.com/Qix-/color/issues/53#issuecomment-656590710 | ||
24 | function darkenAbsolute(originalColor, absoluteChange) { | ||
25 | const originalLightness = originalColor.lightness(); | ||
26 | return originalColor.lightness(originalLightness - absoluteChange); | ||
27 | } | ||
28 | |||
29 | function generateAccentStyle(accentColorStr) { | ||
30 | let style = ''; | ||
31 | |||
32 | let accentColor = color(DEFAULT_APP_SETTINGS.accentColor); | ||
33 | try { | ||
34 | accentColor = color(accentColorStr); | ||
35 | } catch { | ||
36 | // Ignore invalid accent color. | ||
37 | } | ||
38 | const darkerColorStr = darkenAbsolute(accentColor, 5).hex(); | ||
39 | style += ` | ||
40 | a.button:hover, button.button:hover { | ||
41 | background: ${darkenAbsolute(accentColor, 10).hex()}; | ||
42 | } | ||
43 | |||
44 | .franz-form__button:hover, | ||
45 | .franz-form__button.franz-form__button--inverted:hover, | ||
46 | .settings .settings__close:hover, | ||
47 | .theme__dark .franz-form__button:hover, | ||
48 | .theme__dark .franz-form__button.franz-form__button--inverted:hover, | ||
49 | .theme__dark .settings .settings__close:hover { | ||
50 | background: ${darkerColorStr}; | ||
51 | } | ||
52 | |||
53 | .franz-form__button:active, | ||
54 | .theme__dark .franz-form__button:active { | ||
55 | background: ${darkerColorStr}; | ||
56 | } | ||
57 | |||
58 | .theme__dark .franz-form__button.franz-form__button--inverted, | ||
59 | .franz-form__button.franz-form__button--inverted { | ||
60 | background: none; | ||
61 | border-color: ${accentColorStr}; | ||
62 | } | ||
63 | |||
64 | .tab-item.is-active { | ||
65 | background: ${accentColor.lighten(0.35).hex()}; | ||
66 | } | ||
67 | `; | ||
68 | |||
69 | return style; | ||
70 | } | ||
71 | |||
72 | function generateServiceRibbonWidthStyle(widthStr, iconSizeStr, vertical) { | ||
73 | const width = Number(widthStr); | ||
74 | const iconSize = Number(iconSizeStr) - iconSizeBias; | ||
75 | |||
76 | return vertical | ||
77 | ? ` | ||
78 | .tab-item { | ||
79 | width: ${width - 2}px !important; | ||
80 | height: ${width - 5 + iconSize}px !important; | ||
81 | min-height: unset; | ||
82 | } | ||
83 | .tab-item .tab-item__icon { | ||
84 | width: ${width / 2 + iconSize}px !important; | ||
85 | } | ||
86 | .sidebar__button { | ||
87 | font-size: ${width / 3}px !important; | ||
88 | } | ||
89 | ` | ||
90 | : ` | ||
91 | .sidebar { | ||
92 | width: ${width}px !important; | ||
93 | } | ||
94 | .tab-item { | ||
95 | width: ${width}px !important; | ||
96 | height: ${width - 5 + iconSize}px !important; | ||
97 | } | ||
98 | .tab-item .tab-item__icon { | ||
99 | width: ${width / 2 + iconSize}px !important; | ||
100 | } | ||
101 | .sidebar__button { | ||
102 | font-size: ${width / 3}px !important; | ||
103 | } | ||
104 | .todos__todos-panel--expanded { | ||
105 | width: calc(100% - ${300 + width}px) !important; | ||
106 | } | ||
107 | `; | ||
108 | } | ||
109 | |||
110 | function generateShowDragAreaStyle(accentColor) { | ||
111 | return ` | ||
112 | .sidebar { | ||
113 | padding-top: 0px !important; | ||
114 | } | ||
115 | .window-draggable { | ||
116 | position: initial; | ||
117 | background-color: ${accentColor}; | ||
118 | } | ||
119 | #root { | ||
120 | /** Remove 22px from app height, otherwise the page will be too high */ | ||
121 | height: calc(100% - 22px); | ||
122 | } | ||
123 | `; | ||
124 | } | ||
125 | |||
126 | function generateVerticalStyle(widthStr, alwaysShowWorkspaces) { | ||
127 | if (!document.querySelector('#vertical-style')) { | ||
128 | const link = document.createElement('link'); | ||
129 | link.id = 'vertical-style'; | ||
130 | link.rel = 'stylesheet'; | ||
131 | link.type = 'text/css'; | ||
132 | link.href = './styles/vertical.css'; | ||
133 | |||
134 | document.head.append(link); | ||
135 | } | ||
136 | const width = Number(widthStr); | ||
137 | const sidebarWidth = width - 4; | ||
138 | const verticalStyleOffset = 23; | ||
139 | |||
140 | return ` | ||
141 | .sidebar { | ||
142 | height: ${sidebarWidth + verticalStyleOffset + 1}px !important; | ||
143 | ${ | ||
144 | alwaysShowWorkspaces | ||
145 | ? ` | ||
146 | width: calc(100% - 300px) !important; | ||
147 | ` | ||
148 | : '' | ||
149 | } | ||
150 | } | ||
151 | |||
152 | .sidebar .sidebar__button { | ||
153 | width: ${width}px; | ||
154 | } | ||
155 | |||
156 | .app .app__content { | ||
157 | padding-top: ${sidebarWidth + verticalStyleOffset + 1}px !important; | ||
158 | } | ||
159 | |||
160 | .workspaces-drawer { | ||
161 | margin-top: -${sidebarWidth - verticalStyleOffset - 1}px !important; | ||
162 | } | ||
163 | |||
164 | .todos__todos-panel--expanded { | ||
165 | width: calc(100% - 300px) !important; | ||
166 | } | ||
167 | `; | ||
168 | } | ||
169 | |||
170 | function generateOpenWorkspaceStyle() { | ||
171 | return ` | ||
172 | .app .app__content { | ||
173 | width: 100%; | ||
174 | transform: translateX(0px); | ||
175 | } | ||
176 | .sidebar__button--workspaces { | ||
177 | display: none; | ||
178 | } | ||
179 | `; | ||
180 | } | ||
181 | |||
182 | function generateStyle(settings) { | ||
183 | let style = ''; | ||
184 | |||
185 | const { | ||
186 | accentColor, | ||
187 | serviceRibbonWidth, | ||
188 | iconSize, | ||
189 | showDragArea, | ||
190 | useVerticalStyle, | ||
191 | alwaysShowWorkspaces, | ||
192 | } = settings; | ||
193 | |||
194 | if ( | ||
195 | accentColor.toLowerCase() !== DEFAULT_APP_SETTINGS.accentColor.toLowerCase() | ||
196 | ) { | ||
197 | style += generateAccentStyle(accentColor); | ||
198 | } | ||
199 | if ( | ||
200 | serviceRibbonWidth !== DEFAULT_APP_SETTINGS.serviceRibbonWidth || | ||
201 | iconSize !== DEFAULT_APP_SETTINGS.iconSize | ||
202 | ) { | ||
203 | style += generateServiceRibbonWidthStyle( | ||
204 | serviceRibbonWidth, | ||
205 | iconSize, | ||
206 | useVerticalStyle, | ||
207 | ); | ||
208 | } | ||
209 | if (showDragArea) { | ||
210 | style += generateShowDragAreaStyle(accentColor); | ||
211 | } | ||
212 | if (useVerticalStyle) { | ||
213 | style += generateVerticalStyle(serviceRibbonWidth, alwaysShowWorkspaces); | ||
214 | } else if (document.querySelector('#vertical-style')) { | ||
215 | const link = document.querySelector('#vertical-style'); | ||
216 | if (link) { | ||
217 | link.remove(); | ||
218 | } | ||
219 | } | ||
220 | if (alwaysShowWorkspaces) { | ||
221 | style += generateOpenWorkspaceStyle(); | ||
222 | } | ||
223 | |||
224 | return style; | ||
225 | } | ||
226 | function updateStyle(settings) { | ||
227 | const style = generateStyle(settings); | ||
228 | setAppearance(style); | ||
229 | } | ||
230 | |||
231 | export default function initAppearance(stores) { | ||
232 | const { settings } = stores; | ||
233 | createStyleElement(); | ||
234 | |||
235 | // Update style when settings change | ||
236 | reaction( | ||
237 | () => [ | ||
238 | settings.all.app.accentColor, | ||
239 | settings.all.app.serviceRibbonWidth, | ||
240 | settings.all.app.iconSize, | ||
241 | settings.all.app.showDragArea, | ||
242 | settings.all.app.useVerticalStyle, | ||
243 | settings.all.app.alwaysShowWorkspaces, | ||
244 | ], | ||
245 | () => { | ||
246 | updateStyle(settings.all.app); | ||
247 | }, | ||
248 | { fireImmediately: true }, | ||
249 | ); | ||
250 | } | ||