refactor(mobile): widgets (#9291)

* refactor(mobile): widgets

* update
This commit is contained in:
Alex 2024-05-06 23:04:21 -05:00 committed by GitHub
parent 7520ffd6c3
commit 5806a3ce25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
203 changed files with 318 additions and 318 deletions

View file

@ -0,0 +1,217 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoCard({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode(
context.primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
buildSelectedTextBox() {
if (isSelected) {
return Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: Text(
"album_info_card_backup_album_included",
style: TextStyle(
fontSize: 10,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: context.primaryColor,
);
} else if (isExcluded) {
return Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: Text(
"album_info_card_backup_album_excluded",
style: TextStyle(
fontSize: 10,
color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: Colors.red[300],
);
}
return const SizedBox();
}
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
return GestureDetector(
onTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
}
},
onDoubleTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: Card(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // if you need this
side: BorderSide(
color: isDarkTheme
? const Color.fromARGB(255, 37, 35, 35)
: const Color(0xFFC9C9C9),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
ColorFiltered(
colorFilter: buildImageFilter(),
child: const Image(
width: double.infinity,
height: double.infinity,
image: AssetImage(
'assets/immich-logo.png',
),
fit: BoxFit.cover,
),
),
Positioned(
bottom: 10,
right: 25,
child: buildSelectedTextBox(),
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 25,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
album.name,
style: TextStyle(
fontSize: 14,
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: FutureBuilder(
builder: ((context, snapshot) {
if (snapshot.hasData) {
return Text(
snapshot.data.toString() +
(album.isAll
? " (${'backup_all'.tr()})"
: ""),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
);
}
return const Text("0");
}),
future: album.assetCount,
),
),
],
),
),
IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: album.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 24,
),
splashRadius: 25,
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,128 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoListTile({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
var assetCount = useState(0);
useEffect(
() {
album.assetCount.then((value) => assetCount.value = value);
return null;
},
[album],
);
buildTileColor() {
if (isSelected) {
return context.isDarkTheme
? context.primaryColor.withAlpha(100)
: context.primaryColor.withAlpha(25);
} else if (isExcluded) {
return context.isDarkTheme
? Colors.red[300]?.withAlpha(150)
: Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
}
buildIcon() {
if (isSelected) {
return const Icon(
Icons.check_circle_rounded,
color: Colors.green,
);
}
if (isExcluded) {
return const Icon(
Icons.remove_circle_rounded,
color: Colors.red,
);
}
return Icon(
Icons.circle,
color: context.isDarkTheme ? Colors.grey[400] : Colors.black45,
);
}
return GestureDetector(
onDoubleTap: () {
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref.read(backupProvider.notifier).removeExcludedAlbumForBackup(album);
} else {
// Add to exclude album list
if (album.id == 'isAll' || album.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(backupProvider.notifier).addExcludedAlbumForBackup(album);
}
},
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
if (isSelected) {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
}
},
leading: buildIcon(),
title: Text(
album.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(assetCount.value.toString()),
trailing: IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: album.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 24,
),
splashRadius: 25,
),
),
);
}
}

View file

@ -0,0 +1,60 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class BackupInfoCard extends StatelessWidget {
final String title;
final String subtitle;
final String info;
const BackupInfoCard({
super.key,
required this.title,
required this.subtitle,
required this.info,
});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: context.isDarkTheme
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: ListTile(
minVerticalPadding: 18,
isThreeLine: true,
title: Text(
title,
style: context.textTheme.titleMedium,
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0, right: 18.0),
child: Text(
subtitle,
style: context.textTheme.bodyMedium,
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
info,
style: context.textTheme.titleLarge,
),
Text(
"backup_info_card_assets",
style: context.textTheme.labelLarge,
).tr(),
],
),
),
);
}
}

View file

