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,228 @@
|
||||
part of '../message_content.dart';
|
||||
|
||||
/// A decoded, paused video surface used as a posterless message preview.
|
||||
class LoadedVideoPreviewFrame {
|
||||
/// Widget backed by the initialized native video texture.
|
||||
final Widget child;
|
||||
|
||||
/// Aspect ratio reported by the video decoder.
|
||||
final double aspectRatio;
|
||||
|
||||
final Future<void> Function() _dispose;
|
||||
|
||||
/// Creates a loaded preview frame with its resource cleanup callback.
|
||||
const LoadedVideoPreviewFrame({
|
||||
required this.child,
|
||||
required this.aspectRatio,
|
||||
required Future<void> Function() dispose,
|
||||
}) : _dispose = dispose;
|
||||
|
||||
/// Releases the native decoder and any downloaded temporary file.
|
||||
Future<void> dispose() => _dispose();
|
||||
}
|
||||
|
||||
/// Loads a paused first-frame surface for a video URL.
|
||||
typedef VideoPreviewFrameLoader =
|
||||
Future<LoadedVideoPreviewFrame?> Function(String url);
|
||||
|
||||
/// Loader used when an older video event has no NIP-71 poster URL.
|
||||
final videoPreviewFrameLoaderProvider = Provider<VideoPreviewFrameLoader>((
|
||||
ref,
|
||||
) {
|
||||
final auth = ref.watch(mediaGetAuthServiceProvider);
|
||||
return (url) => _loadVideoPreviewFrame(url, headers: auth.headersFor(url));
|
||||
});
|
||||
|
||||
class _MessageVideoPreview extends HookConsumerWidget {
|
||||
final String url;
|
||||
final ImetaEntry? imeta;
|
||||
final VoidCallback? onReply;
|
||||
|
||||
const _MessageVideoPreview({
|
||||
required this.url,
|
||||
required this.imeta,
|
||||
required this.onReply,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final rawAspectRatio = imeta?.aspectRatio ?? (16 / 9);
|
||||
final aspectRatio = rawAspectRatio.clamp(0.75, 1.91);
|
||||
final posterUrl = imeta?.posterUrl;
|
||||
final loadPreviewFrame = ref.watch(videoPreviewFrameLoaderProvider);
|
||||
final previewFrameFuture = useMemoized(
|
||||
() => posterUrl == null
|
||||
? loadPreviewFrame(url)
|
||||
: Future<LoadedVideoPreviewFrame?>.value(),
|
||||
[loadPreviewFrame, posterUrl, url],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
var disposed = false;
|
||||
LoadedVideoPreviewFrame? loadedFrame;
|
||||
unawaited(
|
||||
previewFrameFuture.then((frame) {
|
||||
if (disposed) {
|
||||
if (frame != null) unawaited(frame.dispose());
|
||||
} else {
|
||||
loadedFrame = frame;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return () {
|
||||
disposed = true;
|
||||
final frame = loadedFrame;
|
||||
if (frame != null) unawaited(frame.dispose());
|
||||
};
|
||||
}, [previewFrameFuture]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: Grid.half),
|
||||
child: GestureDetector(
|
||||
onTap: () => openVideoViewer(
|
||||
context,
|
||||
videoUrl: url,
|
||||
posterUrl: posterUrl,
|
||||
onReply: onReply,
|
||||
),
|
||||
child: _MessageMediaPreviewFrame(
|
||||
previewKey: ValueKey('message-media-video-preview:$url'),
|
||||
backgroundColor: Colors.black,
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio.toDouble(),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (posterUrl != null)
|
||||
MediaImage(
|
||||
url: posterUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) => _MediaPreviewFallback(
|
||||
icon: LucideIcons.video,
|
||||
label: context.l10n.videoPreviewUnavailable,
|
||||
),
|
||||
)
|
||||
else
|
||||
FutureBuilder<LoadedVideoPreviewFrame?>(
|
||||
future: previewFrameFuture,
|
||||
builder: (context, snapshot) {
|
||||
final frame = snapshot.data;
|
||||
if (frame == null) {
|
||||
return _MediaPreviewFallback(
|
||||
icon: LucideIcons.video,
|
||||
label: context.l10n.videoAttachment,
|
||||
);
|
||||
}
|
||||
return ColoredBox(
|
||||
key: ValueKey('message-media-video-first-frame:$url'),
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: frame.aspectRatio,
|
||||
child: frame.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const ColoredBox(color: Color.fromRGBO(0, 0, 0, 0.28)),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(0, 0, 0, 0.6),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.play,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: Grid.xxs,
|
||||
right: Grid.xxs,
|
||||
bottom: Grid.xxs,
|
||||
child: Text(
|
||||
context.l10n.video,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<LoadedVideoPreviewFrame?> _loadVideoPreviewFrame(
|
||||
String url, {
|
||||
required Map<String, String> headers,
|
||||
}) async {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || !uri.hasScheme) return null;
|
||||
|
||||
VideoPlayerController? controller;
|
||||
Future<void> disposeResources() async {
|
||||
final currentController = controller;
|
||||
controller = null;
|
||||
if (currentController != null) await currentController.dispose();
|
||||
}
|
||||
|
||||
Future<bool> initialize(VideoPlayerController candidate) async {
|
||||
controller = candidate;
|
||||
await candidate.initialize();
|
||||
final duration = candidate.value.duration;
|
||||
if (duration > const Duration(milliseconds: 100)) {
|
||||
await candidate.seekTo(const Duration(milliseconds: 100));
|
||||
}
|
||||
await candidate.pause();
|
||||
return candidate.value.isInitialized;
|
||||
}
|
||||
|
||||
try {
|
||||
final networkController = VideoPlayerController.networkUrl(
|
||||
uri,
|
||||
httpHeaders: headers,
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
if (!await initialize(networkController)) {
|
||||
await disposeResources();
|
||||
return null;
|
||||
}
|
||||
} on MissingPluginException {
|
||||
await disposeResources();
|
||||
return null;
|
||||
} on UnimplementedError {
|
||||
await disposeResources();
|
||||
return null;
|
||||
} catch (_) {
|
||||
// A timeline preview must stay bounded: falling back to a local file here
|
||||
// would fully download every posterless video encountered while scrolling.
|
||||
// The full-screen viewer can use its authenticated fallback after a tap.
|
||||
await disposeResources();
|
||||
return null;
|
||||
}
|
||||
|
||||
final initializedController = controller;
|
||||
if (initializedController == null) return null;
|
||||
var didDispose = false;
|
||||
return LoadedVideoPreviewFrame(
|
||||
aspectRatio: initializedController.value.aspectRatio > 0
|
||||
? initializedController.value.aspectRatio
|
||||
: 16 / 9,
|
||||
child: VideoPlayer(initializedController),
|
||||
dispose: () async {
|
||||
if (didDispose) return;
|
||||
didDispose = true;
|
||||
await disposeResources();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user