mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: new upload (cont) (#20029)
* new upload button * wip * pr feedback * fix: updateAll override album selection value * feat: status box * feat: handle upload resume * re-enable websocket event * fix: update state condition and upload status * Better backup detail page --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
1dc62fce5f
commit
4d27f187ea
26 changed files with 1558 additions and 413 deletions
428
mobile/lib/pages/backup/drift_upload_detail.page.dart
Normal file
428
mobile/lib/pages/backup/drift_upload_detail.page.dart
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
@RoutePage()
|
||||
class DriftUploadDetailPage extends ConsumerWidget {
|
||||
const DriftUploadDetailPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final uploadItems = ref.watch(
|
||||
driftBackupProvider.select((state) => state.uploadItems),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("upload_details".t(context: context)),
|
||||
backgroundColor: context.colorScheme.surface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
),
|
||||
body: uploadItems.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: _buildUploadList(uploadItems),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_upload_outlined,
|
||||
size: 80,
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"no_uploads_in_progress".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadList(
|
||||
Map<String, DriftUploadStatus> uploadItems,
|
||||
) {
|
||||
return ListView.separated(
|
||||
addAutomaticKeepAlives: true,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: uploadItems.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final item = uploadItems.values.elementAt(index);
|
||||
return _buildUploadCard(context, item);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadCard(
|
||||
BuildContext context,
|
||||
DriftUploadStatus item,
|
||||
) {
|
||||
final isCompleted = item.progress >= 1.0;
|
||||
final double progressPercentage = (item.progress * 100).clamp(0, 100);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _showFileDetailDialog(context, item),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
path.basename(item.filename),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tap for more details',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildProgressIndicator(
|
||||
context,
|
||||
item.progress,
|
||||
progressPercentage,
|
||||
isCompleted,
|
||||
item.networkSpeedAsString,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator(
|
||||
BuildContext context,
|
||||
double progress,
|
||||
double percentage,
|
||||
bool isCompleted,
|
||||
String networkSpeedAsString,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween<double>(begin: 0.0, end: progress),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
backgroundColor:
|
||||
context.colorScheme.outline.withValues(alpha: 0.2),
|
||||
strokeWidth: 3,
|
||||
value: value,
|
||||
color: isCompleted
|
||||
? context.colorScheme.primary
|
||||
: context.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 28,
|
||||
color: context.colorScheme.primary,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
percentage.toStringAsFixed(0),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
networkSpeedAsString,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showFileDetailDialog(
|
||||
BuildContext context,
|
||||
DriftUploadStatus item,
|
||||
) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FileDetailDialog(uploadStatus: item),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FileDetailDialog extends ConsumerWidget {
|
||||
final DriftUploadStatus uploadStatus;
|
||||
|
||||
const FileDetailDialog({
|
||||
super.key,
|
||||
required this.uploadStatus,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AlertDialog(
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: context.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"details".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: FutureBuilder<LocalAsset?>(
|
||||
future: _getAssetDetails(ref, uploadStatus.taskId),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 200,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final asset = snapshot.data;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Thumbnail at the top center
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Container(
|
||||
width: 128,
|
||||
height: 128,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline
|
||||
.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: asset != null
|
||||
? Thumbnail(
|
||||
asset: asset,
|
||||
size: const Size(512, 512),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (asset != null) ...[
|
||||
_buildInfoSection(context, [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Filename",
|
||||
path.basename(uploadStatus.filename),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Local ID",
|
||||
asset.id,
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"File Size",
|
||||
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||
),
|
||||
if (asset.width != null)
|
||||
_buildInfoRow(context, "Width", "${asset.width}px"),
|
||||
if (asset.height != null)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Height",
|
||||
"${asset.height}px",
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Created At",
|
||||
asset.createdAt.toString(),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Updated At",
|
||||
asset.updatedAt.toString(),
|
||||
),
|
||||
if (asset.checksum != null)
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"Checksum",
|
||||
asset.checksum!,
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
"close".t(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoSection(
|
||||
BuildContext context,
|
||||
List<Widget> children,
|
||||
) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...children,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
"$label:",
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.labelMedium?.copyWith(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<LocalAsset?> _getAssetDetails(
|
||||
WidgetRef ref,
|
||||
String localAssetId,
|
||||
) async {
|
||||
try {
|
||||
final repository = ref.read(localAssetRepository);
|
||||
return await repository.getById(localAssetId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue