LCOV - code coverage report
Current view: top level - lib - settings.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 10 226 4.4 %
Date: 2026-09-01 18:28:20 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 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             : }

Generated by: LCOV version 1.14