123 lines
3.7 KiB
Dart
123 lines
3.7 KiB
Dart
import "dart:async";
|
|
import "dart:io";
|
|
|
|
import "package:moxxyv2/xmpp/sasl/errors.dart";
|
|
|
|
import "package:flutter/material.dart";
|
|
import "package:file_picker/file_picker.dart";
|
|
import "package:image_cropping/image_cropping.dart";
|
|
import "package:path_provider/path_provider.dart";
|
|
|
|
/// Shows a dialog asking the user if they are sure that they want to proceed with an
|
|
/// action.
|
|
Future<void> showConfirmationDialog(String title, String body, BuildContext context, void Function() callback) async {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(title),
|
|
content: Text(body),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text("Yes"),
|
|
onPressed: callback
|
|
),
|
|
TextButton(
|
|
child: const Text("No"),
|
|
onPressed: Navigator.of(context).pop
|
|
)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
/// Shows a dialog telling the user that the [feature] feature is not implemented.
|
|
Future<void> showNotImplementedDialog(String feature, BuildContext context) async {
|
|
return showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text("Not Implemented"),
|
|
content: SingleChildScrollView(
|
|
child: ListBody(
|
|
children: [
|
|
Text("The $feature feature is not yet implemented.")
|
|
]
|
|
)
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text("Okay"),
|
|
onPressed: () => Navigator.of(context).pop()
|
|
)
|
|
]
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
/// Dismissed the softkeyboard.
|
|
void dismissSoftKeyboard(BuildContext context) {
|
|
// NOTE: Thank you, https://flutterigniter.com/dismiss-keyboard-form-lose-focus/
|
|
FocusScopeNode current = FocusScope.of(context);
|
|
if (!current.hasPrimaryFocus) {
|
|
current.unfocus();
|
|
}
|
|
}
|
|
|
|
/// Open the file picker to pick an image and open the cropping tool.
|
|
/// The Future either resolves to null if the user cancels the action or
|
|
/// the actual image data.
|
|
Future<dynamic> pickAndCropImage(BuildContext context) async {
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
allowMultiple: false,
|
|
type: FileType.image,
|
|
withData: true
|
|
);
|
|
|
|
if (result != null) {
|
|
Completer completer = Completer();
|
|
ImageCropping.cropImage(
|
|
context: context,
|
|
imageBytes: result.files.single.bytes!,
|
|
onImageDoneListener: (data) => completer.complete(data),
|
|
selectedImageRatio: ImageRatio.RATIO_1_1
|
|
);
|
|
return completer.future;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Open the file picker to pick an image, open the cropping tool and then send it to
|
|
/// the backend. [setAvatarUrl] is the function to mutate the state and set the avatar.
|
|
/// [avatarUrl] is the path of the old avatar or "" if none has been set.
|
|
Future<void> pickAndSetAvatar(BuildContext context, void Function(String) setAvatarUrl, String avatarUrl) async {
|
|
final data = await pickAndCropImage(context);
|
|
|
|
if (data != null) {
|
|
String cacheDir = (await getApplicationDocumentsDirectory()).path;
|
|
Directory accountDir = Directory(cacheDir + "/account");
|
|
await accountDir.create();
|
|
|
|
File oldAvatar = File(avatarUrl);
|
|
if (await oldAvatar.exists()) await oldAvatar.delete();
|
|
|
|
File avatar = File(accountDir.path + "/avatar.png");
|
|
await avatar.writeAsBytes(data);
|
|
|
|
// TODO: If the path doesn't change then the UI won't be updated. Hash it and use that as the filename?
|
|
setAvatarUrl(avatar.path);
|
|
}
|
|
}
|
|
|
|
/// Turn the SASL error into a string that a regular user could understand.
|
|
String saslErrorToHumanReadable(String saslError) {
|
|
switch (saslError) {
|
|
case saslErrorNotAuthorized: return "Wrong XMPP address or password";
|
|
}
|
|
|
|
return "SASL error: " + saslError;
|
|
}
|