settings-pane #16

Merged
erinn merged 7 commits from settings-pane into trunk 2021-03-16 23:29:41 +00:00
27 changed files with 376 additions and 109 deletions

View File

@ -47,7 +47,7 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Save/Load
- [X] Switch Dark / Light Theme
- [X] Switch Language
- [ ] Enable/Disable Experiments
- [X] Enable/Disable Experiments
- [ ] Accessibility Settings (Zoom etc. - needs a deep dive into flutter)
- [X] Display Build & Version Info
- [X] Acknowledgements & Credits

View File

@ -64,4 +64,6 @@ dependencies {
implementation project(':cwtch')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "com.airbnb.android:lottie:3.5.0"
implementation "com.android.support.constraint:constraint-layout:2.0.4"
}

View File

@ -12,7 +12,7 @@
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:theme="@style/NormalTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
@ -24,15 +24,7 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -1,5 +1,6 @@
package com.example.flutter_app
import SplashView
import androidx.annotation.NonNull
import android.content.pm.PackageManager
import android.os.Bundle
@ -10,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@ -25,6 +27,8 @@ import org.json.JSONObject
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
// Channel to get app info
private val CHANNEL_APP_INFO = "test.flutter.dev/applicationInfo"
private val CALL_APP_INFO = "getNativeLibDir"

View File

