Files
buzz/mobile/lib/shared/widgets/android_transparent_avatar.dart
klopez4212 b4509dee8b feat(mobile): add Buzzies and refresh agent avatar picker (#8921)
## Summary

Adds Buzzies and refreshes the agent avatar picker with larger previews,
rounded option strips, and clearer category choices. Also moves Agents
above Appearance in the home menu and adds a custom agent icon.

### Related issue

None found.

### Testing

Analysis, 4,138 app tests, package tests, and 121 configured-auth tests
pass. Earlier visual build exercised on Pixel and iOS simulator, with
iPhone review; snapshots below show that build.

| Android · Pixel | iOS · simulator preview |
| --- | --- |
| ![Buzzies in the Android avatar
picker](https://github.com/user-attachments/assets/bf56120c-0128-4683-a0ad-189587a13c8d)
| ![Buzzies in the iOS avatar
picker](https://github.com/user-attachments/assets/63c692f5-822c-4490-883f-36bec8d2be15)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-02 17:00:12 +00:00

106 lines
3.0 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Uses Chromium's WebM alpha compositing for a bundled Android avatar.
class AndroidTransparentAvatar extends HookWidget {
const AndroidTransparentAvatar({
required this.url,
required this.playing,
required this.fallback,
super.key,
});
final Uri url;
final bool playing;
final Widget fallback;
@override
Widget build(BuildContext context) {
final channel = useState<MethodChannel?>(null);
final failed = useState(false);
final ready = useState(false);
useEffect(() {
final current = channel.value;
if (current == null || !playing || ready.value || failed.value) {
return null;
}
var active = true;
Timer? retry;
Future<void> waitForFrame() async {
try {
final state = await current.invokeMethod<String>('isReady');
if (!active) return;
if (state == 'failed') {
failed.value = true;
} else if (state == 'ready') {
ready.value = true;
} else {
retry = Timer(const Duration(milliseconds: 50), waitForFrame);
}
} on PlatformException {
if (active) failed.value = true;
} on MissingPluginException {
if (active) failed.value = true;
}
}
unawaited(waitForFrame());
return () {
active = false;
retry?.cancel();
};
}, [channel.value, playing, failed.value]);
useEffect(() {
final current = channel.value;
var active = true;
Future<void> updatePlayback() async {
try {
await current?.invokeMethod<void>('setPlaying', playing);
} on PlatformException {
if (active) failed.value = true;
} on MissingPluginException {
if (active) failed.value = true;
}
}
unawaited(updatePlayback());
return () => active = false;
}, [channel.value, playing]);
if (failed.value) return fallback;
return ExcludeSemantics(
child: IgnorePointer(
child: Stack(
fit: StackFit.expand,
children: [
Opacity(
opacity: ready.value ? 1 : 0,
child: AndroidView(
viewType: 'buzz/transparent-avatar',
creationParams: {'url': url.toString()},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (id) {
if (context.mounted) {
channel.value = MethodChannel(
'buzz/transparent-avatar/$id',
);
}
},
),
),
IgnorePointer(
child: AnimatedOpacity(
opacity: ready.value ? 0 : 1,
duration: const Duration(milliseconds: 120),
child: fallback,
),
),
],
),
),
);
}
}