Line data Source code
1 : import 'dart:collection';
2 : import 'dart:core';
3 :
4 : import 'package:cwtch/config.dart';
5 : import 'package:flutter/material.dart';
6 : import 'package:package_info_plus/package_info_plus.dart';
7 :
8 : import 'themes/opaque.dart';
9 : import 'package:flutter_gen/gen_l10n/app_localizations.dart';
10 :
11 : const TapirGroupsExperiment = "tapir-groups-experiment";
12 : const ServerManagementExperiment = "servers-experiment";
13 : const FileSharingExperiment = "filesharing";
14 : const ImagePreviewsExperiment = "filesharing-images";
15 : const ClickableLinksExperiment = "clickable-links";
16 : const FormattingExperiment = "message-formatting";
17 : const QRCodeExperiment = "qrcode-support";
18 : const BlodeuweddExperiment = "blodeuwedd";
19 :
20 : enum DualpaneMode {
21 : Single,
22 : Dual1to2,
23 : Dual1to4,
24 : CopyPortrait,
25 : }
26 :
27 : enum NotificationPolicy {
28 : Mute,
29 : OptIn,
30 : DefaultAll,
31 : }
32 :
33 : enum NotificationContent {
34 : SimpleEvent,
35 : ContactInfo,
36 : }
37 :
38 : /// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments.
39 : /// We also provide access to the version information here as it is also accessed from the
40 : /// Settings Pane.
41 : class Settings extends ChangeNotifier {
42 : Locale locale;
43 : late PackageInfo packageInfo;
44 : bool _themeImages = false;
45 :
46 : // explicitly set experiments to false until told otherwise...
47 : bool experimentsEnabled = false;
48 : HashMap<String, bool> experiments = HashMap.identity();
49 : DualpaneMode _uiColumnModePortrait = DualpaneMode.Single;
50 : DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
51 :
52 : NotificationPolicy _notificationPolicy = NotificationPolicy.DefaultAll;
53 : NotificationContent _notificationContent = NotificationContent.SimpleEvent;
54 :
55 : bool preserveHistoryByDefault = false;
56 : bool blockUnknownConnections = false;
57 : bool streamerMode = false;
58 : String _downloadPath = "";
59 :
60 : bool _allowAdvancedTorConfig = false;
61 : bool _useCustomTorConfig = false;
62 : String _customTorConfig = "";
63 : int _socksPort = -1;
64 : int _controlPort = -1;
65 : String _customTorAuth = "";
66 : bool _useTorCache = false;
67 : String _torCacheDir = "";
68 : bool _useSemanticDebugger = false;
69 : double _fontScaling = 1.0;
70 :
71 : ThemeLoader themeloader = ThemeLoader();
72 :
73 0 : String get torCacheDir => _torCacheDir;
74 :
75 : // Whether to show the profiling interface, not saved
76 : bool _profileMode = false;
77 :
78 0 : bool get profileMode => _profileMode;
79 0 : set profileMode(bool newval) {
80 0 : this._profileMode = newval;
81 0 : notifyListeners();
82 : }
83 :
84 0 : set useSemanticDebugger(bool newval) {
85 0 : this._useSemanticDebugger = newval;
86 0 : notifyListeners();
87 : }
88 :
89 0 : bool get useSemanticDebugger => _useSemanticDebugger;
90 :
91 : String? _themeId;
92 0 : String? get themeId => _themeId;
93 : String? _mode;
94 20 : OpaqueThemeType get theme => themeloader.getTheme(_themeId, _mode);
95 0 : void setTheme(String themeId, String mode) {
96 0 : _themeId = themeId;
97 0 : _mode = mode;
98 0 : notifyListeners();
99 : }
100 :
101 0 : bool get themeImages => _themeImages;
102 0 : set themeImages(bool newVal) {
103 0 : _themeImages = newVal;
104 0 : notifyListeners();
105 : }
106 :
107 : /// Get access to the current theme.
108 4 : OpaqueThemeType current() {
109 4 : return theme;
110 : }
111 :
112 : /// isExperimentEnabled can be used to safely check whether a particular
113 : /// experiment is enabled
114 0 : bool isExperimentEnabled(String experiment) {
115 0 : if (this.experimentsEnabled) {
116 0 : if (this.experiments.containsKey(experiment)) {
117 : // We now know it cannot be null...
118 0 : return this.experiments[experiment]! == true;
119 : }
120 : }
121 :
122 : // allow message formatting to be turned off even when experiments are
123 : // disabled...
124 0 : if (experiment == FormattingExperiment) {
125 0 : if (this.experiments.containsKey(FormattingExperiment)) {
126 : // If message formatting has not explicitly been turned off, then
127 : // turn it on by default (even when experiments are disabled)
128 0 : return this.experiments[experiment]! == true;
129 : } else {
130 : return true; // enable by default
131 : }
132 : }
133 :
134 : return false;
135 : }
136 :
137 : /// Called by the event bus. When new settings are loaded from a file the JSON will
138 : /// be sent to the function and new settings will be instantiated based on the contents.
139 0 : handleUpdate(dynamic settings) {
140 : // Set Theme and notify listeners
141 0 : this.setTheme(settings["Theme"], settings["ThemeMode"] ?? mode_dark);
142 0 : _themeImages = settings["ThemeImages"] ?? false;
143 :
144 : // Set Locale and notify listeners
145 0 : switchLocaleByCode(settings["Locale"]);
146 :
147 : // Decide whether to enable Experiments
148 0 : var fontScale = settings["FontScaling"];
149 : if (fontScale == null) {
150 : fontScale = 1.0;
151 : }
152 0 : _fontScaling = double.parse(fontScale.toString()).clamp(0.5, 2.0);
153 :
154 0 : blockUnknownConnections = settings["BlockUnknownConnections"] ?? false;
155 0 : streamerMode = settings["StreamerMode"] ?? false;
156 :
157 : // Decide whether to enable Experiments
158 0 : experimentsEnabled = settings["ExperimentsEnabled"] ?? false;
159 0 : preserveHistoryByDefault = settings["DefaultSaveHistory"] ?? false;
160 :
161 : // Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
162 0 : experiments = new HashMap<String, bool>.from(settings["Experiments"]);
163 :
164 : // single pane vs dual pane preferences
165 0 : _uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
166 0 : _uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
167 0 : _notificationPolicy = notificationPolicyFromString(settings["NotificationPolicy"]);
168 :
169 0 : _notificationContent = notificationContentFromString(settings["NotificationContent"]);
170 :
171 : // auto-download folder
172 0 : _downloadPath = settings["DownloadPath"] ?? "";
173 0 : _blodeuweddPath = settings["BlodeuweddPath"] ?? "";
174 :
175 : // allow a custom tor config
176 0 : _allowAdvancedTorConfig = settings["AllowAdvancedTorConfig"] ?? false;
177 0 : _useCustomTorConfig = settings["UseCustomTorrc"] ?? false;
178 0 : _customTorConfig = settings["CustomTorrc"] ?? "";
179 0 : _socksPort = settings["CustomSocksPort"] ?? -1;
180 0 : _controlPort = settings["CustomControlPort"] ?? -1;
181 0 : _useTorCache = settings["UseTorCache"] ?? false;
182 0 : _torCacheDir = settings["TorCacheDir"] ?? "";
183 :
184 : // Push the experimental settings to Consumers of Settings
185 0 : notifyListeners();
186 : }
187 :
188 : /// Initialize the Package Version information
189 0 : initPackageInfo() {
190 0 : PackageInfo.fromPlatform().then((PackageInfo newPackageInfo) {
191 0 : packageInfo = newPackageInfo;
192 0 : notifyListeners();
193 : });
194 : }
195 :
196 : /// Switch the Locale of the App by Language Code
197 0 : switchLocaleByCode(String languageCode) {
198 0 : var code = languageCode.split("_");
199 0 : if (code.length == 1) {
200 0 : this.switchLocale(Locale(languageCode));
201 : } else {
202 0 : this.switchLocale(Locale(code[0], code[1]));
203 : }
204 : }
205 :
206 : /// Handle Font Scaling
207 0 : set fontScaling(double newFontScaling) {
208 0 : this._fontScaling = newFontScaling;
209 0 : notifyListeners();
210 : }
211 :
212 8 : double get fontScaling => _fontScaling;
213 :
214 : // a convenience function to scale fonts dynamically...
215 4 : TextStyle scaleFonts(TextStyle input) {
216 16 : return input.copyWith(fontSize: (input.fontSize ?? 12) * this.fontScaling);
217 : }
218 :
219 : /// Switch the Locale of the App
220 0 : switchLocale(Locale newLocale) {
221 0 : locale = newLocale;
222 0 : notifyListeners();
223 : }
224 :
225 0 : setStreamerMode(bool newSteamerMode) {
226 0 : streamerMode = newSteamerMode;
227 0 : notifyListeners();
228 : }
229 :
230 : /// Preserve the History of all Conversations By Default (can be overridden for specific conversations)
231 0 : setPreserveHistoryDefault() {
232 0 : preserveHistoryByDefault = true;
233 0 : notifyListeners();
234 : }
235 :
236 : /// Delete the History of all Conversations By Default (can be overridden for specific conversations)
237 0 : setDeleteHistoryDefault() {
238 0 : preserveHistoryByDefault = false;
239 0 : notifyListeners();
240 : }
241 :
242 : /// Block Unknown Connections will autoblock connections if they authenticate with public key not in our contacts list.
243 : /// This is one of the best tools we have to combat abuse, while it isn't ideal it does allow a user to curate their contacts
244 : /// list without being bothered by spurious requests (either permanently, or as a short term measure).
245 : /// Note: This is not an *appear offline* setting which would explicitly close the listen port, rather than simply auto disconnecting unknown attempts.
246 0 : forbidUnknownConnections() {
247 0 : blockUnknownConnections = true;
248 0 : notifyListeners();
249 : }
250 :
251 : /// Allow Unknown Connections will allow new contact requires from unknown public keys
252 : /// See above for more information.
253 0 : allowUnknownConnections() {
254 0 : blockUnknownConnections = false;
255 0 : notifyListeners();
256 : }
257 :
258 : /// Turn Experiments On, this will also have the side effect of enabling any
259 : /// Experiments that have been previously activated.
260 0 : enableExperiments() {
261 0 : experimentsEnabled = true;
262 0 : notifyListeners();
263 : }
264 :
265 : /// Turn Experiments Off. This will disable **all** active experiments.
266 : /// Note: This will not set the preference for individual experiments, if experiments are enabled
267 : /// any experiments that were active previously will become active again unless they are explicitly disabled.
268 0 : disableExperiments() {
269 0 : experimentsEnabled = false;
270 0 : notifyListeners();
271 : }
272 :
273 : /// Turn on a specific experiment.
274 0 : enableExperiment(String key) {
275 0 : experiments.update(key, (value) => true, ifAbsent: () => true);
276 0 : notifyListeners();
277 : }
278 :
279 : /// Turn off a specific experiment
280 0 : disableExperiment(String key) {
281 0 : experiments.update(key, (value) => false, ifAbsent: () => false);
282 0 : notifyListeners();
283 : }
284 :
285 0 : DualpaneMode get uiColumnModePortrait => _uiColumnModePortrait;
286 :
287 0 : set uiColumnModePortrait(DualpaneMode newval) {
288 0 : this._uiColumnModePortrait = newval;
289 0 : notifyListeners();
290 : }
291 :
292 0 : DualpaneMode get uiColumnModeLandscape => _uiColumnModeLandscape;
293 :
294 0 : set uiColumnModeLandscape(DualpaneMode newval) {
295 0 : this._uiColumnModeLandscape = newval;
296 0 : notifyListeners();
297 : }
298 :
299 0 : NotificationPolicy get notificationPolicy => _notificationPolicy;
300 :
301 0 : set notificationPolicy(NotificationPolicy newpol) {
302 0 : this._notificationPolicy = newpol;
303 0 : notifyListeners();
304 : }
305 :
306 0 : NotificationContent get notificationContent => _notificationContent;
307 :
308 0 : set notificationContent(NotificationContent newcon) {
309 0 : this._notificationContent = newcon;
310 0 : notifyListeners();
311 : }
312 :
313 0 : List<int> uiColumns(bool isLandscape) {
314 0 : var m = (!isLandscape || uiColumnModeLandscape == DualpaneMode.CopyPortrait) ? uiColumnModePortrait : uiColumnModeLandscape;
315 : switch (m) {
316 0 : case DualpaneMode.Single:
317 0 : return [1];
318 0 : case DualpaneMode.Dual1to2:
319 0 : return [1, 2];
320 0 : case DualpaneMode.Dual1to4:
321 0 : return [1, 4];
322 : default:
323 : // this should be unreachable thanks to the check above...
324 0 : EnvironmentConfig.debugLog("impossible column configuration: portrait/$uiColumnModePortrait landscape/$uiColumnModeLandscape");
325 0 : return [1];
326 : }
327 : }
328 :
329 0 : static List<DualpaneMode> uiColumnModeOptions(bool isLandscape) {
330 : if (isLandscape)
331 0 : return [
332 : DualpaneMode.CopyPortrait,
333 : DualpaneMode.Single,
334 : DualpaneMode.Dual1to2,
335 : DualpaneMode.Dual1to4,
336 : ];
337 : else
338 0 : return [DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4];
339 : }
340 :
341 0 : static DualpaneMode uiColumnModeFromString(String m) {
342 : switch (m) {
343 0 : case "DualpaneMode.Single":
344 : return DualpaneMode.Single;
345 0 : case "DualpaneMode.Dual1to2":
346 : return DualpaneMode.Dual1to2;
347 0 : case "DualpaneMode.Dual1to4":
348 : return DualpaneMode.Dual1to4;
349 0 : case "DualpaneMode.CopyPortrait":
350 : return DualpaneMode.CopyPortrait;
351 : }
352 0 : print("Error: ui requested translation of column mode [$m] which doesn't exist");
353 : return DualpaneMode.Single;
354 : }
355 :
356 0 : static String uiColumnModeToString(DualpaneMode m, BuildContext context) {
357 : switch (m) {
358 0 : case DualpaneMode.Single:
359 0 : return AppLocalizations.of(context)!.settingUIColumnSingle;
360 0 : case DualpaneMode.Dual1to2:
361 0 : return AppLocalizations.of(context)!.settingUIColumnDouble12Ratio;
362 0 : case DualpaneMode.Dual1to4:
363 0 : return AppLocalizations.of(context)!.settingUIColumnDouble14Ratio;
364 0 : case DualpaneMode.CopyPortrait:
365 0 : return AppLocalizations.of(context)!.settingUIColumnOptionSame;
366 : }
367 : }
368 :
369 0 : static NotificationPolicy notificationPolicyFromString(String? np) {
370 : switch (np) {
371 0 : case "NotificationPolicy.Mute":
372 : return NotificationPolicy.Mute;
373 0 : case "NotificationPolicy.OptIn":
374 : return NotificationPolicy.OptIn;
375 0 : case "NotificationPolicy.OptOut":
376 : return NotificationPolicy.DefaultAll;
377 : }
378 : return NotificationPolicy.DefaultAll;
379 : }
380 :
381 0 : static NotificationContent notificationContentFromString(String? nc) {
382 : switch (nc) {
383 0 : case "NotificationContent.SimpleEvent":
384 : return NotificationContent.SimpleEvent;
385 0 : case "NotificationContent.ContactInfo":
386 : return NotificationContent.ContactInfo;
387 : }
388 : return NotificationContent.SimpleEvent;
389 : }
390 :
391 0 : static String notificationPolicyToString(NotificationPolicy np, BuildContext context) {
392 : switch (np) {
393 0 : case NotificationPolicy.Mute:
394 0 : return AppLocalizations.of(context)!.notificationPolicyMute;
395 0 : case NotificationPolicy.OptIn:
396 0 : return AppLocalizations.of(context)!.notificationPolicyOptIn;
397 0 : case NotificationPolicy.DefaultAll:
398 0 : return AppLocalizations.of(context)!.notificationPolicyDefaultAll;
399 : }
400 : }
401 :
402 0 : static String notificationContentToString(NotificationContent nc, BuildContext context) {
403 : switch (nc) {
404 0 : case NotificationContent.SimpleEvent:
405 0 : return AppLocalizations.of(context)!.notificationContentSimpleEvent;
406 0 : case NotificationContent.ContactInfo:
407 0 : return AppLocalizations.of(context)!.notificationContentContactInfo;
408 : }
409 : }
410 :
411 : // checks experiment settings and file extension for image previews
412 : // (ignores file size; if the user manually accepts the file, assume it's okay to preview)
413 0 : bool shouldPreview(String path) {
414 0 : return isExperimentEnabled(ImagePreviewsExperiment) && isImage(path);
415 : }
416 :
417 0 : bool isImage(String path) {
418 0 : var lpath = path.toLowerCase();
419 0 : return (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp"));
420 : }
421 :
422 0 : String get downloadPath => _downloadPath;
423 :
424 0 : set downloadPath(String newval) {
425 0 : _downloadPath = newval;
426 0 : notifyListeners();
427 : }
428 :
429 0 : bool get allowAdvancedTorConfig => _allowAdvancedTorConfig;
430 :
431 0 : set allowAdvancedTorConfig(bool torConfig) {
432 0 : _allowAdvancedTorConfig = torConfig;
433 0 : notifyListeners();
434 : }
435 :
436 0 : bool get useTorCache => _useTorCache;
437 :
438 0 : set useTorCache(bool useTorCache) {
439 0 : _useTorCache = useTorCache;
440 0 : notifyListeners();
441 : }
442 :
443 : // Settings / Gettings for setting the custom tor config..
444 0 : String get torConfig => _customTorConfig;
445 :
446 0 : set torConfig(String torConfig) {
447 0 : _customTorConfig = torConfig;
448 0 : notifyListeners();
449 : }
450 :
451 0 : int get socksPort => _socksPort;
452 :
453 0 : set socksPort(int newSocksPort) {
454 0 : _socksPort = newSocksPort;
455 0 : notifyListeners();
456 : }
457 :
458 0 : int get controlPort => _controlPort;
459 :
460 0 : set controlPort(int controlPort) {
461 0 : _controlPort = controlPort;
462 0 : notifyListeners();
463 : }
464 :
465 : // Setters / Getters for toggling whether the app should use a custom tor config
466 0 : bool get useCustomTorConfig => _useCustomTorConfig;
467 :
468 0 : set useCustomTorConfig(bool useCustomTorConfig) {
469 0 : _useCustomTorConfig = useCustomTorConfig;
470 0 : notifyListeners();
471 : }
472 :
473 : /// Construct a default settings object.
474 4 : Settings(this.locale);
475 :
476 : String _blodeuweddPath = "";
477 0 : String get blodeuweddPath => _blodeuweddPath;
478 0 : set blodeuweddPath(String newval) {
479 0 : _blodeuweddPath = newval;
480 0 : notifyListeners();
481 : }
482 :
483 : /// Convert this Settings object to a JSON representation for serialization on the
484 : /// event bus.
485 0 : dynamic asJson() {
486 0 : return {
487 0 : "Locale": this.locale.toString(),
488 0 : "Theme": _themeId,
489 0 : "ThemeMode": theme.mode,
490 0 : "ThemeImages": _themeImages,
491 0 : "PreviousPid": -1,
492 0 : "BlockUnknownConnections": blockUnknownConnections,
493 0 : "NotificationPolicy": _notificationPolicy.toString(),
494 0 : "NotificationContent": _notificationContent.toString(),
495 0 : "StreamerMode": streamerMode,
496 0 : "ExperimentsEnabled": this.experimentsEnabled,
497 0 : "Experiments": experiments,
498 : "StateRootPane": 0,
499 : "FirstTime": false,
500 0 : "UIColumnModePortrait": uiColumnModePortrait.toString(),
501 0 : "UIColumnModeLandscape": uiColumnModeLandscape.toString(),
502 0 : "DownloadPath": _downloadPath,
503 0 : "AllowAdvancedTorConfig": _allowAdvancedTorConfig,
504 0 : "CustomTorRc": _customTorConfig,
505 0 : "UseCustomTorrc": _useCustomTorConfig,
506 0 : "CustomSocksPort": _socksPort,
507 0 : "CustomControlPort": _controlPort,
508 0 : "CustomAuth": _customTorAuth,
509 0 : "UseTorCache": _useTorCache,
510 0 : "TorCacheDir": _torCacheDir,
511 0 : "BlodeuweddPath": _blodeuweddPath,
512 0 : "FontScaling": _fontScaling,
513 0 : "DefaultSaveHistory": preserveHistoryByDefault
514 : };
515 : }
516 : }
|