tane/apps/app_seeds/lib/ui/photo_crop.dart
vjrj f6967e8cbb feat(profile): Material default avatar + square photo crop
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.
2026-07-12 23:39:56 +02:00

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);
}
},
),
);
}
}