Files
buzz/mobile/lib/features/channels/channels_page/quick_actions_launcher.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

185 lines
6.6 KiB
Dart

part of '../channels_page.dart';
const _kQuickActionsTabMotionDuration = Duration(milliseconds: 220);
const _kQuickActionsTabMotionCurve = Cubic(0.77, 0, 0.175, 1);
const _kQuickActionsHiddenOverlap = Grid.half;
const _kQuickActionsHiddenScale = 0.8;
/// Places the channel quick-actions button beside mobile navigation.
///
/// The button remains available only on Home and moves behind the navigation
/// bar when another destination is selected.
class ChannelQuickActionsLauncher extends HookConsumerWidget {
/// Whether the launcher should be visible beside the navigation bar.
final bool visible;
/// Height of the navigation bar used to vertically center the closed button.
final double navigationBarHeight;
/// Space between the navigation bar and the bottom safe area.
final double navigationBarBottomGap;
/// Full width of the navigation bar, including its inner edge padding.
final double navigationBarWidth;
/// Bottom system inset used by the navigation bar's [SafeArea].
final double systemBottomInset;
/// Distance between the launcher and the right edge of the screen.
final double rightInset;
/// Creates a launcher aligned with the supplied navigation geometry.
const ChannelQuickActionsLauncher({
super.key,
required this.visible,
required this.navigationBarHeight,
required this.navigationBarBottomGap,
required this.navigationBarWidth,
required this.systemBottomInset,
required this.rightInset,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPubkey = ref.watch(currentPubkeyProvider);
final quickActionsOpen = useState(false);
final reducedMotion = MediaQuery.of(context).disableAnimations;
final navigationBottomInset = systemBottomInset > navigationBarBottomGap
? systemBottomInset
: navigationBarBottomGap;
final closedBottomInset =
navigationBottomInset + ((navigationBarHeight - _kMorphClosedSize) / 2);
final openLift =
navigationBarHeight +
Grid.xxs -
((navigationBarHeight - _kMorphClosedSize) / 2);
final effectiveOpen = visible && quickActionsOpen.value;
final screenWidth = MediaQuery.sizeOf(context).width;
final navigationBarRight = (screenWidth + navigationBarWidth) / 2;
final launcherRight = screenWidth - rightInset;
final hiddenHorizontalOffset =
navigationBarRight - launcherRight - _kQuickActionsHiddenOverlap;
useEffect(() {
if (!visible && quickActionsOpen.value) {
quickActionsOpen.value = false;
}
return null;
}, [visible]);
Future<void> openChannel(Channel channel) async {
if (!context.mounted) return;
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ChannelDetailPage(channel: channel),
),
);
}
Future<void> selectQuickAction(_QuickAction action) async {
quickActionsOpen.value = false;
if (!reducedMotion) {
await Future<void>.delayed(_kMorphCloseDuration);
}
if (!context.mounted) return;
switch (action) {
case _QuickAction.createChannel:
final created = await showBuzzModalBottomSheet<Channel>(
context: context,
constraints: _quickActionSheetConstraints(context),
isScrollControlled: true,
showDragHandle: true,
builder: (_) => const _CreateChannelSheet(channelType: 'stream'),
);
if (created != null && context.mounted) {
await openChannel(created);
}
case _QuickAction.newDm:
final opened = await showBuzzModalBottomSheet<Channel>(
context: context,
constraints: _quickActionSheetConstraints(context),
isScrollControlled: true,
showDragHandle: true,
builder: (_) =>
_NewDirectMessageSheet(currentPubkey: currentPubkey),
);
if (opened != null && context.mounted) {
await openChannel(opened);
}
}
}
return Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
if (quickActionsOpen.value)
Positioned.fill(
child: Semantics(
button: true,
label: context.l10n.closeQuickActions,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => quickActionsOpen.value = false,
),
),
),
AnimatedPositioned(
duration: reducedMotion
? Duration.zero
: _kQuickActionsTabMotionDuration,
curve: _kQuickActionsTabMotionCurve,
right: rightInset,
bottom: closedBottomInset + (effectiveOpen ? openLift : 0),
child: IgnorePointer(
ignoring: !visible,
child: ExcludeSemantics(
excluding: !visible,
child: TweenAnimationBuilder<double>(
key: const Key('channel-quick-actions-motion'),
tween: Tween(end: visible ? 0 : 1),
duration: reducedMotion
? Duration.zero
: _kQuickActionsTabMotionDuration,
curve: _kQuickActionsTabMotionCurve,
builder: (context, hiddenProgress, child) => Opacity(
key: const Key('channel-quick-actions-opacity'),
opacity: 1 - hiddenProgress,
child: Transform.translate(
key: const Key('channel-quick-actions-transform'),
offset: Offset(hiddenHorizontalOffset * hiddenProgress, 0),
child: Transform.scale(
key: const Key('channel-quick-actions-scale'),
scale:
1 -
((1 - _kQuickActionsHiddenScale) * hiddenProgress),
child: child,
),
),
),
child: _MorphingQuickActionsButton(
open: effectiveOpen,
openEdgeOffset: rightInset - Grid.gutter,
onToggle: () {
unawaited(HapticFeedback.lightImpact());
quickActionsOpen.value = !quickActionsOpen.value;
},
onSelected: (action) => unawaited(selectQuickAction(action)),
),
),
),
),
),
],
);
}
}
BoxConstraints _quickActionSheetConstraints(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
return BoxConstraints(
maxHeight: mediaQuery.size.height - mediaQuery.viewPadding.top - Grid.sm,
);
}