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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
240 lines
8.0 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|