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
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:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user