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,303 @@
|
||||
part of '../message_content.dart';
|
||||
|
||||
const _messageMediaCarouselHeight = 220.0;
|
||||
|
||||
@immutable
|
||||
class _MessageGalleryItem {
|
||||
final String url;
|
||||
final String semanticLabel;
|
||||
final double? aspectRatio;
|
||||
|
||||
const _MessageGalleryItem({
|
||||
required this.url,
|
||||
required this.semanticLabel,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class _TrailingImageGallery {
|
||||
final String content;
|
||||
final List<_MessageGalleryItem> items;
|
||||
|
||||
const _TrailingImageGallery({required this.content, required this.items});
|
||||
}
|
||||
|
||||
class _MessageGalleryPrecache extends HookWidget {
|
||||
final List<ImageProvider<Object>> providers;
|
||||
final int focusedIndex;
|
||||
|
||||
const _MessageGalleryPrecache({
|
||||
required this.providers,
|
||||
required this.focusedIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final providerSignature = Object.hashAll(providers);
|
||||
useEffect(() {
|
||||
var cancelled = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (cancelled || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
for (var index = focusedIndex - 2; index <= focusedIndex + 2; index++) {
|
||||
if (index < 0 || index >= providers.length) {
|
||||
continue;
|
||||
}
|
||||
unawaited(
|
||||
precacheImage(providers[index], context, onError: (_, _) {}),
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => cancelled = true;
|
||||
}, [focusedIndex, providerSignature]);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
_TrailingImageGallery? _extractTrailingImageGallery(
|
||||
String content,
|
||||
Map<String, ImetaEntry> imetaByUrl,
|
||||
String fallbackLabel,
|
||||
) {
|
||||
final lines = content.split('\n');
|
||||
var cursor = lines.length - 1;
|
||||
while (cursor >= 0 && lines[cursor].trim().isEmpty) {
|
||||
cursor -= 1;
|
||||
}
|
||||
|
||||
final items = <_MessageGalleryItem>[];
|
||||
final imagePattern = RegExp(r'^!\[([^\]]*)\]\((https?://[^)\s]+)\)$');
|
||||
while (cursor >= 0) {
|
||||
final match = imagePattern.firstMatch(lines[cursor].trim());
|
||||
if (match == null) break;
|
||||
final url = match.group(2)!;
|
||||
final imeta = imetaByUrl[url];
|
||||
if (classifyMediaUrl(url, imeta: imeta) == MessageMediaKind.video) {
|
||||
break;
|
||||
}
|
||||
final markdownLabel = match.group(1)?.trim();
|
||||
items.insert(
|
||||
0,
|
||||
_MessageGalleryItem(
|
||||
url: url,
|
||||
semanticLabel:
|
||||
imeta?.alt ??
|
||||
(markdownLabel?.isNotEmpty == true
|
||||
? markdownLabel!
|
||||
: fallbackLabel),
|
||||
aspectRatio: imeta?.aspectRatio,
|
||||
),
|
||||
);
|
||||
cursor -= 1;
|
||||
}
|
||||
|
||||
if (items.length < 2) return null;
|
||||
return _TrailingImageGallery(
|
||||
content: lines.take(cursor + 1).join('\n').trimRight(),
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
|
||||
class _MessageImageCarousel extends HookConsumerWidget {
|
||||
final List<_MessageGalleryItem> items;
|
||||
final double leadingOverflow;
|
||||
final double trailingOverflow;
|
||||
final VoidCallback? onReply;
|
||||
final MediaViewerMoreAction? onMore;
|
||||
|
||||
const _MessageImageCarousel({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.leadingOverflow,
|
||||
required this.trailingOverflow,
|
||||
required this.onReply,
|
||||
required this.onMore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final itemSignature = items.map((item) => item.url).join('\u0000');
|
||||
final heroTags = useMemoized(
|
||||
() => [for (var index = 0; index < items.length; index++) Object()],
|
||||
[itemSignature],
|
||||
);
|
||||
final controller = usePageController(viewportFraction: 0.9);
|
||||
final currentIndex = useState(0);
|
||||
final mediaAuth = ref.watch(mediaGetAuthServiceProvider);
|
||||
final mediaClient = ref.watch(mediaHttpClientProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: Grid.half),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.imagesCount(items.length),
|
||||
key: const ValueKey('message-media-carousel-count'),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.half + Grid.quarter),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final contentWidth = constraints.hasBoundedWidth
|
||||
? constraints.maxWidth
|
||||
: _messageMediaMaxWidth(context);
|
||||
final carouselWidth =
|
||||
contentWidth + leadingOverflow + trailingOverflow;
|
||||
final leadingExtent = leadingOverflow;
|
||||
final isLeftToRight =
|
||||
Directionality.of(context) == TextDirection.ltr;
|
||||
final itemTrailingPaddings = [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
index == items.length - 1 ? Grid.gutter : Grid.half,
|
||||
];
|
||||
final previewDecodeWidths = [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
math.max(
|
||||
1.0,
|
||||
carouselWidth * controller.viewportFraction -
|
||||
itemTrailingPaddings[index],
|
||||
),
|
||||
];
|
||||
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final previewProviders = [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
ResizeImage.resizeIfNeeded(
|
||||
(previewDecodeWidths[index] * devicePixelRatio).ceil(),
|
||||
null,
|
||||
MediaImageProvider(
|
||||
url: items[index].url,
|
||||
auth: mediaAuth,
|
||||
client: mediaClient,
|
||||
),
|
||||
),
|
||||
];
|
||||
final viewerItems = [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
MediaViewerImage(
|
||||
url: items[index].url,
|
||||
heroTag: heroTags[index],
|
||||
semanticLabel: items[index].semanticLabel,
|
||||
previewDecodeWidth: previewDecodeWidths[index],
|
||||
aspectRatio: items[index].aspectRatio,
|
||||
preloadProvider: previewProviders[index],
|
||||
),
|
||||
];
|
||||
final carousel = SizedBox(
|
||||
key: const ValueKey('message-media-carousel'),
|
||||
width: carouselWidth,
|
||||
height: _messageMediaCarouselHeight,
|
||||
child: PageView.builder(
|
||||
controller: controller,
|
||||
allowImplicitScrolling: true,
|
||||
// Keep the first image aligned with the message body, but
|
||||
// allow later pages to paint through the avatar gutter as
|
||||
// the row scrolls.
|
||||
clipBehavior: Clip.none,
|
||||
padEnds: false,
|
||||
itemCount: items.length,
|
||||
onPageChanged: (index) => currentIndex.value = index,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Padding(
|
||||
key: ValueKey('message-media-carousel-page:${item.url}'),
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
end: itemTrailingPaddings[index],
|
||||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
excludeSemantics: true,
|
||||
label: context.l10n.openMedia(item.semanticLabel),
|
||||
child: GestureDetector(
|
||||
key: ValueKey(
|
||||
'message-media-carousel-item:${item.url}',
|
||||
),
|
||||
onTap: () => openImageViewer(
|
||||
context,
|
||||
imageUrl: item.url,
|
||||
heroTag: heroTags[index],
|
||||
semanticLabel: item.semanticLabel,
|
||||
previewDecodeWidth: previewDecodeWidths[index],
|
||||
aspectRatio: item.aspectRatio,
|
||||
galleryItems: viewerItems,
|
||||
initialIndex: index,
|
||||
onReply: onReply,
|
||||
onMore: onMore,
|
||||
),
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(Radii.md),
|
||||
border: Border.all(
|
||||
color: context.colors.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: MediaViewerHero(
|
||||
tag: heroTags[index],
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(Radii.md),
|
||||
child: MediaImage(
|
||||
url: item.url,
|
||||
decodeWidth: previewDecodeWidths[index],
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel: item.semanticLabel,
|
||||
errorBuilder: (_, _, _) =>
|
||||
_MediaPreviewFallback(
|
||||
icon: LucideIcons.imageOff,
|
||||
label: context.l10n.imageUnavailable,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final carouselSurface = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
carousel,
|
||||
_MessageGalleryPrecache(
|
||||
providers: previewProviders,
|
||||
focusedIndex: currentIndex.value,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (leadingExtent <= 0 && trailingOverflow <= 0) {
|
||||
return carouselSurface;
|
||||
}
|
||||
return SizedBox(
|
||||
width: contentWidth,
|
||||
height: _messageMediaCarouselHeight,
|
||||
child: OverflowBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
minWidth: carouselWidth,
|
||||
maxWidth: carouselWidth,
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
isLeftToRight ? -leadingExtent : leadingExtent,
|
||||
0,
|
||||
),
|
||||
child: carouselSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user