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