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

Generated by: LCOV version 1.14