diff --git a/lib/main.dart b/lib/main.dart index 6ddf73c..15e70c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,11 @@ import "package:flutter/material.dart"; import "package:permission_handler/permission_handler.dart"; +import "package:shared_preferences/shared_preferences.dart"; import "package:vidar/configuration.dart"; import "package:vidar/pages/contact_list.dart"; import "package:vidar/pages/no_sms_permission.dart"; import "package:vidar/utils/common_object.dart"; +import "package:vidar/utils/settings.dart"; import "package:vidar/utils/sms.dart"; import "package:vidar/utils/storage.dart"; @@ -13,10 +15,28 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); externalConfiguration(); + await loadData(CommonObject.contactList); - await loadData(CommonObject.contactList, CommonObject.settings); + // If last logon is past the wipeout time -> wipe keys + if (Settings.allowWipeoutTime && + CommonObject.lastLogon != null && + DateTime.now().difference(CommonObject.lastLogon!).inDays > + Settings.wipeoutTime) { + if (Settings.keepLogs) { + CommonObject.logger!.info( + "Last logon was more than ${Settings.wipeoutTime} days, wiping all keys...", + ); + } + CommonObject.contactList.wipeKeys(); + wipeSecureStorage(); + } SmsConstants(await retrieveSmsConstantsMap()); smsStatus = await Permission.sms.request(); + CommonObject.lastLogon = DateTime.now(); + await (await SharedPreferences.getInstance()).setString( + "lastlogon", + CommonObject.lastLogon!.toIso8601String(), + ); runApp(const VidarApp()); } diff --git a/lib/pages/edit_contact.dart b/lib/pages/edit_contact.dart index 186f0ff..eb97559 100644 --- a/lib/pages/edit_contact.dart +++ b/lib/pages/edit_contact.dart @@ -121,7 +121,7 @@ class _EditContactPageState extends State { contact.name = newName!; contact.encryptionKey = newKey!; contact.phoneNumber = newPhoneNumber!; - saveData(CommonObject.contactList, CommonObject.settings); + saveData(CommonObject.contactList); clearNavigatorAndPush(context, ChatPage(contact)); } @@ -159,7 +159,7 @@ class _EditContactPageState extends State { contact.name = newName!; contact.encryptionKey = newKey!; contact.phoneNumber = newPhoneNumber!; - saveData(CommonObject.contactList, CommonObject.settings); + saveData(CommonObject.contactList); CommonObject.logger!.info( "New contact ${contact.uuid} has been saved", ); @@ -186,7 +186,7 @@ class _EditContactPageState extends State { contact.name = newName!; contact.encryptionKey = newKey!; contact.phoneNumber = newPhoneNumber!; - saveData(CommonObject.contactList, CommonObject.settings); + saveData(CommonObject.contactList); clearNavigatorAndPush(context, const ContactListPage()); } } diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 9ab808a..9c26a90 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -10,6 +10,7 @@ import "package:vidar/utils/storage.dart"; import "package:vidar/widgets/boolean_setting.dart"; import "package:vidar/widgets/buttons.dart"; import "package:vidar/widgets/color_set_select.dart"; +import "package:vidar/widgets/int_setting.dart"; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -19,7 +20,21 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - _SettingsPageState(); + _SettingsPageState() { + allowWipeoutTime = LightBooleanSetting( + initValue: allowWipeoutTimeValue, + settingText: "Require login every X days", + onChanged: (final bool value) { + setState(() { + allowWipeoutTimeValue = !allowWipeoutTimeValue; + }); + }, + ); + } + + late final LightBooleanSetting allowWipeoutTime; + + bool allowWipeoutTimeValue = Settings.allowWipeoutTime; final BooleanSetting allowUnencryptedMessages = BooleanSetting( setting: Settings.allowUnencryptedMessages, @@ -45,6 +60,12 @@ class _SettingsPageState extends State { selectedSet: Settings.colorSet.name, ); + final IntSetting wipeoutTime = IntSetting( + setting: Settings.wipeoutTime, + settingText: "Max logon interval (days)", + maxLength: 4, + ); + @override void initState() { super.initState(); @@ -55,6 +76,17 @@ class _SettingsPageState extends State { Settings.keepLogs = keepLogs.setting; Settings.showEncryptionKeyInEditContact = showEncryptionKeyInEditContact.setting; + Settings.allowUnencryptedMessages = allowUnencryptedMessages.setting; + Settings.allowWipeoutTime = allowWipeoutTimeValue; + if (allowWipeoutTimeValue) { + if (wipeoutTime.setting < 1) { + Settings.allowWipeoutTime = false; + Settings.wipeoutTime = 0; + } else { + Settings.allowWipeoutTime = true; + Settings.wipeoutTime = wipeoutTime.setting; + } + } if (Settings.keepLogs) { final PermissionStatus manageExternalStorageStatus = await Permission .manageExternalStorage @@ -94,7 +126,7 @@ class _SettingsPageState extends State { Settings.colorSet = getColorSetFromName(colorSetSelect.selectedSet); if (mounted) { - saveSettings(CommonObject.settings, context: context); + saveSettings(context: context); clearNavigatorAndPush(context, const ContactListPage()); } } @@ -117,6 +149,15 @@ class _SettingsPageState extends State { allowUnencryptedMessages, keepLogs, showEncryptionKeyInEditContact, + Column( + children: [ + allowWipeoutTime, + Visibility( + visible: allowWipeoutTimeValue, + child: wipeoutTime, + ), + ], + ), showMessageBarHints, colorSetSelect, ], diff --git a/lib/utils/common_object.dart b/lib/utils/common_object.dart index f57d6d5..b27e41b 100644 --- a/lib/utils/common_object.dart +++ b/lib/utils/common_object.dart @@ -1,13 +1,11 @@ import "package:logging/logging.dart"; import "package:vidar/utils/contact.dart"; import "package:vidar/utils/conversation.dart"; -import "package:vidar/utils/settings.dart"; class CommonObject { static ContactList contactList = ContactList([]); - static Settings settings = Settings(); static Logger? logger; static List logs = []; - static Conversation? currentConversation; + static DateTime? lastLogon; } diff --git a/lib/utils/settings.dart b/lib/utils/settings.dart index d6d0030..c5c4c50 100644 --- a/lib/utils/settings.dart +++ b/lib/utils/settings.dart @@ -1,5 +1,4 @@ import "package:vidar/utils/colors.dart"; -import "package:vidar/utils/common_object.dart"; /// Static class for storing the active user settings of the program. class Settings { @@ -10,16 +9,22 @@ class Settings { static bool showEncryptionKeyInEditContact = false; + static bool allowWipeoutTime = false; + + static int wipeoutTime = 0; + static bool showMessageBarHints = true; static ColorSet colorSet = vidarColorSet; /// Get map of the state of all instance variable of Settings. - Map toMap() { + static Map toMap() { return { "allowUnencryptedMessages": allowUnencryptedMessages, "keepLogs": keepLogs, "showEncryptionKeyInEditContact": showEncryptionKeyInEditContact, + "allowWipeoutTime": allowWipeoutTime, + "wipeoutTime": wipeoutTime, "showMessageBarHints": showMessageBarHints, "colorSet": colorSet.name, }; @@ -27,25 +32,18 @@ class Settings { /// Set state of all instance variables of Settings from map. /// If setting is not found then it goes to the default setting - void fromMap(final Map map) { + static void fromMap(final Map map) { keepLogs = map["keepLogs"] as bool? ?? keepLogs; showEncryptionKeyInEditContact = map["showEncryptionKeyInEditContact"] as bool? ?? showEncryptionKeyInEditContact; showMessageBarHints = map["showMessageBarHints"] as bool? ?? showMessageBarHints; + allowWipeoutTime = map["allowWipeoutTime"] as bool? ?? allowWipeoutTime; + wipeoutTime = map["wipeoutTime"] as int? ?? wipeoutTime; colorSet = getColorSetFromName(map["colorSet"] as String? ?? "default"); - final bool? newAllowUnencryptedMessages = - map["allowUnencryptedMessages"]! as bool?; - if (newAllowUnencryptedMessages == null) { - if (keepLogs) { - CommonObject.logger!.info( - "allowUnencryptedMessages was not in map, defaulting to $allowUnencryptedMessages", - ); - } - } else { - allowUnencryptedMessages = newAllowUnencryptedMessages; - } + allowUnencryptedMessages = + map["allowUnencryptedMessages"]! as bool? ?? allowUnencryptedMessages; } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 525a5ba..b93d7de 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -10,8 +10,7 @@ import "package:vidar/utils/settings.dart"; import "package:vidar/widgets/error_popup.dart"; Future saveData( - final ContactList contactList, - final Settings settings, { + final ContactList contactList, { final BuildContext? context, }) async { try { @@ -27,7 +26,7 @@ Future saveData( } _saveKeys(contactList); - saveSettings(settings, sharedPreferences: prefs); + saveSettings(sharedPreferences: prefs); await prefs.setStringList("contacts", jsonContacts); @@ -52,8 +51,7 @@ Future saveData( } Future loadData( - final ContactList contactList, - final Settings settings, { + final ContactList contactList, { final BuildContext? context, }) async { try { @@ -63,6 +61,12 @@ Future loadData( prefs.getStringList("contacts") ?? []; final String? jsonSettings = prefs.getString("settings"); + final String? lastLogon = prefs.getString("lastlogon"); + if (lastLogon == null) { + CommonObject.lastLogon = null; + } else { + CommonObject.lastLogon = DateTime.tryParse(lastLogon); + } final List listOfContacts = []; @@ -76,7 +80,7 @@ Future loadData( _loadKeys(contactList); if (jsonSettings != null) { - settings.fromMap(jsonDecode(jsonSettings) as Map); + Settings.fromMap(jsonDecode(jsonSettings) as Map); } if (Settings.keepLogs) { @@ -100,23 +104,25 @@ Future loadData( } /// Only saves settings, to save settings and contacts use [saveData] -Future saveSettings( - final Settings settings, { +Future saveSettings({ final SharedPreferences? sharedPreferences, final BuildContext? context, }) async { try { final SharedPreferences prefs = sharedPreferences ?? await SharedPreferences.getInstance(); - await prefs.setString("settings", jsonEncode(settings.toMap())); + await prefs.setString("settings", jsonEncode(Settings.toMap())); if (Settings.keepLogs) { // Showing Settings.keepLogs is redundant but it's there for consistency CommonObject.logger!.config(""" - ======== SETTINGS ======== - Allow Unencrypted Messages: ${Settings.allowUnencryptedMessages} - Keep Logs: ${Settings.keepLogs} - Color set: ${Settings.colorSet.name} - Show Message Bar Hints: ${Settings.showMessageBarHints} +======== SETTINGS ======== +Allow unencrypted messages: ${Settings.allowUnencryptedMessages} +Keep Logs: ${Settings.keepLogs} +Color set: ${Settings.colorSet.name} +Show message bar hints: ${Settings.showMessageBarHints} +Show encryption key in edit contact: ${Settings.showEncryptionKeyInEditContact} +Allow wipeout: ${Settings.allowWipeoutTime} +Wipeout time: ${Settings.wipeoutTime} days """); } } on Exception catch (error, stackTrace) { diff --git a/lib/widgets/boolean_setting.dart b/lib/widgets/boolean_setting.dart index 2af5491..981c7a8 100644 --- a/lib/widgets/boolean_setting.dart +++ b/lib/widgets/boolean_setting.dart @@ -1,10 +1,17 @@ +// ignore_for_file: inference_failure_on_function_return_type, avoid_positional_boolean_parameters + import "package:flutter/material.dart"; import "package:vidar/configuration.dart"; import "package:vidar/utils/settings.dart"; // ignore: must_be_immutable class BooleanSetting extends StatefulWidget { - BooleanSetting({required this.setting, required this.settingText, super.key}); + BooleanSetting({ + required this.setting, + required this.settingText, + this.onChanged, + super.key, + }); /// The initial state of the setting, /// will be updated to reflect changes in the setting. @@ -12,7 +19,7 @@ class BooleanSetting extends StatefulWidget { /// The text shown to the user explaining the setting. final String settingText; - + final Function(bool value)? onChanged; @override _BooleanSettingState createState() => _BooleanSettingState(); } @@ -21,11 +28,18 @@ class _BooleanSettingState extends State { _BooleanSettingState(); late final String settingText; + late final Function(bool value)? onChanged; @override void initState() { super.initState(); settingText = widget.settingText; + onChanged = + (widget.onChanged ?? + (final bool value) { + widget.setting = value; + }) + as Function(bool value)?; } @override @@ -53,9 +67,89 @@ class _BooleanSettingState extends State { : Settings.colorSet.secondary, ), value: widget.setting, + onChanged: onChanged, + ), + ), + ), + + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: Text( + settingText, + style: TextStyle( + color: Settings.colorSet.text, + fontSize: SizeConfiguration.settingInfoText, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ); + } +} + +/// This version does not automate the setting handling +class LightBooleanSetting extends StatefulWidget { + const LightBooleanSetting({ + required this.settingText, + required this.onChanged, + required this.initValue, + super.key, + }); + + /// The text shown to the user explaining the setting. + final String settingText; + final bool initValue; + final Function(bool value) onChanged; + @override + _LightBooleanSettingState createState() => _LightBooleanSettingState(); +} + +class _LightBooleanSettingState extends State { + _LightBooleanSettingState(); + + late final String settingText; + late final Function(bool value) onChanged; + late bool value; + + @override + void initState() { + super.initState(); + settingText = widget.settingText; + onChanged = widget.onChanged; + value = widget.initValue; + } + + @override + Widget build(final BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 40), + color: Settings.colorSet.primary, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only( + right: MediaQuery.of(context).size.width * 0.1, + ), + child: Material( + color: Colors.transparent, + child: Switch( + activeColor: Settings.colorSet.tertiary, + inactiveThumbColor: Settings.colorSet.secondary, + inactiveTrackColor: Settings.colorSet.inactiveTrack, + trackOutlineColor: WidgetStateProperty.resolveWith( + (final Set states) => + states.contains(WidgetState.selected) + ? null + : Settings.colorSet.secondary, + ), + value: value, onChanged: (final bool value) { setState(() { - widget.setting = value; + onChanged(value); + this.value = value; }); }, ), diff --git a/lib/widgets/contact_badge.dart b/lib/widgets/contact_badge.dart index 5b59a00..1544eca 100644 --- a/lib/widgets/contact_badge.dart +++ b/lib/widgets/contact_badge.dart @@ -73,10 +73,7 @@ class ContactBadge extends StatelessWidget { onPressed: () { final bool success = CommonObject.contactList .removeContactByContact(contact); - saveData( - CommonObject.contactList, - CommonObject.settings, - ); + saveData(CommonObject.contactList); if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) { CommonObject.logger!.info( diff --git a/lib/widgets/int_setting.dart b/lib/widgets/int_setting.dart new file mode 100644 index 0000000..888412f --- /dev/null +++ b/lib/widgets/int_setting.dart @@ -0,0 +1,105 @@ +import "package:flutter/material.dart"; +import "package:vidar/configuration.dart"; +import "package:vidar/utils/settings.dart"; + +// ignore: must_be_immutable +class IntSetting extends StatefulWidget { + IntSetting({ + required this.setting, + required this.settingText, + this.maxLength, + super.key, + }); + + /// The initial state of the setting, + /// will be updated to reflect changes in the setting. + int setting; + + /// The text shown to the user explaining the setting. + final String settingText; + final int? maxLength; + + @override + _IntSettingState createState() => _IntSettingState(); +} + +class _IntSettingState extends State { + _IntSettingState(); + + late final String settingText; + late final int? maxLength; + + @override + void initState() { + super.initState(); + settingText = widget.settingText; + maxLength = widget.maxLength; + } + + @override + Widget build(final BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 40), + color: Settings.colorSet.primary, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only( + right: MediaQuery.of(context).size.width * 0.1, + ), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.15, + child: Material( + color: Colors.transparent, + child: TextField( + keyboardType: TextInputType.number, + maxLength: maxLength, + controller: TextEditingController( + text: widget.setting.toString(), + ), + style: TextStyle(color: Settings.colorSet.text), + decoration: InputDecoration( + fillColor: Settings.colorSet.secondary, + counter: const SizedBox.shrink(), + + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide( + color: Settings.colorSet.secondary, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide(color: Settings.colorSet.tertiary), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.0), + borderSide: BorderSide(color: Settings.colorSet.tertiary), + ), + ), + onChanged: (final String value) { + widget.setting = int.tryParse(value) ?? widget.setting; + }, + ), + ), + ), + ), + + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: Text( + settingText, + style: TextStyle( + color: Settings.colorSet.text, + fontSize: SizeConfiguration.settingInfoText, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 538a565..9320f1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: vidar description: "Private Communications Application" publish_to: "none" -version: 1.1.0-beta+2 +version: 1.2.0-beta+3 repository: "https://github.com/DrSolidDevil/Vidar" issue_tracker: "https://github.com/DrSolidDevil/Vidar/issues"