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: 2024-08-19 21:58:30 Functions: 0 0 -

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

Generated by: LCOV version 1.14