Add information for uploading asset and error indication with error message for each failed upload. (#315)

* Added info box

* Fixed upload endpoint doesn't report error status code

* Added chip to show update error

* Added chip to show failed upload

* Add duplication check for upload

* Better duplication-checking placement

* Remove check for duplicated asset

* Added failed backup status route

* added page

* Display error card with thumbnail

* Improved styling

* Set thumbnail with better quality

* Remove force upload error
This commit is contained in:
Alex 2022-07-06 16:12:55 -05:00 committed by GitHub
parent 357f7d1c31
commit 58ec7553ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 706 additions and 87 deletions

View file

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -9,6 +10,7 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
import 'package:intl/intl.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class BackupControllerPage extends HookConsumerWidget {
@ -42,7 +44,7 @@ class BackupControllerPage extends HookConsumerWidget {
color: Theme.of(context).primaryColor,
),
title: const Text(
"Server Storage",
"Server storage",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Padding(
@ -56,7 +58,7 @@ class BackupControllerPage extends HookConsumerWidget {
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
barRadius: const Radius.circular(2),
lineHeight: 6.0,
lineHeight: 10.0,
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
backgroundColor: Colors.grey,
progressColor: Theme.of(context).primaryColor,
@ -246,6 +248,141 @@ class BackupControllerPage extends HookConsumerWidget {
);
}
_buildCurrentBackupAssetInfoCard() {
return ListTile(
leading: Icon(
Icons.info_outline_rounded,
color: Theme.of(context).primaryColor,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Uploading file info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
if (ref.watch(errorBackupListProvider).isNotEmpty)
ActionChip(
avatar: Icon(
Icons.info,
size: 24,
color: Colors.red[400],
),
elevation: 1,
visualDensity: VisualDensity.compact,
label: Text(
"Failed (${ref.watch(errorBackupListProvider).length})",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
backgroundColor: Colors.white,
onPressed: () {
AutoRouter.of(context).push(const FailedBackupStatusRoute());
},
),
],
),
subtitle: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: LinearPercentIndicator(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
barRadius: const Radius.circular(2),
lineHeight: 10.0,
trailing: Text(
" ${backupState.progressInPercentage.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12),
),
percent: backupState.progressInPercentage / 100.0,
backgroundColor: Colors.grey,
progressColor: Theme.of(context).primaryColor,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Table(
border: TableBorder.all(
color: Colors.black12,
width: 1,
),
children: [
TableRow(
decoration: BoxDecoration(
color: Colors.grey[100],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
'File name: ${backupState.currentUploadAsset.fileName} [${backupState.currentUploadAsset.fileType.toLowerCase()}]',
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 10.0),
),
),
),
],
),
TableRow(
decoration: BoxDecoration(
color: Colors.grey[200],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
"Created on: ${DateFormat.yMMMMd('en_US').format(
DateTime.parse(
backupState.currentUploadAsset.createdAt
.toString(),
),
)}",
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 10.0),
),
),
),
],
),
TableRow(
decoration: BoxDecoration(
color: Colors.grey[100],
),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
"ID: ${backupState.currentUploadAsset.id}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
),
),
),
],
),
],
),
),
],
),
);
}
void startBackup() {
ref.watch(errorBackupListProvider.notifier).empty();
ref.watch(backupProvider.notifier).startBackupProcess();
}
return Scaffold(
appBar: AppBar(
elevation: 0,
@ -264,7 +401,7 @@ class BackupControllerPage extends HookConsumerWidget {
)),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
child: ListView(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -297,23 +434,11 @@ class BackupControllerPage extends HookConsumerWidget {
const Divider(),
_buildStorageInformation(),
const Divider(),
_buildCurrentBackupAssetInfoCard(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Row(children: [
const Text("Backup Progress:"),
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
backupState.backupProgress == BackUpProgressEnum.inProgress
? const CircularProgressIndicator.adaptive()
: const Text("Done"),
]),
),
Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.only(
top: 24,
),
child: Container(
child:
backupState.backupProgress == BackUpProgressEnum.inProgress
@ -321,25 +446,33 @@ class BackupControllerPage extends HookConsumerWidget {
style: ElevatedButton.styleFrom(
primary: Colors.red[300],
onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14),
),
onPressed: () {
ref.read(backupProvider.notifier).cancelBackup();
},
child: const Text("Cancel"),
child: const Text(
"CANCEL",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
)
: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14),
),
onPressed: shouldBackup ? startBackup : null,
child: const Text(
"START BACKUP",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
onPressed: shouldBackup
? () {
ref
.read(backupProvider.notifier)
.startBackupProcess();
}
: null,
child: const Text("Start Backup"),
),
),
)

View file

@ -0,0 +1,139 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:intl/intl.dart';
import 'package:photo_manager/photo_manager.dart';
class FailedBackupStatusPage extends HookConsumerWidget {
const FailedBackupStatusPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final errorBackupList = ref.watch(errorBackupListProvider);
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(
"Failed Backup (${errorBackupList.length})",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
leading: IconButton(
onPressed: () {
AutoRouter.of(context).pop(true);
},
splashRadius: 24,
icon: const Icon(
Icons.arrow_back_ios_rounded,
)),
),
body: ListView.builder(
shrinkWrap: true,
itemCount: errorBackupList.length,
itemBuilder: ((context, index) {
var errorAsset = errorBackupList.elementAt(index);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 4,
),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), // if you need this
side: const BorderSide(
color: Colors.black12,
width: 1,
),
),
elevation: 0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
minHeight: 150,
maxWidth: 100,
maxHeight: 200,
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(15),
topLeft: Radius.circular(15),
),
clipBehavior: Clip.hardEdge,
child: Image(
fit: BoxFit.cover,
image: AssetEntityImageProvider(
errorAsset.asset,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(512),
thumbnailFormat: ThumbnailFormat.jpeg,
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
DateFormat.yMMMMd('en_US').format(
DateTime.parse(
errorAsset.createdAt.toString(),
),
),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[700]),
),
Icon(
Icons.error,
color: Colors.red.withAlpha(200),
size: 18,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
errorAsset.fileName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Theme.of(context).primaryColor,
),
),
),
Text(
errorAsset.errorMessage,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
),
],
),
),
)
],
),
),
);
}),
),
);
}
}