LCOV - code coverage report
Current view: top level - lib - settings.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 7 226 3.1 %
Date: 2026-07-15 19:28:30 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14