Profile Images
continuous-integration/drone/pr Build is pending
Details
continuous-integration/drone/pr Build is pending
Details
This commit is contained in:
parent
e22db92dc1
commit
3d85883f8e
|
@ -77,7 +77,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val loader = FlutterInjector.instance().flutterLoader()
|
val loader = FlutterInjector.instance().flutterLoader()
|
||||||
val key = loader.getLookupKeyForAsset("assets/" + data.getString("Picture"))//"assets/profiles/001-centaur.png")
|
val key = loader.getLookupKeyForAsset("assets/" + data.getString("picture"))//"assets/profiles/001-centaur.png")
|
||||||
val fh = applicationContext.assets.open(key)
|
val fh = applicationContext.assets.open(key)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ class CwtchNotifier {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"],
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"],
|
||||||
blocked: false, // we created
|
blocked: false, // we created
|
||||||
accepted: true, // we created
|
accepted: true, // we created
|
||||||
imagePath: data["PicturePath"],
|
imagePath: data["picture"],
|
||||||
nickname: data["GroupName"],
|
nickname: data["GroupName"],
|
||||||
status: status,
|
status: status,
|
||||||
server: data["GroupServer"],
|
server: data["GroupServer"],
|
||||||
|
@ -147,7 +147,7 @@ class CwtchNotifier {
|
||||||
var messageID = int.parse(data["Index"]);
|
var messageID = int.parse(data["Index"]);
|
||||||
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
|
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
|
||||||
var senderHandle = data['RemotePeer'];
|
var senderHandle = data['RemotePeer'];
|
||||||
var senderImage = data['Picture'];
|
var senderImage = data['picture'];
|
||||||
var isAuto = data['Auto'] == "true";
|
var isAuto = data['Auto'] == "true";
|
||||||
String? contenthash = data['ContentHash'];
|
String? contenthash = data['ContentHash'];
|
||||||
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
||||||
|
@ -199,7 +199,7 @@ class CwtchNotifier {
|
||||||
if (data["ProfileOnion"] != data["RemotePeer"]) {
|
if (data["ProfileOnion"] != data["RemotePeer"]) {
|
||||||
var idx = int.parse(data["Index"]);
|
var idx = int.parse(data["Index"]);
|
||||||
var senderHandle = data['RemotePeer'];
|
var senderHandle = data['RemotePeer'];
|
||||||
var senderImage = data['Picture'];
|
var senderImage = data['picture'];
|
||||||
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
||||||
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
||||||
var currentTotal = contact!.totalMessages;
|
var currentTotal = contact!.totalMessages;
|
||||||
|
@ -301,7 +301,7 @@ class CwtchNotifier {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
|
||||||
blocked: false, // NewGroup only issued on accepting invite
|
blocked: false, // NewGroup only issued on accepting invite
|
||||||
accepted: true, // NewGroup only issued on accepting invite
|
accepted: true, // NewGroup only issued on accepting invite
|
||||||
imagePath: data["PicturePath"],
|
imagePath: data["picture"],
|
||||||
nickname: groupInvite["GroupName"],
|
nickname: groupInvite["GroupName"],
|
||||||
server: groupInvite["ServerHost"],
|
server: groupInvite["ServerHost"],
|
||||||
status: status,
|
status: status,
|
||||||
|
@ -322,15 +322,28 @@ class CwtchNotifier {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
|
||||||
break;
|
break;
|
||||||
case "NewRetValMessageFromPeer":
|
case "NewRetValMessageFromPeer":
|
||||||
if (data["Path"] == "profile.name") {
|
if (data["Path"] == "profile.name" && data["Exists"] == "true") {
|
||||||
if (data["Data"].toString().trim().length > 0) {
|
if (data["Data"].toString().trim().length > 0) {
|
||||||
// Update locally on the UI...
|
// Update locally on the UI...
|
||||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
|
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (data['Path'] == "profile.picture") {
|
} else if (data['Path'] == "profile.custom-profile-image" && data["Exists"] == "true") {
|
||||||
// Not yet..
|
EnvironmentConfig.debugLog("received ret val of custom profile image: $data");
|
||||||
|
String fileKey = data['Data'];
|
||||||
|
String filePath = data['FilePath'];
|
||||||
|
bool downloaded = data['FileDownloadFinished'] == "true";
|
||||||
|
if (downloaded) {
|
||||||
|
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
|
||||||
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.imagePath = filePath;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
|
||||||
|
if (contact != null) {
|
||||||
|
profileCN.getProfile(data["ProfileOnion"])?.waitForDownloadComplete(contact.identifier, fileKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
|
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
int _unreadMessages = 0;
|
int _unreadMessages = 0;
|
||||||
bool _online = false;
|
bool _online = false;
|
||||||
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||||
|
Map<String, int> _downloadTriggers = Map<String, int>();
|
||||||
|
|
||||||
// assume profiles are encrypted...this will be set to false
|
// assume profiles are encrypted...this will be set to false
|
||||||
// in the constructor if the profile is encrypted with the defacto password.
|
// in the constructor if the profile is encrypted with the defacto password.
|
||||||
|
@ -178,22 +179,14 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
this._contacts.resort();
|
this._contacts.resort();
|
||||||
}
|
}
|
||||||
|
|
||||||
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) {
|
void newMessage(
|
||||||
|
int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) {
|
||||||
if (!selectedProfile) {
|
if (!selectedProfile) {
|
||||||
unreadMessages++;
|
unreadMessages++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
contactList.newMessage(
|
contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
|
||||||
identifier,
|
|
||||||
messageID,
|
|
||||||
timestamp,
|
|
||||||
senderHandle,
|
|
||||||
senderImage,
|
|
||||||
isAuto,
|
|
||||||
data,
|
|
||||||
contenthash,
|
|
||||||
selectedConversation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void downloadInit(String fileKey, int numChunks) {
|
void downloadInit(String fileKey, int numChunks) {
|
||||||
|
@ -232,6 +225,15 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
// so setting numChunks correctly shouldn't matter
|
// so setting numChunks correctly shouldn't matter
|
||||||
this.downloadInit(fileKey, 1);
|
this.downloadInit(fileKey, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the contact with a custom profile image if we are
|
||||||
|
// waiting for one...
|
||||||
|
if (this._downloadTriggers.containsKey(fileKey)) {
|
||||||
|
int identifier = this._downloadTriggers[fileKey]!;
|
||||||
|
this.contactList.getContact(identifier)!.imagePath = finalPath;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// only update if different
|
// only update if different
|
||||||
if (!this._downloads[fileKey]!.complete) {
|
if (!this._downloads[fileKey]!.complete) {
|
||||||
this._downloads[fileKey]!.timeEnd = DateTime.now();
|
this._downloads[fileKey]!.timeEnd = DateTime.now();
|
||||||
|
@ -308,4 +310,9 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
return prettyBytes((bytes / seconds).round()) + "/s";
|
return prettyBytes((bytes / seconds).round()) + "/s";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void waitForDownloadComplete(int identifier, String fileKey) {
|
||||||
|
_downloadTriggers[fileKey] = identifier;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,5 @@ class ProfileListState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
int generateUnreadCount(String selectedProfile) => _profiles.where( (p) => p.onion != selectedProfile ).fold(0, (i, p) => i + p.unreadMessages);
|
int generateUnreadCount(String selectedProfile) => _profiles.where((p) => p.onion != selectedProfile).fold(0, (i, p) => i + p.unreadMessages);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:cwtch/config.dart';
|
import 'package:cwtch/config.dart';
|
||||||
import 'package:cwtch/cwtch/cwtch.dart';
|
import 'package:cwtch/cwtch/cwtch.dart';
|
||||||
|
import 'package:cwtch/models/appstate.dart';
|
||||||
import 'package:cwtch/models/profile.dart';
|
import 'package:cwtch/models/profile.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:cwtch/widgets/buttontextfield.dart';
|
import 'package:cwtch/widgets/buttontextfield.dart';
|
||||||
|
@ -66,6 +68,37 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showFilePicker(BuildContext ctx) async {
|
||||||
|
// only allow one file picker at a time
|
||||||
|
// note: ideally we would destroy file picker when leaving a conversation
|
||||||
|
// but we don't currently have that option.
|
||||||
|
// we need to store AppState in a variable because ctx might be destroyed
|
||||||
|
// while awaiting for pickFiles.
|
||||||
|
var appstate = Provider.of<AppState>(ctx, listen: false);
|
||||||
|
appstate.disableFilePicker = true;
|
||||||
|
// currently lockParentWindow only works on Windows...
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles(lockParentWindow: true);
|
||||||
|
appstate.disableFilePicker = false;
|
||||||
|
if (result != null && result.files.first.path != null) {
|
||||||
|
File file = File(result.files.first.path!);
|
||||||
|
// We have a maximum number of bytes we can represent in terms of
|
||||||
|
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
|
||||||
|
if (file.lengthSync() <= 10737418240) {
|
||||||
|
var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||||
|
// Share this image publically (conversation handle == -1)
|
||||||
|
Provider.of<FlwtchState>(context, listen: false).cwtch.ShareFile(profile, -1, file.path);
|
||||||
|
// update the image cache locally
|
||||||
|
Provider.of<ProfileInfoState>(context, listen: false).imagePath = file.path;
|
||||||
|
} else {
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)!.msgFileTooBig),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(ctx).showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A few implementation notes
|
// A few implementation notes
|
||||||
// We use Visibility to hide optional structures when they are not requested.
|
// We use Visibility to hide optional structures when they are not requested.
|
||||||
// We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together.
|
// We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together.
|
||||||
|
@ -89,14 +122,20 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
|
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
|
||||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||||
ProfileImage(
|
MouseRegion(
|
||||||
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
|
cursor: SystemMouseCursors.click,
|
||||||
diameter: 120,
|
child: GestureDetector(
|
||||||
maskOut: false,
|
onTap: () {
|
||||||
border: theme.theme.portraitOnlineBorderColor,
|
_showFilePicker(context);
|
||||||
badgeTextColor: Colors.red,
|
},
|
||||||
badgeColor: Colors.red,
|
child: ProfileImage(
|
||||||
)
|
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
|
||||||
|
diameter: 120,
|
||||||
|
maskOut: false,
|
||||||
|
border: theme.theme.portraitOnlineBorderColor,
|
||||||
|
badgeTextColor: Colors.red,
|
||||||
|
badgeColor: Colors.red,
|
||||||
|
)))
|
||||||
])),
|
])),
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
|
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
|
||||||
|
|
|
@ -86,11 +86,10 @@ class _ContactsViewState extends State<ContactsView> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
StreamBuilder<bool>(
|
StreamBuilder<bool>(
|
||||||
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
|
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
|
||||||
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
|
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
|
||||||
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "") ;
|
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
|
||||||
|
|
||||||
return Visibility(
|
return Visibility(
|
||||||
visible: unreadCount > 0,
|
visible: unreadCount > 0,
|
||||||
|
|
|
@ -152,6 +152,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
var contact = Provider.of<ContactInfoState>(context);
|
var contact = Provider.of<ContactInfoState>(context);
|
||||||
|
ContactInfoState? sender = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
|
||||||
Widget wdgPortrait = GestureDetector(
|
Widget wdgPortrait = GestureDetector(
|
||||||
onTap: !isGroup
|
onTap: !isGroup
|
||||||
? null
|
? null
|
||||||
|
@ -162,7 +163,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
padding: EdgeInsets.all(4.0),
|
padding: EdgeInsets.all(4.0),
|
||||||
child: ProfileImage(
|
child: ProfileImage(
|
||||||
diameter: 48.0,
|
diameter: 48.0,
|
||||||
imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath,
|
// default to the contact image...otherwise use a derived sender image...
|
||||||
|
imagePath: sender?.imagePath ?? Provider.of<MessageMetadata>(context).senderImage!,
|
||||||
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,
|
||||||
badgeTextColor: Colors.red,
|
badgeTextColor: Colors.red,
|
||||||
badgeColor: Colors.red,
|
badgeColor: Colors.red,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cwtch/themes/opaque.dart';
|
import 'package:cwtch/themes/opaque.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -23,8 +25,11 @@ class ProfileImage extends StatefulWidget {
|
||||||
class _ProfileImageState extends State<ProfileImage> {
|
class _ProfileImageState extends State<ProfileImage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var image = Image(
|
var file = new File(widget.imagePath);
|
||||||
image: AssetImage("assets/" + widget.imagePath),
|
var image = Image.file(
|
||||||
|
file,
|
||||||
|
cacheWidth: 512,
|
||||||
|
cacheHeight: 512,
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
// We need some theme specific blending here...we might want to consider making this a theme level attribute
|
// We need some theme specific blending here...we might want to consider making this a theme level attribute
|
||||||
colorBlendMode: !widget.maskOut
|
colorBlendMode: !widget.maskOut
|
||||||
|
@ -36,6 +41,21 @@ class _ProfileImageState extends State<ProfileImage> {
|
||||||
isAntiAlias: true,
|
isAntiAlias: true,
|
||||||
width: widget.diameter,
|
width: widget.diameter,
|
||||||
height: widget.diameter,
|
height: widget.diameter,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
// on android the above will fail for asset images, in which case try to load them the original way
|
||||||
|
return Image.asset(widget.imagePath,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
// We need some theme specific blending here...we might want to consider making this a theme level attribute
|
||||||
|
colorBlendMode: !widget.maskOut
|
||||||
|
? Provider.of<Settings>(context).theme.mode == mode_dark
|
||||||
|
? BlendMode.softLight
|
||||||
|
: BlendMode.darken
|
||||||
|
: BlendMode.srcOut,
|
||||||
|
color: Provider.of<Settings>(context).theme.portraitBackgroundColor,
|
||||||
|
isAntiAlias: true,
|
||||||
|
width: widget.diameter,
|
||||||
|
height: widget.diameter);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
|
|
|
@ -105,11 +105,12 @@ class _ProfileRowState extends State<ProfileRow> {
|
||||||
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
|
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
|
||||||
Provider.of<ErrorHandler>(context, listen: false).reset();
|
Provider.of<ErrorHandler>(context, listen: false).reset();
|
||||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext bcontext) {
|
||||||
|
var profile = Provider.of<FlwtchState>(bcontext).profs.getProfile(onion)!;
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<ProfileInfoState>(
|
ChangeNotifierProvider<ProfileInfoState>.value(
|
||||||
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted),
|
value: profile,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
builder: (context, widget) => AddEditProfileView(),
|
builder: (context, widget) => AddEditProfileView(),
|
||||||
|
|
|
@ -8,7 +8,8 @@ doNothing(String x) {}
|
||||||
// Provides a styled Text Field for use in Form Widgets.
|
// Provides a styled Text Field for use in Form Widgets.
|
||||||
// Callers must provide a text controller, label helper text and a validator.
|
// Callers must provide a text controller, label helper text and a validator.
|
||||||
class CwtchTextField extends StatefulWidget {
|
class CwtchTextField extends StatefulWidget {
|
||||||
CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key, this.testKey});
|
CwtchTextField(
|
||||||
|
{required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key, this.testKey});
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final FormFieldValidator? validator;
|
final FormFieldValidator? validator;
|
||||||
|
|
Loading…
Reference in New Issue