Line data Source code
1 : import 'dart:io';
2 : import 'dart:core';
3 :
4 : import 'package:cwtch/themes/cwtch.dart';
5 : import 'package:cwtch/themes/yamltheme.dart';
6 : import 'package:flutter/material.dart';
7 : import 'package:cwtch/settings.dart';
8 : import 'package:flutter/services.dart';
9 : import 'package:path/path.dart' as path;
10 :
11 : const custom_themes_subdir = "themes";
12 :
13 : const mode_light = "light";
14 : const mode_dark = "dark";
15 :
16 0 : final TextStyle defaultSmallTextStyle = TextStyle(fontFamily: "Inter", fontWeight: FontWeight.normal, fontSize: 10);
17 20 : final TextStyle defaultMessageTextStyle = TextStyle(fontFamily: "Inter", fontWeight: FontWeight.w400, fontSize: 13, fontFamilyFallback: [Platform.isWindows ? 'Segoe UI Emoji' : "Noto Color Emoji"]);
18 12 : final TextStyle defaultFormLabelTextStyle = TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: 20);
19 12 : final TextStyle defaultTextStyle = TextStyle(fontFamily: "Inter", fontWeight: FontWeight.w500, fontSize: 12);
20 16 : final TextStyle defaultTextButtonStyle = defaultTextStyle.copyWith(fontWeight: FontWeight.bold);
21 12 : final TextStyle defaultDropDownMenuItemTextStyle = TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: 16);
22 :
23 : class ThemeLoader extends ChangeNotifier {
24 : Map<String, Map<String, OpaqueThemeType>> themes = Map();
25 :
26 0 : LoadThemes(String cwtchDir) async {
27 0 : themes.clear(); // clear themes...
28 0 : loadBuiltinThemes().then((builtinThemes) {
29 0 : themes.addAll(builtinThemes);
30 0 : notifyListeners();
31 0 : loadCustomThemes(path.join(cwtchDir, custom_themes_subdir)).then((customThemes) {
32 0 : themes.addAll(customThemes);
33 0 : notifyListeners();
34 : });
35 : });
36 : }
37 :
38 4 : OpaqueThemeType getTheme(String? themeId, String? mode) {
39 : if (themeId == null) {
40 : themeId = cwtch_theme;
41 : }
42 4 : if (themeId == mode_light) {
43 : mode = mode_light;
44 : }
45 4 : if (themeId == mode_dark) {
46 : mode = mode_dark;
47 : }
48 16 : var theme = themes[themeId]?[mode] ?? themes[themeId]?[flipMode(mode ?? mode_dark)];
49 4 : return theme ?? CwtchDark();
50 : }
51 :
52 0 : String flipMode(String mode) {
53 0 : if (mode == mode_dark) {
54 : return mode_light;
55 : }
56 : return mode_dark;
57 : }
58 : }
59 :
60 4 : Color lighten(Color color, [double amount = 0.15]) {
61 4 : final hsl = HSLColor.fromColor(color);
62 16 : final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
63 :
64 4 : return hslLight.toColor();
65 : }
66 :
67 0 : Color darken(Color color, [double amount = 0.15]) {
68 0 : final hsl = HSLColor.fromColor(color);
69 0 : final hslDarken = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
70 :
71 0 : return hslDarken.toColor();
72 : }
73 :
74 : abstract class OpaqueThemeType {
75 0 : static final Color red = Color(0xFFFF0000);
76 :
77 0 : get theme => "dummy";
78 0 : get mode => mode_light;
79 :
80 : // Main screen background color (message pane, item rows)
81 0 : get backgroundMainColor => red;
82 :
83 : // pane colors (settings)
84 0 : get backgroundPaneColor => red;
85 :
86 0 : get topbarColor => red;
87 :
88 0 : get mainTextColor => red;
89 :
90 : // pressed row, offline heart
91 0 : get hilightElementColor => red;
92 : // Selected Row
93 0 : get backgroundHilightElementColor => red;
94 : // Faded text color for suggestions in textfields
95 : // Todo: implement way more places
96 0 : get sendHintTextColor => red;
97 :
98 0 : get defaultButtonColor => red;
99 12 : get defaultButtonActiveColor => /*mode == mode_light ? darken(defaultButtonColor) :*/ lighten(defaultButtonColor);
100 0 : get defaultButtonTextColor => red;
101 0 : get defaultButtonDisabledColor => red;
102 0 : get textfieldBackgroundColor => red;
103 0 : get textfieldBorderColor => red;
104 0 : get textfieldHintColor => red;
105 0 : get textfieldErrorColor => red;
106 0 : get textfieldSelectionColor => red;
107 0 : get scrollbarDefaultColor => red;
108 0 : get portraitBackgroundColor => red;
109 0 : get portraitOnlineBorderColor => red;
110 0 : get portraitOfflineBorderColor => red;
111 0 : get portraitBlockedBorderColor => red;
112 0 : get portraitBlockedTextColor => red;
113 0 : get portraitContactBadgeColor => red;
114 0 : get portraitContactBadgeTextColor => red;
115 0 : get portraitProfileBadgeColor => red;
116 0 : get portraitProfileBadgeTextColor => red;
117 :
118 0 : get portraitOnlineAwayColor => Color(0xFFFFF59D);
119 0 : get portraitOnlineBusyColor => Color(0xFFEF9A9A);
120 :
121 : // dropshaddpow
122 : // todo: probably should not be reply icon color in messagerow
123 0 : get dropShadowColor => red;
124 :
125 0 : get toolbarIconColor => red;
126 0 : get toolbarBackgroundColor => red;
127 0 : get chatReactionIconColor => red;
128 0 : get messageFromMeBackgroundColor => red;
129 0 : get messageFromMeTextColor => red;
130 0 : get messageFromOtherBackgroundColor => red;
131 0 : get messageFromOtherTextColor => red;
132 0 : get messageSelectionColor => red;
133 :
134 0 : get menuBackgroundColor => red;
135 :
136 0 : get snackbarBackgroundColor => red;
137 0 : get snackbarTextColor => red;
138 :
139 : // Images
140 :
141 0 : get chatImageColor => red;
142 0 : get chatImage => null;
143 :
144 0 : ImageProvider loadImage(String key, {BuildContext? context}) {
145 0 : return AssetImage("");
146 : }
147 :
148 : // Sizes
149 0 : double contactOnionTextSize() {
150 : return 18;
151 : }
152 : }
153 :
154 : // Borrowed from Stackoverflow
155 0 : MaterialColor getMaterialColor(Color color) {
156 0 : final int red = color.red;
157 0 : final int green = color.green;
158 0 : final int blue = color.blue;
159 :
160 0 : final Map<int, Color> shades = {
161 0 : 50: Color.fromRGBO(red, green, blue, .1),
162 0 : 100: Color.fromRGBO(red, green, blue, .2),
163 0 : 200: Color.fromRGBO(red, green, blue, .3),
164 0 : 300: Color.fromRGBO(red, green, blue, .4),
165 0 : 400: Color.fromRGBO(red, green, blue, .5),
166 0 : 500: Color.fromRGBO(red, green, blue, .6),
167 0 : 600: Color.fromRGBO(red, green, blue, .7),
168 0 : 700: Color.fromRGBO(red, green, blue, .8),
169 0 : 800: Color.fromRGBO(red, green, blue, .9),
170 0 : 900: Color.fromRGBO(red, green, blue, 1),
171 : };
172 :
173 0 : return MaterialColor(color.value, shades);
174 : }
175 :
176 4 : ThemeData mkThemeData(Settings opaque) {
177 4 : return ThemeData(
178 12 : hoverColor: opaque.current().backgroundHilightElementColor.withOpacity(0.5),
179 4 : visualDensity: VisualDensity.adaptivePlatformDensity,
180 12 : primaryIconTheme: IconThemeData(color: opaque.current().mainTextColor),
181 8 : primaryColor: opaque.current().mainTextColor,
182 8 : canvasColor: opaque.current().backgroundMainColor,
183 24 : highlightColor: opaque.current().mode == mode_light ? darken(opaque.current().backgroundHilightElementColor) : lighten(opaque.current().backgroundHilightElementColor),
184 12 : iconTheme: IconThemeData(color: opaque.current().toolbarIconColor),
185 8 : cardColor: opaque.current().backgroundMainColor,
186 4 : bottomSheetTheme: BottomSheetThemeData(
187 8 : backgroundColor: opaque.current().backgroundPaneColor,
188 : constraints: const BoxConstraints(maxWidth: double.infinity),
189 : ),
190 4 : appBarTheme: AppBarTheme(
191 4 : systemOverlayStyle: SystemUiOverlayStyle(
192 : // Status bar color
193 8 : statusBarColor: opaque.current().topbarColor,
194 : // Status bar brightness (optional)
195 12 : statusBarIconBrightness: opaque.current().mode == mode_light ? Brightness.dark : Brightness.light, // For Android (dark icons)
196 12 : statusBarBrightness: opaque.current().mode == mode_light ? Brightness.dark : Brightness.light, // For iOS (dark icons)
197 : ),
198 8 : backgroundColor: opaque.current().topbarColor,
199 : // surfaceTint when not set changes when anything scrolls under it :(
200 8 : surfaceTintColor: opaque.current().topbarColor,
201 12 : iconTheme: IconThemeData(color: opaque.current().mainTextColor),
202 20 : titleTextStyle: TextStyle(fontWeight: FontWeight.bold, fontFamily: "Inter", color: opaque.current().mainTextColor, fontSize: opaque.fontScaling * 18.0),
203 12 : actionsIconTheme: IconThemeData(color: opaque.current().mainTextColor),
204 : ),
205 4 : listTileTheme: ListTileThemeData(
206 16 : titleTextStyle: defaultFormLabelTextStyle.copyWith(color: opaque.current().mainTextColor),
207 16 : subtitleTextStyle: defaultMessageTextStyle.copyWith(color: opaque.current().mainTextColor),
208 : ),
209 : //bottomNavigationBarTheme: BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed, backgroundColor: opaque.current().backgroundHilightElementColor), // Can't determine current use
210 4 : textButtonTheme: TextButtonThemeData(
211 4 : style: ButtonStyle(
212 12 : backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor),
213 12 : foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor),
214 12 : overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor),
215 8 : padding: MaterialStateProperty.all(EdgeInsets.all(20)),
216 : ),
217 : ),
218 8 : hintColor: opaque.current().textfieldHintColor,
219 4 : elevatedButtonTheme: ElevatedButtonThemeData(
220 4 : style: ButtonStyle(
221 4 : backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor : opaque.current().defaultButtonColor),
222 12 : foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor),
223 4 : overlayColor: MaterialStateProperty.resolveWith(
224 0 : (states) => (states.contains(MaterialState.pressed) && states.contains(MaterialState.hovered))
225 0 : ? opaque.current().defaultButtonActiveColor
226 0 : : states.contains(MaterialState.disabled)
227 0 : ? opaque.current().defaultButtonDisabledColor
228 : : null,
229 : ),
230 : enableFeedback: true,
231 12 : textStyle: MaterialStateProperty.all(opaque.scaleFonts(defaultTextButtonStyle)),
232 8 : padding: MaterialStateProperty.all(EdgeInsets.all(20)),
233 : ),
234 : ),
235 4 : filledButtonTheme: FilledButtonThemeData(
236 4 : style: ButtonStyle(
237 4 : backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor : opaque.current().defaultButtonColor),
238 12 : foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor),
239 4 : overlayColor: MaterialStateProperty.resolveWith(
240 0 : (states) => (states.contains(MaterialState.pressed) && states.contains(MaterialState.hovered))
241 0 : ? opaque.current().defaultButtonActiveColor
242 0 : : states.contains(MaterialState.disabled)
243 0 : ? opaque.current().defaultButtonDisabledColor
244 : : null,
245 : ),
246 : enableFeedback: true,
247 12 : textStyle: MaterialStateProperty.all(opaque.scaleFonts(defaultTextButtonStyle)),
248 8 : padding: MaterialStateProperty.all(EdgeInsets.all(20)),
249 : ),
250 : ),
251 4 : outlinedButtonTheme: OutlinedButtonThemeData(
252 4 : style: ButtonStyle(
253 4 : backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor : opaque.current().backgroundMainColor),
254 12 : foregroundColor: MaterialStateProperty.all(opaque.current().mainTextColor),
255 16 : side: MaterialStateProperty.all(BorderSide(color: opaque.current().defaultButtonColor)),
256 4 : overlayColor: MaterialStateProperty.resolveWith(
257 0 : (states) => (states.contains(MaterialState.pressed) && states.contains(MaterialState.hovered))
258 0 : ? opaque.current().defaultButtonActiveColor
259 0 : : states.contains(MaterialState.disabled)
260 0 : ? opaque.current().defaultButtonDisabledColor
261 : : null,
262 : ),
263 : enableFeedback: true,
264 12 : textStyle: MaterialStateProperty.all(opaque.scaleFonts(defaultTextButtonStyle)),
265 8 : padding: MaterialStateProperty.all(EdgeInsets.all(20)),
266 : ),
267 : ),
268 20 : scrollbarTheme: ScrollbarThemeData(thumbVisibility: MaterialStateProperty.all(false), thumbColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor)),
269 4 : sliderTheme: SliderThemeData(
270 8 : activeTrackColor: opaque.current().defaultButtonColor, // color of slider bar for left active region
271 8 : inactiveTrackColor: opaque.theme.defaultButtonDisabledColor, // color of slider bar for right inactive region
272 8 : thumbColor: opaque.current().mainTextColor, // color of slider widget
273 8 : overlayColor: opaque.current().mainTextColor, // color around active widget
274 8 : valueIndicatorColor: opaque.current().backgroundHilightElementColor,
275 20 : valueIndicatorTextStyle: opaque.scaleFonts(defaultDropDownMenuItemTextStyle).copyWith(color: opaque.current().hilightElementColor),
276 4 : valueIndicatorShape: RectangularSliderValueIndicatorShape(), //RoundSliderThumbShape(),
277 : ),
278 4 : tabBarTheme: TabBarThemeData(
279 8 : labelColor: opaque.current().mainTextColor,
280 8 : unselectedLabelColor: opaque.current().mainTextColor,
281 16 : indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor)),
282 20 : labelStyle: opaque.scaleFonts(defaultTextButtonStyle).copyWith(color: opaque.current().mainTextColor),
283 20 : unselectedLabelStyle: opaque.scaleFonts(defaultTextStyle).copyWith(color: opaque.current().mainTextColor),
284 : tabAlignment: TabAlignment.center,
285 : ),
286 4 : dialogTheme: DialogThemeData(
287 8 : backgroundColor: opaque.current().backgroundPaneColor,
288 12 : titleTextStyle: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, color: opaque.current().mainTextColor),
289 12 : contentTextStyle: TextStyle(fontFamily: "Inter", color: opaque.current().mainTextColor),
290 : ),
291 4 : textTheme: TextTheme(
292 : // NOTE: The following font scales were arrived at after consulting the material text scale
293 : // docs: https://m3.material.io/styles/typography/type-scale-tokens and some trial and error
294 20 : displaySmall: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 14.0, color: opaque.current().mainTextColor),
295 20 : displayMedium: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 16.0, color: opaque.current().mainTextColor),
296 20 : displayLarge: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 18.0, color: opaque.current().mainTextColor),
297 20 : titleSmall: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 16.0, color: opaque.current().mainTextColor),
298 20 : titleLarge: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 18.0, color: opaque.current().mainTextColor),
299 20 : titleMedium: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 20.0, color: opaque.current().mainTextColor),
300 20 : bodySmall: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 12.0, color: opaque.current().mainTextColor),
301 20 : bodyMedium: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 14.0, color: opaque.current().mainTextColor),
302 20 : bodyLarge: TextStyle(fontFamily: "Inter", fontSize: opaque.fontScaling * 16.0, color: opaque.current().mainTextColor),
303 20 : headlineSmall: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 24.0, color: opaque.current().mainTextColor),
304 20 : headlineMedium: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 26.0, color: opaque.current().mainTextColor),
305 20 : headlineLarge: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: opaque.fontScaling * 28.0, color: opaque.current().mainTextColor),
306 20 : labelSmall: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.w100, fontSize: opaque.fontScaling * 14.0, color: opaque.current().mainTextColor),
307 20 : labelMedium: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.w300, fontSize: opaque.fontScaling * 16.0, color: opaque.current().mainTextColor),
308 20 : labelLarge: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.w200, fontSize: opaque.fontScaling * 18.0, color: opaque.current().mainTextColor),
309 : ),
310 4 : switchTheme: SwitchThemeData(
311 12 : overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor),
312 12 : thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor),
313 12 : trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor),
314 : ),
315 : // the only way to change the text Selection Context Menu Color ?!
316 12 : brightness: opaque.current().mode == mode_dark ? Brightness.dark : Brightness.light,
317 4 : floatingActionButtonTheme: FloatingActionButtonThemeData(
318 8 : foregroundColor: opaque.current().mainTextColor,
319 8 : backgroundColor: opaque.current().defaultButtonColor,
320 8 : hoverColor: opaque.current().defaultButtonActiveColor,
321 : enableFeedback: true,
322 8 : splashColor: opaque.current().defaultButtonActiveColor,
323 : ),
324 4 : textSelectionTheme: TextSelectionThemeData(
325 8 : cursorColor: opaque.current().textfieldSelectionColor,
326 8 : selectionColor: opaque.current().textfieldSelectionColor,
327 8 : selectionHandleColor: opaque.current().textfieldSelectionColor,
328 : ),
329 16 : popupMenuTheme: PopupMenuThemeData(color: opaque.current().backgroundPaneColor.withOpacity(0.9)),
330 4 : snackBarTheme: SnackBarThemeData(
331 8 : backgroundColor: opaque.current().snackbarBackgroundColor,
332 12 : contentTextStyle: TextStyle(color: opaque.current().snackbarTextColor),
333 : ),
334 : );
335 : }
|