@ -0,0 +1,15 @@
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import com.example.flutter_app.R
import io.flutter.embedding.android.SplashScreen
class SplashView : SplashScreen {
override fun createSplashView(context: Context, savedInstanceState: Bundle?): View? =
LayoutInflater.from(context).inflate(R.layout.splash_view, null, false)
override fun transitionToFlutter(onTransitionComplete: Runnable) {
onTransitionComplete.run()
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.lottie.LottieAnimationView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="true"
app:lottie_rawRes="@raw/cwtch_animated_logo"
app:lottie_loop="true"
app:lottie_speed="1.00" />
</androidx.constraintlayout.widget.ConstraintLayout>

File diff suppressed because one or more lines are too long

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>

View File

@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

Binary file not shown.

View File

@ -47,6 +47,8 @@
"dontSavePeerHistory": "Peer-Verlauf löschen",
"editProfile": "Profil bearbeiten",
"editProfileTitle": "Profil bearbeiten",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"experimentsEnabled": "Experimente aktiviert",

View File

@ -47,9 +47,11 @@
"dontSavePeerHistory": "Delete Peer History",
"editProfile": "Edit Profille",
"editProfileTitle": "Edit Profile",
"enableGroups": "Enable Group Chat",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enterProfilePassword": "Enter a password to view your profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"experimentsEnabled": "Experiments enabled",
"experimentsEnabled": "Enable Experiments",
"groupAddr": "Address",
"groupName": "Group name",
"groupNameLabel": "Group Name",

View File

@ -47,6 +47,8 @@
"dontSavePeerHistory": "Eliminar historial de contacto",
"editProfile": "Editar perfil",
"editProfileTitle": "Editar perfil",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"experimentsEnabled": "Experimentos habilitados",

View File

@ -47,6 +47,8 @@
"dontSavePeerHistory": "",
"editProfile": "",
"editProfileTitle": "",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "",
"error0ProfilesLoadedForPassword": "",
"experimentsEnabled": "",

View File

@ -47,6 +47,8 @@
"dontSavePeerHistory": "Elimina cronologia dei peer",
"editProfile": "Modifica profilo",
"editProfileTitle": "Modifica profilo",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"experimentsEnabled": "Esperimenti abilitati",

View File

@ -47,6 +47,8 @@
"dontSavePeerHistory": "",
"editProfile": "",
"editProfileTitle": "",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "",
"error0ProfilesLoadedForPassword": "",
"experimentsEnabled": "",

118
lib/licenses.dart Normal file
View File

@ -0,0 +1,118 @@
import 'package:flutter/foundation.dart';
Stream<LicenseEntry> licenses() async* {
/// Open Privacy Code
yield LicenseEntryWithLineBreaks(["cwtch", "tapir", "connectivity", "log"], '''MIT License
Copyright (c) 2018 Open Privacy Research Society
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files ("the Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''');
/// Ristretto Code
yield LicenseEntryWithLineBreaks(["ristretto255"], '''Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright (c) 2017 George Tankersley. All rights reserved.
Copyright (c) 2019 Henry de Valence. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''');
/// Package pretty provides pretty-printing for Go values. (via Cwtch)
yield LicenseEntryWithLineBreaks(["pretty"], '''Copyright 2012 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.''');
yield LicenseEntryWithLineBreaks(["pidusage"], '''MIT License
Copyright (c) 2017 David
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''');
/// Go Standard Lib
yield LicenseEntryWithLineBreaks(["crypto, net"], '''Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''');
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_app/cwtch/ffi.dart';
import 'package:flutter_app/cwtch/gomobile.dart';
import 'package:flutter/material.dart';
@ -6,6 +7,7 @@ import 'package:flutter_app/views/triplecolview.dart';
import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart';
import 'cwtch/cwtchNotifier.dart';
import 'licenses.dart';
import 'model.dart';
import 'views/profilemgrview.dart';
import 'views/splashView.dart';
@ -13,9 +15,13 @@ import 'dart:io' show Platform;
import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var GlobalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalSettings = Settings(Locale("en", ''), Opaque.dark);
void main() => runApp(Flwtch());
void main() {
LicenseRegistry.addLicense(() => licenses());
runApp(Flwtch());
}
class Flwtch extends StatefulWidget {
final Key flwtch = GlobalKey();
@ -41,7 +47,7 @@ class FlwtchState extends State<Flwtch> {
cwtchInit = false;
profs = ProfileListState();
var cwtchNotifier = new CwtchNotifier(profs, GlobalSettings);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings);
if (Platform.isAndroid) {
cwtch = CwtchGomobile(cwtchNotifier);
@ -59,7 +65,7 @@ class FlwtchState extends State<Flwtch> {
}
ChangeNotifierProvider<Settings> getSettingsProvider() =>
ChangeNotifierProvider(create: (context) => GlobalSettings);
ChangeNotifierProvider(create: (context) => globalSettings);
Provider<FlwtchState> getFlwtchStateProvider() =>
Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() =>

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:ui';
import 'dart:core';
@ -6,38 +7,57 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'opaque.dart';
/// 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;
PackageInfo packageInfo;
OpaqueThemeType theme;
bool experimentsEnabled;
HashMap<String, bool> experiments = HashMap.identity();
/// Set the dark theme.
void setDark() {
theme = Opaque.dark;
notifyListeners();
}
/// Set the Light theme.
void setLight() {
theme = Opaque.light;
notifyListeners();
}
/// Get access to the current theme.
OpaqueThemeType current() {
return theme;
}
/// 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) {
print("Settings ${settings}");
switchLocale(Locale(settings["Locale"]));
// Set Theme and notify listeners
if (settings["Theme"] == "light") {
this.setLight();
} else {
this.setDark();
}
// Set Locale and notify listeners
switchLocale(Locale(settings["Locale"]));
// Decide whether to enable Experiments
experimentsEnabled = settings["ExperimentsEnabled"];
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
// Push the experimental settings to Consumers of Settings
notifyListeners();
}
/// Initialize the Package Version information
initPackageInfo() {
PackageInfo.fromPlatform().then((PackageInfo newPackageInfo) {
packageInfo = newPackageInfo;
@ -45,13 +65,42 @@ class Settings extends ChangeNotifier {
});
}
// Switch the Locale of the App
switchLocale(Locale newLocale) {
locale = newLocale;
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);
}
/// Turn off a specific experiment
disableExperiment(String key) {
experiments.update(key, (value) => false, ifAbsent: () => false);
}
/// 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() {
var themeString = "light";
if (theme == Opaque.dark) {
@ -62,8 +111,8 @@ class Settings extends ChangeNotifier {
"Locale": this.locale.languageCode,
"Theme": themeString,
"PreviousPid": -1,
"ExperimentsEnabled": false,
"Experiments": {},
"ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments,
"StateRootPane": 0,
"FirstTime": false
};

View File

@ -359,8 +359,9 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox(
height: 20,
),
Tooltip(message: AppLocalizations.of(context).enterCurrentPasswordForDelete, child:
ElevatedButton.icon(
onPressed: () {
onPressed: checkCurrentPassword() ? null : () {
showAlertDialog(context);
},
style: ElevatedButton.styleFrom(
@ -368,10 +369,11 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
.current()
.defaultButtonColor()),
icon: Icon(Icons.delete_forever),
label: Text(
AppLocalizations.of(context)
.deleteBtn),
)
))
]))
]))))));
});
@ -452,6 +454,12 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
}
}
}
// TODO Stub - wire this into a libCwtch call.
bool checkCurrentPassword() {
return ctrlrOldPass.value.text.isEmpty;
}
}
showAlertDialog(BuildContext context) {
@ -503,3 +511,4 @@ showAlertDialog(BuildContext context) {
},
);
}

View File

