You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
14 KiB
442 lines
14 KiB
import 'dart:collection';
|
|
import 'dart:ui';
|
|
import 'dart:core';
|
|
|
|
import 'package:cwtch/themes/cwtch.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
|
import 'themes/opaque.dart';
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
|
|
const TapirGroupsExperiment = "tapir-groups-experiment";
|
|
const ServerManagementExperiment = "servers-experiment";
|
|
const FileSharingExperiment = "filesharing";
|
|
const ImagePreviewsExperiment = "filesharing-images";
|
|
const ClickableLinksExperiment = "clickable-links";
|
|
const FormattingExperiment = "message-formatting";
|
|
const QRCodeExperiment = "qrcode-support";
|
|
|
|
enum DualpaneMode {
|
|
Single,
|
|
Dual1to2,
|
|
Dual1to4,
|
|
CopyPortrait,
|
|
}
|
|
|
|
enum NotificationPolicy {
|
|
Mute,
|
|
OptIn,
|
|
DefaultAll,
|
|
}
|
|
|
|
enum NotificationContent {
|
|
SimpleEvent,
|
|
ContactInfo,
|
|
}
|
|
|
|
/// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments.
|
|
/// We also provide access to the version information here as it is also accessed from the
|
|
/// Settings Pane.
|
|
class Settings extends ChangeNotifier {
|
|
Locale locale;
|
|
late PackageInfo packageInfo;
|
|
OpaqueThemeType theme;
|
|
|
|
// explicitly set experiments to false until told otherwise...
|
|
bool experimentsEnabled = false;
|
|
HashMap<String, bool> experiments = HashMap.identity();
|
|
DualpaneMode _uiColumnModePortrait = DualpaneMode.Single;
|
|
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
|
|
|
|
NotificationPolicy _notificationPolicy = NotificationPolicy.DefaultAll;
|
|
NotificationContent _notificationContent = NotificationContent.SimpleEvent;
|
|
|
|
bool blockUnknownConnections = false;
|
|
bool streamerMode = false;
|
|
String _downloadPath = "";
|
|
|
|
bool _allowAdvancedTorConfig = false;
|
|
bool _useCustomTorConfig = false;
|
|
String _customTorConfig = "";
|
|
int _socksPort = -1;
|
|
int _controlPort = -1;
|
|
String _customTorAuth = "";
|
|
bool _useTorCache = false;
|
|
String _torCacheDir = "";
|
|
bool _useSemanticDebugger = false;
|
|
|
|
String get torCacheDir => _torCacheDir;
|
|
|
|
set useSemanticDebugger(bool newval) {
|
|
this._useSemanticDebugger = newval;
|
|
notifyListeners();
|
|
}
|
|
|
|
bool get useSemanticDebugger => _useSemanticDebugger;
|
|
|
|
void setTheme(String themeId, String mode) {
|
|
theme = getTheme(themeId, mode);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Get access to the current theme.
|
|
OpaqueThemeType current() {
|
|
return theme;
|
|
}
|
|
|
|
/// isExperimentEnabled can be used to safely check whether a particular
|
|
/// experiment is enabled
|
|
bool isExperimentEnabled(String experiment) {
|
|
if (this.experimentsEnabled) {
|
|
if (this.experiments.containsKey(experiment)) {
|
|
// We now know it cannot be null...
|
|
return this.experiments[experiment]! == true;
|
|
}
|
|
}
|
|
|
|
// allow message formatting to be turned off even when experiments are
|
|
// disabled...
|
|
if (experiment == FormattingExperiment) {
|
|
if (this.experiments.containsKey(FormattingExperiment)) {
|
|
// If message formatting has not explicitly been turned off, then
|
|
// turn it on by default (even when experiments are disabled)
|
|
return this.experiments[experiment]! == true;
|
|
} else {
|
|
return true; // enable by default
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Called by the event bus. When new settings are loaded from a file the JSON will
|
|
/// be sent to the function and new settings will be instantiated based on the contents.
|
|
handleUpdate(dynamic settings) {
|
|
// Set Theme and notify listeners
|
|
this.setTheme(settings["Theme"], settings["ThemeMode"] ?? mode_dark);
|
|
|
|
// Set Locale and notify listeners
|
|
switchLocaleByCode(settings["Locale"]);
|
|
|
|
blockUnknownConnections = settings["BlockUnknownConnections"] ?? false;
|
|
streamerMode = settings["StreamerMode"] ?? false;
|
|
|
|
// Decide whether to enable Experiments
|
|
experimentsEnabled = settings["ExperimentsEnabled"] ?? false;
|
|
|
|
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
|
|
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
|
|
|
|
// single pane vs dual pane preferences
|
|
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
|
|
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
|
|
_notificationPolicy = notificationPolicyFromString(settings["NotificationPolicy"]);
|
|
|
|
_notificationContent = notificationContentFromString(settings["NotificationContent"]);
|
|
|
|
// auto-download folder
|
|
_downloadPath = settings["DownloadPath"] ?? "";
|
|
|
|
// allow a custom tor config
|
|
_allowAdvancedTorConfig = settings["AllowAdvancedTorConfig"] ?? false;
|
|
_useCustomTorConfig = settings["UseCustomTorrc"] ?? false;
|
|
_customTorConfig = settings["CustomTorrc"] ?? "";
|
|
_socksPort = settings["CustomSocksPort"] ?? -1;
|
|
_controlPort = settings["CustomControlPort"] ?? -1;
|
|
_useTorCache = settings["UseTorCache"] ?? false;
|
|
_torCacheDir = settings["TorCacheDir"] ?? "";
|
|
|
|
// Push the experimental settings to Consumers of Settings
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Initialize the Package Version information
|
|
initPackageInfo() {
|
|
PackageInfo.fromPlatform().then((PackageInfo newPackageInfo) {
|
|
packageInfo = newPackageInfo;
|
|
notifyListeners();
|
|
});
|
|
}
|
|
|
|
/// Switch the Locale of the App by Language Code
|
|
switchLocaleByCode(String languageCode) {
|
|
var code = languageCode.split("_");
|
|
if (code.length == 1) {
|
|
this.switchLocale(Locale(languageCode));
|
|
} else {
|
|
this.switchLocale(Locale(code[0], code[1]));
|
|
}
|
|
}
|
|
|
|
/// Switch the Locale of the App
|
|
switchLocale(Locale newLocale) {
|
|
locale = newLocale;
|
|
notifyListeners();
|
|
}
|
|
|
|
setStreamerMode(bool newSteamerMode) {
|
|
streamerMode = newSteamerMode;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Block Unknown Connections will autoblock connections if they authenticate with public key not in our contacts list.
|
|
/// 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
|
|
/// list without being bothered by spurious requests (either permanently, or as a short term measure).
|
|
/// Note: This is not an *appear offline* setting which would explicitly close the listen port, rather than simply auto disconnecting unknown attempts.
|
|
forbidUnknownConnections() {
|
|
blockUnknownConnections = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Allow Unknown Connections will allow new contact requires from unknown public keys
|
|
/// See above for more information.
|
|
allowUnknownConnections() {
|
|
blockUnknownConnections = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Turn Experiments On, this will also have the side effect of enabling any
|
|
/// Experiments that have been previously activated.
|
|
enableExperiments() {
|
|
experimentsEnabled = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Turn Experiments Off. This will disable **all** active experiments.
|
|
/// Note: This will not set the preference for individual experiments, if experiments are enabled
|
|
/// any experiments that were active previously will become active again unless they are explicitly disabled.
|
|
disableExperiments() {
|
|
experimentsEnabled = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Turn on a specific experiment.
|
|
enableExperiment(String key) {
|
|
experiments.update(key, (value) => true, ifAbsent: () => true);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Turn off a specific experiment
|
|
disableExperiment(String key) {
|
|
experiments.update(key, (value) => false, ifAbsent: () => false);
|
|
notifyListeners();
|
|
}
|
|
|
|
DualpaneMode get uiColumnModePortrait => _uiColumnModePortrait;
|
|
|
|
set uiColumnModePortrait(DualpaneMode newval) {
|
|
this._uiColumnModePortrait = newval;
|
|
notifyListeners();
|
|
}
|
|
|
|
DualpaneMode get uiColumnModeLandscape => _uiColumnModeLandscape;
|
|
|
|
set uiColumnModeLandscape(DualpaneMode newval) {
|
|
this._uiColumnModeLandscape = newval;
|
|
notifyListeners();
|
|
}
|
|
|
|
NotificationPolicy get notificationPolicy => _notificationPolicy;
|
|
|
|
set notificationPolicy(NotificationPolicy newpol) {
|
|
this._notificationPolicy = newpol;
|
|
notifyListeners();
|
|
}
|
|
|
|
NotificationContent get notificationContent => _notificationContent;
|
|
|
|
set notificationContent(NotificationContent newcon) {
|
|
this._notificationContent = newcon;
|
|
notifyListeners();
|
|
}
|
|
|
|
List<int> uiColumns(bool isLandscape) {
|
|
var m = (!isLandscape || uiColumnModeLandscape == DualpaneMode.CopyPortrait) ? uiColumnModePortrait : uiColumnModeLandscape;
|
|
switch (m) {
|
|
case DualpaneMode.Single:
|
|
return [1];
|
|
case DualpaneMode.Dual1to2:
|
|
return [1, 2];
|
|
case DualpaneMode.Dual1to4:
|
|
return [1, 4];
|
|
}
|
|
print("impossible column configuration: portrait/$uiColumnModePortrait landscape/$uiColumnModeLandscape");
|
|
return [1];
|
|
}
|
|
|
|
static List<DualpaneMode> uiColumnModeOptions(bool isLandscape) {
|
|
if (isLandscape)
|
|
return [
|
|
DualpaneMode.CopyPortrait,
|
|
DualpaneMode.Single,
|
|
DualpaneMode.Dual1to2,
|
|
DualpaneMode.Dual1to4,
|
|
];
|
|
else
|
|
return [DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4];
|
|
}
|
|
|
|
static DualpaneMode uiColumnModeFromString(String m) {
|
|
switch (m) {
|
|
case "DualpaneMode.Single":
|
|
return DualpaneMode.Single;
|
|
case "DualpaneMode.Dual1to2":
|
|
return DualpaneMode.Dual1to2;
|
|
case "DualpaneMode.Dual1to4":
|
|
return DualpaneMode.Dual1to4;
|
|
case "DualpaneMode.CopyPortrait":
|
|
return DualpaneMode.CopyPortrait;
|
|
}
|
|
print("Error: ui requested translation of column mode [$m] which doesn't exist");
|
|
return DualpaneMode.Single;
|
|
}
|
|
|
|
static String uiColumnModeToString(DualpaneMode m, BuildContext context) {
|
|
switch (m) {
|
|
case DualpaneMode.Single:
|
|
return AppLocalizations.of(context)!.settingUIColumnSingle;
|
|
case DualpaneMode.Dual1to2:
|
|
return AppLocalizations.of(context)!.settingUIColumnDouble12Ratio;
|
|
case DualpaneMode.Dual1to4:
|
|
return AppLocalizations.of(context)!.settingUIColumnDouble14Ratio;
|
|
case DualpaneMode.CopyPortrait:
|
|
return AppLocalizations.of(context)!.settingUIColumnOptionSame;
|
|
}
|
|
}
|
|
|
|
static NotificationPolicy notificationPolicyFromString(String? np) {
|
|
switch (np) {
|
|
case "NotificationPolicy.Mute":
|
|
return NotificationPolicy.Mute;
|
|
case "NotificationPolicy.OptIn":
|
|
return NotificationPolicy.OptIn;
|
|
case "NotificationPolicy.OptOut":
|
|
return NotificationPolicy.DefaultAll;
|
|
}
|
|
return NotificationPolicy.DefaultAll;
|
|
}
|
|
|
|
static NotificationContent notificationContentFromString(String? nc) {
|
|
switch (nc) {
|
|
case "NotificationContent.SimpleEvent":
|
|
return NotificationContent.SimpleEvent;
|
|
case "NotificationContent.ContactInfo":
|
|
return NotificationContent.ContactInfo;
|
|
}
|
|
return NotificationContent.SimpleEvent;
|
|
}
|
|
|
|
static String notificationPolicyToString(NotificationPolicy np, BuildContext context) {
|
|
switch (np) {
|
|
case NotificationPolicy.Mute:
|
|
return AppLocalizations.of(context)!.notificationPolicyMute;
|
|
case NotificationPolicy.OptIn:
|
|
return AppLocalizations.of(context)!.notificationPolicyOptIn;
|
|
case NotificationPolicy.DefaultAll:
|
|
return AppLocalizations.of(context)!.notificationPolicyDefaultAll;
|
|
}
|
|
}
|
|
|
|
static String notificationContentToString(NotificationContent nc, BuildContext context) {
|
|
switch (nc) {
|
|
case NotificationContent.SimpleEvent:
|
|
return AppLocalizations.of(context)!.notificationContentSimpleEvent;
|
|
case NotificationContent.ContactInfo:
|
|
return AppLocalizations.of(context)!.notificationContentContactInfo;
|
|
}
|
|
}
|
|
|
|
// checks experiment settings and file extension for image previews
|
|
// (ignores file size; if the user manually accepts the file, assume it's okay to preview)
|
|
bool shouldPreview(String path) {
|
|
var lpath = path.toLowerCase();
|
|
return isExperimentEnabled(ImagePreviewsExperiment) &&
|
|
(lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp"));
|
|
}
|
|
|
|
String get downloadPath => _downloadPath;
|
|
|
|
set downloadPath(String newval) {
|
|
_downloadPath = newval;
|
|
notifyListeners();
|
|
}
|
|
|
|
bool get allowAdvancedTorConfig => _allowAdvancedTorConfig;
|
|
|
|
set allowAdvancedTorConfig(bool torConfig) {
|
|
_allowAdvancedTorConfig = torConfig;
|
|
notifyListeners();
|
|
}
|
|
|
|
bool get useTorCache => _useTorCache;
|
|
|
|
set useTorCache(bool useTorCache) {
|
|
_useTorCache = useTorCache;
|
|
notifyListeners();
|
|
}
|
|
|
|
// Settings / Gettings for setting the custom tor config..
|
|
String get torConfig => _customTorConfig;
|
|
|
|
set torConfig(String torConfig) {
|
|
_customTorConfig = torConfig;
|
|
notifyListeners();
|
|
}
|
|
|
|
int get socksPort => _socksPort;
|
|
|
|
set socksPort(int newSocksPort) {
|
|
_socksPort = newSocksPort;
|
|
notifyListeners();
|
|
}
|
|
|
|
int get controlPort => _controlPort;
|
|
|
|
set controlPort(int controlPort) {
|
|
_controlPort = controlPort;
|
|
notifyListeners();
|
|
}
|
|
|
|
// Setters / Getters for toggling whether the app should use a custom tor config
|
|
bool get useCustomTorConfig => _useCustomTorConfig;
|
|
|
|
set useCustomTorConfig(bool useCustomTorConfig) {
|
|
_useCustomTorConfig = useCustomTorConfig;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Construct a default settings object.
|
|
Settings(this.locale, this.theme);
|
|
|
|
/// Convert this Settings object to a JSON representation for serialization on the
|
|
/// event bus.
|
|
dynamic asJson() {
|
|
return {
|
|
"Locale": this.locale.toString(),
|
|
"Theme": theme.theme,
|
|
"ThemeMode": theme.mode,
|
|
"PreviousPid": -1,
|
|
"BlockUnknownConnections": blockUnknownConnections,
|
|
"NotificationPolicy": _notificationPolicy.toString(),
|
|
"NotificationContent": _notificationContent.toString(),
|
|
"StreamerMode": streamerMode,
|
|
"ExperimentsEnabled": this.experimentsEnabled,
|
|
"Experiments": experiments,
|
|
"StateRootPane": 0,
|
|
"FirstTime": false,
|
|
"UIColumnModePortrait": uiColumnModePortrait.toString(),
|
|
"UIColumnModeLandscape": uiColumnModeLandscape.toString(),
|
|
"DownloadPath": _downloadPath,
|
|
"AllowAdvancedTorConfig": _allowAdvancedTorConfig,
|
|
"CustomTorRc": _customTorConfig,
|
|
"UseCustomTorrc": _useCustomTorConfig,
|
|
"CustomSocksPort": _socksPort,
|
|
"CustomControlPort": _controlPort,
|
|
"CustomAuth": _customTorAuth,
|
|
"UseTorCache": _useTorCache,
|
|
"TorCacheDir": _torCacheDir
|
|
};
|
|
}
|
|
}
|