Files
buzz/mobile/lib/features/channels/channels_page/quick_actions.dart
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

343 lines
12 KiB
Dart

part of '../channels_page.dart';
const _kMorphOpenDuration = Duration(milliseconds: 280);
const _kMorphCloseDuration = Duration(milliseconds: 240);
const _kMorphFadeDuration = Duration(milliseconds: 200);
const _kMorphCloseCurve = Cubic(0.22, 1, 0.36, 1);
const double _kMorphOpenBounce = 0.14;
const double _kMorphCloseBounce = 0.06;
const double _kMorphClosedSize = 56;
const double _kMorphOpenHeight = 160;
const double _kMorphOpenRadius = 20;
const double _kMorphSlide = 40;
const double _kMorphScale = 0.97;
const double _kMorphBlur = 2;
const double _kQuickActionCardOverlayOpacity = 0.1;
const double _kQuickActionCardRadius = _kMorphOpenRadius - Grid.xxs;
class _MorphingQuickActionsButton extends HookWidget {
final bool open;
final double openEdgeOffset;
final VoidCallback onToggle;
final ValueChanged<_QuickAction> onSelected;
const _MorphingQuickActionsButton({
required this.open,
required this.openEdgeOffset,
required this.onToggle,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
final reducedMotion = MediaQuery.of(context).disableAnimations;
final mediaQuery = MediaQuery.of(context);
final openWidth = (mediaQuery.size.width - (Grid.gutter * 2))
.clamp(_kMorphClosedSize, double.infinity)
.toDouble();
final surfaceController = useAnimationController(
initialValue: open ? 1 : 0,
upperBound: 1.02,
);
final fadeController = useAnimationController(
duration: reducedMotion ? Duration.zero : _kMorphFadeDuration,
reverseDuration: reducedMotion ? Duration.zero : _kMorphFadeDuration,
initialValue: open ? 1 : 0,
);
final fadeAnimation = useMemoized(
() => CurvedAnimation(
parent: fadeController,
curve: _kMorphCloseCurve,
reverseCurve: _kMorphCloseCurve,
),
[fadeController],
);
final animation = useMemoized(
() => Listenable.merge([surfaceController, fadeAnimation]),
[surfaceController, fadeAnimation],
);
useEffect(() => fadeAnimation.dispose, [fadeAnimation]);
useEffect(() {
fadeController.duration = reducedMotion
? Duration.zero
: _kMorphFadeDuration;
fadeController.reverseDuration = reducedMotion
? Duration.zero
: _kMorphFadeDuration;
if (reducedMotion) {
surfaceController.value = open ? 1 : 0;
fadeController.value = open ? 1 : 0;
} else {
final target = open ? 1.0 : 0.0;
if ((surfaceController.value - target).abs() > 0.001) {
unawaited(
surfaceController.animateWith(
SpringSimulation(
SpringDescription.withDurationAndBounce(
duration: open ? _kMorphOpenDuration : _kMorphCloseDuration,
bounce: open ? _kMorphOpenBounce : _kMorphCloseBounce,
),
surfaceController.value,
target,
surfaceController.velocity,
snapToEnd: true,
),
),
);
}
if (open) {
unawaited(fadeController.forward());
} else {
unawaited(fadeController.reverse());
}
}
return null;
}, [fadeController, open, reducedMotion, surfaceController]);
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final surfaceValue = surfaceController.value;
final surfaceProgress = surfaceValue.clamp(0.0, 1.0);
final fadeValue = fadeAnimation.value.clamp(0.0, 1.0);
final width = lerpDouble(_kMorphClosedSize, openWidth, surfaceValue)!;
final height = lerpDouble(
_kMorphClosedSize,
_kMorphOpenHeight,
surfaceValue,
)!;
final radius = lerpDouble(
_kMorphClosedSize / 2,
_kMorphOpenRadius,
surfaceValue,
)!;
final borderRadius = BorderRadius.circular(radius);
return Transform.translate(
offset: Offset(openEdgeOffset * surfaceProgress, 0),
child: SizedBox(
key: const Key('quick-actions-surface'),
width: width,
height: height,
child: DecoratedBox(
decoration: BoxDecoration(
color: context.colors.primary,
borderRadius: borderRadius,
boxShadow: [
BoxShadow(
color: context.colors.shadow.withValues(alpha: 0.24),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: Material(
type: MaterialType.transparency,
child: Stack(
children: [
Positioned.fill(
child: OverflowBox(
alignment: Alignment.bottomRight,
minWidth: openWidth,
maxWidth: openWidth,
minHeight: _kMorphOpenHeight,
maxHeight: _kMorphOpenHeight,
child: IgnorePointer(
ignoring: !open || fadeValue < 0.9,
child: ExcludeSemantics(
excluding: !open,
child: Opacity(
opacity: fadeValue,
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: _kMorphBlur * (1 - fadeValue),
sigmaY: _kMorphBlur * (1 - fadeValue),
),
child: Transform.translate(
offset: Offset(
_kMorphSlide * (1 - surfaceProgress),
0,
),
child: Transform.scale(
alignment: Alignment.bottomRight,
scale:
_kMorphScale +
((1 - _kMorphScale) *
surfaceProgress),
child: _QuickActionsMenu(
onSelected: onSelected,
),
),
),
),
),
),
),
),
),
Positioned(
right: 0,
bottom: 0,
width: _kMorphClosedSize,
height: _kMorphClosedSize,
child: IgnorePointer(
ignoring: open,
child: ExcludeSemantics(
excluding: open,
child: Opacity(
opacity: 1 - fadeValue,
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: _kMorphBlur * fadeValue,
sigmaY: _kMorphBlur * fadeValue,
),
child: Transform.translate(
offset: Offset(
-_kMorphSlide * surfaceProgress,
0,
),
child: Transform.rotate(
angle: (pi / 4) * surfaceProgress,
child: Transform.scale(
scale:
1 -
((1 - _kMorphScale) *
surfaceProgress),
child: Tooltip(
message:
context.l10n.createOrStartConversation,
child: Semantics(
button: true,
label:
context.l10n.createOrStartConversation,
expanded: open,
child: InkWell(
customBorder: const CircleBorder(),
onTap: onToggle,
child: Center(
child: Icon(
LucideIcons.plus,
color: context.colors.onPrimary,
),
),
),
),
),
),
),
),
),
),
),
),
),
],
),
),
),
),
),
);
},
);
}
}
class _QuickActionsMenu extends StatelessWidget {
final ValueChanged<_QuickAction> onSelected;
const _QuickActionsMenu({required this.onSelected});
@override
Widget build(BuildContext context) {
return Padding(
key: const Key('quick-actions-menu'),
padding: const EdgeInsets.all(Grid.xxs),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_QuickActionItem(
icon: LucideIcons.hash,
title: context.l10n.createChannel,
key: const Key('quick-action-create-channel-card'),
onTap: () => onSelected(_QuickAction.createChannel),
),
const SizedBox(height: Grid.xxs),
_QuickActionItem(
icon: LucideIcons.messagesSquare,
title: context.l10n.newDirectMessage,
key: const Key('quick-action-new-dm-card'),
onTap: () => onSelected(_QuickAction.newDm),
),
],
),
);
}
}
class _QuickActionItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
const _QuickActionItem({
super.key,
required this.icon,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final foreground = context.colors.onPrimary;
final background = Color.alphaBlend(
foreground.withValues(alpha: _kQuickActionCardOverlayOpacity),
context.colors.primary,
);
return Expanded(
child: Material(
color: background,
borderRadius: BorderRadius.circular(_kQuickActionCardRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.xs),
child: Row(
children: [
Icon(icon, size: 24, color: foreground),
const SizedBox(width: Grid.xs),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyLarge?.copyWith(
color: foreground,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
),
);
}
}