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