Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into integtests
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
erinn 2021-04-20 17:24:37 -07:00
commit 5a9621f562
36 changed files with 748 additions and 106 deletions

View File

@ -144,17 +144,37 @@ steps:
image: openpriv/flutter-desktop:windows-dev
commands:
- git fetch --tags
- git describe --tags > VERSION
- date +%G-%m-%d-%H-%M > BUILDDATE
- powershell -command "git describe --tags > VERSION"
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm' >.\BUILDDATE"
- name: build-windows
image: openpriv/flutter-desktop:windows-dev
commands:
- flutter pub get
- mkdir deploy
- flutter build windows
# flwtch-`cat VERSION`-`cat BUILDDATE`
- $Env:builddir = 'deploy\flwtch-win-'
- $Env:builddir += type .\VERSION
- $Env:builddir += '-'
- $Env:builddir += type .\BUILDDATE
- mkdir deploy
- move windows/runner/Release/ $Env:builddir
- name: deploy-windows
image: appleboy/drone-scp:v1.4.0-windows
when:
event: push
status: [ success ]
settings:
host: openprivacy.ca
username: buildfiles
key:
from_secret: buildfiles_key
port: 22
target: /home/buildfiles/buildfiles/
#/var/www/deploy/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME}
source: deploy/*
trigger:
repo: flutter/flutter_app
branch: trunk
event:
- push
- push

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ libCwtch.so
android/cwtch/cwtch.aar
coverage
test/failures
.gradle

View File

@ -157,6 +157,9 @@ class MainActivity: FlutterActivity() {
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendAppEvent(jsonEvent);
}
"ResetTor" -> {
Cwtch.resetTor();
}
else -> result.notImplemented()
}
}

Binary file not shown.

BIN
assets/core/Tor_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

58
assets/core/Tor_icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -1,13 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
</style>
<path class="st0" d="M0,0h24v24H0V0z"/>
<path id="Subtraction_1" d="M18.5,16L18.5,16L5.3,4.4C6.3,3.5,7.6,3,9,3c1.7,0,3.3,0.7,4.4,2c1.1-1.3,2.7-2,4.4-2
C20.6,3,23,5.3,23,8.2c0,0,0,0.1,0,0.1c0,0.6-0.1,1.3-0.3,1.9c-0.2,0.7-0.5,1.3-0.9,1.9C21.1,13.2,20.1,14.4,18.5,16L18.5,16z"/>
<path d="M20.2,18.6L2.3,3.1L1,4.6l2.6,2.2C3.2,7.5,3,8.4,3,9.2c0,3.7,3.3,6.6,8.3,11.2l1.4,1.3l1.4-1.3c0.9-0.8,1.7-1.6,2.5-2.3
l2.3,2L20.2,18.6z"/>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 24 24"
style="enable-background:new 0 0 24 24;"
xml:space="preserve"
sodipodi:docname="negative_heart_24px.svg"
inkscape:export-filename="/home/sarah/AndroidStudioProjects/flutter_app/assets/core/negative_heart_512px.png"
inkscape:export-xdpi="4096"
inkscape:export-ydpi="4096"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1015"
id="namedview10"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="14.687942"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:none;}
</style>
<g
id="g824"
transform="translate(4.0677965,4.0677965)"><path
style="fill:none;stroke-width:0.66101694"
inkscape:connector-curvature="0"
id="path4"
d="M 0,0 H 15.864407 V 15.864407 H 0 Z"
class="st0" /><path
style="stroke-width:0.66101694"
inkscape:connector-curvature="0"
d="m 12.228814,10.576271 v 0 L 3.5033898,2.9084746 c 0.661017,-0.5949153 1.520339,-0.9254238 2.4457627,-0.9254238 1.1237289,0 2.181356,0.4627119 2.9084746,1.3220339 0.7271187,-0.859322 1.7847459,-1.3220339 2.9084749,-1.3220339 1.850847,0 3.437288,1.520339 3.437288,3.4372882 0,0 0,0.066102 0,0.066102 0,0.3966101 -0.0661,0.859322 -0.198305,1.2559322 -0.132204,0.4627118 -0.330509,0.859322 -0.594916,1.2559322 -0.462711,0.7271186 -1.123728,1.520339 -2.181355,2.5779656 z"
id="Subtraction_1" /><path
style="stroke-width:0.66101694"
inkscape:connector-curvature="0"
id="path7"
d="M 13.352542,12.294915 1.520339,2.0491525 0.66101695,3.040678 2.379661,4.4949153 C 2.1152542,4.9576271 1.9830508,5.5525424 1.9830508,6.0813559 c 0,2.4457627 2.181356,4.3627121 5.4864407,7.4033901 l 0.9254238,0.859322 0.9254237,-0.859322 c 0.5949152,-0.528814 1.123729,-1.057627 1.652542,-1.520339 l 1.520339,1.322034 z" /></g>
</svg>

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -9,6 +9,9 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
void LoadProfiles(String pass);
// ignore: non_constant_identifier_names
void ResetTor();
// todo: remove these
// ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String jsonEvent);

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter_app/torstatus.dart';
import '../errorHandler.dart';
import '../model.dart';
import '../settings.dart';
@ -11,17 +13,20 @@ class CwtchNotifier {
ProfileListState profileCN;
Settings settings;
ErrorHandler error;
TorStatus torStatus;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN) {
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN) {
profileCN = pcn;
settings = settingsCN;
error = errorCN;
torStatus = torStatusCN;
}
void handleMessage(String type, dynamic data) {
switch (type) {
case "NewPeer":
profileCN.add(ProfileInfoState(onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], online: data["Online"] == "true"));
profileCN.add(ProfileInfoState(
onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], serversJson: data["ServerList"], online: data["Online"] == "true"));
break;
case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(
@ -35,7 +40,8 @@ class CwtchNotifier {
savePeerHistory: data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]),
lastMessageTime: DateTime.now(),//show at the top of the contact list even if no messages yet
isGroup: data["isGroup"],
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
));
break;
case "PeerStateChange":
@ -75,6 +81,10 @@ class CwtchNotifier {
break;
case "ACNStatus":
print("acn status: $data");
torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
break;
case "UpdateServerInfo":
profileCN.getProfile(data["ProfileOnion"]).replaceServers(data["ServerList"]);
break;
default:
print("unhandled event: $type");

View File

@ -297,4 +297,11 @@ class CwtchFfi implements Cwtch {
final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length);
}
@override
void ResetTor() {
var resetTor = library.lookup<NativeFunction<Void Function()>>("c_ResetTor");
final ResetTor = resetTor.asFunction<void Function()>();
ResetTor();
}
}

View File

@ -142,4 +142,10 @@ class CwtchGomobile implements Cwtch {
void SendMessage(String profileOnion, String contactHandle, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message});
}
@override
// ignore: non_constant_identifier_names
void ResetTor() {
cwtchPlatform.invokeMethod("ResetTor", {});
}
}

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Tolle Gruppe",
"defaultProfileName": "Alice",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
@ -123,10 +134,12 @@
"settingLanguage": "Sprache",
"settingTheme": "Thema",
"smallTextLabel": "Klein",
"successfullAddedContact": "",
"themeDark": "Dunkel",
"themeLight": "Licht",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "Titel...",
"todoPlaceholder": "noch zu erledigen",
"tooltipAddContact": "",

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "Click to cycle colours.\\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\\nRight-click to reset.",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateLastMonth": "Last Month",
"dateLastYear": "Last Year",
"dateMinutesAgo": "Minutes Ago",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateRightNow": "Right Now",
"dateWeeksAgo": "Weeks Ago",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateYesterday": "Yesterday",
"defaultGroupName": "Awesome Group",
"defaultProfileName": "Alice",
"defaultScalingText": "Default size text (scale factor:",
@ -123,10 +134,12 @@
"settingLanguage": "Language",
"settingTheme": "Theme",
"smallTextLabel": "Small",
"successfullAddedContact": "Successfully added ",
"themeDark": "Dark",
"themeLight": "Light",
"titleManageContacts": "Manage Contacts",
"titleManageProfiles": "Manage Cwtch Profiles",
"titleManageServers": "Manage Servers",
"titlePlaceholder": "title...",
"todoPlaceholder": "Todo...",
"tooltipAddContact": "Add a new contact",

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "El Grupo Asombroso",
"defaultProfileName": "Alicia",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
@ -123,10 +134,12 @@
"settingLanguage": "Idioma",
"settingTheme": "Tema",
"smallTextLabel": "Pequeño",
"successfullAddedContact": "",
"themeDark": "Oscuro",
"themeLight": "Claro",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "título...",
"todoPlaceholder": "Por hacer...",
"tooltipAddContact": "",

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Un super groupe",
"defaultProfileName": "",
"defaultScalingText": "Taille par défaut du texte (échelle:",
@ -123,10 +134,12 @@
"settingLanguage": "",
"settingTheme": "",
"smallTextLabel": "Petit",
"successfullAddedContact": "",
"themeDark": "",
"themeLight": "",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "titre...",
"todoPlaceholder": "A faire...",
"tooltipAddContact": "",

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "Fare clic per scorrere i colori.\\nCliccare con il tasto destro per resettare.",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\\nCliccare con il tasto destro per resettare.",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Gruppo fantastico",
"defaultProfileName": "Alice",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
@ -123,10 +134,12 @@
"settingLanguage": "Lingua",
"settingTheme": "Tema",
"smallTextLabel": "Piccolo",
"successfullAddedContact": "",
"themeDark": "Scuro",
"themeLight": "Chiaro",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "titolo...",
"todoPlaceholder": "Da fare...",
"tooltipAddContact": "",

View File

@ -36,6 +36,17 @@
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Grupo incrível",
"defaultProfileName": "",
"defaultScalingText": "Texto tamanho padrão (fator de escala: ",
@ -123,10 +134,12 @@
"settingLanguage": "",
"settingTheme": "",
"smallTextLabel": "Pequeno",
"successfullAddedContact": "",
"themeDark": "",
"themeLight": "",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "título…",
"todoPlaceholder": "Afazer…",
"tooltipAddContact": "",

View File

@ -4,6 +4,7 @@ import 'package:flutter_app/cwtch/gomobile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/errorHandler.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/torstatus.dart';
import 'package:flutter_app/views/triplecolview.dart';
import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart';
@ -18,6 +19,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var globalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus();
void main() {
LicenseRegistry.addLicense(() => licenses());
@ -48,7 +50,7 @@ class FlwtchState extends State<Flwtch> {
cwtchInit = false;
profs = ProfileListState();
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus);
if (Platform.isAndroid) {
cwtch = CwtchGomobile(cwtchNotifier);
@ -65,6 +67,7 @@ class FlwtchState extends State<Flwtch> {
appStatus = AppModel(cwtch: cwtch);
}
ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus);
ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler);
ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings);
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
@ -75,7 +78,7 @@ class FlwtchState extends State<Flwtch> {
//appStatus = AppModel(cwtch: cwtch);
return MultiProvider(
providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider()],
providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider()],
builder: (context, widget) {
Provider.of<Settings>(context).initPackageInfo();
return Consumer<Settings>(

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter_app/models/servers.dart';
import 'package:provider/provider.dart';
import 'dart:async';
import 'dart:collection';
@ -34,16 +35,6 @@ class ContactModel {
ContactModel({this.onion, this.nickname, this.status, this.isInvitation, this.isBlocked, this.imagePath});
}
//todo: delete
class DanMessageModel {
// ignore: non_constant_identifier_names
String Timestamp;
// ignore: non_constant_identifier_names
bool Acknowledged;
// ignore: non_constant_identifier_names
String Message;
}
class ChatMessage {
final int o;
final String d;
@ -123,6 +114,7 @@ class ContactListState extends ChangeNotifier {
class ProfileInfoState extends ChangeNotifier {
ContactListState _contacts = ContactListState();
ServerListState _servers = ServerListState();
final String onion;
String _nickname = "";
String _imagePath = "";
@ -135,6 +127,7 @@ class ProfileInfoState extends ChangeNotifier {
imagePath = "",
unreadMessages = 0,
contactsJson = "",
serversJson = "",
online = false,
}) {
this._nickname = nickname;
@ -164,6 +157,20 @@ class ProfileInfoState extends ChangeNotifier {
this._contacts.updateLastMessageTime(this._contacts._contacts.first.onion, this._contacts._contacts.first.lastMessageTime);
}
}
this.replaceServers(serversJson);
}
// Parse out the server list json into our server info state struct...
void replaceServers(String serversJson) {
if (serversJson != null && serversJson != "" && serversJson != "null") {
print("got servers $serversJson");
List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) {
// TODO Keys...
return ServerInfoState(onion: server["onion"], status: server["status"]);
}));
}
}
// Getters and Setters for Online Status
@ -192,6 +199,7 @@ class ProfileInfoState extends ChangeNotifier {
}
ContactListState get contactList => this._contacts;
ServerListState get serverList => this._servers;
@override
void dispose() {

26
lib/models/servers.dart Normal file
View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class ServerListState extends ChangeNotifier {
List<ServerInfoState> _servers = [];
void replace(Iterable<ServerInfoState> newServers) {
_servers.clear();
_servers.addAll(newServers);
notifyListeners();
}
ServerInfoState getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
List<ServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}
class ServerInfoState extends ChangeNotifier {
final String onion;
final String status;
ServerInfoState({this.onion, this.status});
}

21
lib/torstatus.dart Normal file
View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class TorStatus extends ChangeNotifier {
int progress;
String status;
bool connected;
/// Called by the event bus.
handleUpdate(int new_progress, String new_status) {
if (progress == 100) {
connected = true;
} else {
connected = false;
}
progress = new_progress;
status = new_status;
notifyListeners();
}
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_app/errorHandler.dart';
import 'package:flutter_app/models/servers.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/widgets/buttontextfield.dart';
import 'package:flutter_app/widgets/cwtchlabel.dart';
@ -24,8 +25,11 @@ class AddContactView extends StatefulWidget {
class _AddContactViewState extends State<AddContactView> {
final _formKey = GlobalKey<FormState>();
final _createGroupFormKey = GlobalKey<FormState>();
final ctrlrOnion = TextEditingController(text: "");
final ctrlrContact = TextEditingController(text: "");
final ctrlrGroupName = TextEditingController(text: "");
String server = "";
@override
Widget build(BuildContext context) {
@ -49,7 +53,7 @@ class _AddContactViewState extends State<AddContactView> {
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded(
child: TabBarView(
children: (groupsEnabled ? [addPeerTab(), addGroupTab(), joinGroupTab()] : [addPeerTab()]),
children: (groupsEnabled ? [addPeerTab(), manageServersTab(), addGroupTab(), joinGroupTab()] : [addPeerTab()]),
)),
]));
});
@ -57,7 +61,8 @@ class _AddContactViewState extends State<AddContactView> {
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
// TODO Toast
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
/// A Tab Bar with only the Add Peer Tab
@ -80,6 +85,7 @@ class _AddContactViewState extends State<AddContactView> {
icon: Icon(Icons.person_add_rounded),
text: AppLocalizations.of(context).addPeer,
),
Tab(icon: Icon(Icons.backup), text: AppLocalizations.of(context).titleManageServers),
Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context).createGroup),
Tab(icon: Icon(Icons.group_add), text: AppLocalizations.of(context).joinGroup),
],
@ -137,6 +143,8 @@ class _AddContactViewState extends State<AddContactView> {
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.explicitAddContactSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + peerAddr));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
}
});
@ -147,11 +155,78 @@ class _AddContactViewState extends State<AddContactView> {
/// TODO Add Group Pane
Widget addGroupTab() {
return Icon(Icons.group_add);
// TODO We should replace with with a "Paste in Server Key Bundle"
if (Provider.of<ProfileInfoState>(context).serverList.servers.isEmpty) {
return Text("You need to add a server before you can create a group.");
}
// if we haven't picked a server yet, pick the first one in the list...
if (server.isEmpty) {
server = Provider.of<ProfileInfoState>(context).serverList.servers.first.onion;
}
return Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Form(
autovalidateMode: AutovalidateMode.always,
key: _createGroupFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CwtchLabel(label: AppLocalizations.of(context).server),
SizedBox(
height: 20,
),
DropdownButton(
onChanged: (newServer) {
server = newServer;
},
value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(serverInfo.onion),
);
}).toList()),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).groupName),
SizedBox(
height: 20,
),
CwtchTextField(controller: ctrlrGroupName, labelText: AppLocalizations.of(context).groupNameLabel, onChanged: (newValue) {}),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {},
child: Text(AppLocalizations.of(context).createGroupBtn),
),
],
)));
}
/// TODO Join Group Pane
Widget joinGroupTab() {
return Icon(Icons.group);
}
/// TODO Manage Servers Tab
Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
value: server,
child: ListTile(
title: Text(server.onion),
));
});
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return ListView(children: divided);
}
}

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../settings.dart';
import '../widgets/messagelist.dart';
class MessageView extends StatefulWidget {
@ -87,7 +88,7 @@ class _MessageViewState extends State<MessageView> {
Widget _buildComposeBox() {
return Container(
color: Opaque.current().backgroundMainColor(),
color: Provider.of<Settings>(context).theme.backgroundMainColor(),
height: 100,
padding: EdgeInsets.all(8.0),
child: Row(
@ -107,10 +108,10 @@ class _MessageViewState extends State<MessageView> {
Padding(
padding: EdgeInsets.fromLTRB(2, 2, 2, 2),
child: ElevatedButton(
child: Icon(Icons.send, color: Opaque.current().mainTextColor()),
child: Icon(Icons.send, color: Provider.of<Settings>(context).theme.mainTextColor()),
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(86, 40)),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()),
),
onPressed: _sendMessage,
)),
@ -120,10 +121,10 @@ class _MessageViewState extends State<MessageView> {
child: SizedBox(
width: 41,
child: ElevatedButton(
child: Icon(Icons.emoji_emotions_outlined, color: Opaque.current().mainTextColor()),
child: Icon(Icons.emoji_emotions_outlined, color: Provider.of<Settings>(context).theme.mainTextColor()),
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(41, 40)),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()),
),
onPressed: placeHolder,
))),
@ -132,10 +133,10 @@ class _MessageViewState extends State<MessageView> {
child: SizedBox(
width: 41,
child: ElevatedButton(
child: Icon(Icons.attach_file, color: Opaque.current().mainTextColor()),
child: Icon(Icons.attach_file, color: Provider.of<Settings>(context).theme.mainTextColor()),
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(41, 40)),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()),
),
onPressed: placeHolder,
))),

View File

@ -2,6 +2,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/torstatus.dart';
import 'package:flutter_app/views/torstatusview.dart';
import 'package:flutter_app/widgets/passwordfield.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_app/widgets/profilerow.dart';
@ -34,6 +36,15 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
appBar: AppBar(
title: Text(AppLocalizations.of(context).titleManageProfiles),
actions: [
IconButton(
icon: Image(
image: AssetImage(Provider.of<TorStatus>(context).progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_icon_error.png"),
filterQuality: FilterQuality.low,
isAntiAlias: false,
color: Provider.of<Settings>(context).theme.mainTextColor(),
colorBlendMode: BlendMode.srcIn,
),
onPressed: _pushTorStatus),
IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _setLoggingLevelDebug),
IconButton(
icon: Icon(Icons.lock_open),
@ -75,6 +86,17 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushTorStatus() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
child: TorStatusView(),
);
},
));
}
void _pushAddEditProfile({onion: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/torstatus.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
/// Tor Status View provides all info on Tor network state and the (future) ability to configure the network in a variety
/// of ways (restart, enable bridges, enable pluggable transports etc)
class TorStatusView extends StatefulWidget {
@override
_TorStatusView createState() => _TorStatusView();
}
class _TorStatusView extends State<TorStatusView> {
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tor Network Status"),
),
body: _buildSettingsList(),
);
}
Widget _buildSettingsList() {
return Consumer<TorStatus>(builder: (context, torStatus, 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(
leading: Image(
image: AssetImage(torStatus.progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_icon_error.png"),
filterQuality: FilterQuality.low,
isAntiAlias: false,
// Color the onion per the text color...
color: Provider.of<Settings>(context).theme.mainTextColor(),
colorBlendMode: BlendMode.srcIn,
),
title: Text("Tor Status"),
subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context).networkStatusOnline : torStatus.status),
trailing: ElevatedButton(
child: Text("Reset"),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ResetTor();
},
),
)
]))));
});
});
}
}

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_app/views/messageview.dart';
import 'package:flutter_app/widgets/profileimage.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../settings.dart';
import 'package:intl/intl.dart';
class ContactRow extends StatefulWidget {
@override
@ -29,21 +30,24 @@ class _ContactRowState extends State<ContactRow> {
badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor(),
diameter: 64.0,
imagePath: contact.imagePath,
maskOut: contact.status != "Authenticated",
maskOut: contact.isGroup ? false : contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
),
Expanded(
child: Column(
children: [
Text(
contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),//
style: Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.visible,
),
Text(contact.status),
],
)),
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),//
style: Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.visible,
),
Text(contact.onion),
],
))),
Padding(
padding: const EdgeInsets.all(5.0),
child: contact.isInvitation != null && contact.isInvitation
@ -51,13 +55,13 @@ class _ContactRowState extends State<ContactRow> {
IconButton(
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Icons.favorite, color: Opaque.current().mainTextColor()),
icon: Icon(Icons.favorite, color: Provider.of<Settings>(context).theme.mainTextColor()),
onPressed: _btnApprove,
),
IconButton(
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Icons.delete, color: Opaque.current().mainTextColor()),
icon: Icon(Icons.delete, color: Provider.of<Settings>(context).theme.mainTextColor()),
onPressed: _btnReject,
)
])
@ -65,10 +69,10 @@ class _ContactRowState extends State<ContactRow> {
? IconButton(
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Icons.block, color: Opaque.current().mainTextColor()),
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor()),
onPressed: () {},
)
: Text(contact.unreadMessages.toString())),
: Text(dateToNiceString(contact.lastMessageTime))),
),
]),
onTap: () {
@ -109,4 +113,11 @@ class _ContactRowState extends State<ContactRow> {
.cwtch
.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
}
String dateToNiceString(DateTime date) {
if (date.millisecondsSinceEpoch == 0) {
return AppLocalizations.of(context).dateNever;
}
return DateFormat.yMd().add_jm().format(date.toLocal());
}
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import '../opaque.dart';
import 'package:intl/intl.dart';
import '../settings.dart';
class MessageBubble extends StatefulWidget {
@override
_MessageBubbleState createState() => _MessageBubbleState();
@ -21,28 +22,40 @@ class _MessageBubbleState extends State<MessageBubble> {
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
}
return Container(
decoration: BoxDecoration(
color: fromMe ? Opaque.current().messageFromMeBackgroundColor() : Opaque.current().messageFromOtherBackgroundColor(),
border: Border.all(color: fromMe ? Opaque.current().messageFromMeBackgroundColor() : Opaque.current().messageFromOtherBackgroundColor(), width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
child: Padding(padding: EdgeInsets.all(9.0), child:Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [SelectableText(
Provider.of<MessageState>(context).message,
textAlign: TextAlign.left,
),
Row(
children: [
Text(prettyDate, style: TextStyle(fontSize: 9.0), textAlign: fromMe ? TextAlign.right : TextAlign.left),
Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Opaque.current().mainTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Opaque.current().mainTextColor(), size: 12)
],
)])),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [
SelectableText(
Provider.of<MessageState>(context).message,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
),
Row(
children: [
Text(prettyDate,
style: TextStyle(
fontSize: 9.0,
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
],
)
])),
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'messagebubble.dart';
import '../settings.dart';
import 'messagerow.dart';
class MessageList extends StatefulWidget {
@ -43,22 +43,31 @@ class _MessageListState extends State<MessageList> {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom(false));
return Card(
child: Scrollbar(
isAlwaysShown: true,
controller: ctrlr1,
child: ListView.builder(
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(context).totalMessages,
itemBuilder: (context, index) {
return ChangeNotifierProvider(
create: (_) => MessageState(
context: context,
profileOnion: Provider.of<ProfileInfoState>(outerContext).onion,
contactHandle: Provider.of<ContactInfoState>(outerContext).onion,
messageIndex: index,
),
child: MessageRow());
},
),
));
isAlwaysShown: true,
controller: ctrlr1,
child: Container(
// Only show broken heart is the contact is offline...
decoration: Provider.of<ContactInfoState>(outerContext).status == "Authenticated"
? null
: BoxDecoration(
image: DecorationImage(
fit: BoxFit.contain,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
child: ListView.builder(
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(context).totalMessages,
itemBuilder: (context, index) {
return ChangeNotifierProvider(
create: (_) => MessageState(
context: context,
profileOnion: Provider.of<ProfileInfoState>(outerContext).onion,
contactHandle: Provider.of<ContactInfoState>(outerContext).onion,
messageIndex: index,
),
child: MessageRow());
},
),
)));
}
}

View File

@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../settings.dart';
import 'messagebubble.dart';
@ -20,8 +19,8 @@ class _MessageRowState extends State<MessageRow> {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
Widget wdgBubble = MessageBubble();
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Opaque.current().dropShadowColor());
Widget wdgSpacer = Expanded(child:SizedBox(width: 60, height: 10));
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor());
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[];
if (fromMe) {
@ -36,8 +35,7 @@ class _MessageRowState extends State<MessageRow> {
diameter: 48.0,
imagePath: contact.imagePath,
maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()
);
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor());
widgetRow = <Widget>[
wdgPortrait,

View File

@ -142,7 +142,7 @@ packages:
name: integration_test
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+2"
version: "1.0.2+3"
intl:
dependency: transitive
description:

View File

@ -84,7 +84,9 @@ flutter:
assets:
- assets/
- assets/core/
- assets/profiles/
- assets/servers/
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a

BIN
test/profileimage_init.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,72 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_app/opaque.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/widgets/cwtchlabel.dart';
import 'package:flutter_app/widgets/profileimage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
String file(String slug) {
return "profileimage_" + slug + ".png";
}
void main() {
testWidgets('ProfileImage widget test', (WidgetTester tester) async {
tester.binding.window.physicalSizeTestValue = Size(200, 200);
// await tester.pumpWidget(MultiProvider(
// providers:[getSettingsEnglishDark()],
// child: Directionality(textDirection: TextDirection.ltr, child: CwtchLabel(label: testingStr))
// ));
Widget testWidget = ProfileImage(
imagePath: "profiles/001-centaur.png",
badgeTextColor: settingsEnglishDark.theme.portraitProfileBadgeTextColor(),
badgeColor: settingsEnglishDark.theme.portraitProfileBadgeColor(),
maskOut: false,
border: settingsEnglishDark.theme.portraitOfflineBorderColor(),
diameter: 64.0,
badgeCount: 10,
);
Widget testHarness = MultiProvider(
providers:[getSettingsEnglishDark()],
builder: (context, child) { return MaterialApp(
locale: Provider.of<Settings>(context).locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Test',
theme: mkThemeData(Provider.of<Settings>(context)),
home: Card(child: testWidget),
);}
);
// Verify that our counter starts at 0.
//expect(find.text(testingStr), findsOneWidget);
//expect(find.text('1'), findsNothing);
await tester.pumpWidget(testHarness);
await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('init')));
// Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
//
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
});
}