@ -9,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
/// Global Settings View provides access to modify all the Globally Relevant Settings including Locale, Theme and Experiments.
class GlobalSettingsView extends StatefulWidget {
@override
_GlobalSettingsViewState createState() => _GlobalSettingsViewState();
@ -24,76 +25,137 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).cwtchSettingsTitle),
title: Text(AppLocalizations
.of(context)
.cwtchSettingsTitle),
),
body: _buildSettingsList(),
);
}
Widget _buildSettingsList() {
return Consumer<Settings>(builder: (context, theme, child) {
return Center(
child: Column(children: [
ListTile(
title: Text(AppLocalizations.of(context).settingLanguage,
style: TextStyle(color: theme.current().mainTextColor())),
leading:
Icon(Icons.language, color: theme.current().mainTextColor()),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String newValue) {
setState(() {
var settings =
Provider.of<Settings>(context, listen: false);
settings.switchLocale(Locale(newValue, ''));
saveSettings(context);
});
},
items: AppLocalizations.supportedLocales
.map<DropdownMenuItem<String>>((Locale value) {
return DropdownMenuItem<String>(
value: value.languageCode,
child: Text(getLanguageFull(context, value.languageCode)),
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context).settingTheme,
style: TextStyle(color: theme.current().mainTextColor())),
value: theme.current() == Opaque.light,
onChanged: (bool value) {
if (value) {
theme.setLight();
} else {
theme.setDark();
}
return Consumer<Settings>(builder: (context, settings, child) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints viewportConstraints) {
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Column(children: [
ListTile(
title: Text(AppLocalizations
.of(context)
.settingLanguage,
style: TextStyle(color: settings.current().mainTextColor())),
leading:
Icon(Icons.language, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: Provider
.of<Settings>(context)
.locale
.languageCode,
onChanged: (String newValue) {
setState(() {
settings.switchLocale(Locale(newValue, ''));
saveSettings(context);
});
},
items: AppLocalizations.supportedLocales
.map<DropdownMenuItem<String>>((Locale value) {
return DropdownMenuItem<String>(
value: value.languageCode,
child: Text(getLanguageFull(context, value.languageCode)),
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations
.of(context)
.settingTheme,
style: TextStyle(color: settings.current().mainTextColor())),
value: settings.current() == Opaque.light,
onChanged: (bool value) {
if (value) {
settings.setLight();
} else {
settings.setDark();
}
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.lightbulb_outline,
color: theme.current().mainTextColor()),
),
AboutListTile(
icon: Icon(Icons.info, color: theme.current().mainTextColor()),
applicationIcon: Padding(
padding: EdgeInsets.all(20),
child: Image(
image: AssetImage("assets/knott.png"),
width: 128,
height: 128,
)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context).version.replaceAll(
"%1",
constructVersionString(
Provider.of<Settings>(context).packageInfo)),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
),
]));
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.lightbulb_outline,
color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations
.of(context)
.experimentsEnabled,
style: TextStyle(color: settings.current().mainTextColor())),
value: settings.experimentsEnabled,
onChanged: (bool value) {
if (value) {
settings.enableExperiments();
} else {
settings.disableExperiments();
}
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.science,
color: settings.current().mainTextColor()),
),
Visibility(visible: settings.experimentsEnabled, child: Column(children: [
SwitchListTile(
title: Text(AppLocalizations
.of(context)
.enableGroups,
style: TextStyle(color: settings.current().mainTextColor())),
value: settings.experiments.containsKey("tapir-groups-experiment") && settings.experiments["tapir-groups-experiment"],
onChanged: (bool value) {
if (value) {
settings.enableExperiment("tapir-groups-experiment");
} else {
settings.disableExperiment("tapir-groups-experiment");
}
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.group_sharp,
color: settings.current().mainTextColor()),
),
],)),
AboutListTile(
icon: Icon(Icons.info, color: settings.current().mainTextColor()),
applicationIcon: Padding(
padding: EdgeInsets.all(20),
child: Image(
image: AssetImage("assets/knott.png"),
width: 128,
height: 128,
)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations
.of(context)
.version
.replaceAll(
"%1",
constructVersionString(
Provider
.of<Settings>(context)
.packageInfo)),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
),
]))));
});
});
}
}
/// Construct a version string from Package Info
String constructVersionString(PackageInfo pinfo) {
if (pinfo == null) {
return "";
@ -101,6 +163,8 @@ String constructVersionString(PackageInfo pinfo) {
return pinfo.version + "." + pinfo.buildNumber;
}
/// A slightly verbose way to extract the full language name from
/// an individual language code. There might be a more efficient way of doing this.
String getLanguageFull(context, String languageCode) {
if (languageCode == "en") {
return AppLocalizations.of(context).localeEn;
@ -123,6 +187,7 @@ String getLanguageFull(context, String languageCode) {
return languageCode;
}
/// Send an UpdateGlobalSettings to the Event Bus
saveSettings(context) {
var settings = Provider.of<Settings>(context, listen: false);
final updateSettingsEvent = {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:provider/provider.dart';
import '../opaque.dart';
// Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../opaque.dart';
import '../settings.dart';
// Provides a styled Label

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../opaque.dart';
import '../settings.dart';
// Provides a styled Password Input Field for use in Form Widgets.

View File

@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../settings.dart';
class ProfileRow extends StatefulWidget {

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../opaque.dart';
import '../settings.dart';
// Provides a styled Text Field for use in Form Widgets.

View File

@ -35,7 +35,6 @@ dependencies:
ffi: ^1.0.0
path_provider: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter