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