mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
Replace the mobile Agents sheet with a full-page directory of verified owned agents. Add dedicated creation and avatar pages with catalog-backed model selection, reuse the profile emoji/photo/camera controls, and expose Builderbot model settings while labeling desktop agents “Manage on desktop.” Share one account-scoped model catalog across editable cards and retries. Choose Bestie from the agent profile using the shared encrypted main-agent record (text-only action, filled star once selected), and move confirmed deletion to a top-left trash control with native iOS glass. Include uploaded avatars in owner registrations so desktop can display them. Validation: mobile analysis and all 3,958 hosted-enabled app tests passed; default-build tests run by the push hook. Earlier iPhone build installed and iterated with the user; the current iOS build succeeds, but final changes still need a device pass. Independent review found no new blockers. Actual camera capture and live Bestie sync remain unconfirmed; the existing shared `main-agent` wire format and Bestie terminology are retained. ### Snapshots Widget-harness captures with test data (not native iOS chrome). | Agents directory | Image avatar editor | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
320 lines
11 KiB
Dart
320 lines
11 KiB
Dart
import 'package:buzz/shared/theme/buzz_icons.dart';
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart' show PlatformViewHitTestBehavior;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
/// The navigation glyph displayed by [IosGlassNavigationButton].
|
|
enum IosGlassNavigationIcon {
|
|
back,
|
|
close,
|
|
camera,
|
|
photoLibrary,
|
|
palette,
|
|
droplet,
|
|
emoji,
|
|
person,
|
|
frame,
|
|
rotateCamera,
|
|
shutter,
|
|
colorSwatch,
|
|
sun,
|
|
moon,
|
|
systemAppearance,
|
|
more,
|
|
reply,
|
|
trash,
|
|
}
|
|
|
|
/// Leading width used by iOS channel-style headers.
|
|
const iosGlassChannelHeaderLeadingWidth = 58.0;
|
|
|
|
/// Horizontal center of the native button inside a channel-style leading.
|
|
const iosGlassChannelHeaderButtonCenterX = 38.0;
|
|
|
|
/// Space between the leading region and a channel-style title.
|
|
const iosGlassChannelHeaderTitleSpacing = Grid.xs;
|
|
|
|
/// A native iOS navigation control using the system glass button treatment.
|
|
///
|
|
/// Callers should only insert this widget on iOS and retain their existing
|
|
/// Flutter control on other platforms.
|
|
class IosGlassNavigationButton extends HookWidget {
|
|
const IosGlassNavigationButton({
|
|
super.key,
|
|
required this.icon,
|
|
required this.semanticLabel,
|
|
required this.onPressed,
|
|
this.label,
|
|
this.width = 48,
|
|
this.height = 48,
|
|
this.controlSize = 40,
|
|
this.fillWidth = false,
|
|
this.buttonCenterX,
|
|
this.foregroundColor,
|
|
this.swatchColor,
|
|
this.isBusy = false,
|
|
this.isSelected = false,
|
|
this.nativeViewSuppressed,
|
|
});
|
|
|
|
static const viewType = 'buzz/navigation_glass';
|
|
|
|
/// The SF Symbol-style glyph rendered by the native control.
|
|
final IosGlassNavigationIcon icon;
|
|
|
|
/// Optional text rendered by the native control instead of [icon].
|
|
final String? label;
|
|
|
|
/// Accessibility label exposed by the native view or Flutter fallback.
|
|
final String semanticLabel;
|
|
|
|
/// Invoked when the enabled control is activated.
|
|
final VoidCallback? onPressed;
|
|
|
|
/// Width of the platform-view hit target.
|
|
final double width;
|
|
|
|
/// Height of the platform-view hit target.
|
|
final double height;
|
|
|
|
/// Diameter of the visual glass control inside its hit target.
|
|
final double controlSize;
|
|
|
|
/// Whether the native visual control fills the available width.
|
|
final bool fillWidth;
|
|
|
|
/// Horizontal center for the visual control within its hit target.
|
|
final double? buttonCenterX;
|
|
|
|
/// Optional foreground tint for the native control and Flutter fallback.
|
|
final Color? foregroundColor;
|
|
|
|
/// Optional inset color swatch used by [IosGlassNavigationIcon.colorSwatch].
|
|
final Color? swatchColor;
|
|
|
|
/// Whether the native control presents its busy state.
|
|
final bool isBusy;
|
|
|
|
/// Whether the native control exposes its selected state.
|
|
final bool isSelected;
|
|
|
|
/// When true, substitutes an accessible Flutter control for the native view.
|
|
final ValueListenable<bool>? nativeViewSuppressed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(defaultTargetPlatform == TargetPlatform.iOS);
|
|
final nativeChannel = useState<MethodChannel?>(null);
|
|
final onPressedRef = useRef(onPressed)..value = onPressed;
|
|
final brightness = context.theme.brightness.name;
|
|
final effectiveForeground = foregroundColor ?? context.colors.primary;
|
|
final foregroundValue = effectiveForeground.toARGB32();
|
|
final swatchColorValue = swatchColor?.toARGB32();
|
|
final enabled = onPressed != null;
|
|
|
|
useEffect(() {
|
|
final channel = nativeChannel.value;
|
|
if (channel == null) return null;
|
|
channel.setMethodCallHandler((call) async {
|
|
if (call.method == 'pressed') {
|
|
onPressedRef.value?.call();
|
|
}
|
|
});
|
|
return () => channel.setMethodCallHandler(null);
|
|
}, [nativeChannel.value]);
|
|
|
|
useEffect(
|
|
() {
|
|
final channel = nativeChannel.value;
|
|
if (channel != null) {
|
|
unawaited(
|
|
channel.invokeMethod<void>('setAppearance', <String, Object>{
|
|
'brightness': brightness,
|
|
'foregroundColor': foregroundValue,
|
|
'enabled': enabled,
|
|
'busy': isBusy,
|
|
'selected': isSelected,
|
|
'swatchColor': ?swatchColorValue,
|
|
}),
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
[
|
|
nativeChannel.value,
|
|
brightness,
|
|
foregroundValue,
|
|
enabled,
|
|
isBusy,
|
|
isSelected,
|
|
swatchColorValue,
|
|
],
|
|
);
|
|
|
|
useEffect(() {
|
|
final channel = nativeChannel.value;
|
|
if (channel != null) {
|
|
final content = <String, Object>{
|
|
'icon': icon.name,
|
|
'accessibilityLabel': semanticLabel,
|
|
};
|
|
if (label != null) content['label'] = label!;
|
|
unawaited(channel.invokeMethod<void>('setContent', content));
|
|
}
|
|
return null;
|
|
}, [nativeChannel.value, icon, label, semanticLabel]);
|
|
|
|
Widget buildControl({required bool suppressNativeView}) {
|
|
if (suppressNativeView) {
|
|
final resolvedButtonCenterX = buttonCenterX ?? width / 2;
|
|
return Semantics(
|
|
container: true,
|
|
button: true,
|
|
enabled: enabled,
|
|
selected: isSelected,
|
|
label: semanticLabel,
|
|
onTap: onPressed,
|
|
child: ExcludeSemantics(
|
|
child: Stack(
|
|
key: const ValueKey('ios-glass-navigation-flutter-fallback'),
|
|
children: [
|
|
Positioned(
|
|
left: resolvedButtonCenterX - controlSize / 2,
|
|
top: (height - controlSize) / 2,
|
|
width: controlSize,
|
|
height: controlSize,
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: context.colors.surface.withValues(alpha: 0.72),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: context.colors.inverseSurface.withValues(
|
|
alpha: 0.07,
|
|
),
|
|
),
|
|
),
|
|
child: isBusy
|
|
? Center(
|
|
child: SizedBox.square(
|
|
dimension: 22,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: effectiveForeground,
|
|
),
|
|
),
|
|
)
|
|
: icon == IosGlassNavigationIcon.colorSwatch
|
|
? Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: swatchColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
)
|
|
: label != null
|
|
? Text(
|
|
label!,
|
|
maxLines: 1,
|
|
style: context.textTheme.labelMedium?.copyWith(
|
|
color: effectiveForeground,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
)
|
|
: Icon(
|
|
switch (icon) {
|
|
IosGlassNavigationIcon.more => BuzzIcons.ellipsis,
|
|
IosGlassNavigationIcon.trash => BuzzIcons.trash2,
|
|
IosGlassNavigationIcon.reply =>
|
|
BuzzIcons.cornerUpLeft,
|
|
IosGlassNavigationIcon.back =>
|
|
BuzzIcons.chevronLeft,
|
|
IosGlassNavigationIcon.close => BuzzIcons.x,
|
|
IosGlassNavigationIcon.camera => BuzzIcons.camera,
|
|
IosGlassNavigationIcon.photoLibrary =>
|
|
BuzzIcons.images,
|
|
IosGlassNavigationIcon.palette =>
|
|
BuzzIcons.palette,
|
|
IosGlassNavigationIcon.droplet =>
|
|
BuzzIcons.droplet,
|
|
IosGlassNavigationIcon.emoji => BuzzIcons.smile,
|
|
IosGlassNavigationIcon.person => BuzzIcons.user,
|
|
IosGlassNavigationIcon.frame => BuzzIcons.square,
|
|
IosGlassNavigationIcon.rotateCamera =>
|
|
BuzzIcons.switchCamera,
|
|
IosGlassNavigationIcon.shutter =>
|
|
BuzzIcons.circle,
|
|
IosGlassNavigationIcon.colorSwatch =>
|
|
BuzzIcons.circle,
|
|
IosGlassNavigationIcon.sun => BuzzIcons.sun,
|
|
IosGlassNavigationIcon.moon => BuzzIcons.moon,
|
|
IosGlassNavigationIcon.systemAppearance =>
|
|
BuzzIcons.sunMoon,
|
|
},
|
|
size: icon == IosGlassNavigationIcon.shutter
|
|
? controlSize * 0.72
|
|
: 24,
|
|
color: icon == IosGlassNavigationIcon.colorSwatch
|
|
? swatchColor
|
|
: effectiveForeground,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
final creationParams = <String, Object>{
|
|
'icon': icon.name,
|
|
'accessibilityLabel': semanticLabel,
|
|
'brightness': brightness,
|
|
'foregroundColor': foregroundValue,
|
|
'enabled': enabled,
|
|
'busy': isBusy,
|
|
'selected': isSelected,
|
|
'controlSize': controlSize,
|
|
'controlWidth': controlSize,
|
|
'fillWidth': fillWidth,
|
|
'buttonCenterX': buttonCenterX ?? width / 2,
|
|
'hitTargetWidth': width,
|
|
'hitTargetHeight': height,
|
|
'swatchColor': ?swatchColorValue,
|
|
};
|
|
if (label != null) creationParams['label'] = label!;
|
|
return UiKitView(
|
|
viewType: viewType,
|
|
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
|
creationParams: creationParams,
|
|
creationParamsCodec: const StandardMessageCodec(),
|
|
onPlatformViewCreated: (viewId) {
|
|
nativeChannel.value = MethodChannel('$viewType/$viewId');
|
|
},
|
|
);
|
|
}
|
|
|
|
return Tooltip(
|
|
message: semanticLabel,
|
|
excludeFromSemantics: true,
|
|
child: SizedBox(
|
|
width: width,
|
|
height: height,
|
|
child: nativeViewSuppressed == null
|
|
? buildControl(suppressNativeView: false)
|
|
: ValueListenableBuilder<bool>(
|
|
valueListenable: nativeViewSuppressed!,
|
|
builder: (context, suppressNativeView, _) =>
|
|
buildControl(suppressNativeView: suppressNativeView),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|