@ -0,0 +1,315 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart';
class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
var isManualUpload = ref.watch(backupProvider).backupProgress ==
BackUpProgressEnum.manualInProgress;
var asset = !isManualUpload
? ref.watch(backupProvider).currentUploadAsset
: ref.watch(manualUploadProvider).currentUploadAsset;
var uploadProgress = !isManualUpload
? ref.watch(backupProvider).progressInPercentage
: ref.watch(manualUploadProvider).progressInPercentage;
var uploadFileProgress = !isManualUpload
? ref.watch(backupProvider).progressInFileSize
: ref.watch(manualUploadProvider).progressInFileSize;
var uploadFileSpeed = !isManualUpload
? ref.watch(backupProvider).progressInFileSpeed
: ref.watch(manualUploadProvider).progressInFileSpeed;
var iCloudDownloadProgress =
ref.watch(backupProvider).iCloudDownloadProgress;
final isShowThumbnail = useState(false);
String formatUploadFileSpeed(double uploadFileSpeed) {
if (uploadFileSpeed < 1024) {
return '${uploadFileSpeed.toStringAsFixed(2)} B/s';
} else if (uploadFileSpeed < 1024 * 1024) {
return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s';
} else if (uploadFileSpeed < 1024 * 1024 * 1024) {
return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s';
} else {
return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s';
}
}
String getAssetCreationDate() {
return DateFormat.yMMMMd().format(
DateTime.parse(
asset.fileCreatedAt.toString(),
).toLocal(),
);
}
Widget buildErrorChip() {
return ActionChip(
avatar: Icon(
Icons.info,
color: Colors.red[400],
),
elevation: 1,
visualDensity: VisualDensity.compact,
label: Text(
"backup_controller_page_failed",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
fontSize: 11,
),
).tr(
args: [ref.watch(errorBackupListProvider).length.toString()],
),
backgroundColor: Colors.white,
onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
);
}
Widget buildAssetInfoTable() {
return Table(
border: TableBorder.all(
color: context.themeData.primaryColorLight,
width: 1,
),
children: [
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
'backup_controller_page_filename',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [asset.fileName, asset.fileType.toLowerCase()],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[200],
),
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_created",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
args: [getAssetCreationDate()],
),
),
),
],
),
TableRow(
decoration: const BoxDecoration(
// color: Colors.grey[100],
),
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: const Text(
"backup_controller_page_id",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(args: [asset.id]),
),
),
],
),
],
);
}
buildAssetThumbnail() async {
var assetEntity = await AssetEntity.fromId(asset.id);
if (assetEntity != null) {
return assetEntity.thumbnailDataWithSize(
const ThumbnailSize(500, 500),
quality: 100,
);
}
}
buildiCloudDownloadProgerssBar() {
if (asset.iCloudAsset != null && asset.iCloudAsset!) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
SizedBox(
width: 110,
child: Text(
"iCloud Download",
style: context.textTheme.labelSmall,
),
),
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
value: uploadProgress / 100.0,
backgroundColor: Colors.grey,
color: context.primaryColor,
),
),
Text(
" ${iCloudDownloadProgress.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12),
),
],
),
);
}
return const SizedBox();
}
buildUploadProgressBar() {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
if (asset.iCloudAsset != null && asset.iCloudAsset!)
SizedBox(
width: 110,
child: Text(
"Immich Upload",
style: context.textTheme.labelSmall,
),
),
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
value: uploadProgress / 100.0,
backgroundColor: Colors.grey,
color: context.primaryColor,
),
),
Text(
" ${uploadProgress.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
),
],
),
);
}
buildUploadStats() {
return Padding(
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
uploadFileProgress,
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
),
Text(
formatUploadFileSpeed(uploadFileSpeed),
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
),
],
),
);
}
return FutureBuilder<Uint8List?>(
future: buildAssetThumbnail(),
builder: (context, thumbnail) => ListTile(
isThreeLine: true,
leading: AnimatedCrossFade(
alignment: Alignment.centerLeft,
firstChild: GestureDetector(
onTap: () => isShowThumbnail.value = false,
child: thumbnail.hasData
? ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image.memory(
thumbnail.data!,
fit: BoxFit.cover,
width: 50,
height: 50,
),
)
: const SizedBox(
width: 50,
height: 50,
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator.adaptive(
strokeWidth: 1,
),
),
),
),
secondChild: GestureDetector(
onTap: () => isShowThumbnail.value = true,
child: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 30,
),
),
crossFadeState: isShowThumbnail.value
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 200),
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"backup_controller_page_uploading_file_info",
style: context.textTheme.titleSmall,
).tr(),
if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(),
],
),
subtitle: Column(
children: [
if (Platform.isIOS) buildiCloudDownloadProgerssBar(),
buildUploadProgressBar(),
buildUploadStats(),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: buildAssetInfoTable(),
),
],
),
),
);
}
}

View file

@ -0,0 +1,62 @@
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/providers/backup/ios_background_settings.provider.dart';
import 'package:intl/intl.dart';
/// This is a simple debug widget which should be removed later on when we are
/// more confident about background sync
class IosDebugInfoTile extends HookConsumerWidget {
final IOSBackgroundSettings settings;
const IosDebugInfoTile({
super.key,
required this.settings,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fetch = settings.timeOfLastFetch;
final processing = settings.timeOfLastProcessing;
final processes = settings.numberOfBackgroundTasksQueued;
final processOrProcesses = processes == 1 ? 'process' : 'processes';
final numberOrZero = processes == 0 ? 'No' : processes.toString();
final title = '$numberOrZero background $processOrProcesses queued';
final df = DateFormat.yMd().add_jm();
final String subtitle;
if (fetch == null && processing == null) {
subtitle = 'No background sync job has run yet';
} else if (fetch != null && processing == null) {
subtitle = 'Fetch ran ${df.format(fetch)}';
} else if (processing != null && fetch == null) {
subtitle = 'Processing ran ${df.format(processing)}';
} else {
final fetchOrProcessing =
fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
}
return ListTile(
key: ValueKey(title),
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: context.primaryColor,
),
),
subtitle: Text(
subtitle,
style: const TextStyle(
fontSize: 14,
),
),
leading: Icon(
Icons.bug_report,
color: context.primaryColor,
),
);
}
}