mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
41f013387f
add schemas sync variants formatting initial implementation use existing db, wip move to separate folder fix table definitions wip wiring it up
222 lines
7.9 KiB
Dart
222 lines
7.9 KiB
Dart
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<ShareIntentAttachment> 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<ShareIntentAttachment> attachments) {
|
|
ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments);
|
|
}
|
|
|
|
void upload() async {
|
|
await ref.read(shareIntentUploadProvider.notifier).upload(candidates);
|
|
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<ShareIntentAttachment> 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<double>(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(),
|
|
),
|
|
UploadStatus.preparing => Icon(
|
|
Icons.hourglass_top_rounded,
|
|
color: context.primaryColor,
|
|
semanticLabel: 'preparing'.tr(),
|
|
),
|
|
};
|
|
|
|
return statusIcon;
|
|
}
|
|
}
|