feat: import Chinese-localized Buzz source snapshot
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

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
@@ -0,0 +1,344 @@
part of '../emoji_picker.dart';
/// Emoji per row. Matches desktop's `perLine={8}`.
const _emojiPerLine = 8;
/// Rendered glyph size. Standard and custom emoji share this so a community's
/// own emoji sit in the grid at the same weight as the native set.
const _emojiGlyphSize = 28.0;
/// Height of one grid cell. Fixed rather than derived from the cell width so
/// section offsets are exact — [_EmojiSection.extent] is what the rail scrolls
/// to, and a width-dependent row height would make it a guess.
const _cellExtent = 40.0;
/// Height of a pinned section header, padding included. Also fixed, for the
/// same reason.
const _sectionHeaderExtent = 28.0;
/// Cells divide the content width evenly with no gaps, the way emoji-mart lays
/// its grid out; the space around each glyph comes from the cell being larger
/// than the glyph.
SliverGridDelegate get _gridDelegate =>
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _emojiPerLine,
mainAxisExtent: _cellExtent,
);
/// One labelled run of tiles in the continuous grid.
///
/// Sections know their own scroll extent so the rail can jump to them without
/// the grid having been laid out — with ~1.9k emoji most sections are far
/// off-screen, so there is no render object to measure or `ensureVisible`.
class _EmojiSection {
final String id;
final String label;
final IconData icon;
final int itemCount;
final Widget Function(BuildContext context, int index) itemBuilder;
const _EmojiSection({
required this.id,
required this.label,
required this.icon,
required this.itemCount,
required this.itemBuilder,
});
int get rowCount => (itemCount / _emojiPerLine).ceil();
double get extent => _sectionHeaderExtent + rowCount * _cellExtent;
}
/// Offset of each section's header, in scroll order. Element `i` is where the
/// rail should land for section `i`.
List<double> _sectionOffsets(List<_EmojiSection> sections) {
final offsets = <double>[];
var running = 0.0;
for (final section in sections) {
offsets.add(running);
running += section.extent;
}
return offsets;
}
/// Which section owns [offset] — the one whose header is pinned right now.
int _activeSectionIndex(List<double> offsets, double offset) {
var active = 0;
for (var i = 0; i < offsets.length; i++) {
// Half a header of slack so the highlight flips as a header reaches the
// top rather than a pixel before.
if (offsets[i] <= offset + _sectionHeaderExtent / 2) active = i;
}
return active;
}
/// One tappable standard emoji.
///
/// [keyPrefix] exists because the frequently-used section repeats emoji that
/// also appear in their own category, and both are in the same scroll view — two
/// widgets keyed `emoji-tile-fire` would be indistinguishable to a test.
class _EmojiTile extends StatelessWidget {
final EmojiEntry entry;
final VoidCallback onTap;
final String keyPrefix;
const _EmojiTile({
required this.entry,
required this.onTap,
this.keyPrefix = 'emoji-tile',
});
@override
Widget build(BuildContext context) {
return GestureDetector(
key: ValueKey('$keyPrefix-${entry.tileId}'),
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Semantics(
button: true,
label: entry.name,
child: Center(
child: NativeEmojiGlyph(emoji: entry.native, size: _emojiGlyphSize),
),
),
);
}
}
/// One tappable custom (image) emoji. Same cell and same glyph size as
/// [_EmojiTile] — a community's emoji shouldn't be laid out on its own looser
/// grid, which is what made the custom tab read as a different component.
class _CustomEmojiTile extends StatelessWidget {
final CustomEmoji emoji;
final VoidCallback onTap;
final String keyPrefix;
const _CustomEmojiTile({
required this.emoji,
required this.onTap,
this.keyPrefix = 'emoji-tile-custom',
});
@override
Widget build(BuildContext context) {
return GestureDetector(
key: ValueKey('$keyPrefix-${emoji.shortcode}'),
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Tooltip(
message: ':${emoji.shortcode}:',
child: Center(
child: CustomEmojiImage(
shortcode: emoji.shortcode,
url: emoji.url,
size: _emojiGlyphSize,
),
),
),
);
}
}
/// Every section in one scroll view, headers pinning as they reach the top.
///
/// Replaces the old tab-per-category swap: the rail is a shortcut into a single
/// list, not a set of separate pages, which is how both desktop and every
/// system emoji keyboard behave.
class _ContinuousEmojiGrid extends StatelessWidget {
final List<_EmojiSection> sections;
final ScrollController controller;
const _ContinuousEmojiGrid({
required this.sections,
required this.controller,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
key: const ValueKey('emoji-picker-grid'),
controller: controller,
slivers: [
for (final section in sections)
SliverMainAxisGroup(
key: ValueKey('emoji-section-${section.id}'),
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SectionHeaderDelegate(section.label),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
sliver: SliverGrid.builder(
gridDelegate: _gridDelegate,
itemCount: section.itemCount,
itemBuilder: section.itemBuilder,
),
),
],
),
// Clear the home indicator so the last row isn't half-swallowed. Sits
// outside the sections so it doesn't skew their offsets.
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.viewPaddingOf(context).bottom + Grid.xxs,
),
),
],
);
}
}
/// Search results: custom emoji first (a community's own emoji are the ones a
/// user is most likely hunting for by name), then the standard set.
class _EmojiSearchResults extends StatelessWidget {
final List<EmojiEntry> entries;
final List<CustomEmoji> customEmoji;
final void Function(String emoji) onSelect;
const _EmojiSearchResults({
required this.entries,
required this.customEmoji,
required this.onSelect,
});
@override
Widget build(BuildContext context) {
if (entries.isEmpty && customEmoji.isEmpty) {
return _EmojiEmptyState(
icon: LucideIcons.searchX,
message: context.l10n.emojiNoResults,
);
}
return CustomScrollView(
key: const ValueKey('emoji-picker-search-results'),
slivers: [
if (customEmoji.isNotEmpty) ...[
_SectionHeader(label: context.l10n.emojiCustom),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
sliver: SliverGrid.builder(
gridDelegate: _gridDelegate,
itemCount: customEmoji.length,
itemBuilder: (context, index) {
final entry = customEmoji[index];
return _CustomEmojiTile(
emoji: entry,
onTap: () => onSelect(':${entry.shortcode}:'),
);
},
),
),
],
if (entries.isNotEmpty) ...[
if (customEmoji.isNotEmpty)
_SectionHeader(label: context.l10n.emojiStandard),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
sliver: SliverGrid.builder(
gridDelegate: _gridDelegate,
itemCount: entries.length,
itemBuilder: (context, index) {
final entry = entries[index];
return _EmojiTile(
entry: entry,
onTap: () => onSelect(entry.native),
);
},
),
),
],
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.viewPaddingOf(context).bottom + Grid.xxs,
),
),
],
);
}
}
/// Pinned header for a section of the continuous grid. Opaque, so rows scroll
/// under it rather than showing through.
class _SectionHeaderDelegate extends SliverPersistentHeaderDelegate {
final String label;
const _SectionHeaderDelegate(this.label);
@override
double get minExtent => _sectionHeaderExtent;
@override
double get maxExtent => _sectionHeaderExtent;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: context.colors.surfaceContainerHighest,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
child: Text(label, style: _sectionLabelStyle(context)),
);
}
@override
bool shouldRebuild(_SectionHeaderDelegate oldDelegate) =>
oldDelegate.label != label;
}
/// Non-pinned header, for the search results list.
class _SectionHeader extends StatelessWidget {
final String label;
const _SectionHeader({required this.label});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Container(
height: _sectionHeaderExtent,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: Grid.gutter),
child: Text(label, style: _sectionLabelStyle(context)),
),
);
}
}
TextStyle? _sectionLabelStyle(BuildContext context) =>
context.textTheme.labelMedium?.copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
);
class _EmojiEmptyState extends StatelessWidget {
final IconData icon;
final String message;
const _EmojiEmptyState({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: Grid.md, color: context.colors.onSurfaceVariant),
const SizedBox(height: Grid.xxs),
Text(
message,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
),
);
}
}