Drop the DiceBear generated avatar (and dicebear_core/dicebear_styles/ flutter_svg): the default is again a Material coloured-initial disc from the pubkey. Picking a photo now goes through a square crop step (crop_your_image — pure Flutter, all platforms incl. desktop, Apache-2.0, only pulls the image package we already had) before the 24 KB thumbnail.
87 lines
2.5 KiB
Dart
87 lines
2.5 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:crop_your_image/crop_your_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'theme.dart';
|
|
|
|
/// Lets the user square-crop the picked [bytes] before it's saved as an avatar.
|
|
/// Returns the cropped image bytes, or null if cancelled. Pure Flutter (works on
|
|
/// every platform, desktop included) — no native cropper, no plaintext on disk.
|
|
Future<Uint8List?> cropToSquare(BuildContext context, Uint8List bytes) {
|
|
return Navigator.of(context).push<Uint8List>(
|
|
MaterialPageRoute(
|
|
fullscreenDialog: true,
|
|
builder: (context) => _CropPage(bytes: bytes),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _CropPage extends StatefulWidget {
|
|
const _CropPage({required this.bytes});
|
|
|
|
final Uint8List bytes;
|
|
|
|
@override
|
|
State<_CropPage> createState() => _CropPageState();
|
|
}
|
|
|
|
class _CropPageState extends State<_CropPage> {
|
|
final _controller = CropController();
|
|
var _cropping = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.black,
|
|
foregroundColor: Colors.white,
|
|
leading: IconButton(
|
|
key: const Key('crop.cancel'),
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
actions: [
|
|
if (_cropping)
|
|
const Padding(
|
|
padding: EdgeInsets.all(14),
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
)
|
|
else
|
|
IconButton(
|
|
key: const Key('crop.confirm'),
|
|
icon: const Icon(Icons.check),
|
|
onPressed: () {
|
|
setState(() => _cropping = true);
|
|
_controller.crop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: Crop(
|
|
image: widget.bytes,
|
|
controller: _controller,
|
|
aspectRatio: 1,
|
|
withCircleUi: true,
|
|
baseColor: Colors.black,
|
|
maskColor: Colors.black.withValues(alpha: 0.6),
|
|
cornerDotBuilder: (size, edgeAlignment) =>
|
|
const DotControl(color: seedGreen),
|
|
onCropped: (result) {
|
|
if (!mounted) return;
|
|
switch (result) {
|
|
case CropSuccess(:final croppedImage):
|
|
Navigator.of(context).pop(croppedImage);
|
|
case CropFailure():
|
|
setState(() => _cropping = false);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|