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