mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
## Summary - invert mobile utility surfaces so pages and sheets use the softer page background with raised containers - rebuild the community theme picker around swipeable Home/Chat previews, accent and appearance controls, and native iOS glass interactions - align shared sheet/profile spacing and radii, and move Theme into the Community settings card ### Related issue None found. ### Testing - `just mobile-check` - `just mobile-test` (1,871 tests) - signed iOS Release build installed on a physical iPhone --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
318 lines
11 KiB
Dart
318 lines
11 KiB
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,
|
|
}
|
|
|
|
/// 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.back =>
|
|
Icons.arrow_back_ios_new_rounded,
|
|
IosGlassNavigationIcon.close =>
|
|
Icons.close_rounded,
|
|
IosGlassNavigationIcon.camera =>
|
|
Icons.camera_alt_rounded,
|
|
IosGlassNavigationIcon.photoLibrary =>
|
|
Icons.photo_library_rounded,
|
|
IosGlassNavigationIcon.palette =>
|
|
Icons.palette_rounded,
|
|
IosGlassNavigationIcon.droplet =>
|
|
Icons.water_drop_rounded,
|
|
IosGlassNavigationIcon.emoji =>
|
|
Icons.emoji_emotions_rounded,
|
|
IosGlassNavigationIcon.person =>
|
|
Icons.person_rounded,
|
|
IosGlassNavigationIcon.frame =>
|
|
Icons.photo_size_select_actual_rounded,
|
|
IosGlassNavigationIcon.rotateCamera =>
|
|
Icons.cameraswitch_rounded,
|
|
IosGlassNavigationIcon.shutter => Icons.circle,
|
|
IosGlassNavigationIcon.colorSwatch =>
|
|
Icons.circle,
|
|
IosGlassNavigationIcon.sun =>
|
|
Icons.light_mode_rounded,
|
|
IosGlassNavigationIcon.moon =>
|
|
Icons.dark_mode_rounded,
|
|
IosGlassNavigationIcon.systemAppearance =>
|
|
Icons.brightness_auto_rounded,
|
|
},
|
|
size: icon == IosGlassNavigationIcon.shutter
|
|
? controlSize * 0.72
|
|
: 22,
|
|
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|