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