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,322 @@
|
||||
part of '../compose_bar.dart';
|
||||
|
||||
class _InlineCameraPreview extends HookConsumerWidget {
|
||||
final bool initializeCamera;
|
||||
final Future<void> Function(XFile image) onCapture;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _InlineCameraPreview({
|
||||
required this.initializeCamera,
|
||||
required this.onCapture,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useState<camera.CameraController?>(null);
|
||||
final controllerRef = useRef<camera.CameraController?>(null);
|
||||
final isInitializing = useState(true);
|
||||
final isCapturing = useState(false);
|
||||
final error = useState<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (!initializeCamera) return null;
|
||||
|
||||
var disposed = false;
|
||||
var generation = 0;
|
||||
|
||||
Future<void> disposeCurrent() async {
|
||||
generation += 1;
|
||||
final current = controllerRef.value;
|
||||
controllerRef.value = null;
|
||||
if (!disposed) controller.value = null;
|
||||
await current?.dispose();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
final currentGeneration = ++generation;
|
||||
if (!disposed) {
|
||||
isInitializing.value = true;
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
camera.CameraController? next;
|
||||
try {
|
||||
final available = await camera.availableCameras();
|
||||
if (disposed || currentGeneration != generation) return;
|
||||
if (available.isEmpty) {
|
||||
throw camera.CameraException(
|
||||
'no-cameras',
|
||||
context.l10n.cameraNoDevices,
|
||||
);
|
||||
}
|
||||
|
||||
final description = available.firstWhere(
|
||||
(candidate) =>
|
||||
candidate.lensDirection == camera.CameraLensDirection.back,
|
||||
orElse: () => available.first,
|
||||
);
|
||||
next = camera.CameraController(
|
||||
description,
|
||||
camera.ResolutionPreset.high,
|
||||
enableAudio: false,
|
||||
);
|
||||
await next.initialize();
|
||||
|
||||
if (disposed || currentGeneration != generation) {
|
||||
await next.dispose();
|
||||
return;
|
||||
}
|
||||
controllerRef.value = next;
|
||||
controller.value = next;
|
||||
isInitializing.value = false;
|
||||
} catch (cameraError) {
|
||||
await next?.dispose();
|
||||
if (!disposed && currentGeneration == generation) {
|
||||
error.value = _cameraErrorMessage(context, cameraError);
|
||||
isInitializing.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final lifecycleListener = AppLifecycleListener(
|
||||
onInactive: () => unawaited(disposeCurrent()),
|
||||
onResume: () => unawaited(initialize()),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!disposed) unawaited(initialize());
|
||||
});
|
||||
|
||||
return () {
|
||||
disposed = true;
|
||||
lifecycleListener.dispose();
|
||||
generation += 1;
|
||||
final current = controllerRef.value;
|
||||
controllerRef.value = null;
|
||||
unawaited(current?.dispose() ?? Future<void>.value());
|
||||
};
|
||||
}, [initializeCamera]);
|
||||
|
||||
Future<void> capture() async {
|
||||
final activeController = controller.value;
|
||||
if (activeController == null ||
|
||||
isCapturing.value ||
|
||||
activeController.value.isTakingPicture) {
|
||||
return;
|
||||
}
|
||||
|
||||
isCapturing.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
final image = await activeController.takePicture();
|
||||
if (context.mounted) {
|
||||
await processCapturedImage(image, onCapture);
|
||||
} else {
|
||||
await processCapturedImage(image, (_) async {});
|
||||
}
|
||||
} catch (captureError) {
|
||||
if (context.mounted) {
|
||||
error.value = _cameraErrorMessage(context, captureError);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) isCapturing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
final activeController = controller.value;
|
||||
final usesAndroidCameraLayout =
|
||||
defaultTargetPlatform == TargetPlatform.android;
|
||||
return ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (activeController case final initialized?)
|
||||
_CameraFeed(controller: initialized)
|
||||
else
|
||||
_CameraPlaceholder(
|
||||
isInitializing: isInitializing.value,
|
||||
message: error.value,
|
||||
),
|
||||
if (activeController != null)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Grid.twelve),
|
||||
child: _CameraCaptureButton(
|
||||
isPressed: isCapturing.value,
|
||||
onTap: capture,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: usesAndroidCameraLayout ? null : Grid.xxs,
|
||||
left: usesAndroidCameraLayout ? Grid.twelve : null,
|
||||
right: usesAndroidCameraLayout ? null : Grid.xxs,
|
||||
bottom: usesAndroidCameraLayout
|
||||
? Grid.twelve + ((_cameraCaptureSize - _cameraBackSize) / 2)
|
||||
: null,
|
||||
child: _CameraCloseButton(
|
||||
onTap: onClose,
|
||||
emphasized: usesAndroidCameraLayout,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraFeed extends StatelessWidget {
|
||||
final camera.CameraController controller;
|
||||
|
||||
const _CameraFeed({required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final previewSize = controller.value.previewSize;
|
||||
if (previewSize == null) return const ColoredBox(color: Colors.black);
|
||||
|
||||
return ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: previewSize.height,
|
||||
height: previewSize.width,
|
||||
child: camera.CameraPreview(controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraPlaceholder extends StatelessWidget {
|
||||
final bool isInitializing;
|
||||
final String? message;
|
||||
|
||||
const _CameraPlaceholder({
|
||||
required this.isInitializing,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Grid.sm),
|
||||
child: isInitializing
|
||||
? BuzzLoadingIndicator(
|
||||
size: 44,
|
||||
color: Colors.white,
|
||||
semanticLabel: context.l10n.startingCamera,
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.cameraOff,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
message ?? context.l10n.cameraUnavailable,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraCaptureButton extends StatelessWidget {
|
||||
final bool isPressed;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _CameraCaptureButton({required this.isPressed, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = MediaQuery.disableAnimationsOf(context)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 100);
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: context.l10n.takePhoto,
|
||||
child: GestureDetector(
|
||||
onTap: isPressed ? null : () => _runComposerAction(onTap),
|
||||
child: AnimatedScale(
|
||||
scale: isPressed ? 0.92 : 1,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Container(
|
||||
width: _cameraCaptureSize,
|
||||
height: _cameraCaptureSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.24),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
),
|
||||
padding: const EdgeInsets.all(Grid.half),
|
||||
child: const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraCloseButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
final bool emphasized;
|
||||
|
||||
const _CameraCloseButton({required this.onTap, required this.emphasized});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: emphasized ? _cameraBackSize : 36,
|
||||
child: IconButton(
|
||||
onPressed: () => _runComposerAction(onTap),
|
||||
tooltip: context.l10n.backAttachmentOptions,
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black.withValues(
|
||||
alpha: emphasized ? 0.68 : 0.56,
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
icon: Icon(LucideIcons.arrowLeft, size: emphasized ? 24 : 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _cameraCaptureSize = 64.0;
|
||||
const _cameraBackSize = 44.0;
|
||||
|
||||
String _cameraErrorMessage(BuildContext context, Object error) {
|
||||
if (error is camera.CameraException) {
|
||||
return switch (error.code) {
|
||||
'CameraAccessDenied' ||
|
||||
'CameraAccessDeniedWithoutPrompt' ||
|
||||
'CameraAccessRestricted' => context.l10n.cameraAccessDisabled,
|
||||
'no-cameras' => context.l10n.cameraUnavailableDevice,
|
||||
_ => context.l10n.cameraStartRetry,
|
||||
};
|
||||
}
|
||||
return context.l10n.cameraStartRetry;
|
||||
}
|
||||
Reference in New Issue
Block a user