import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @RoutePage() class ShareIntentPage extends HookConsumerWidget { const ShareIntentPage({super.key, required this.attachments}); final List attachments; @override Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl() ?? '--'; final candidates = ref.watch(shareIntentUploadProvider); final isUploaded = useState(false); useOnAppLifecycleStateChange((previous, current) { if (current == AppLifecycleState.resumed) { isUploaded.value = false; } }); void removeAttachment(ShareIntentAttachment attachment) { ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment); } void addAttachments(List attachments) { ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments); } void upload() async { for (final attachment in candidates) { await ref.read(shareIntentUploadProvider.notifier).upload(attachment.file); } isUploaded.value = true; } bool isSelected(ShareIntentAttachment attachment) { return candidates.contains(attachment); } void toggleSelection(ShareIntentAttachment attachment) { if (isSelected(attachment)) { removeAttachment(attachment); } else { addAttachments([attachment]); } } return Scaffold( appBar: AppBar( title: Column( children: [ const Text('upload_to_immich').tr(namedArgs: {'count': candidates.length.toString()}), Text( currentEndpoint, style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), ], ), leading: IconButton( onPressed: () { context.navigateTo(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); }, icon: const Icon(Icons.arrow_back), ), ), body: ListView.builder( itemCount: attachments.length, itemBuilder: (context, index) { final attachment = attachments[index]; final target = candidates.firstWhere((element) => element.id == attachment.id, orElse: () => attachment); return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16), child: LargeLeadingTile( onTap: () => toggleSelection(attachment), disabled: isUploaded.value, selected: isSelected(attachment), leading: Stack( children: [ ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(16)), child: attachment.isImage ? Image.file(attachment.file, width: 64, height: 64, fit: BoxFit.cover) : const SizedBox( width: 64, height: 64, child: Center(child: Icon(Icons.videocam, color: Colors.white)), ), ), if (attachment.isImage) const Positioned( top: 8, right: 8, child: Icon( Icons.image, color: Colors.white, size: 20, shadows: [Shadow(offset: Offset(0, 0), blurRadius: 8.0, color: Colors.black45)], ), ), ], ), title: Text(attachment.fileName, style: context.textTheme.titleSmall), subtitle: Text(attachment.fileSize, style: context.textTheme.labelLarge), trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: UploadStatusIcon( selected: isSelected(attachment), status: target.status, progress: target.uploadProgress, ), ), ), ); }, ), bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( height: 48, child: ElevatedButton( onPressed: isUploaded.value ? null : upload, child: isUploaded.value ? UploadingText(candidates: candidates) : const Text('upload').tr(), ), ), ), ), ); } } class UploadingText extends StatelessWidget { const UploadingText({super.key, required this.candidates}); final List candidates; @override Widget build(BuildContext context) { final uploadedCount = candidates.where((element) { return element.status == UploadStatus.complete; }).length; return const Text( "shared_intent_upload_button_progress_text", ).tr(namedArgs: {'current': uploadedCount.toString(), 'total': candidates.length.toString()}); } } class UploadStatusIcon extends StatelessWidget { const UploadStatusIcon({super.key, required this.status, required this.selected, this.progress = 0}); final UploadStatus status; final double progress; final bool selected; @override Widget build(BuildContext context) { if (!selected) { return Icon( Icons.check_circle_outline_rounded, color: context.colorScheme.onSurface.withAlpha(100), semanticLabel: 'not_selected'.tr(), ); } final statusIcon = switch (status) { UploadStatus.enqueued => Icon( Icons.check_circle_rounded, color: context.primaryColor, semanticLabel: 'enqueued'.tr(), ), UploadStatus.running => Stack( alignment: AlignmentDirectional.center, children: [ SizedBox( width: 40, height: 40, child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: progress), duration: const Duration(milliseconds: 500), builder: (context, value, _) => CircularProgressIndicator( backgroundColor: context.colorScheme.surfaceContainerLow, strokeWidth: 3, value: value, semanticsLabel: 'uploading'.tr(), ), ), ), Text( (progress * 100).toStringAsFixed(0), style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.bold), ), ], ), UploadStatus.complete => Icon(Icons.check_circle_rounded, color: Colors.green, semanticLabel: 'completed'.tr()), UploadStatus.notFound || UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: 'failed'.tr()), UploadStatus.canceled => Icon(Icons.cancel_rounded, color: Colors.red, semanticLabel: 'canceled'.tr()), UploadStatus.waitingToRetry || UploadStatus.paused => Icon( Icons.pause_circle_rounded, color: context.primaryColor, semanticLabel: 'paused'.tr(), ), }; return statusIcon; } }