mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Summary - add inline profile photo capture with smooth avatar-to-viewfinder transitions - add close, flip, shutter, retry, and use-photo states with haptics and front-camera mirroring - polish iOS liquid-glass controls and avatar editor motion ### Related issue Follow-up to #6583. ### Testing - just mobile-check - just mobile-test (1,743 tests) - built, installed, and launched on Pixel 10 and iPhone Air --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
133 lines
4.4 KiB
Dart
133 lines
4.4 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import '../../shared/animated_avatar.dart';
|
|
import '../../shared/relay/relay.dart';
|
|
|
|
/// A prepared profile-avatar change that is uploaded only when the user saves.
|
|
sealed class ProfileAvatarDraft {
|
|
/// Creates a prepared profile-avatar draft.
|
|
const ProfileAvatarDraft();
|
|
|
|
/// Returns the avatar URL for this draft using [service] when upload is
|
|
/// required.
|
|
///
|
|
/// Implementations cache successful uploads for the same service so a
|
|
/// profile-publish retry does not create duplicate media. Failed uploads may
|
|
/// be retried, and changing services starts a new upload for that community.
|
|
Future<String> upload(MediaUploadService service);
|
|
}
|
|
|
|
/// An avatar draft that already has its final URL and needs no media upload.
|
|
final class ProfileUrlAvatarDraft extends ProfileAvatarDraft {
|
|
/// Creates a draft backed by [url].
|
|
const ProfileUrlAvatarDraft(this.url);
|
|
|
|
/// The URL that will be written to the profile.
|
|
final String url;
|
|
|
|
@override
|
|
Future<String> upload(MediaUploadService service) async => url;
|
|
}
|
|
|
|
/// A locally prepared still image awaiting upload on Save.
|
|
final class ProfileImageAvatarDraft extends ProfileAvatarDraft {
|
|
/// Creates a still-image draft from JPEG [bytes].
|
|
ProfileImageAvatarDraft(this.bytes);
|
|
|
|
/// The prepared JPEG payload.
|
|
final Uint8List bytes;
|
|
MediaUploadService? _uploadService;
|
|
Future<String>? _uploadedUrl;
|
|
|
|
@override
|
|
Future<String> upload(MediaUploadService service) async {
|
|
if (!identical(_uploadService, service)) {
|
|
_uploadService = service;
|
|
_uploadedUrl = null;
|
|
}
|
|
final existing = _uploadedUrl;
|
|
if (existing != null) return existing;
|
|
final upload = service
|
|
.uploadBytes(bytes, mimeType: 'image/jpeg')
|
|
.then((descriptor) => descriptor.url);
|
|
_uploadedUrl = upload;
|
|
try {
|
|
return await upload;
|
|
} catch (_) {
|
|
if (identical(_uploadedUrl, upload)) _uploadedUrl = null;
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A locally prepared animated avatar and its still poster awaiting upload.
|
|
final class ProfileAnimatedAvatarDraft extends ProfileAvatarDraft {
|
|
/// Creates an animated draft from PNG [animation] and [poster] payloads.
|
|
ProfileAnimatedAvatarDraft({required this.animation, required this.poster});
|
|
|
|
/// The animated PNG payload.
|
|
final Uint8List animation;
|
|
|
|
/// The still PNG poster shown when animation is unavailable or disabled.
|
|
final Uint8List poster;
|
|
MediaUploadService? _uploadService;
|
|
Future<String>? _uploadedUrl;
|
|
Future<BlobDescriptor>? _posterUpload;
|
|
Future<BlobDescriptor>? _animationUpload;
|
|
|
|
Future<BlobDescriptor> _uploadPoster(MediaUploadService service) {
|
|
final existing = _posterUpload;
|
|
if (existing != null) return existing;
|
|
late final Future<BlobDescriptor> upload;
|
|
upload = service.uploadBytes(poster, mimeType: 'image/png').catchError((
|
|
Object error,
|
|
StackTrace stackTrace,
|
|
) {
|
|
if (identical(_posterUpload, upload)) _posterUpload = null;
|
|
Error.throwWithStackTrace(error, stackTrace);
|
|
});
|
|
_posterUpload = upload;
|
|
return upload;
|
|
}
|
|
|
|
Future<BlobDescriptor> _uploadAnimation(MediaUploadService service) {
|
|
final existing = _animationUpload;
|
|
if (existing != null) return existing;
|
|
late final Future<BlobDescriptor> upload;
|
|
upload = service.uploadBytes(animation, mimeType: 'image/png').catchError((
|
|
Object error,
|
|
StackTrace stackTrace,
|
|
) {
|
|
if (identical(_animationUpload, upload)) _animationUpload = null;
|
|
Error.throwWithStackTrace(error, stackTrace);
|
|
});
|
|
_animationUpload = upload;
|
|
return upload;
|
|
}
|
|
|
|
@override
|
|
Future<String> upload(MediaUploadService service) async {
|
|
if (!identical(_uploadService, service)) {
|
|
_uploadService = service;
|
|
_uploadedUrl = null;
|
|
_posterUpload = null;
|
|
_animationUpload = null;
|
|
}
|
|
final existing = _uploadedUrl;
|
|
if (existing != null) return existing;
|
|
// Cache each content-addressed part independently. If one request fails,
|
|
// retry only that part so a successful counterpart remains attached to
|
|
// this draft instead of becoming an abandoned duplicate.
|
|
final upload = Future.wait(
|
|
[_uploadPoster(service), _uploadAnimation(service)],
|
|
).then((uploads) => buildAnimatedAvatarUrl(uploads[0].url, uploads[1].url));
|
|
_uploadedUrl = upload;
|
|
try {
|
|
return await upload;
|
|
} catch (_) {
|
|
if (identical(_uploadedUrl, upload)) _uploadedUrl = null;
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|