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,398 @@
|
||||
part of '../compose_bar.dart';
|
||||
|
||||
class _PhotoGalleryPicker extends StatelessWidget {
|
||||
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;
|
||||
|
||||
const _PhotoGalleryPicker({
|
||||
required this.onBack,
|
||||
required this.onPickAllPhotos,
|
||||
required this.onChoosePhotos,
|
||||
required this.onChooseAllPhotos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fallback = _RecentPhotoGalleryPicker(
|
||||
onBack: onBack,
|
||||
onPickAllPhotos: onPickAllPhotos,
|
||||
onChoosePhotos: onChoosePhotos,
|
||||
onChooseAllPhotos: onChooseAllPhotos,
|
||||
);
|
||||
if (defaultTargetPlatform != TargetPlatform.iOS) return fallback;
|
||||
return _IOSInlinePhotoPicker(
|
||||
onBack: onBack,
|
||||
onPickAllPhotos: onPickAllPhotos,
|
||||
onChoosePhotos: onChoosePhotos,
|
||||
onChooseAllPhotos: onChooseAllPhotos,
|
||||
fallback: fallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentPhotoGalleryPicker extends HookConsumerWidget {
|
||||
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;
|
||||
|
||||
const _RecentPhotoGalleryPicker({
|
||||
required this.onBack,
|
||||
required this.onPickAllPhotos,
|
||||
required this.onChoosePhotos,
|
||||
required this.onChooseAllPhotos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final recentPhotos = useMemoized(
|
||||
ref.read(photoLibraryProvider).loadRecentPhotos,
|
||||
);
|
||||
final recentSnapshot = useFuture(recentPhotos);
|
||||
final selection = useState<List<RecentPhoto>>([]);
|
||||
final isResolving = useState(false);
|
||||
final actionError = useState<String?>(null);
|
||||
final reducedMotion = MediaQuery.disableAnimationsOf(context);
|
||||
|
||||
void togglePhoto(RecentPhoto photo) {
|
||||
if (isResolving.value) return;
|
||||
final current = selection.value;
|
||||
final existingIndex = current.indexWhere((item) => item.id == photo.id);
|
||||
selection.value = existingIndex < 0
|
||||
? [...current, photo]
|
||||
: [
|
||||
...current.take(existingIndex),
|
||||
...current.skip(existingIndex + 1),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> choosePhotos() async {
|
||||
if (isResolving.value) return;
|
||||
isResolving.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
final photos = selection.value.isEmpty
|
||||
? await onPickAllPhotos()
|
||||
: await ref
|
||||
.read(photoLibraryProvider)
|
||||
.resolveSelectedPhotos(selection.value);
|
||||
if (photos.isNotEmpty && context.mounted) {
|
||||
await (selection.value.isEmpty ? onChooseAllPhotos : onChoosePhotos)(
|
||||
photos,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
actionError.value = selection.value.isEmpty
|
||||
? context.l10n.unablePhotoLibrary
|
||||
: context.l10n.preparingPhotos;
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) isResolving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
final selectedCount = selection.value.length;
|
||||
final actionLabel = selectedCount == 0
|
||||
? context.l10n.allPhotos
|
||||
: context.l10n.addPhoto(selectedCount);
|
||||
|
||||
Widget buildGalleryBody() {
|
||||
if (recentSnapshot.connectionState != ConnectionState.done) {
|
||||
return Center(
|
||||
child: BuzzLoadingIndicator(
|
||||
size: 44,
|
||||
semanticLabel: context.l10n.loadingRecentPhotos,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (recentSnapshot.hasError) {
|
||||
return _PhotoGalleryMessage(
|
||||
icon: LucideIcons.images,
|
||||
title: context.l10n.recentPhotosUnavailableTitle,
|
||||
message: context.l10n.recentPhotosUnavailableMessage,
|
||||
);
|
||||
}
|
||||
|
||||
final photos = recentSnapshot.data ?? const <RecentPhoto>[];
|
||||
if (photos.isEmpty) {
|
||||
return _PhotoGalleryMessage(
|
||||
icon: LucideIcons.images,
|
||||
title: context.l10n.noRecentPhotosTitle,
|
||||
message: context.l10n.noRecentPhotosMessage,
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
key: const ValueKey('recent-photo-grid'),
|
||||
padding: EdgeInsets.zero,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: Grid.quarter,
|
||||
mainAxisSpacing: Grid.quarter,
|
||||
),
|
||||
itemCount: photos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final photo = photos[index];
|
||||
final selectionIndex = selection.value.indexWhere(
|
||||
(item) => item.id == photo.id,
|
||||
);
|
||||
return _RecentPhotoTile(
|
||||
photo: photo,
|
||||
selectionIndex: selectionIndex,
|
||||
reducedMotion: reducedMotion,
|
||||
onTap: () => _runComposerAction(() => togglePhoto(photo)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
key: const ValueKey('photo-gallery-picker'),
|
||||
padding: const EdgeInsets.all(Grid.xxs),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
key: const ValueKey('photo-gallery-back'),
|
||||
onPressed: isResolving.value
|
||||
? null
|
||||
: () => _runComposerAction(onBack),
|
||||
tooltip: context.l10n.backAttachmentOptions,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(LucideIcons.arrowLeft, size: 20),
|
||||
),
|
||||
const SizedBox(width: Grid.quarter),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.recentPhotos,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selectedCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: Grid.half),
|
||||
child: Text(
|
||||
context.l10n.selectedCount(selectedCount),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.half),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: buildGalleryBody()),
|
||||
if (actionError.value case final error?) ...[
|
||||
const SizedBox(height: Grid.half),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 72),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
error,
|
||||
key: const ValueKey('photo-gallery-error'),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: selectedCount == 0
|
||||
? OutlinedButton.icon(
|
||||
key: const ValueKey('photo-gallery-action'),
|
||||
onPressed: isResolving.value
|
||||
? null
|
||||
: () => _runComposerAction(
|
||||
() => unawaited(choosePhotos()),
|
||||
),
|
||||
icon: isResolving.value
|
||||
? BuzzLoadingIndicator(
|
||||
size: 22,
|
||||
color: context.colors.primary,
|
||||
semanticLabel: context.l10n.openingAllPhotos,
|
||||
)
|
||||
: const Icon(LucideIcons.images, size: 18),
|
||||
label: Text(actionLabel),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
key: const ValueKey('photo-gallery-action'),
|
||||
onPressed: isResolving.value
|
||||
? null
|
||||
: () => _runComposerAction(
|
||||
() => unawaited(choosePhotos()),
|
||||
),
|
||||
icon: isResolving.value
|
||||
? BuzzLoadingIndicator(
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
semanticLabel: context.l10n.preparingPhotos,
|
||||
)
|
||||
: const Icon(LucideIcons.plus, size: 18),
|
||||
label: Text(actionLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentPhotoTile extends StatelessWidget {
|
||||
final RecentPhoto photo;
|
||||
final int selectionIndex;
|
||||
final bool reducedMotion;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _RecentPhotoTile({
|
||||
required this.photo,
|
||||
required this.selectionIndex,
|
||||
required this.reducedMotion,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelected = selectionIndex >= 0;
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
label: isSelected
|
||||
? context.l10n.photoSelected(selectionIndex + 1)
|
||||
: context.l10n.selectPhoto,
|
||||
child: GestureDetector(
|
||||
key: ValueKey('recent-photo-${photo.id}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(Radii.sm),
|
||||
child: Image.memory(
|
||||
photo.thumbnailBytes,
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: reducedMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOutCubic,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(Radii.sm),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? context.colors.primary
|
||||
: Colors.transparent,
|
||||
width: isSelected ? 3 : 0,
|
||||
),
|
||||
color: isSelected
|
||||
? Colors.black.withValues(alpha: 0.08)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: Grid.half,
|
||||
end: Grid.half,
|
||||
child: AnimatedScale(
|
||||
scale: isSelected ? 1 : 0.8,
|
||||
duration: reducedMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: AnimatedOpacity(
|
||||
opacity: isSelected ? 1 : 0,
|
||||
duration: reducedMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
key: ValueKey('photo-selection-index-${photo.id}'),
|
||||
width: 24,
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
),
|
||||
child: Text(
|
||||
'${selectionIndex + 1}',
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colors.onPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhotoGalleryMessage extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String message;
|
||||
|
||||
const _PhotoGalleryMessage({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Grid.sm),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 32, color: context.colors.onSurfaceVariant),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colors.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: Grid.quarter),
|
||||
Text(
|
||||
message,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user