Files
buzz/mobile/lib/features/channels/compose_bar/ios_photo_picker.dart
T
cls 9dfa06ffee
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
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

240 lines
8.0 KiB
Dart

part of '../compose_bar.dart';
const _inlinePhotoPickerViewType = 'buzz/inline_photo_picker';
const _inlinePhotoPickerSupportChannel = MethodChannel(
'buzz/inline_photo_picker',
);
class _IOSInlinePhotoPicker extends HookWidget {
final VoidCallback onBack;
final Future<List<XFile>> Function() onPickAllPhotos;
final Future<void> Function(List<XFile> photos) onChoosePhotos;
final Future<void> Function(List<XFile> photos) onChooseAllPhotos;
final Widget fallback;
const _IOSInlinePhotoPicker({
required this.onBack,
required this.onPickAllPhotos,
required this.onChoosePhotos,
required this.onChooseAllPhotos,
required this.fallback,
});
@override
Widget build(BuildContext context) {
final supportFuture = useMemoized(
() async =>
await _inlinePhotoPickerSupportChannel.invokeMethod<bool>(
'isSupported',
) ??
false,
);
final support = useFuture(supportFuture);
final pickerChannel = useState<MethodChannel?>(null);
final selectedCount = useState(0);
final selectedPaths = useState<List<String>>(const []);
final isPreparingSelection = useState(false);
final isProcessing = useState(false);
useEffect(() {
final channel = pickerChannel.value;
if (channel == null) return null;
channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'selectionCountChanged':
selectedCount.value = call.arguments as int? ?? 0;
selectedPaths.value = const [];
isPreparingSelection.value = selectedCount.value > 0;
case 'selectionDidChange':
final paths = (call.arguments as List<Object?>? ?? const [])
.whereType<String>()
.toList();
selectedPaths.value = paths;
isPreparingSelection.value = false;
case 'didFail':
isPreparingSelection.value = false;
if (!context.mounted) return;
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(
content: Text(
call.arguments as String? ??
context.l10n.preparingPhotos,
),
),
);
}
});
return () => channel.setMethodCallHandler(null);
}, [pickerChannel.value]);
final canSelect =
selectedCount.value > 0 &&
selectedPaths.value.length == selectedCount.value &&
!isPreparingSelection.value &&
!isProcessing.value;
Future<void> submitSelection() async {
if (!canSelect) return;
isProcessing.value = true;
try {
final paths = List<String>.from(selectedPaths.value);
final claimed =
await pickerChannel.value?.invokeMethod<bool>(
'claimSelection',
paths,
) ??
false;
if (!claimed) return;
final photos = [for (final path in paths) XFile(path)];
await processTemporaryImages(photos, onChoosePhotos);
} finally {
if (context.mounted) isProcessing.value = false;
}
}
Future<void> openAllPhotos() async {
if (isPreparingSelection.value || isProcessing.value) return;
isProcessing.value = true;
try {
final photos = await onPickAllPhotos();
if (photos.isNotEmpty) {
await onChooseAllPhotos(photos);
}
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(content: Text(context.l10n.unablePhotoLibrary)),
);
}
} finally {
if (context.mounted) isProcessing.value = false;
}
}
if (support.connectionState != ConnectionState.done) {
return const _NativePhotoPickerLoading();
}
if (support.hasError || support.data != true) return fallback;
return SizedBox(
key: const ValueKey('ios-inline-photo-picker'),
height: _attachmentExpandedHeight,
width: double.infinity,
child: Stack(
fit: StackFit.expand,
children: [
UiKitView(
viewType: _inlinePhotoPickerViewType,
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (viewId) {
pickerChannel.value = MethodChannel(
'buzz/inline_photo_picker/$viewId',
);
},
),
PositionedDirectional(
start: Grid.twelve,
bottom: Grid.twelve,
child: SafeArea(
top: false,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.62),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.28),
),
),
child: IconButton(
key: const ValueKey('ios-inline-photo-picker-back'),
onPressed: isProcessing.value
? null
: () => _runComposerAction(onBack),
tooltip: context.l10n.backAttachmentOptions,
icon: const Icon(
LucideIcons.chevronLeft,
color: Colors.white,
),
),
),
),
),
PositionedDirectional(
end: Grid.twelve,
bottom: Grid.twelve,
child: SafeArea(
top: false,
child: FilledButton(
key: const ValueKey('ios-inline-photo-picker-select'),
onPressed: canSelect
? () =>
_runComposerAction(() => unawaited(submitSelection()))
: selectedCount.value == 0 &&
!isPreparingSelection.value &&
!isProcessing.value
? () => _runComposerAction(() => unawaited(openAllPhotos()))
: null,
style: FilledButton.styleFrom(
backgroundColor: Colors.black.withValues(alpha: 0.76),
disabledBackgroundColor: Colors.black.withValues(alpha: 0.32),
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white70,
minimumSize: const Size(0, 48),
padding: const EdgeInsets.symmetric(horizontal: Grid.twelve),
shape: const StadiumBorder(),
side: BorderSide(color: Colors.white.withValues(alpha: 0.28)),
),
child: isPreparingSelection.value
? SizedBox.square(
dimension: 20,
child: BuzzLoadingIndicator(
size: 20,
color: Colors.white,
semanticLabel: context.l10n.preparingPhotos,
),
)
: Text(
selectedCount.value == 0
? context.l10n.allPhotos
: context.l10n.addPhoto(selectedCount.value),
),
),
),
),
if (isProcessing.value)
ColoredBox(
color: Color.fromRGBO(0, 0, 0, 0.28),
child: Center(
child: BuzzLoadingIndicator(
size: 44,
color: Colors.white,
semanticLabel: context.l10n.preparingPhotos,
),
),
),
],
),
);
}
}
class _NativePhotoPickerLoading extends StatelessWidget {
const _NativePhotoPickerLoading();
@override
Widget build(BuildContext context) {
return SizedBox(
key: const ValueKey('ios-inline-photo-picker-loading'),
height: _attachmentExpandedHeight,
width: double.infinity,
child: Center(
child: BuzzLoadingIndicator(
size: 44,
semanticLabel: context.l10n.openingPhotos,
),
),
);
}
}