chore: bump dart sdk to 3.8 (#20355)

* chore: bump dart sdk to 3.8

* chore: make build

* make pigeon

* chore: format files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2025-07-29 00:34:03 +05:30
committed by GitHub
parent 9b3718120b
commit e52b9d15b5
643 changed files with 32561 additions and 35292 deletions
@@ -13,12 +13,7 @@ class ActivityTextField extends HookConsumerWidget {
final String? likeId;
final Function(String) onSubmit;
const ActivityTextField({
required this.onSubmit,
this.isEnabled = true,
this.likeId,
super.key,
});
const ActivityTextField({required this.onSubmit, this.isEnabled = true, this.likeId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -31,13 +26,10 @@ class ActivityTextField extends HookConsumerWidget {
final liked = likeId != null;
// Show keyboard immediately on activities open
useEffect(
() {
inputFocusNode.requestFocus();
return null;
},
[],
);
useEffect(() {
inputFocusNode.requestFocus();
return null;
}, []);
// Pass text to callback and reset controller
void onEditingComplete() {
@@ -70,29 +62,19 @@ class ActivityTextField extends HookConsumerWidget {
prefixIcon: user != null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: UserCircleAvatar(
user: user,
size: 30,
radius: 15,
),
child: UserCircleAvatar(user: user, size: 30, radius: 15),
)
: null,
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
icon: Icon(
liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
),
icon: Icon(liked ? Icons.favorite_rounded : Icons.favorite_border_rounded),
onPressed: liked ? removeLike : addLike,
),
),
suffixIconColor: liked ? Colors.red[700] : null,
hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(),
hintStyle: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
color: Colors.grey[600],
),
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
),
onEditingComplete: onEditingComplete,
onTapOutside: (_) => inputFocusNode.unfocus(),
@@ -26,10 +26,7 @@ class ActivityTile extends HookConsumerWidget {
? Container(
width: 44,
alignment: Alignment.center,
child: Icon(
Icons.favorite_rounded,
color: Colors.red[700],
),
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
)
: UserCircleAvatar(user: activity.user),
title: _ActivityTitle(
@@ -50,11 +47,7 @@ class _ActivityTitle extends StatelessWidget {
final String createdAt;
final bool leftAlign;
const _ActivityTitle({
required this.userName,
required this.createdAt,
required this.leftAlign,
});
const _ActivityTitle({required this.userName, required this.createdAt, required this.leftAlign});
@override
Widget build(BuildContext context) {
@@ -65,16 +58,8 @@ class _ActivityTitle extends StatelessWidget {
mainAxisAlignment: leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween,
mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max,
children: [
Text(
userName,
style: textStyle,
overflow: TextOverflow.ellipsis,
),
if (leftAlign)
Text(
"",
style: textStyle,
),
Text(userName, style: textStyle, overflow: TextOverflow.ellipsis),
if (leftAlign) Text("", style: textStyle),
Expanded(
child: Text(
createdAt,
@@ -101,9 +86,7 @@ class _ActivityAssetThumbnail extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
image: DecorationImage(
image: ImmichRemoteThumbnailProvider(
assetId: assetId,
),
image: ImmichRemoteThumbnailProvider(assetId: assetId),
fit: BoxFit.cover,
),
),
@@ -8,20 +8,13 @@ class DismissibleActivity extends StatelessWidget {
final ActivityTile body;
final Function(String)? onDismiss;
const DismissibleActivity(
this.activityId,
this.body, {
this.onDismiss,
super.key,
});
const DismissibleActivity(this.activityId, this.body, {this.onDismiss, super.key});
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key(activityId),
dismissThresholds: const {
DismissDirection.horizontal: 0.7,
},
dismissThresholds: const {DismissDirection.horizontal: 0.7},
direction: DismissDirection.horizontal,
confirmDismiss: (direction) => onDismiss != null
? showDialog(
@@ -51,10 +44,7 @@ class _DismissBackground extends StatelessWidget {
final AlignmentDirectional alignment;
final bool withDeleteIcon;
const _DismissBackground({
required this.withDeleteIcon,
this.alignment = AlignmentDirectional.centerStart,
});
const _DismissBackground({required this.withDeleteIcon, this.alignment = AlignmentDirectional.centerStart});
@override
Widget build(BuildContext context) {
@@ -64,10 +54,7 @@ class _DismissBackground extends StatelessWidget {
child: withDeleteIcon
? const Padding(
padding: EdgeInsets.all(15),
child: Icon(
Icons.delete_sweep_rounded,
color: Colors.black,
),
child: Icon(Icons.delete_sweep_rounded, color: Colors.black),
)
: null,
);
@@ -17,46 +17,33 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
/// The asset to add to an album
final List<Asset> assets;
const AddToAlbumBottomSheet({
super.key,
required this.assets,
});
const AddToAlbumBottomSheet({super.key, required this.assets});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
final albumService = ref.watch(albumServiceProvider);
useEffect(
() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).refreshRemoteAlbums();
useEffect(() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).refreshRemoteAlbums();
return null;
},
[],
);
return null;
}, []);
void addToAlbum(Album album) async {
final result = await albumService.addAssets(
album,
assets,
);
final result = await albumService.addAssets(album, assets);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
namedArgs: {"album": album.name},
),
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
);
} else {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(
namedArgs: {"album": album.name},
),
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}),
);
}
}
@@ -66,10 +53,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
return Card(
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
),
child: CustomScrollView(
slivers: [
@@ -80,33 +64,17 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Align(
alignment: Alignment.center,
child: CustomDraggingHandle(),
),
const Align(alignment: Alignment.center, child: CustomDraggingHandle()),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'add_to_album'.tr(),
style: context.textTheme.displayMedium,
),
Text('add_to_album'.tr(), style: context.textTheme.displayMedium),
TextButton.icon(
icon: Icon(
Icons.add,
color: context.primaryColor,
),
label: Text(
'common_create_new_album'.tr(),
style: TextStyle(color: context.primaryColor),
),
icon: Icon(Icons.add, color: context.primaryColor),
label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)),
onPressed: () {
context.pushRoute(
CreateAlbumRoute(
assets: assets,
),
);
context.pushRoute(CreateAlbumRoute(assets: assets));
},
),
],
@@ -28,8 +28,10 @@ class AddToAlbumSliverList extends HookConsumerWidget {
final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse);
return SliverList(
delegate:
SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), (context, index) {
delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), (
context,
index,
) {
// Build shared expander
if (index == 0 && sortedSharedAlbums.isNotEmpty) {
return Padding(
@@ -56,10 +58,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
// Build albums list
final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0);
final album = sortedAlbums[offset];
return AlbumThumbnailListTile(
album: album,
onTap: enabled ? () => onAddToAlbum(album) : () {},
);
return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {});
}),
);
}
@@ -6,12 +6,7 @@ class AlbumActionFilledButton extends StatelessWidget {
final String labelText;
final IconData iconData;
const AlbumActionFilledButton({
super.key,
this.onPressed,
required this.labelText,
required this.iconData,
});
const AlbumActionFilledButton({super.key, this.onPressed, required this.labelText, required this.iconData});
@override
Widget build(BuildContext context) {
@@ -20,24 +15,12 @@ class AlbumActionFilledButton extends StatelessWidget {
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
side: BorderSide(
color: context.colorScheme.surfaceContainerHighest,
width: 1,
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
side: BorderSide(color: context.colorScheme.surfaceContainerHighest, width: 1),
backgroundColor: context.colorScheme.surfaceContainerHigh,
),
icon: Icon(
iconData,
size: 18,
color: context.primaryColor,
),
label: Text(
labelText,
style: context.textTheme.labelLarge?.copyWith(),
),
icon: Icon(iconData, size: 18, color: context.primaryColor),
label: Text(labelText, style: context.textTheme.labelLarge?.copyWith()),
onPressed: onPressed,
),
);
@@ -16,13 +16,7 @@ class AlbumThumbnailCard extends ConsumerWidget {
final bool showOwner;
final bool showTitle;
const AlbumThumbnailCard({
super.key,
required this.album,
this.onTap,
this.showOwner = false,
this.showTitle = true,
});
const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true});
final Album album;
@@ -36,24 +30,14 @@ class AlbumThumbnailCard extends ConsumerWidget {
return Container(
height: cardSize,
width: cardSize,
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerHigh,
),
decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh),
child: Center(
child: Icon(
Icons.no_photography,
size: cardSize * .15,
color: context.colorScheme.primary,
),
child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary),
),
);
}
buildAlbumThumbnail() => ImmichThumbnail(
asset: album.thumbnail.value,
width: cardSize,
height: cardSize,
);
buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize);
buildAlbumTextRow() {
// Add the owner name to the subtitle
@@ -62,12 +46,7 @@ class AlbumThumbnailCard extends ConsumerWidget {
if (album.ownerId == ref.read(currentUserProvider)?.id) {
owner = 'owned'.tr();
} else if (album.ownerName != null) {
owner = 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName!,
},
);
owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!});
}
}
@@ -75,19 +54,12 @@ class AlbumThumbnailCard extends ConsumerWidget {
TextSpan(
children: [
TextSpan(
text: 'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
),
text: 'items_count'.t(context: context, args: {'count': album.assetCount}),
),
if (owner != null) const TextSpan(text: ''),
if (owner != null) TextSpan(text: owner),
],
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
overflow: TextOverflow.fade,
);
@@ -106,9 +78,7 @@ class AlbumThumbnailCard extends ConsumerWidget {
width: cardSize,
height: cardSize,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(),
),
),
@@ -11,11 +11,7 @@ import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumThumbnailListTile extends StatelessWidget {
const AlbumThumbnailListTile({
super.key,
required this.album,
this.onTap,
});
const AlbumThumbnailListTile({super.key, required this.album, this.onTap});
final Album album;
final void Function()? onTap;
@@ -26,15 +22,11 @@ class AlbumThumbnailListTile extends StatelessWidget {
buildEmptyThumbnail() {
return Container(
decoration: BoxDecoration(
color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200],
),
decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]),
child: SizedBox(
height: cardSize,
width: cardSize,
child: const Center(
child: Icon(Icons.no_photography),
),
child: const Center(child: Icon(Icons.no_photography)),
),
);
}
@@ -45,10 +37,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(
album,
type: AssetMediaSize.thumbnail,
),
imageUrl: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail),
httpHeaders: ApiService.getRequestHeaders(),
cacheKey: getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail),
errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined),
@@ -57,7 +46,8 @@ class AlbumThumbnailListTile extends StatelessWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap ??
onTap:
onTap ??
() {
context.pushRoute(AlbumViewerRoute(albumId: album.id));
},
@@ -79,37 +69,18 @@ class AlbumThumbnailListTile extends StatelessWidget {
Text(
album.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
),
style: const TextStyle(
fontSize: 12,
),
'items_count'.t(context: context, args: {'count': album.assetCount}),
style: const TextStyle(fontSize: 12),
),
if (album.shared) ...[
const Text(
'',
style: TextStyle(
fontSize: 12,
),
),
Text(
'shared'.tr(),
style: const TextStyle(
fontSize: 12,
),
),
const Text('', style: TextStyle(fontSize: 12)),
Text('shared'.tr(), style: const TextStyle(fontSize: 12)),
],
],
),
@@ -31,11 +31,7 @@ class AlbumTitleTextField extends ConsumerWidget {
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
},
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(
fontSize: 28,
color: context.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold),
controller: albumTitleController,
onTap: () {
isAlbumTitleTextFieldFocus.value = true;
@@ -52,24 +48,17 @@ class AlbumTitleTextField extends ConsumerWidget {
albumTitleController.clear();
isAlbumTitleEmpty.value = true;
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
hintText: 'add_a_title'.tr(),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
@@ -82,10 +82,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
onPressed: () => context.pop('Cancel'),
child: Text(
'cancel',
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
@@ -95,10 +92,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
},
child: Text(
'confirm',
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.colorScheme.error,
),
style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error),
).tr(),
),
],
@@ -128,10 +122,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
album.ownerId == userId
? ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'delete_album',
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(),
onTap: onDeleteAlbumPressed,
)
: ListTile(
@@ -172,18 +163,12 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
onAddUsers();
}
},
title: const Text(
"album_viewer_page_share_add_users",
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.swap_vert_rounded),
onTap: onSortOrderToggled,
title: const Text(
"change_display_order",
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.link_rounded),
@@ -191,18 +176,12 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId));
context.pop();
},
title: const Text(
"control_bottom_app_bar_share_link",
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.settings_rounded),
onTap: () => context.navigateTo(const AlbumOptionsRoute()),
title: const Text(
"options",
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
];
@@ -216,10 +195,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
onAddPhotos();
}
},
title: const Text(
"add_photos",
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
];
showModalBottomSheet(
@@ -250,18 +226,13 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.mode_comment_outlined,
),
const Icon(Icons.mode_comment_outlined),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor),
),
),
],
@@ -285,8 +256,9 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
}
titleFocusNode.unfocus();
} else if (newAlbumDescription.isNotEmpty) {
bool isSuccessDescription =
await ref.watch(albumViewerProvider.notifier).changeAlbumDescription(album, newAlbumDescription);
bool isSuccessDescription = await ref
.watch(albumViewerProvider.notifier)
.changeAlbumDescription(album, newAlbumDescription);
if (!isSuccessDescription) {
ImmichToast.show(
context: context,
@@ -322,11 +294,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
actions: [
if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(),
if (album.isRemote) ...[
IconButton(
splashRadius: 25,
onPressed: buildBottomSheet,
icon: const Icon(Icons.more_horiz_rounded),
),
IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)),
],
],
);
@@ -8,11 +8,7 @@ import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
class AlbumViewerEditableDescription extends HookConsumerWidget {
final String albumDescription;
final FocusNode descriptionFocusNode;
const AlbumViewerEditableDescription({
super.key,
required this.albumDescription,
required this.descriptionFocusNode,
});
const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -31,15 +27,12 @@ class AlbumViewerEditableDescription extends HookConsumerWidget {
}
}
useEffect(
() {
descriptionFocusNode.addListener(onFocusModeChange);
return () {
descriptionFocusNode.removeListener(onFocusModeChange);
};
},
[],
);
useEffect(() {
descriptionFocusNode.addListener(onFocusModeChange);
return () {
descriptionFocusNode.removeListener(onFocusModeChange);
};
}, []);
return Material(
color: Colors.transparent,
@@ -72,19 +65,12 @@ class AlbumViewerEditableDescription extends HookConsumerWidget {
onPressed: () {
descriptionTextEditController.clear();
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusColor: Colors.grey[300],
fillColor: context.scaffoldBackgroundColor,
filled: descriptionFocusNode.hasFocus,
@@ -8,11 +8,7 @@ import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget {
final String albumName;
final FocusNode titleFocusNode;
const AlbumViewerEditableTitle({
super.key,
required this.albumName,
required this.titleFocusNode,
});
const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -31,15 +27,12 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
}
useEffect(
() {
titleFocusNode.addListener(onFocusModeChange);
return () {
titleFocusNode.removeListener(onFocusModeChange);
};
},
[],
);
useEffect(() {
titleFocusNode.addListener(onFocusModeChange);
return () {
titleFocusNode.removeListener(onFocusModeChange);
};
}, []);
return Material(
color: Colors.transparent,
@@ -51,9 +44,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
focusNode: titleFocusNode,
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w700,
),
style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700),
controller: titleTextEditController,
onTap: () {
context.focusScope.requestFocus(titleFocusNode);
@@ -66,35 +57,23 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
suffixIcon: titleFocusNode.hasFocus
? IconButton(
onPressed: () {
titleTextEditController.clear();
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusColor: Colors.grey[300],
fillColor: context.scaffoldBackgroundColor,
filled: titleFocusNode.hasFocus,
hintText: 'add_a_title'.tr(),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
fontSize: 28,
),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28),
),
),
);
@@ -5,9 +5,7 @@ import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dar
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
const RemoteAlbumSharedUserIcons({
super.key,
});
const RemoteAlbumSharedUserIcons({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -31,12 +29,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget {
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: UserCircleAvatar(
user: sharedUsers[index],
radius: 18,
size: 36,
hasBorder: true,
),
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
);
}),
itemCount: sharedUsers.length,
@@ -14,15 +14,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
onTap: () {
// debugPrint("View ${asset.id}");
},
child: Stack(
children: [
ImmichThumbnail(
asset: asset,
width: 500,
height: 500,
),
],
),
child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]),
);
}
}
@@ -163,12 +163,7 @@ class AssetIndexWrapper extends SingleChildRenderObjectWidget {
final int rowIndex;
final int sectionIndex;
const AssetIndexWrapper({
required Widget super.child,
required this.rowIndex,
required this.sectionIndex,
super.key,
});
const AssetIndexWrapper({required Widget super.child, required this.rowIndex, required this.sectionIndex, super.key});
@override
// ignore: library_private_types_in_public_api
@@ -191,19 +186,14 @@ class AssetIndexWrapper extends SingleChildRenderObjectWidget {
class _AssetIndexProxy extends RenderProxyBox {
AssetIndex index;
_AssetIndexProxy({
required this.index,
});
_AssetIndexProxy({required this.index});
}
class AssetIndex {
final int rowIndex;
final int sectionIndex;
const AssetIndex({
required this.rowIndex,
required this.sectionIndex,
});
const AssetIndex({required this.rowIndex, required this.sectionIndex});
@override
bool operator ==(covariant AssetIndex other) {
@@ -8,12 +8,7 @@ import 'package:logging/logging.dart';
final log = Logger('AssetGridDataStructure');
enum RenderAssetGridElementType {
assets,
assetRow,
groupDividerTitle,
monthTitle;
}
enum RenderAssetGridElementType { assets, assetRow, groupDividerTitle, monthTitle }
class RenderAssetGridElement {
final RenderAssetGridElementType type;
@@ -33,13 +28,7 @@ class RenderAssetGridElement {
});
}
enum GroupAssetsBy {
day,
month,
auto,
none,
;
}
enum GroupAssetsBy { day, month, auto, none }
class RenderList {
final List<RenderAssetGridElement> elements;
@@ -87,10 +76,7 @@ class RenderList {
// when scrolling backward, end shortly after the requested offset...
// ... to guard against the user scrolling in the other direction
// a tiny bit resulting in a another required load from the DB
final start = max(
0,
forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len),
);
final start = max(0, forward ? offset - oppositeSize : (len > batchSize ? offset : offset + count - len));
// load the calculated batch (start:start+len) from the DB and put it into the buffer
_buf = query!.offset(start).limit(len).findAllSync();
_bufOffset = start;
@@ -117,19 +103,14 @@ class RenderList {
// request the asset from the database (not changing the buffer!)
final asset = query!.offset(index).findFirstSync();
if (asset == null) {
throw Exception(
"Asset at index $index does no longer exist in database",
);
throw Exception("Asset at index $index does no longer exist in database");
}
return asset;
}
throw Exception("RenderList has neither assets nor query");
}
static Future<RenderList> fromQuery(
QueryBuilder<Asset, Asset, QAfterSortBy> query,
GroupAssetsBy groupBy,
) =>
static Future<RenderList> fromQuery(QueryBuilder<Asset, Asset, QAfterSortBy> query, GroupAssetsBy groupBy) =>
_buildRenderList(null, query, groupBy);
static Future<RenderList> _buildRenderList(
@@ -145,12 +126,7 @@ class RenderList {
if (groupBy == GroupAssetsBy.none) {
final int total = assets?.length ?? query!.countSync();
final dateLoader = query != null
? DateBatchLoader(
query: query,
batchSize: 1000 * sectionSize,
)
: null;
final dateLoader = query != null ? DateBatchLoader(query: query, batchSize: 1000 * sectionSize) : null;
for (int i = 0; i < total; i += sectionSize) {
final date = assets != null ? assets[i].fileCreatedAt : await dateLoader?.getDate(i);
@@ -224,11 +200,11 @@ class RenderList {
for (int j = 0; j < count; j += sectionSize) {
final type = j == 0
? (groupBy != GroupAssetsBy.month && newMonth
? RenderAssetGridElementType.monthTitle
: RenderAssetGridElementType.groupDividerTitle)
? RenderAssetGridElementType.monthTitle
: RenderAssetGridElementType.groupDividerTitle)
: (groupBy == GroupAssetsBy.auto
? RenderAssetGridElementType.groupDividerTitle
: RenderAssetGridElementType.assets);
? RenderAssetGridElementType.groupDividerTitle
: RenderAssetGridElementType.assets);
final sectionCount = j + sectionSize > count ? count - j : sectionSize;
assert(sectionCount > 0 && sectionCount <= sectionSize);
elements.add(
@@ -257,11 +233,7 @@ class RenderList {
: await query!.offset(offset).limit(pageSize).fileCreatedAtProperty().findAll();
int i = 0;
for (final date in dates) {
final d = DateTime(
date.year,
date.month,
groupBy == GroupAssetsBy.month ? 1 : date.day,
);
final d = DateTime(date.year, date.month, groupBy == GroupAssetsBy.month ? 1 : date.day);
current ??= d;
if (current != d) {
addElems(current, prevDate);
@@ -288,10 +260,7 @@ class RenderList {
static RenderList empty() => RenderList([], null, []);
static Future<RenderList> fromAssets(
List<Asset> assets,
GroupAssetsBy groupBy,
) =>
static Future<RenderList> fromAssets(List<Asset> assets, GroupAssetsBy groupBy) =>
_buildRenderList(assets, null, groupBy);
/// Deletes an asset from the render list and clears the buffer
@@ -310,10 +279,7 @@ class DateBatchLoader {
List<DateTime> _buffer = [];
int _bufferStart = 0;
DateBatchLoader({
required this.query,
required this.batchSize,
});
DateBatchLoader({required this.query, required this.batchSize});
Future<DateTime?> getDate(int index) async {
if (!_isIndexInBuffer(index)) {
@@ -81,43 +81,26 @@ class ControlBottomAppBar extends HookConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider);
void minimize() {
scrollController.animateTo(
bottomPadding,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
scrollController.animateTo(bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
useEffect(
() {
controlBottomAppBarNotifier.addListener(minimize);
return () {
controlBottomAppBarNotifier.removeListener(minimize);
};
},
[],
);
useEffect(() {
controlBottomAppBarNotifier.addListener(minimize);
return () {
controlBottomAppBarNotifier.removeListener(minimize);
};
}, []);
void showForceDeleteDialog(
Function(bool) deleteCb, {
String? alertMsg,
}) {
void showForceDeleteDialog(Function(bool) deleteCb, {String? alertMsg}) {
showDialog(
context: context,
builder: (BuildContext context) {
return DeleteDialog(
alert: alertMsg,
onDelete: () => deleteCb(true),
);
return DeleteDialog(alert: alertMsg, onDelete: () => deleteCb(true));
},
);
}
void handleRemoteDelete(
bool force,
Function(bool) deleteCb, {
String? alertMsg,
}) {
void handleRemoteDelete(bool force, Function(bool) deleteCb, {String? alertMsg}) {
if (!force) {
deleteCb(force);
return;
@@ -153,11 +136,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
if (hasRemote && onDownload != null)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 90),
child: ControlBoxButton(
iconData: Icons.download,
label: "download".tr(),
onPressed: onDownload,
),
child: ControlBoxButton(iconData: Icons.download, label: "download".tr(), onPressed: onDownload),
),
if (hasLocal && hasRemote && onDelete != null && !isInLockedView)
ConstrainedBox(
@@ -178,17 +157,10 @@ class ControlBottomAppBar extends HookConsumerWidget {
? "control_bottom_app_bar_trash_from_immich".tr()
: "control_bottom_app_bar_delete_from_immich".tr(),
onPressed: enabled
? () => handleRemoteDelete(
!trashEnabled,
onDeleteServer!,
alertMsg: "delete_dialog_alert_remote",
)
? () => handleRemoteDelete(!trashEnabled, onDeleteServer!, alertMsg: "delete_dialog_alert_remote")
: null,
onLongPressed: enabled
? () => showForceDeleteDialog(
onDeleteServer!,
alertMsg: "delete_dialog_alert_remote",
)
? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote")
: null,
),
),
@@ -199,10 +171,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
iconData: Icons.delete_forever,
label: "delete_dialog_title".tr(),
onPressed: enabled
? () => showForceDeleteDialog(
onDeleteServer!,
alertMsg: "delete_dialog_alert_remote",
)
? () => showForceDeleteDialog(onDeleteServer!, alertMsg: "delete_dialog_alert_remote")
: null,
),
),
@@ -221,9 +190,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
showDialog(
context: context,
builder: (BuildContext context) {
return DeleteLocalOnlyDialog(
onDeleteLocal: onDeleteLocal!,
);
return DeleteLocalOnlyDialog(onDeleteLocal: onDeleteLocal!);
},
);
}
@@ -281,13 +248,11 @@ class ControlBottomAppBar extends HookConsumerWidget {
label: "upload".tr(),
onPressed: enabled
? () => showDialog(
context: context,
builder: (BuildContext context) {
return UploadDialog(
onUpload: onUpload,
);
},
)
context: context,
builder: (BuildContext context) {
return UploadDialog(onUpload: onUpload);
},
)
: null,
),
];
@@ -325,10 +290,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
surfaceTintColor: context.colorScheme.surfaceContainerHigh,
elevation: 6.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
),
margin: const EdgeInsets.all(0),
child: CustomScrollView(
@@ -349,14 +311,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
),
),
if (hasRemote && !isInLockedView) ...[
const Divider(
indent: 16,
endIndent: 16,
thickness: 1,
),
_AddToAlbumTitleRow(
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
),
const Divider(indent: 16, endIndent: 16, thickness: 1),
_AddToAlbumTitleRow(onCreateNewAlbum: enabled ? onCreateNewAlbum : null),
],
],
),
@@ -380,9 +336,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
}
class _AddToAlbumTitleRow extends StatelessWidget {
const _AddToAlbumTitleRow({
required this.onCreateNewAlbum,
});
const _AddToAlbumTitleRow({required this.onCreateNewAlbum});
final VoidCallback? onCreateNewAlbum;
@@ -393,23 +347,13 @@ class _AddToAlbumTitleRow extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"add_to_album",
style: context.textTheme.titleSmall,
).tr(),
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
TextButton.icon(
onPressed: onCreateNewAlbum,
icon: Icon(
Icons.add,
color: context.primaryColor,
),
icon: Icon(Icons.add, color: context.primaryColor),
label: Text(
"common_create_new_album",
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
),
],
@@ -5,22 +5,19 @@ import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
class DeleteDialog extends ConfirmDialog {
const DeleteDialog({super.key, String? alert, required Function onDelete})
: super(
title: "delete_dialog_title",
content: alert ?? "delete_dialog_alert",
cancel: "cancel",
ok: "delete",
onOk: onDelete,
);
: super(
title: "delete_dialog_title",
content: alert ?? "delete_dialog_alert",
cancel: "cancel",
ok: "delete",
onOk: onDelete,
);
}
class DeleteLocalOnlyDialog extends StatelessWidget {
final void Function(bool onlyMerged) onDeleteLocal;
const DeleteLocalOnlyDialog({
super.key,
required this.onDeleteLocal,
});
const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal});
@override
Widget build(BuildContext context) {
@@ -35,9 +32,7 @@ class DeleteLocalOnlyDialog extends StatelessWidget {
}
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
title: const Text("delete_dialog_title").tr(),
content: const Text("delete_dialog_alert_local_non_backed_up").tr(),
actions: [
@@ -45,30 +40,21 @@ class DeleteLocalOnlyDialog extends StatelessWidget {
onPressed: () => context.pop(),
child: Text(
"cancel",
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: onDeleteBackedUpOnly,
child: Text(
"delete_local_dialog_ok_backed_up_only",
style: TextStyle(
color: context.colorScheme.tertiary,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.colorScheme.tertiary, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: onForceDelete,
child: Text(
"delete_local_dialog_ok_force",
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
style: TextStyle(color: Colors.red[400], fontWeight: FontWeight.bold),
).tr(),
),
],
@@ -3,11 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class DisableMultiSelectButton extends ConsumerWidget {
const DisableMultiSelectButton({
super.key,
required this.onPressed,
required this.selectedItemCount,
});
const DisableMultiSelectButton({super.key, required this.onPressed, required this.selectedItemCount});
final Function onPressed;
final int selectedItemCount;
@@ -22,16 +18,10 @@ class DisableMultiSelectButton extends ConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton.icon(
onPressed: () => onPressed(),
icon: Icon(
Icons.close_rounded,
color: context.colorScheme.onPrimary,
),
icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary),
label: Text(
'$selectedItemCount',
style: context.textTheme.titleMedium?.copyWith(
height: 2.5,
color: context.colorScheme.onPrimary,
),
style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary),
),
),
),
@@ -3,14 +3,15 @@ import 'dart:async';
import 'package:flutter/material.dart';
/// Build the Scroll Thumb and label using the current configuration
typedef ScrollThumbBuilder = Widget Function(
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
});
typedef ScrollThumbBuilder =
Widget Function(
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
});
/// Build a Text widget using the current scroll offset
typedef LabelTextBuilder = Text Function(double offsetY);
@@ -79,8 +80,8 @@ class DraggableScrollbar extends StatefulWidget {
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.labelTextBuilder,
this.labelConstraints,
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb);
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb);
DraggableScrollbar.arrows({
super.key,
@@ -95,8 +96,8 @@ class DraggableScrollbar extends StatefulWidget {
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.labelTextBuilder,
this.labelConstraints,
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb);
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb);
DraggableScrollbar.semicircle({
super.key,
@@ -111,12 +112,8 @@ class DraggableScrollbar extends StatefulWidget {
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.labelTextBuilder,
this.labelConstraints,
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbSemicircleBuilder(
heightScrollThumb * 0.6,
scrollThumbKey,
alwaysVisibleScrollThumb,
);
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb);
@override
DraggableScrollbarState createState() => DraggableScrollbarState();
@@ -149,17 +146,10 @@ class DraggableScrollbar extends StatefulWidget {
if (alwaysVisibleScrollThumb) {
return scrollThumbAndLabel;
}
return SlideFadeTransition(
animation: thumbAnimation!,
child: scrollThumbAndLabel,
);
return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel);
}
static ScrollThumbBuilder _thumbSemicircleBuilder(
double width,
Key? scrollThumbKey,
bool alwaysVisibleScrollThumb,
) {
static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
return (
Color backgroundColor,
Animation<double> thumbAnimation,
@@ -180,9 +170,7 @@ class DraggableScrollbar extends StatefulWidget {
topRight: const Radius.circular(4.0),
bottomRight: const Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(Size(width, height)),
),
child: Container(constraints: BoxConstraints.tight(Size(width, height))),
),
);
@@ -198,9 +186,7 @@ class DraggableScrollbar extends StatefulWidget {
};
}
static ScrollThumbBuilder _thumbArrowBuilder(
bool alwaysVisibleScrollThumb,
) {
static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) {
return (
Color backgroundColor,
Animation<double> thumbAnimation,
@@ -216,9 +202,7 @@ class DraggableScrollbar extends StatefulWidget {
width: 20.0,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(12.0),
),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
),
);
@@ -235,9 +219,7 @@ class DraggableScrollbar extends StatefulWidget {
};
}
static ScrollThumbBuilder _thumbRRectBuilder(
bool alwaysVisibleScrollThumb,
) {
static ScrollThumbBuilder _thumbRRectBuilder(bool alwaysVisibleScrollThumb) {
return (
Color backgroundColor,
Animation<double> thumbAnimation,
@@ -250,11 +232,7 @@ class DraggableScrollbar extends StatefulWidget {
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
child: Container(
constraints: BoxConstraints.tight(
Size(16.0, height),
),
),
child: Container(constraints: BoxConstraints.tight(Size(16.0, height))),
);
return buildScrollThumbAndLabel(
@@ -296,11 +274,7 @@ class ScrollLabel extends StatelessWidget {
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: Container(
constraints: constraints ?? _defaultConstraints,
alignment: Alignment.center,
child: child,
),
child: Container(constraints: constraints ?? _defaultConstraints, alignment: Alignment.center, child: child),
),
),
);
@@ -325,25 +299,13 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
_viewOffset = 0.0;
_isDragInProcess = false;
_thumbAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastOutSlowIn,
);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn);
_labelAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@override
@@ -366,9 +328,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
Widget build(BuildContext context) {
Text? labelText;
if (widget.labelTextBuilder != null && _isDragInProcess) {
labelText = widget.labelTextBuilder!(
_viewOffset + _barOffset + widget.heightScrollThumb / 2,
);
labelText = widget.labelTextBuilder!(_viewOffset + _barOffset + widget.heightScrollThumb / 2);
}
return LayoutBuilder(
@@ -382,9 +342,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
},
child: Stack(
children: <Widget>[
RepaintBoundary(
child: widget.child,
),
RepaintBoundary(child: widget.child),
RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart,
@@ -422,11 +380,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
setState(() {
if (notification is ScrollUpdateNotification) {
_barOffset += getBarDelta(
notification.scrollDelta!,
barMaxScrollExtent,
viewMaxScrollExtent,
);
_barOffset += getBarDelta(notification.scrollDelta!, barMaxScrollExtent, viewMaxScrollExtent);
if (_barOffset < barMinScrollExtent) {
_barOffset = barMinScrollExtent;
@@ -459,19 +413,11 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
});
}
double getBarDelta(
double scrollViewDelta,
double barMaxScrollExtent,
double viewMaxScrollExtent,
) {
double getBarDelta(double scrollViewDelta, double barMaxScrollExtent, double viewMaxScrollExtent) {
return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent;
}
double getScrollViewDelta(
double barDelta,
double barMaxScrollExtent,
double viewMaxScrollExtent,
) {
double getScrollViewDelta(double barDelta, double barMaxScrollExtent, double viewMaxScrollExtent) {
return barDelta * viewMaxScrollExtent / barMaxScrollExtent;
}
@@ -498,11 +444,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
_barOffset = barMaxScrollExtent;
}
double viewDelta = getScrollViewDelta(
details.delta.dy,
barMaxScrollExtent,
viewMaxScrollExtent,
);
double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
_viewOffset = widget.controller.position.pixels + viewDelta;
if (_viewOffset < widget.controller.position.minScrollExtent) {
@@ -545,14 +487,8 @@ class ArrowCustomPainter extends CustomPainter {
final baseX = size.width / 2;
final baseY = size.height / 2;
canvas.drawPath(
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
paint,
);
canvas.drawPath(
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
paint,
);
canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint);
canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint);
}
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
@@ -583,10 +519,7 @@ class ArrowClipper extends CustomClipper<Path> {
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(
startPointX + arrowWidth / 2,
startPointY - arrowWidth / 2 + 1.0,
);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
@@ -595,10 +528,7 @@ class ArrowClipper extends CustomClipper<Path> {
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(
startPointX + arrowWidth / 2,
startPointY + arrowWidth / 2 - 1.0,
);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
@@ -613,11 +543,7 @@ class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
const SlideFadeTransition({super.key, required this.animation, required this.child});
@override
Widget build(BuildContext context) {
@@ -625,14 +551,8 @@ class SlideFadeTransition extends StatelessWidget {
animation: animation,
builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation),
child: FadeTransition(opacity: animation, child: child),
),
);
}
@@ -4,14 +4,15 @@ import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
/// Build the Scroll Thumb and label using the current configuration
typedef ScrollThumbBuilder = Widget Function(
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
});
typedef ScrollThumbBuilder =
Widget Function(
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
});
/// Build a Text widget using the current scroll offset
typedef LabelTextBuilder = Text Function(int item);
@@ -75,12 +76,8 @@ class DraggableScrollbar extends StatefulWidget {
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.labelTextBuilder,
this.labelConstraints,
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbSemicircleBuilder(
heightScrollThumb * 0.6,
scrollThumbKey,
alwaysVisibleScrollThumb,
);
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb);
@override
DraggableScrollbarState createState() => DraggableScrollbarState();
@@ -113,17 +110,10 @@ class DraggableScrollbar extends StatefulWidget {
if (alwaysVisibleScrollThumb) {
return scrollThumbAndLabel;
}
return SlideFadeTransition(
animation: thumbAnimation!,
child: scrollThumbAndLabel,
);
return SlideFadeTransition(animation: thumbAnimation!, child: scrollThumbAndLabel);
}
static ScrollThumbBuilder _thumbSemicircleBuilder(
double width,
Key? scrollThumbKey,
bool alwaysVisibleScrollThumb,
) {
static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
return (
Color backgroundColor,
Animation<double> thumbAnimation,
@@ -144,9 +134,7 @@ class DraggableScrollbar extends StatefulWidget {
topRight: const Radius.circular(4.0),
bottomRight: const Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(Size(width, height)),
),
child: Container(constraints: BoxConstraints.tight(Size(width, height))),
),
);
@@ -219,25 +207,13 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
_isDragInProcess = false;
_currentItem = 0;
_thumbAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_thumbAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastOutSlowIn,
);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastOutSlowIn);
_labelAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_labelAnimationController = AnimationController(vsync: this, duration: widget.scrollbarAnimationDuration);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@override
@@ -272,9 +248,7 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
},
child: Stack(
children: <Widget>[
RepaintBoundary(
child: widget.child,
),
RepaintBoundary(child: widget.child),
RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart,
@@ -370,16 +344,12 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
/// If the bar is at the bottom but the item position is still smaller than the max item count (due to rounding error)
/// jump to the end of the list
if (barMaxScrollExtent - _barOffset < 10 && itemPosition < maxItemCount) {
widget.controller.jumpTo(
index: maxItemCount,
);
widget.controller.jumpTo(index: maxItemCount);
return;
}
widget.controller.jumpTo(
index: itemPosition,
);
widget.controller.jumpTo(index: itemPosition);
}
Timer? dragHaltTimer;
@@ -405,12 +375,9 @@ class DraggableScrollbarState extends State<DraggableScrollbar> with TickerProvi
dragHaltTimer?.cancel();
widget.scrollStateListener(true);
dragHaltTimer = Timer(
const Duration(milliseconds: 500),
() {
widget.scrollStateListener(false);
},
);
dragHaltTimer = Timer(const Duration(milliseconds: 500), () {
widget.scrollStateListener(false);
});
}
_jumpToBarPosition();
@@ -451,14 +418,8 @@ class ArrowCustomPainter extends CustomPainter {
final baseX = size.width / 2;
final baseY = size.height / 2;
canvas.drawPath(
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
paint,
);
canvas.drawPath(
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
paint,
);
canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint);
canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint);
}
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
@@ -489,10 +450,7 @@ class ArrowClipper extends CustomClipper<Path> {
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(
startPointX + arrowWidth / 2,
startPointY - arrowWidth / 2 + 1.0,
);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
@@ -501,10 +459,7 @@ class ArrowClipper extends CustomClipper<Path> {
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(
startPointX + arrowWidth / 2,
startPointY + arrowWidth / 2 - 1.0,
);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
@@ -519,11 +474,7 @@ class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
const SlideFadeTransition({super.key, required this.animation, required this.child});
@override
Widget build(BuildContext context) {
@@ -531,14 +482,8 @@ class SlideFadeTransition extends StatelessWidget {
animation: animation,
builder: (context, child) => animation.value == 0.0 ? const SizedBox() : child!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(animation),
child: FadeTransition(opacity: animation, child: child),
),
);
}
@@ -30,13 +30,10 @@ class GroupDividerTitle extends HookConsumerWidget {
final appSettingService = ref.watch(appSettingsServiceProvider);
final groupBy = useState(GroupAssetsBy.day);
useEffect(
() {
groupBy.value = GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
return null;
},
[],
);
useEffect(() {
groupBy.value = GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
return null;
}, []);
void handleTitleIconClick() {
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
@@ -59,9 +56,7 @@ class GroupDividerTitle extends HookConsumerWidget {
Text(
text,
style: groupBy.value == GroupAssetsBy.month
? context.textTheme.bodyLarge?.copyWith(
fontSize: 24.0,
)
? context.textTheme.bodyLarge?.copyWith(fontSize: 24.0)
: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color?.withAlpha(250),
fontWeight: FontWeight.w500,
@@ -60,9 +60,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var settings = ref.watch(appSettingsServiceProvider);
final perRow = useState(
assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!,
);
final perRow = useState(assetsPerRow ?? settings.getSetting(AppSettingsEnum.tilesPerRow)!);
final scaleFactor = useState(7.0 - perRow.value);
final baseScaleFactor = useState(7.0 - perRow.value);
@@ -82,22 +80,21 @@ class ImmichAssetGrid extends HookConsumerWidget {
return RawGestureDetector(
gestures: {
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
() => CustomScaleGestureRecognizer(), (CustomScaleGestureRecognizer scale) {
scale.onStart = (details) {
baseScaleFactor.value = scaleFactor.value;
};
() => CustomScaleGestureRecognizer(),
(CustomScaleGestureRecognizer scale) {
scale.onStart = (details) {
baseScaleFactor.value = scaleFactor.value;
};
scale.onUpdate = (details) {
scaleFactor.value = max(
min(5.0, baseScaleFactor.value * details.scale),
1.0,
);
if (7 - scaleFactor.value.toInt() != perRow.value) {
perRow.value = 7 - scaleFactor.value.toInt();
settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value);
}
};
}),
scale.onUpdate = (details) {
scaleFactor.value = max(min(5.0, baseScaleFactor.value * details.scale), 1.0);
if (7 - scaleFactor.value.toInt() != perRow.value) {
perRow.value = 7 - scaleFactor.value.toInt();
settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value);
}
};
},
),
},
child: ImmichAssetGridView(
onRefresh: onRefresh,
@@ -125,9 +122,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
if (renderList != null) return buildAssetGridView(renderList!);
final renderListFuture = ref.watch(assetsTimelineProvider(assets!));
return renderListFuture.widgetWhen(
onData: (renderList) => buildAssetGridView(renderList),
);
return renderListFuture.widgetWhen(onData: (renderList) => buildAssetGridView(renderList));
}
}
@@ -34,10 +34,7 @@ import 'disable_multi_select_button.dart';
import 'draggable_scrollbar_custom.dart';
import 'group_divider_title.dart';
typedef ImmichAssetGridSelectionListener = void Function(
bool,
Set<Asset>,
);
typedef ImmichAssetGridSelectionListener = void Function(bool, Set<Asset>);
class ImmichAssetGridView extends ConsumerStatefulWidget {
final RenderList renderList;
@@ -161,16 +158,10 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
// the scroll_position widget crashes. This is a workaround to prevent this.
// If the index is within the last 10 elements, we jump instead of scrolling.
if (widget.renderList.elements.length <= index + 10) {
_itemScrollController.jumpTo(
index: index,
);
_itemScrollController.jumpTo(index: index);
return;
}
await _itemScrollController.scrollTo(
index: index,
alignment: 0,
duration: const Duration(milliseconds: 500),
);
await _itemScrollController.scrollTo(index: index, alignment: 0, duration: const Duration(milliseconds: 500));
}
Widget _itemBuilder(BuildContext c, int position) {
@@ -219,18 +210,12 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
return Text(
DateFormat.yMMMM().format(date),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
);
}
Widget _buildMultiSelectIndicator() {
return DisableMultiSelectButton(
onPressed: () => _deselectAll(),
selectedItemCount: _selectedAssets.length,
);
return DisableMultiSelectButton(onPressed: () => _deselectAll(), selectedItemCount: _selectedAssets.length);
}
Widget _buildAssetGrid() {
@@ -250,10 +235,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
}
final listWidget = ScrollablePositionedList.builder(
padding: EdgeInsets.only(
top: appBarOffset() ? 60 : 0,
bottom: 220,
),
padding: EdgeInsets.only(top: appBarOffset() ? 60 : 0, bottom: 220),
itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener,
physics: _scrollPhysics,
@@ -269,8 +251,9 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor:
context.isDarkTheme ? context.colorScheme.primary.darken(amount: .5) : context.colorScheme.primary,
backgroundColor: context.isDarkTheme
? context.colorScheme.primary.darken(amount: .5)
: context.colorScheme.primary,
labelTextBuilder: widget.showLabel ? _labelBuilder : null,
padding: appBarOffset() ? const EdgeInsets.only(top: 60) : const EdgeInsets.only(),
heightOffset: appBarOffset() ? 60 : 0,
@@ -284,12 +267,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
return widget.onRefresh == null
? child
: appBarOffset()
? RefreshIndicator(
onRefresh: widget.onRefresh!,
edgeOffset: 30,
child: child,
)
: RefreshIndicator(onRefresh: widget.onRefresh!, child: child);
? RefreshIndicator(onRefresh: widget.onRefresh!, edgeOffset: 30, child: child)
: RefreshIndicator(onRefresh: widget.onRefresh!, child: child);
}
void _scrollToDate() {
@@ -312,9 +291,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
// If the exact date is not found, the timeline is grouped by month,
// thus we search for the month
if (index == -1) {
index = widget.renderList.elements.indexWhere(
(e) => e.date.year == date.year && e.date.month == date.month,
);
index = widget.renderList.elements.indexWhere((e) => e.date.year == date.year && e.date.month == date.month);
}
if (index < widget.renderList.elements.length) {
@@ -411,13 +388,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
void _scrollToTop() {
// for some reason, this is necessary as well in order
// to correctly reposition the drag thumb scroll bar
_itemScrollController.jumpTo(
index: 0,
);
_itemScrollController.scrollTo(
index: 0,
duration: const Duration(milliseconds: 200),
);
_itemScrollController.jumpTo(index: 0);
_itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 200));
}
void _setDragStartIndex(AssetIndex index) {
@@ -495,9 +467,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
final sectionAssets = widget.renderList.loadAssets(section.offset, section.count);
if (currentSectionIndex == startSectionIndex) {
selectedAssets.addAll(
sectionAssets.slice(startSectionAssetIndex, sectionAssets.length),
);
selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, sectionAssets.length));
} else {
selectedAssets.addAll(sectionAssets);
}
@@ -509,13 +479,9 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
if (section != null) {
final sectionAssets = widget.renderList.loadAssets(section.offset, section.count);
if (startSectionIndex == endSectionIndex) {
selectedAssets.addAll(
sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1),
);
selectedAssets.addAll(sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1));
} else {
selectedAssets.addAll(
sectionAssets.slice(0, endSectionAssetIndex + 1),
);
selectedAssets.addAll(sectionAssets.slice(0, endSectionAssetIndex + 1));
}
}
@@ -554,9 +520,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
onAssetEnter: _handleDragAssetEnter,
onEnd: _stopDrag,
onScroll: _dragDragScroll,
onScrollStart: () => WidgetsBinding.instance.addPostFrameCallback(
(_) => controlBottomAppBarNotifier.minimize(),
),
onScrollStart: () =>
WidgetsBinding.instance.addPostFrameCallback((_) => controlBottomAppBarNotifier.minimize()),
child: _buildAssetGrid(),
),
if (widget.showMultiSelectIndicator && widget.selectionActive) _buildMultiSelectIndicator(),
@@ -590,10 +555,7 @@ class _PlaceholderRow extends StatelessWidget {
key: ValueKey(i),
width: width,
height: height,
margin: EdgeInsets.only(
bottom: margin,
right: i + 1 == number ? 0.0 : margin,
),
margin: EdgeInsets.only(bottom: margin, right: i + 1 == number ? 0.0 : margin),
),
],
);
@@ -639,9 +601,7 @@ class _Section extends StatelessWidget {
});
@override
Widget build(
BuildContext context,
) {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth / assetsPerRow - margin * (assetsPerRow - 1) / assetsPerRow;
@@ -675,10 +635,7 @@ class _Section extends StatelessWidget {
key: ValueKey(i),
rowStartIndex: i * assetsPerRow,
sectionIndex: sectionIndex,
assets: assetsToRender.nestedSlice(
i * assetsPerRow,
min((i + 1) * assetsPerRow, section.count),
),
assets: assetsToRender.nestedSlice(i * assetsPerRow, min((i + 1) * assetsPerRow, section.count)),
absoluteOffset: section.offset + i * assetsPerRow,
width: width,
assetsPerRow: assetsPerRow,
@@ -706,9 +663,7 @@ class _Section extends StatelessWidget {
class _MonthTitle extends StatelessWidget {
final DateTime date;
const _MonthTitle({
required this.date,
});
const _MonthTitle({required this.date});
@override
Widget build(BuildContext context) {
@@ -719,10 +674,7 @@ class _MonthTitle extends StatelessWidget {
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
child: Text(
toBeginningOfSentenceCase(title, context.locale.languageCode),
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w500),
),
);
}
@@ -821,11 +773,7 @@ class _AssetRow extends StatelessWidget {
// Normalize:
final sum = arConfiguration.sum;
widthDistribution.setRange(
0,
widthDistribution.length,
arConfiguration.map((e) => (e * assets.length) / sum),
);
widthDistribution.setRange(0, widthDistribution.length, arConfiguration.map((e) => (e * assets.length) / sum));
}
return Row(
key: key,
@@ -835,10 +783,7 @@ class _AssetRow extends StatelessWidget {
return Container(
width: width * widthDistribution[index],
height: width,
margin: EdgeInsets.only(
bottom: margin,
right: last ? 0.0 : margin,
),
margin: EdgeInsets.only(bottom: margin, right: last ? 0.0 : margin),
child: GestureDetector(
onTap: () {
if (selectionActive) {
@@ -79,53 +79,38 @@ class MultiselectGrid extends HookConsumerWidget {
final currentUser = ref.watch(currentUserProvider);
final processing = useProcessingOverlay();
useEffect(
() {
selectionEnabledHook.addListener(() {
multiselectEnabled.state = selectionEnabledHook.value;
});
useEffect(() {
selectionEnabledHook.addListener(() {
multiselectEnabled.state = selectionEnabledHook.value;
});
return () {
// This does not work in tests
if (kReleaseMode) {
selectionEnabledHook.dispose();
}
};
},
[],
);
return () {
// This does not work in tests
if (kReleaseMode) {
selectionEnabledHook.dispose();
}
};
}, []);
void selectionListener(
bool multiselect,
Set<Asset> selectedAssets,
) {
void selectionListener(bool multiselect, Set<Asset> selectedAssets) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
selectionAssetState.value = AssetSelectionState.fromSelection(selectedAssets);
}
errorBuilder(String? msg) => msg != null && msg.isNotEmpty
? () => ImmichToast.show(
context: context,
msg: msg,
gravity: ToastGravity.BOTTOM,
)
? () => ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM)
: null;
Iterable<Asset> ownedRemoteSelection({
String? localErrorMessage,
String? ownerErrorMessage,
}) {
Iterable<Asset> ownedRemoteSelection({String? localErrorMessage, String? ownerErrorMessage}) {
final assets = selection.value;
return assets.remoteOnly(errorCallback: errorBuilder(localErrorMessage)).ownedOnly(
currentUser,
errorCallback: errorBuilder(ownerErrorMessage),
);
return assets
.remoteOnly(errorCallback: errorBuilder(localErrorMessage))
.ownedOnly(currentUser, errorCallback: errorBuilder(ownerErrorMessage));
}
Iterable<Asset> remoteSelection({String? errorMessage}) => selection.value.remoteOnly(
errorCallback: errorBuilder(errorMessage),
);
Iterable<Asset> remoteSelection({String? errorMessage}) =>
selection.value.remoteOnly(errorCallback: errorBuilder(errorMessage));
void onShareAssets(bool shareLocal) {
processing.value = true;
@@ -174,10 +159,7 @@ class MultiselectGrid extends HookConsumerWidget {
processing.value = true;
try {
final toDelete = selection.value
.ownedOnly(
currentUser,
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
)
.ownedOnly(currentUser, errorCallback: errorBuilder('home_page_delete_err_partner'.tr()))
.toList();
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(toDelete, force: force);
@@ -237,25 +219,10 @@ class MultiselectGrid extends HookConsumerWidget {
final failedCount = totalCount - successCount;
final msg = failedCount > 0
? 'assets_downloaded_failed'.t(
context: context,
args: {
'count': successCount,
'error': failedCount,
},
)
: 'assets_downloaded_successfully'.t(
context: context,
args: {
'count': successCount,
},
);
? 'assets_downloaded_failed'.t(context: context, args: {'count': successCount, 'error': failedCount})
: 'assets_downloaded_successfully'.t(context: context, args: {'count': successCount});
ImmichToast.show(
context: context,
msg: msg,
gravity: ToastGravity.BOTTOM,
);
ImmichToast.show(context: context, msg: msg, gravity: ToastGravity.BOTTOM);
} finally {
processing.value = false;
selectionEnabledHook.value = false;
@@ -270,10 +237,9 @@ class MultiselectGrid extends HookConsumerWidget {
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
).toList();
final isDeleted = await ref.read(assetProvider.notifier).deleteRemoteAssets(
toDelete,
shouldDeletePermanently: shouldDeletePermanently,
);
final isDeleted = await ref
.read(assetProvider.notifier)
.deleteRemoteAssets(toDelete, shouldDeletePermanently: shouldDeletePermanently);
if (isDeleted) {
ImmichToast.show(
context: context,
@@ -293,10 +259,9 @@ class MultiselectGrid extends HookConsumerWidget {
processing.value = true;
selectionEnabledHook.value = false;
try {
ref.read(manualUploadProvider.notifier).uploadAssets(
context,
selection.value.where((a) => a.storage == AssetState.local),
);
ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, selection.value.where((a) => a.storage == AssetState.local));
} finally {
processing.value = false;
}
@@ -305,16 +270,11 @@ class MultiselectGrid extends HookConsumerWidget {
void onAddToAlbum(Album album) async {
processing.value = true;
try {
final Iterable<Asset> assets = remoteSelection(
errorMessage: "home_page_add_to_album_err_local".tr(),
);
final Iterable<Asset> assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr());
if (assets.isEmpty) {
return;
}
final result = await ref.read(albumServiceProvider).addAssets(
album,
assets,
);
final result = await ref.read(albumServiceProvider).addAssets(album, assets);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
@@ -332,10 +292,7 @@ class MultiselectGrid extends HookConsumerWidget {
ImmichToast.show(
context: context,
msg: "home_page_add_to_album_success".tr(
namedArgs: {
"album": album.name,
"added": result.successfullyAdded.toString(),
},
namedArgs: {"album": album.name, "added": result.successfullyAdded.toString()},
),
toastType: ToastType.success,
);
@@ -350,9 +307,7 @@ class MultiselectGrid extends HookConsumerWidget {
void onCreateNewAlbum() async {
processing.value = true;
try {
final Iterable<Asset> assets = remoteSelection(
errorMessage: "home_page_add_to_album_err_local".tr(),
);
final Iterable<Asset> assets = remoteSelection(errorMessage: "home_page_add_to_album_err_local".tr());
if (assets.isEmpty) {
return;
}
@@ -376,9 +331,7 @@ class MultiselectGrid extends HookConsumerWidget {
return;
}
await ref.read(stackServiceProvider).createStack(
selection.value.map((e) => e.remoteId!).toList(),
);
await ref.read(stackServiceProvider).createStack(selection.value.map((e) => e.remoteId!).toList());
} finally {
processing.value = false;
selectionEnabledHook.value = false;
@@ -426,12 +379,7 @@ class MultiselectGrid extends HookConsumerWidget {
final isInLockedView = ref.read(inLockedViewProvider);
final visibility = isInLockedView ? AssetVisibilityEnum.timeline : AssetVisibilityEnum.locked;
await handleSetAssetsVisibility(
ref,
context,
visibility,
remoteAssets.toList(),
);
await handleSetAssetsVisibility(ref, context, visibility, remoteAssets.toList());
}
} finally {
processing.value = false;
@@ -439,41 +387,34 @@ class MultiselectGrid extends HookConsumerWidget {
}
}
Future<T> Function() wrapLongRunningFun<T>(
Future<T> Function() fun, {
bool showOverlay = true,
}) =>
() async {
if (showOverlay) processing.value = true;
try {
final result = await fun();
if (result.runtimeType != bool || result == true) {
selectionEnabledHook.value = false;
}
return result;
} finally {
if (showOverlay) processing.value = false;
}
};
Future<T> Function() wrapLongRunningFun<T>(Future<T> Function() fun, {bool showOverlay = true}) => () async {
if (showOverlay) processing.value = true;
try {
final result = await fun();
if (result.runtimeType != bool || result == true) {
selectionEnabledHook.value = false;
}
return result;
} finally {
if (showOverlay) processing.value = false;
}
};
return SafeArea(
top: true,
bottom: false,
child: Stack(
children: [
ref.watch(renderListProvider).when(
ref
.watch(renderListProvider)
.when(
data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null)
? (buildLoadingIndicator ?? buildEmptyIndicator)()
: ImmichAssetGrid(
renderList: data,
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
onRefresh: onRefresh == null
? null
: wrapLongRunningFun(
onRefresh!,
showOverlay: false,
),
onRefresh: onRefresh == null ? null : wrapLongRunningFun(onRefresh!, showOverlay: false),
topWidget: topWidget,
showStack: stackEnabled,
showDragScrollLabel: dragScrollLabelEnabled,
@@ -506,9 +447,7 @@ class MultiselectGrid extends HookConsumerWidget {
unarchive: unarchive,
onToggleLocked: onToggleLockedVisibility,
onRemoveFromAlbum: onRemoveFromAlbum != null
? wrapLongRunningFun(
() => onRemoveFromAlbum!(selection.value),
)
? wrapLongRunningFun(() => onRemoveFromAlbum!(selection.value))
: null,
),
],
@@ -5,11 +5,7 @@ import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
class MultiselectGridStatusIndicator extends HookConsumerWidget {
const MultiselectGridStatusIndicator({
super.key,
this.buildLoadingIndicator,
this.emptyIndicator,
});
const MultiselectGridStatusIndicator({super.key, this.buildLoadingIndicator, this.emptyIndicator});
final Widget Function()? buildLoadingIndicator;
final Widget? emptyIndicator;
@@ -18,16 +14,13 @@ class MultiselectGridStatusIndicator extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final renderListStatus = ref.watch(renderListStatusProvider);
return switch (renderListStatus) {
RenderListStatusEnum.loading => buildLoadingIndicator == null
? const Center(
child: DelayedLoadingIndicator(
delay: Duration(milliseconds: 500),
),
)
: buildLoadingIndicator!(),
RenderListStatusEnum.loading =>
buildLoadingIndicator == null
? const Center(child: DelayedLoadingIndicator(delay: Duration(milliseconds: 500)))
: buildLoadingIndicator!(),
RenderListStatusEnum.empty => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()),
RenderListStatusEnum.error => Center(child: const Text("error_loading_assets").tr()),
RenderListStatusEnum.complete => const SizedBox()
RenderListStatusEnum.complete => const SizedBox(),
};
}
}
@@ -41,8 +41,9 @@ class ThumbnailImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final assetContainerColor =
context.isDarkTheme ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8);
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
return Stack(
children: [
@@ -52,16 +53,13 @@ class ThumbnailImage extends StatelessWidget {
decoration: BoxDecoration(
border: multiselectEnabled && isSelected
? canDeselect
? Border.all(
color: assetContainerColor,
width: 8,
)
: const Border(
top: BorderSide(color: Colors.grey, width: 8),
right: BorderSide(color: Colors.grey, width: 8),
bottom: BorderSide(color: Colors.grey, width: 8),
left: BorderSide(color: Colors.grey, width: 8),
)
? Border.all(color: assetContainerColor, width: 8)
: const Border(
top: BorderSide(color: Colors.grey, width: 8),
right: BorderSide(color: Colors.grey, width: 8),
bottom: BorderSide(color: Colors.grey, width: 8),
left: BorderSide(color: Colors.grey, width: 8),
)
: const Border(),
),
child: Stack(
@@ -76,21 +74,9 @@ class ThumbnailImage extends StatelessWidget {
),
if (showStorageIndicator) _StorageIcon(storage: asset.storage),
if (asset.isFavorite)
const Positioned(
left: 8,
bottom: 5,
child: Icon(
Icons.favorite,
color: Colors.white,
size: 16,
),
),
const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)),
if (asset.isVideo) _VideoIcon(duration: asset.duration),
if (asset.stackCount > 0)
_StackIcon(
isVideo: asset.isVideo,
stackCount: asset.stackCount,
),
if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount),
],
),
),
@@ -98,15 +84,9 @@ class ThumbnailImage extends StatelessWidget {
isSelected
? const Padding(
padding: EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _SelectedIcon(),
),
child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()),
)
: const Icon(
Icons.circle_outlined,
color: Colors.white,
),
: const Icon(Icons.circle_outlined, color: Colors.white),
],
);
}
@@ -117,18 +97,13 @@ class _SelectedIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final assetContainerColor =
context.isDarkTheme ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8);
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
return DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: assetContainerColor,
),
child: Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor),
child: Icon(Icons.check_circle_rounded, color: context.primaryColor),
);
}
}
@@ -147,18 +122,10 @@ class _VideoIcon extends StatelessWidget {
children: [
Text(
duration.format(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
),
const SizedBox(width: 3),
const Icon(
Icons.play_circle_fill_rounded,
color: Colors.white,
size: 18,
),
const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18),
],
),
);
@@ -181,21 +148,10 @@ class _StackIcon extends StatelessWidget {
if (stackCount > 1)
Text(
"$stackCount",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
),
if (stackCount > 1)
const SizedBox(
width: 3,
),
const Icon(
Icons.burst_mode_rounded,
color: Colors.white,
size: 18,
),
if (stackCount > 1) const SizedBox(width: 3),
const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18),
],
),
);
@@ -211,53 +167,35 @@ class _StorageIcon extends StatelessWidget {
Widget build(BuildContext context) {
return switch (storage) {
AssetState.local => const Positioned(
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_off_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
),
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_off_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
),
),
AssetState.remote => const Positioned(
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
),
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
),
),
AssetState.merged => const Positioned(
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_done_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
),
right: 8,
bottom: 5,
child: Icon(
Icons.cloud_done_outlined,
color: Color.fromRGBO(255, 255, 255, 0.8),
size: 16,
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
),
),
};
}
}
@@ -288,13 +226,7 @@ class _ImageIcon extends StatelessWidget {
tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset,
child: Stack(
children: [
SizedBox.expand(
child: ImmichThumbnail(
asset: asset,
height: 250,
width: 250,
),
),
SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -321,10 +253,7 @@ class _ImageIcon extends StatelessWidget {
return DecoratedBox(
decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15.0)),
child: image,
),
child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image),
);
}
}
@@ -7,12 +7,7 @@ class ThumbnailPlaceholder extends StatelessWidget {
final double width;
final double height;
const ThumbnailPlaceholder({
super.key,
this.margin = EdgeInsets.zero,
this.width = 250,
this.height = 250,
});
const ThumbnailPlaceholder({super.key, this.margin = EdgeInsets.zero, this.width = 250, this.height = 250});
@override
Widget build(BuildContext context) {
@@ -26,11 +21,7 @@ class ThumbnailPlaceholder extends StatelessWidget {
height: height,
margin: margin,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
gradient: LinearGradient(colors: gradientColors, begin: Alignment.topCenter, end: Alignment.bottomCenter),
),
);
}
@@ -4,11 +4,11 @@ class UploadDialog extends ConfirmDialog {
final Function onUpload;
const UploadDialog({super.key, required this.onUpload})
: super(
title: 'upload_dialog_title',
content: 'upload_dialog_info',
cancel: 'cancel',
ok: 'upload',
onOk: onUpload,
);
: super(
title: 'upload_dialog_title',
content: 'upload_dialog_info',
cancel: 'cancel',
ok: 'upload',
onOk: onUpload,
);
}
@@ -8,11 +8,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
final Asset assetDetail;
final ScrollController? scrollController;
const AdvancedBottomSheet({
super.key,
required this.assetDetail,
this.scrollController,
});
const AdvancedBottomSheet({super.key, required this.assetDetail, this.scrollController});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -26,27 +22,15 @@ class AdvancedBottomSheet extends HookConsumerWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Align(
child: Text(
"ADVANCED INFO",
style: TextStyle(fontSize: 12.0),
),
),
const Align(child: Text("ADVANCED INFO", style: TextStyle(fontSize: 12.0))),
const SizedBox(height: 32.0),
Container(
decoration: BoxDecoration(
color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[200],
borderRadius: const BorderRadius.all(
Radius.circular(15.0),
),
borderRadius: const BorderRadius.all(Radius.circular(15.0)),
),
child: Padding(
padding: const EdgeInsets.only(
right: 16.0,
left: 16,
top: 8,
bottom: 16,
),
padding: const EdgeInsets.only(right: 16.0, left: 16, top: 8, bottom: 16),
child: ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -55,28 +39,18 @@ class AdvancedBottomSheet extends HookConsumerWidget {
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () {
Clipboard.setData(
ClipboardData(
text: assetDetail.toString(),
),
).then((_) {
Clipboard.setData(ClipboardData(text: assetDetail.toString())).then((_) {
context.scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(
"Copied to clipboard",
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
});
},
icon: Icon(
Icons.copy,
size: 16.0,
color: context.primaryColor,
),
icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor),
),
),
SelectableText(
@@ -2,12 +2,7 @@ import 'package:flutter/material.dart';
/// A widget that animates implicitly between a play and a pause icon.
class AnimatedPlayPause extends StatefulWidget {
const AnimatedPlayPause({
super.key,
required this.playing,
this.size,
this.color,
});
const AnimatedPlayPause({super.key, required this.playing, this.size, this.color});
final double? size;
final bool playing;
@@ -72,10 +72,7 @@ class BottomGalleryBar extends ConsumerWidget {
void handleDelete() async {
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{asset},
force: force,
);
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets({asset}, force: force);
if (isDeleted && isStackPrimaryAsset) {
// Workaround for asset remaining in the gallery
renderList.deleteAsset(asset);
@@ -101,12 +98,7 @@ class BottomGalleryBar extends ConsumerWidget {
if (isDeleted) {
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && asset.isRemote && isStackPrimaryAsset) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
ImmichToast.show(durationInSecond: 1, context: context, msg: 'Asset trashed', gravity: ToastGravity.BOTTOM);
}
removeAssetFromStack();
}
@@ -149,19 +141,13 @@ class BottomGalleryBar extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(
Icons.filter_none_outlined,
size: 18,
),
leading: const Icon(Icons.filter_none_outlined, size: 18),
onTap: () async {
await unStack();
ctx.pop();
context.maybePop();
},
title: const Text(
"viewer_unstack",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
),
],
),
@@ -189,11 +175,7 @@ class BottomGalleryBar extends ConsumerWidget {
context.navigator.push(
MaterialPageRoute(
builder: (context) => EditImagePage(
asset: asset,
image: image,
isEdited: false,
),
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
),
);
}
@@ -221,9 +203,7 @@ class BottomGalleryBar extends ConsumerWidget {
return;
}
ref.read(downloadStateProvider.notifier).downloadAsset(
asset,
);
ref.read(downloadStateProvider.notifier).downloadAsset(asset);
}
handleRemoveFromAlbum() async {
@@ -258,12 +238,11 @@ class BottomGalleryBar extends ConsumerWidget {
final List<Map<BottomNavigationBarItem, Function(int)>> albumActions = [
{
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
),
icon: Icon(Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded),
label: 'share'.tr(),
tooltip: 'share'.tr(),
): (_) => shareAsset(),
): (_) =>
shareAsset(),
},
if (asset.isImage && !isInLockedView)
{
@@ -271,7 +250,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.tune_outlined),
label: 'edit'.tr(),
tooltip: 'edit'.tr(),
): (_) => handleEdit(),
): (_) =>
handleEdit(),
},
if (isOwner && !isInLockedView)
{
@@ -285,7 +265,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.archive_outlined),
label: 'archive'.tr(),
tooltip: 'archive'.tr(),
): (_) => handleArchive(),
): (_) =>
handleArchive(),
},
if (isOwner && asset.stackCount > 0 && !isInLockedView)
{
@@ -293,7 +274,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.burst_mode_outlined),
label: 'stack'.tr(),
tooltip: 'stack'.tr(),
): (_) => showStackActionItems(),
): (_) =>
showStackActionItems(),
},
if (isOwner && !isInAlbum)
{
@@ -301,7 +283,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.delete_outline),
label: 'delete'.tr(),
tooltip: 'delete'.tr(),
): (_) => handleDelete(),
): (_) =>
handleDelete(),
},
if (!isOwner)
{
@@ -309,7 +292,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.download_outlined),
label: 'download'.tr(),
tooltip: 'download'.tr(),
): (_) => handleDownload(),
): (_) =>
handleDownload(),
},
if (isInAlbum)
{
@@ -317,7 +301,8 @@ class BottomGalleryBar extends ConsumerWidget {
icon: const Icon(Icons.remove_circle_outline),
label: 'remove_from_album'.tr(),
tooltip: 'remove_from_album'.tr(),
): (_) => handleRemoveFromAlbum(),
): (_) =>
handleRemoveFromAlbum(),
},
];
return IgnorePointer(
@@ -344,16 +329,8 @@ class BottomGalleryBar extends ConsumerWidget {
backgroundColor: Colors.transparent,
unselectedIconTheme: const IconThemeData(color: Colors.white),
selectedIconTheme: const IconThemeData(color: Colors.white),
unselectedLabelStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
height: 2.3,
),
selectedLabelStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
height: 2.3,
),
unselectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3),
selectedLabelStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, height: 2.3),
unselectedFontSize: 14,
selectedFontSize: 14,
selectedItemColor: Colors.white,
@@ -21,10 +21,7 @@ class CastDialog extends ConsumerWidget {
}
return AlertDialog(
title: const Text(
"cast",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
title: const Text("cast", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
content: SizedBox(
width: 250,
height: 250,
@@ -32,20 +29,13 @@ class CastDialog extends ConsumerWidget {
future: ref.read(castProvider.notifier).getDevices(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error.toString()}',
);
return Text('Error: ${snapshot.error.toString()}');
} else if (!snapshot.hasData) {
return const SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
);
return const SizedBox(height: 48, child: Center(child: CircularProgressIndicator()));
}
if (snapshot.data!.isEmpty) {
return const Text(
'no_cast_devices_found',
).tr();
return const Text('no_cast_devices_found').tr();
}
final devices = snapshot.data!;
@@ -74,13 +64,7 @@ class CastDialog extends ConsumerWidget {
// It's a section header
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
item,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
).tr(),
child: Text(item, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)).tr(),
);
} else {
final (deviceName, type, deviceObj) = item as (String, CastDestinationType, dynamic);
@@ -88,9 +72,7 @@ class CastDialog extends ConsumerWidget {
return ListTile(
title: Text(
deviceName,
style: TextStyle(
color: isCurrentDevice(deviceName) ? context.colorScheme.primary : null,
),
style: TextStyle(color: isCurrentDevice(deviceName) ? context.colorScheme.primary : null),
),
leading: Icon(
type == CastDestinationType.googleCast ? Icons.cast : Icons.cast_connected,
@@ -99,8 +81,8 @@ class CastDialog extends ConsumerWidget {
trailing: isCurrentDevice(deviceName)
? Icon(Icons.check, color: context.colorScheme.primary)
: isDeviceConnecting(deviceName)
? const CircularProgressIndicator()
: null,
? const CircularProgressIndicator()
: null,
onTap: () async {
if (isDeviceConnecting(deviceName)) {
return;
@@ -127,20 +109,14 @@ class CastDialog extends ConsumerWidget {
onPressed: () => ref.read(castProvider.notifier).disconnect(),
child: Text(
"stop_casting",
style: TextStyle(
color: context.colorScheme.secondary,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.colorScheme.secondary, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: () => context.pop(),
child: Text(
"close",
style: TextStyle(
color: context.colorScheme.primary,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.colorScheme.primary, fontWeight: FontWeight.bold),
).tr(),
),
],
@@ -29,19 +29,13 @@ class CenterPlayButton extends StatelessWidget {
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
child: IconButton(
iconSize: 32,
padding: const EdgeInsets.all(12.0),
icon: isFinished
? Icon(Icons.replay, color: iconColor)
: AnimatedPlayPause(
color: iconColor,
playing: isPlaying,
),
: AnimatedPlayPause(color: iconColor, playing: isPlaying),
onPressed: onPressed,
),
),
@@ -13,36 +13,28 @@ import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
class CustomVideoPlayerControls extends HookConsumerWidget {
final Duration hideTimerDuration;
const CustomVideoPlayerControls({
super.key,
this.hideTimerDuration = const Duration(seconds: 5),
});
const CustomVideoPlayerControls({super.key, this.hideTimerDuration = const Duration(seconds: 5)});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetIsVideo = ref.watch(
currentAssetProvider.select((asset) => asset != null && asset.isVideo),
);
final assetIsVideo = ref.watch(currentAssetProvider.select((asset) => asset != null && asset.isVideo));
final showControls = ref.watch(showControlsProvider);
final VideoPlaybackState state = ref.watch(videoPlaybackValueProvider.select((value) => value.state));
final cast = ref.watch(castProvider);
// A timer to hide the controls
final hideTimer = useTimer(
hideTimerDuration,
() {
if (!context.mounted) {
return;
}
final state = ref.read(videoPlaybackValueProvider).state;
final hideTimer = useTimer(hideTimerDuration, () {
if (!context.mounted) {
return;
}
final state = ref.read(videoPlaybackValueProvider).state;
// Do not hide on paused
if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) {
ref.read(showControlsProvider.notifier).show = false;
}
},
);
// Do not hide on paused
if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) {
ref.read(showControlsProvider.notifier).show = false;
}
});
final showBuffering = state == VideoPlaybackState.buffering && !cast.isCasting;
/// Shows the controls and starts the timer to hide them
@@ -93,11 +85,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
child: Stack(
children: [
if (showBuffering)
const Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
),
)
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
else
GestureDetector(
onTap: () => ref.read(showControlsProvider.notifier).show = false,
@@ -14,11 +14,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
class DescriptionInput extends HookConsumerWidget {
DescriptionInput({
super.key,
required this.asset,
this.exifInfo,
});
DescriptionInput({super.key, required this.asset, this.exifInfo});
final Asset asset;
final ExifInfo? exifInfo;
@@ -37,16 +33,13 @@ class DescriptionInput extends HookConsumerWidget {
final hasDescription = useState(false);
final isOwner = fastHash(owner?.id ?? '') == asset.ownerId;
useEffect(
() {
assetService.getDescription(asset).then((value) {
controller.text = value;
hasDescription.value = value.isNotEmpty;
});
return null;
},
[assetWithExif.value],
);
useEffect(() {
assetService.getDescription(asset).then((value) {
controller.text = value;
hasDescription.value = value.isNotEmpty;
});
return null;
}, [assetWithExif.value]);
if (!isOwner && !hasDescription.value) {
return const SizedBox.shrink();
@@ -55,19 +48,12 @@ class DescriptionInput extends HookConsumerWidget {
submitDescription(String description) async {
hasError.value = false;
try {
await assetService.setDescription(
asset,
description,
);
await assetService.setDescription(asset, description);
controller.text = description;
} catch (error, stack) {
hasError.value = true;
_log.severe("Error updating description", error, stack);
ImmichToast.show(
context: context,
msg: "description_input_submit_error".tr(),
toastType: ToastType.error,
);
ImmichToast.show(context: context, msg: "description_input_submit_error".tr(), toastType: ToastType.error);
}
}
@@ -80,10 +66,7 @@ class DescriptionInput extends HookConsumerWidget {
controller.clear();
isTextEmpty.value = true;
},
icon: Icon(
Icons.cancel_rounded,
color: context.colorScheme.onSurfaceSecondary,
),
icon: Icon(Icons.cancel_rounded, color: context.colorScheme.onSurfaceSecondary),
splashRadius: 10,
);
}
@@ -36,18 +36,8 @@ class AssetDateTime extends ConsumerWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
formattedDateTime,
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (asset.isRemote)
IconButton(
onPressed: editDateTime,
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
Text(formattedDateTime, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600)),
if (asset.isRemote) IconButton(onPressed: editDateTime, icon: const Icon(Icons.edit_outlined), iconSize: 20),
],
);
}
@@ -12,11 +12,7 @@ class AssetDetails extends ConsumerWidget {
final Asset asset;
final ExifInfo? exifInfo;
const AssetDetails({
super.key,
required this.asset,
this.exifInfo,
});
const AssetDetails({super.key, required this.asset, this.exifInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -11,10 +11,7 @@ import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
class AssetLocation extends HookConsumerWidget {
final Asset asset;
const AssetLocation({
super.key,
required this.asset,
});
const AssetLocation({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -35,10 +32,7 @@ class AssetLocation extends HookConsumerWidget {
leading: const Icon(Icons.location_on),
title: Text(
"add_a_location",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
).tr(),
onTap: editLocation,
)
@@ -56,10 +50,7 @@ class AssetLocation extends HookConsumerWidget {
bool hasLocationName = (cityName != null && stateName != null);
return hasLocationName
? Text(
"$cityName, $stateName",
style: context.textTheme.labelLarge,
)
? Text("$cityName, $stateName", style: context.textTheme.labelLarge)
: const SizedBox.shrink();
}
@@ -79,25 +70,16 @@ class AssetLocation extends HookConsumerWidget {
),
).tr(),
if (asset.isRemote)
IconButton(
onPressed: editLocation,
icon: const Icon(Icons.edit_outlined),
iconSize: 20,
),
IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_outlined), iconSize: 20),
],
),
asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16),
ExifMap(
exifInfo: exifInfo!,
markerId: asset.remoteId,
),
ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId),
const SizedBox(height: 16),
getLocationName(),
Text(
"${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)),
),
],
),
@@ -5,10 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
class CameraInfo extends StatelessWidget {
final ExifInfo exifInfo;
const CameraInfo({
super.key,
required this.exifInfo,
});
const CameraInfo({super.key, required this.exifInfo});
@override
Widget build(BuildContext context) {
@@ -16,14 +13,8 @@ class CameraInfo extends StatelessWidget {
return ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.camera,
color: textColor.withAlpha(200),
),
title: Text(
"${exifInfo.make} ${exifInfo.model}",
style: context.textTheme.labelLarge,
),
leading: Icon(Icons.camera, color: textColor.withAlpha(200)),
title: Text("${exifInfo.make} ${exifInfo.model}", style: context.textTheme.labelLarge),
subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
@@ -11,12 +11,7 @@ class ExifMap extends StatelessWidget {
final String? markerId;
final MapCreatedCallback? onMapCreated;
const ExifMap({
super.key,
required this.exifInfo,
this.markerId = 'marker',
this.onMapCreated,
});
const ExifMap({super.key, required this.exifInfo, this.markerId = 'marker', this.onMapCreated});
@override
Widget build(BuildContext context) {
@@ -35,20 +30,13 @@ class ExifMap extends StatelessWidget {
Uri uri = Uri(
scheme: 'geo',
host: '$latitude,$longitude',
queryParameters: {
'z': '$zoomLevel',
'q': '$latitude,$longitude',
},
queryParameters: {'z': '$zoomLevel', 'q': '$latitude,$longitude'},
);
if (await canLaunchUrl(uri)) {
return uri;
}
} else if (Platform.isIOS) {
var params = {
'll': '$latitude,$longitude',
'q': '$latitude,$longitude',
'z': '$zoomLevel',
};
var params = {'ll': '$latitude,$longitude', 'q': '$latitude,$longitude', 'z': '$zoomLevel'};
Uri uri = Uri.https('maps.apple.com', '/', params);
if (await canLaunchUrl(uri)) {
return uri;
@@ -66,10 +54,7 @@ class ExifMap extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
return MapThumbnail(
centre: LatLng(
exifInfo.latitude ?? 0,
exifInfo.longitude ?? 0,
),
centre: LatLng(exifInfo.latitude ?? 0, exifInfo.longitude ?? 0),
height: 150,
width: constraints.maxWidth,
zoom: 12.0,
@@ -6,10 +6,7 @@ import 'package:immich_mobile/utils/bytes_units.dart';
class FileInfo extends StatelessWidget {
final Asset asset;
const FileInfo({
super.key,
required this.asset,
});
const FileInfo({super.key, required this.asset});
@override
Widget build(BuildContext context) {
@@ -41,15 +38,9 @@ class FileInfo extends StatelessWidget {
return ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.image,
color: textColor.withAlpha(200),
),
leading: Icon(Icons.image, color: textColor.withAlpha(200)),
titleAlignment: ListTileTitleAlignment.center,
title: Text(
title,
style: context.textTheme.labelLarge,
),
title: Text(title, style: context.textTheme.labelLarge),
subtitle: subtitle == null ? null : Text(subtitle),
);
}
@@ -21,10 +21,7 @@ class PeopleInfo extends ConsumerWidget {
final peopleProvider = ref.watch(assetPeopleNotifierProvider(asset).notifier);
final people = ref.watch(assetPeopleNotifierProvider(asset)).value?.where((p) => !p.isHidden);
showPersonNameEditModel(
String personId,
String personName,
) {
showPersonNameEditModel(String personId, String personName) {
return showDialog(
context: context,
useRootNavigator: false,
@@ -37,7 +34,8 @@ class PeopleInfo extends ConsumerWidget {
});
}
final curatedPeople = people
final curatedPeople =
people
?.map(
(p) => SearchCuratedContent(
id: p.id,
@@ -78,17 +76,10 @@ class PeopleInfo extends ConsumerWidget {
content: curatedPeople,
onTap: (content, index) {
context
.pushRoute(
PersonResultRoute(
personId: content.id,
personName: content.label,
),
)
.pushRoute(PersonResultRoute(personId: content.id, personName: content.label))
.then((_) => peopleProvider.refresh());
},
onNameTap: (person, index) => {
showPersonNameEditModel(person.id, person.label),
},
onNameTap: (person, index) => {showPersonNameEditModel(person.id, person.label)},
),
),
],
@@ -11,11 +11,7 @@ class FormattedDuration extends StatelessWidget {
width: data.inHours > 0 ? 70 : 60, // use a fixed width to prevent jitter
child: Text(
data.format(),
style: const TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 14.0, color: Colors.white, fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
);
@@ -51,11 +51,7 @@ class GalleryAppBar extends ConsumerWidget {
final result = await ref.read(trashProvider.notifier).restoreAssets([asset]);
if (result && context.mounted) {
ImmichToast.show(
context: context,
msg: 'asset_restored_successfully'.tr(),
gravity: ToastGravity.BOTTOM,
);
ImmichToast.show(context: context, msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM);
}
}
@@ -75,16 +71,10 @@ class GalleryAppBar extends ConsumerWidget {
addToAlbum(Asset addToAlbumAsset) {
showModalBottomSheet(
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))),
context: context,
builder: (BuildContext _) {
return AddToAlbumBottomSheet(
assets: [addToAlbumAsset],
);
return AddToAlbumBottomSheet(assets: [addToAlbumAsset]);
},
);
}
@@ -58,10 +58,7 @@ class TopControlAppBar extends HookConsumerWidget {
Widget buildFavoriteButton(a) {
return IconButton(
onPressed: () => onFavorite(a),
icon: Icon(
a.isFavorite ? Icons.favorite : Icons.favorite_border,
color: Colors.grey[200],
),
icon: Icon(a.isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.grey[200]),
);
}
@@ -70,10 +67,7 @@ class TopControlAppBar extends HookConsumerWidget {
onPressed: () {
onLocatePressed();
},
icon: Icon(
Icons.image_search,
color: Colors.grey[200],
),
icon: Icon(Icons.image_search, color: Colors.grey[200]),
);
}
@@ -82,20 +76,14 @@ class TopControlAppBar extends HookConsumerWidget {
onPressed: () {
onMoreInfoPressed();
},
icon: Icon(
Icons.info_outline_rounded,
color: Colors.grey[200],
),
icon: Icon(Icons.info_outline_rounded, color: Colors.grey[200]),
);
}
Widget buildDownloadButton() {
return IconButton(
onPressed: onDownloadPressed,
icon: Icon(
Icons.cloud_download_outlined,
color: Colors.grey[200],
),
icon: Icon(Icons.cloud_download_outlined, color: Colors.grey[200]),
);
}
@@ -104,10 +92,7 @@ class TopControlAppBar extends HookConsumerWidget {
onPressed: () {
onAddToAlbumPressed();
},
icon: Icon(
Icons.add,
color: Colors.grey[200],
),
icon: Icon(Icons.add, color: Colors.grey[200]),
);
}
@@ -116,10 +101,7 @@ class TopControlAppBar extends HookConsumerWidget {
onPressed: () {
onRestorePressed();
},
icon: Icon(
Icons.history_rounded,
color: Colors.grey[200],
),
icon: Icon(Icons.history_rounded, color: Colors.grey[200]),
);
}
@@ -131,19 +113,13 @@ class TopControlAppBar extends HookConsumerWidget {
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.mode_comment_outlined,
color: Colors.grey[200],
),
Icon(Icons.mode_comment_outlined, color: Colors.grey[200]),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[200],
),
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey[200]),
),
),
],
@@ -154,10 +130,7 @@ class TopControlAppBar extends HookConsumerWidget {
Widget buildUploadButton() {
return IconButton(
onPressed: onUploadPressed,
icon: Icon(
Icons.backup_outlined,
color: Colors.grey[200],
),
icon: Icon(Icons.backup_outlined, color: Colors.grey[200]),
);
}
@@ -166,21 +139,14 @@ class TopControlAppBar extends HookConsumerWidget {
onPressed: () {
context.maybePop();
},
icon: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20.0,
color: Colors.grey[200],
),
icon: Icon(Icons.arrow_back_ios_new_rounded, size: 20.0, color: Colors.grey[200]),
);
}
Widget buildCastButton() {
return IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
showDialog(context: context, builder: (context) => const CastDialog());
},
icon: Icon(
isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
@@ -12,9 +12,6 @@ class VideoControls extends ConsumerWidget {
final isPortrait = context.orientation == Orientation.portrait;
return isPortrait
? const VideoPosition()
: const Padding(
padding: EdgeInsets.symmetric(horizontal: 60.0),
child: VideoPosition(),
);
: const Padding(padding: EdgeInsets.symmetric(horizontal: 60.0), child: VideoPosition());
}
}
@@ -17,12 +17,8 @@ class VideoPosition extends HookConsumerWidget {
final isCasting = ref.watch(castProvider).isCasting;
final (position, duration) = isCasting
? ref.watch(
castProvider.select((c) => (c.currentTime, c.duration)),
)
: ref.watch(
videoPlaybackValueProvider.select((v) => (v.position, v.duration)),
);
? ref.watch(castProvider.select((c) => (c.currentTime, c.duration)))
: ref.watch(videoPlaybackValueProvider.select((v) => (v.position, v.duration)));
final wasPlaying = useRef<bool>(true);
return duration == Duration.zero
@@ -34,20 +30,14 @@ class VideoPosition extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FormattedDuration(position),
FormattedDuration(duration),
],
children: [FormattedDuration(position), FormattedDuration(duration)],
),
),
Row(
children: [
Expanded(
child: Slider(
value: min(
position.inMicroseconds / duration.inMicroseconds * 100,
100,
),
value: min(position.inMicroseconds / duration.inMicroseconds * 100, 100),
min: 0,
max: 100,
thumbColor: Colors.white,
@@ -98,10 +88,7 @@ class _VideoPositionPlaceholder extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FormattedDuration(Duration.zero),
FormattedDuration(Duration.zero),
],
children: [FormattedDuration(Duration.zero), FormattedDuration(Duration.zero)],
),
),
Row(
+11 -42
View File
@@ -16,10 +16,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
const AlbumInfoCard({
super.key,
required this.album,
});
const AlbumInfoCard({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -29,10 +26,7 @@ class AlbumInfoCard extends HookConsumerWidget {
final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode(
context.primaryColor.withAlpha(100),
BlendMode.darken,
);
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);
@@ -40,9 +34,7 @@ class AlbumInfoCard extends HookConsumerWidget {
if (isSelected) {
return Chip(
visualDensity: VisualDensity.compact,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
label: Text(
"album_info_card_backup_album_included",
style: TextStyle(
@@ -56,9 +48,7 @@ class AlbumInfoCard extends HookConsumerWidget {
} else if (isExcluded) {
return Chip(
visualDensity: VisualDensity.compact,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
label: Text(
"album_info_card_backup_album_excluded",
style: TextStyle(
@@ -145,24 +135,16 @@ class AlbumInfoCard extends HookConsumerWidget {
child: const Image(
width: double.infinity,
height: double.infinity,
image: AssetImage(
'assets/immich-logo.png',
),
image: AssetImage('assets/immich-logo.png'),
fit: BoxFit.cover,
),
),
Positioned(
bottom: 10,
right: 25,
child: buildSelectedTextBox(),
),
Positioned(bottom: 10, right: 25, child: buildSelectedTextBox()),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 25,
),
padding: const EdgeInsets.only(left: 25),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -173,20 +155,13 @@ class AlbumInfoCard extends HookConsumerWidget {
children: [
Text(
album.name,
style: TextStyle(
fontSize: 14,
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 14, color: context.primaryColor, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
album.assetCount.toString() + (album.isAll ? " (${'all'.tr()})" : ""),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
@@ -194,15 +169,9 @@ class AlbumInfoCard extends HookConsumerWidget {
),
IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: album.album),
);
context.pushRoute(AlbumPreviewRoute(album: album.album));
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 24,
),
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
],
@@ -35,23 +35,14 @@ class AlbumInfoListTile extends HookConsumerWidget {
buildIcon() {
if (isSelected) {
return Icon(
Icons.check_circle_rounded,
color: context.colorScheme.primary,
);
return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary);
}
if (isExcluded) {
return Icon(
Icons.remove_circle_rounded,
color: context.colorScheme.error,
);
return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error);
}
return Icon(
Icons.circle,
color: context.colorScheme.surfaceContainerHighest,
);
return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest);
}
return GestureDetector(
@@ -92,25 +83,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
}
},
leading: buildIcon(),
title: Text(
album.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
subtitle: Text(album.assetCount.toString()),
trailing: IconButton(
onPressed: () {
context.pushRoute(
AlbumPreviewRoute(album: album.album),
);
context.pushRoute(AlbumPreviewRoute(album: album.album));
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 24,
),
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
),
+18 -33
View File
@@ -14,9 +14,7 @@ class BackupAssetInfoTable extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isManualUpload = ref.watch(
backupProvider.select(
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
),
backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress),
);
final isUploadInProgress = ref.watch(
@@ -29,18 +27,13 @@ class BackupAssetInfoTable extends ConsumerWidget {
);
final asset = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.currentUploadAsset),
)
? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset))
: ref.watch(backupProvider.select((value) => value.currentUploadAsset));
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Table(
border: TableBorder.all(
color: context.colorScheme.outlineVariant,
width: 1,
),
border: TableBorder.all(color: context.colorScheme.outlineVariant, width: 1),
children: [
TableRow(
children: [
@@ -48,21 +41,19 @@ class BackupAssetInfoTable extends ConsumerWidget {
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
'backup_controller_page_filename',
style: TextStyle(
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
namedArgs: isUploadInProgress
? {
'filename': asset.fileName,
'size': asset.fileType.toLowerCase(),
}
: {'filename': "-", 'size': "-"},
),
child:
Text(
'backup_controller_page_filename',
style: TextStyle(
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
namedArgs: isUploadInProgress
? {'filename': asset.fileName, 'size': asset.fileType.toLowerCase()}
: {'filename': "-", 'size': "-"},
),
),
),
],
@@ -80,11 +71,7 @@ class BackupAssetInfoTable extends ConsumerWidget {
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
namedArgs: {
'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-",
},
),
).tr(namedArgs: {'date': isUploadInProgress ? _getAssetCreationDate(asset) : "-"}),
),
),
],
@@ -101,9 +88,7 @@ class BackupAssetInfoTable extends ConsumerWidget {
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
).tr(
namedArgs: {'id': isUploadInProgress ? asset.id : "-"},
),
).tr(namedArgs: {'id': isUploadInProgress ? asset.id : "-"}),
),
),
],
@@ -7,12 +7,7 @@ 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,
});
const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info});
@override
Widget build(BuildContext context) {
@@ -21,40 +16,26 @@ class BackupInfoCard extends StatelessWidget {
borderRadius: const BorderRadius.all(
Radius.circular(20), // if you need this
),
side: BorderSide(
color: context.colorScheme.outlineVariant,
width: 1,
),
side: BorderSide(color: context.colorScheme.outlineVariant, width: 1),
),
elevation: 0,
borderOnForeground: false,
child: ListTile(
minVerticalPadding: 18,
isThreeLine: true,
title: Text(
title,
style: context.textTheme.titleMedium,
),
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?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
info,
style: context.textTheme.titleLarge,
),
Text(
"backup_info_card_assets",
style: context.textTheme.labelLarge,
).tr(),
Text(info, style: context.textTheme.titleLarge),
Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(),
],
),
),
@@ -16,18 +16,11 @@ class CurrentUploadingAssetInfoBox extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
isThreeLine: true,
leading: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 30,
),
leading: Icon(Icons.image_outlined, color: context.primaryColor, size: 30),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"backup_controller_page_uploading_file_info",
style: context.textTheme.titleSmall,
).tr(),
Text("backup_controller_page_uploading_file_info", style: context.textTheme.titleSmall).tr(),
const BackupErrorChip(),
],
),
@@ -36,23 +36,14 @@ class DriftAlbumInfoListTile extends HookConsumerWidget {
buildIcon() {
if (isSelected) {
return Icon(
Icons.check_circle_rounded,
color: context.colorScheme.primary,
);
return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary);
}
if (isExcluded) {
return Icon(
Icons.remove_circle_rounded,
color: context.colorScheme.error,
);
return Icon(Icons.remove_circle_rounded, color: context.colorScheme.error);
}
return Icon(
Icons.circle,
color: context.colorScheme.surfaceContainerHighest,
);
return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest);
}
return GestureDetector(
@@ -90,23 +81,13 @@ class DriftAlbumInfoListTile extends HookConsumerWidget {
}
},
leading: buildIcon(),
title: Text(
album.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
subtitle: Text(album.assetCount.toString()),
trailing: IconButton(
onPressed: () {
context.pushRoute(LocalTimelineRoute(album: album));
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 24,
),
icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24),
splashRadius: 25,
),
),
+1 -4
View File
@@ -17,10 +17,7 @@ class BackupErrorChip extends ConsumerWidget {
}
return ActionChip(
avatar: const Icon(
Icons.info,
color: red400,
),
avatar: const Icon(Icons.info, color: red400),
elevation: 1,
visualDensity: VisualDensity.compact,
label: const BackupErrorChipText(),
@@ -16,11 +16,7 @@ class BackupErrorChipText extends ConsumerWidget {
return const Text(
"backup_controller_page_failed",
style: TextStyle(
color: red400,
fontWeight: FontWeight.bold,
fontSize: 11,
),
style: TextStyle(color: red400, fontWeight: FontWeight.bold, fontSize: 11),
).tr(namedArgs: {'count': count.toString()});
}
}
@@ -10,18 +10,12 @@ class IcloudDownloadProgressBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isManualUpload = ref.watch(
backupProvider.select(
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
),
backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress),
);
final isIcloudAsset = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset),
)
: ref.watch(
backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset),
);
? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset))
: ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset));
if (!isIcloudAsset) {
return const SizedBox();
@@ -33,13 +27,7 @@ class IcloudDownloadProgressBar extends ConsumerWidget {
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
SizedBox(
width: 110,
child: Text(
"iCloud Download",
style: context.textTheme.labelSmall,
),
),
SizedBox(width: 110, child: Text("iCloud Download", style: context.textTheme.labelSmall)),
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
@@ -47,10 +35,7 @@ class IcloudDownloadProgressBar extends ConsumerWidget {
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
),
),
Text(
" ${iCloudDownloadProgress ~/ 1}%",
style: const TextStyle(fontSize: 12),
),
Text(" ${iCloudDownloadProgress ~/ 1}%", style: const TextStyle(fontSize: 12)),
],
),
);
@@ -9,10 +9,7 @@ import 'package:immich_mobile/providers/backup/ios_background_settings.provider.
/// more confident about background sync
class IosDebugInfoTile extends HookConsumerWidget {
final IOSBackgroundSettings settings;
const IosDebugInfoTile({
super.key,
required this.settings,
});
const IosDebugInfoTile({super.key, required this.settings});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -37,31 +34,16 @@ class IosDebugInfoTile extends HookConsumerWidget {
subtitle = 'ios_debug_info_processing_ran_at'.t(context: context, args: {'dateTime': df.format(processing)});
} else {
final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing;
subtitle = 'ios_debug_info_last_sync_at'.t(
context: context,
args: {'dateTime': df.format(fetchOrProcessing)},
);
subtitle = 'ios_debug_info_last_sync_at'.t(context: context, args: {'dateTime': df.format(fetchOrProcessing)});
}
return ListTile(
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,
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),
);
}
}
@@ -11,39 +11,22 @@ class BackupUploadProgressBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isManualUpload = ref.watch(
backupProvider.select(
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
),
backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress),
);
final isIcloudAsset = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset),
)
: ref.watch(
backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset),
);
? ref.watch(manualUploadProvider.select((value) => value.currentUploadAsset.isIcloudAsset))
: ref.watch(backupProvider.select((value) => value.currentUploadAsset.isIcloudAsset));
final uploadProgress = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.progressInPercentage),
)
: ref.watch(
backupProvider.select((value) => value.progressInPercentage),
);
? ref.watch(manualUploadProvider.select((value) => value.progressInPercentage))
: ref.watch(backupProvider.select((value) => value.progressInPercentage));
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
if (isIcloudAsset)
SizedBox(
width: 110,
child: Text(
"Immich Upload",
style: context.textTheme.labelSmall,
),
),
if (isIcloudAsset) SizedBox(width: 110, child: Text("Immich Upload", style: context.textTheme.labelSmall)),
Expanded(
child: LinearProgressIndicator(
minHeight: 10.0,
+5 -16
View File
@@ -10,34 +10,23 @@ class BackupUploadStats extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isManualUpload = ref.watch(
backupProvider.select(
(value) => value.backupProgress == BackUpProgressEnum.manualInProgress,
),
backupProvider.select((value) => value.backupProgress == BackUpProgressEnum.manualInProgress),
);
final uploadFileProgress = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.progressInFileSize),
)
? ref.watch(manualUploadProvider.select((value) => value.progressInFileSize))
: ref.watch(backupProvider.select((value) => value.progressInFileSize));
final uploadFileSpeed = isManualUpload
? ref.watch(
manualUploadProvider.select((value) => value.progressInFileSpeed),
)
: ref.watch(
backupProvider.select((value) => value.progressInFileSpeed),
);
? ref.watch(manualUploadProvider.select((value) => value.progressInFileSpeed))
: ref.watch(backupProvider.select((value) => value.progressInFileSpeed));
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(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono")),
Text(
_formatUploadFileSpeed(uploadFileSpeed),
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
@@ -34,27 +34,18 @@ class ImmichAppBarDialog extends HookConsumerWidget {
final user = ref.watch(currentUserProvider);
final isLoggingOut = useState(false);
useEffect(
() {
ref.read(backupProvider.notifier).updateDiskInfo();
ref.read(currentUserProvider.notifier).refresh();
return null;
},
[],
);
useEffect(() {
ref.read(backupProvider.notifier).updateDiskInfo();
ref.read(currentUserProvider.notifier).refresh();
return null;
}, []);
buildTopRow() {
return Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: InkWell(
onTap: () => context.pop(),
child: const Icon(
Icons.close,
size: 20,
),
),
child: InkWell(onTap: () => context.pop(), child: const Icon(Icons.close, size: 20)),
),
Center(
child: Image.asset(
@@ -66,29 +57,16 @@ class ImmichAppBarDialog extends HookConsumerWidget {
);
}
buildActionButton(
IconData icon,
String text,
Function() onTap, {
Widget? trailing,
}) {
buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) {
return ListTile(
dense: true,
visualDensity: VisualDensity.standard,
contentPadding: const EdgeInsets.only(left: 30, right: 30),
minLeadingWidth: 40,
leading: SizedBox(
child: Icon(
icon,
color: theme.textTheme.labelLarge?.color?.withAlpha(250),
size: 20,
),
),
leading: SizedBox(child: Icon(icon, color: theme.textTheme.labelLarge?.color?.withAlpha(250), size: 20)),
title: Text(
text,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.textTheme.labelLarge?.color?.withAlpha(250),
),
style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)),
).tr(),
onTap: onTap,
trailing: trailing,
@@ -96,11 +74,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
}
buildSettingButton() {
return buildActionButton(
Icons.settings_outlined,
"settings",
() => context.pushRoute(const SettingsRoute()),
);
return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute()));
}
buildAppLogButton() {
@@ -142,10 +116,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
);
},
trailing: isLoggingOut.value
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
? const SizedBox.square(dimension: 20, child: CircularProgressIndicator(strokeWidth: 2))
: null,
);
}
@@ -165,20 +136,13 @@ class ImmichAppBarDialog extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: context.colorScheme.surface,
),
decoration: BoxDecoration(color: context.colorScheme.surface),
child: ListTile(
minLeadingWidth: 50,
leading: Icon(
Icons.storage_rounded,
color: theme.primaryColor,
),
leading: Icon(Icons.storage_rounded, color: theme.primaryColor),
title: Text(
"backup_controller_page_server_storage",
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w500,
),
style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500),
).tr(),
isThreeLine: true,
subtitle: Padding(
@@ -196,12 +160,9 @@ class ImmichAppBarDialog extends HookConsumerWidget {
),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: const Text('backup_controller_page_storage_format').tr(
namedArgs: {
'used': usedDiskSpace,
'total': totalDiskSpace,
},
),
child: const Text(
'backup_controller_page_storage_format',
).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}),
),
],
),
@@ -220,43 +181,19 @@ class ImmichAppBarDialog extends HookConsumerWidget {
InkWell(
onTap: () {
context.pop();
launchUrl(
Uri.parse('https://immich.app'),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse('https://immich.app'), mode: LaunchMode.externalApplication);
},
child: Text(
"documentation",
style: context.textTheme.bodySmall,
).tr(),
),
const SizedBox(
width: 20,
child: Text(
"",
textAlign: TextAlign.center,
),
child: Text("documentation", style: context.textTheme.bodySmall).tr(),
),
const SizedBox(width: 20, child: Text("", textAlign: TextAlign.center)),
InkWell(
onTap: () {
context.pop();
launchUrl(
Uri.parse('https://github.com/immich-app/immich'),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication);
},
child: Text(
"profile_drawer_github",
style: context.textTheme.bodySmall,
).tr(),
),
const SizedBox(
width: 20,
child: Text(
"",
textAlign: TextAlign.center,
),
child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(),
),
const SizedBox(width: 20, child: Text("", textAlign: TextAlign.center)),
InkWell(
onTap: () async {
context.pop();
@@ -291,20 +228,13 @@ class ImmichAppBarDialog extends HookConsumerWidget {
right: horizontalPadding,
bottom: isHorizontal ? 20 : 100,
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(20),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
child: SizedBox(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
child: buildTopRow(),
),
Container(padding: const EdgeInsets.all(20), child: buildTopRow()),
const AppBarProfileInfoBox(),
buildStorageInformation(),
const AppBarServerInfo(),
@@ -10,9 +10,7 @@ import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class AppBarProfileInfoBox extends HookConsumerWidget {
const AppBarProfileInfoBox({
super.key,
});
const AppBarProfileInfoBox({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -29,38 +27,24 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
);
}
final userImage = UserCircleAvatar(
radius: 22,
size: 44,
user: user,
);
final userImage = UserCircleAvatar(radius: 22, size: 44, user: user);
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const SizedBox(
height: 40,
width: 40,
child: ImmichLoadingIndicator(borderRadius: 20),
);
return const SizedBox(height: 40, width: 40, child: ImmichLoadingIndicator(borderRadius: 20));
}
return userImage;
}
pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxHeight: 1024,
maxWidth: 1024,
);
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
if (image != null) {
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
ref.watch(authProvider.notifier).updateUserProfileImagePath(
profileImagePath,
);
ref.watch(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
if (user != null) {
ref.read(currentUserProvider.notifier).refresh();
}
@@ -74,10 +58,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
width: double.infinity,
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)),
),
child: ListTile(
minLeadingWidth: 50,
@@ -93,16 +74,10 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
child: Material(
color: context.colorScheme.surfaceContainerHighest,
elevation: 3,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(50.0)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(
Icons.camera_alt_outlined,
color: context.primaryColor,
size: 14,
),
child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14),
),
),
),
@@ -111,16 +86,11 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
),
title: Text(
authState.name,
style: context.textTheme.titleMedium?.copyWith(
color: context.primaryColor,
fontWeight: FontWeight.w500,
),
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500),
),
subtitle: Text(
authState.userEmail,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
),
),
@@ -11,9 +11,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AppBarServerInfo extends HookConsumerWidget {
const AppBarServerInfo({
super.key,
});
const AppBarServerInfo({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -27,29 +25,20 @@ class AppBarServerInfo extends HookConsumerWidget {
getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {
"version": packageInfo.version,
"buildNumber": packageInfo.buildNumber,
};
appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber};
}
useEffect(
() {
getPackageInfo();
return null;
},
[],
);
useEffect(() {
getPackageInfo();
return null;
}, []);
return Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
@@ -63,17 +52,10 @@ class AppBarServerInfo extends HookConsumerWidget {
? serverInfoState.versionMismatchErrorMessage
: "profile_drawer_client_server_up_to_date".tr(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: context.primaryColor,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 11, color: context.primaryColor, fontWeight: FontWeight.w500),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Divider(thickness: 1),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -106,10 +88,7 @@ class AppBarServerInfo extends HookConsumerWidget {
),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Divider(thickness: 1),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -144,10 +123,7 @@ class AppBarServerInfo extends HookConsumerWidget {
),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Divider(thickness: 1),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -197,10 +173,7 @@ class AppBarServerInfo extends HookConsumerWidget {
),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Divider(thickness: 1),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -212,11 +185,7 @@ class AppBarServerInfo extends HookConsumerWidget {
if (serverInfoState.isNewReleaseAvailable)
const Padding(
padding: EdgeInsets.only(right: 5.0),
child: Icon(
Icons.info,
color: Color.fromARGB(255, 243, 188, 106),
size: 12,
),
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
),
Text(
"latest_version".tr(),
+3 -11
View File
@@ -26,9 +26,7 @@ class ConfirmDialog extends StatelessWidget {
}
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
title: Text(title).tr(),
content: Text(content).tr(),
actions: [
@@ -36,20 +34,14 @@ class ConfirmDialog extends StatelessWidget {
onPressed: () => context.pop(false),
child: Text(
cancel,
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: onOkPressed,
child: Text(
ok,
style: TextStyle(
color: context.colorScheme.error,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.bold),
).tr(),
),
],
+20 -74
View File
@@ -16,11 +16,8 @@ Future<String?> showDateTimePicker({
}) {
return showDialog<String?>(
context: context,
builder: (context) => _DateTimePicker(
initialDateTime: initialDateTime,
initialTZ: initialTZ,
initialTZOffset: initialTZOffset,
),
builder: (context) =>
_DateTimePicker(initialDateTime: initialDateTime, initialTZ: initialTZ, initialTZOffset: initialTZOffset),
);
}
@@ -33,18 +30,12 @@ class _DateTimePicker extends HookWidget {
final String? initialTZ;
final Duration? initialTZOffset;
const _DateTimePicker({
this.initialDateTime,
this.initialTZ,
this.initialTZOffset,
});
const _DateTimePicker({this.initialDateTime, this.initialTZ, this.initialTZOffset});
_TimeZoneOffset _getInitiationLocation() {
if (initialTZ != null) {
try {
return _TimeZoneOffset.fromLocation(
tz.timeZoneDatabase.get(initialTZ!),
);
return _TimeZoneOffset.fromLocation(tz.timeZoneDatabase.get(initialTZ!));
} on LocationNotFoundException {
// no-op
}
@@ -59,10 +50,8 @@ class _DateTimePicker extends HookWidget {
(location) => location.currentTimeZone.offset == offsetInMilli,
);
// Prefer locations with abbreviation first
final location = locations.firstWhereOrNull(
(e) => !e.currentTimeZone.abbreviation.contains("0"),
) ??
locations.firstOrNull;
final location =
locations.firstWhereOrNull((e) => !e.currentTimeZone.abbreviation.contains("0")) ?? locations.firstOrNull;
if (location != null) {
return _TimeZoneOffset.fromLocation(location);
}
@@ -86,11 +75,7 @@ class _DateTimePicker extends HookWidget {
(timezone) => DropdownMenuEntry<_TimeZoneOffset>(
value: timezone,
label: timezone.display,
style: ButtonStyle(
textStyle: WidgetStatePropertyAll(
context.textTheme.bodyMedium,
),
),
style: ButtonStyle(textStyle: WidgetStatePropertyAll(context.textTheme.bodyMedium)),
),
)
.toList();
@@ -109,10 +94,7 @@ class _DateTimePicker extends HookWidget {
return;
}
final newTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(date.value),
);
final newTime = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(date.value));
if (newTime == null) {
return;
@@ -145,10 +127,7 @@ class _DateTimePicker extends HookWidget {
onPressed: popWithDateTime,
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
).tr(),
),
],
@@ -156,46 +135,22 @@ class _DateTimePicker extends HookWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"date_and_time",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
).tr(),
const Text("date_and_time", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)).tr(),
const SizedBox(height: 32),
ListTile(
tileColor: context.colorScheme.surfaceContainerHighest,
shape: ShapeBorder.lerp(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
1,
),
trailing: Icon(
Icons.edit_outlined,
size: 18,
color: context.primaryColor,
),
title: Text(
DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
style: context.textTheme.bodyMedium,
).tr(),
trailing: Icon(Icons.edit_outlined, size: 18, color: context.primaryColor),
title: Text(DateFormat("dd-MM-yyyy hh:mm a").format(date.value), style: context.textTheme.bodyMedium).tr(),
onTap: pickDate,
),
const SizedBox(height: 24),
DropdownSearchMenu(
trailingIcon: Icon(
Icons.arrow_drop_down,
color: context.primaryColor,
),
trailingIcon: Icon(Icons.arrow_drop_down, color: context.primaryColor),
hintText: "timezone".tr(),
label: const Text('timezone').tr(),
textStyle: context.textTheme.bodyMedium,
@@ -213,26 +168,17 @@ class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
final String display;
final Location location;
const _TimeZoneOffset({
required this.display,
required this.location,
});
const _TimeZoneOffset({required this.display, required this.location});
_TimeZoneOffset copyWith({
String? display,
Location? location,
}) {
return _TimeZoneOffset(
display: display ?? this.display,
location: location ?? this.location,
);
_TimeZoneOffset copyWith({String? display, Location? location}) {
return _TimeZoneOffset(display: display ?? this.display, location: location ?? this.location);
}
int get offsetInMilliseconds => location.currentTimeZone.offset;
_TimeZoneOffset.fromLocation(tz.Location l)
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
location = l;
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
location = l;
@override
int compareTo(_TimeZoneOffset other) {
@@ -11,12 +11,7 @@ class DelayedLoadingIndicator extends StatelessWidget {
/// An optional fade in duration to animate the loading
final Duration? fadeInDuration;
const DelayedLoadingIndicator({
super.key,
this.delay = const Duration(seconds: 3),
this.child,
this.fadeInDuration,
});
const DelayedLoadingIndicator({super.key, this.delay = const Duration(seconds: 3), this.child, this.fadeInDuration});
@override
Widget build(BuildContext context) {
@@ -25,18 +20,12 @@ class DelayedLoadingIndicator extends StatelessWidget {
builder: (context, snapshot) {
late Widget c;
if (snapshot.connectionState == ConnectionState.done) {
c = child ??
const ImmichLoadingIndicator(
key: ValueKey('loading'),
);
c = child ?? const ImmichLoadingIndicator(key: ValueKey('loading'));
} else {
c = Container(key: const ValueKey('hiding'));
}
return AnimatedSwitcher(
duration: fadeInDuration ?? Duration.zero,
child: c,
);
return AnimatedSwitcher(duration: fadeInDuration ?? Duration.zero, child: c);
},
);
}
+2 -10
View File
@@ -18,13 +18,7 @@ class CustomDraggingHandle extends StatelessWidget {
}
class ControlBoxButton extends StatelessWidget {
const ControlBoxButton({
super.key,
required this.label,
required this.iconData,
this.onPressed,
this.onLongPressed,
});
const ControlBoxButton({super.key, required this.label, required this.iconData, this.onPressed, this.onLongPressed});
final String label;
final IconData iconData;
@@ -37,9 +31,7 @@ class ControlBoxButton extends StatelessWidget {
return MaterialButton(
padding: const EdgeInsets.all(10),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
onPressed: onPressed,
onLongPress: onLongPressed,
minWidth: minWidth,
@@ -34,13 +34,8 @@ class DropdownSearchMenu<T> extends HookWidget {
);
final showTimeZoneDropdown = useState<bool>(false);
final effectiveConstraints = menuConstraints ??
const BoxConstraints(
minWidth: 280,
maxWidth: 280,
minHeight: 0,
maxHeight: 280,
);
final effectiveConstraints =
menuConstraints ?? const BoxConstraints(minWidth: 280, maxWidth: 280, minHeight: 0, maxHeight: 280);
final inputDecoration = InputDecoration(
contentPadding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
@@ -58,12 +53,7 @@ class DropdownSearchMenu<T> extends HookWidget {
child: InputDecorator(
decoration: inputDecoration,
child: selectedItem.value != null
? Text(
selectedItem.value!.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textStyle,
)
? Text(selectedItem.value!.label, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle)
: null,
),
),
@@ -89,9 +79,7 @@ class DropdownSearchMenu<T> extends HookWidget {
autofocus: true,
focusNode: focusNode,
controller: textEditingController,
decoration: inputDecoration.copyWith(
hintText: "search_timezone".tr(),
),
decoration: inputDecoration.copyWith(hintText: "search_timezone".tr()),
maxLines: 1,
style: context.textTheme.bodyMedium,
expands: false,
@@ -125,23 +113,14 @@ class DropdownSearchMenu<T> extends HookWidget {
builder: (BuildContext context) {
final bool highlight = AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance.addPostFrameCallback(
(Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0.5,
);
},
debugLabel: 'AutocompleteOptions.ensureVisible',
);
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
}, debugLabel: 'AutocompleteOptions.ensureVisible');
}
return Container(
color: highlight ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.12) : null,
padding: const EdgeInsets.all(16.0),
child: Text(
option.label,
style: textStyle,
),
child: Text(option.label, style: textStyle),
);
},
),
@@ -22,12 +22,7 @@ class FadeInPlaceholderImage extends StatelessWidget {
fit: StackFit.expand,
children: [
placeholder,
FadeInImage(
fadeInDuration: duration,
image: image,
fit: fit,
placeholder: MemoryImage(kTransparentImage),
),
FadeInImage(fadeInDuration: duration, image: image, fit: fit, placeholder: MemoryImage(kTransparentImage)),
],
),
);
+14 -58
View File
@@ -36,23 +36,13 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
buildProfileIndicator() {
return InkWell(
onTap: () => showDialog(
context: context,
useRootNavigator: false,
builder: (ctx) => const ImmichAppBarDialog(),
),
onTap: () =>
showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Badge(
label: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(widgetSize / 2),
),
child: const Icon(
Icons.info,
color: Color.fromARGB(255, 243, 188, 106),
size: widgetSize / 2,
),
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)),
child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
),
backgroundColor: Colors.transparent,
alignment: Alignment.bottomRight,
@@ -60,17 +50,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
offset: const Offset(-2, -12),
child: user == null
? const Icon(
Icons.face_outlined,
size: widgetSize,
)
? const Icon(Icons.face_outlined, size: widgetSize)
: Semantics(
label: "logged_in_as".tr(namedArgs: {"user": user.name}),
child: UserCircleAvatar(
radius: 17,
size: 31,
user: user,
),
child: UserCircleAvatar(radius: 17, size: 31, user: user),
),
),
);
@@ -124,9 +107,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
height: widgetSize / 2,
decoration: BoxDecoration(
color: badgeBackground,
border: Border.all(
color: context.colorScheme.outline.withValues(alpha: .3),
),
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
borderRadius: BorderRadius.circular(widgetSize / 2),
),
child: indicatorIcon,
@@ -135,22 +116,14 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
alignment: Alignment.bottomRight,
isLabelVisible: indicatorIcon != null,
offset: const Offset(-2, -12),
child: Icon(
Icons.backup_rounded,
size: widgetSize,
color: context.primaryColor,
),
child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor),
),
);
}
return AppBar(
backgroundColor: context.themeData.appBarTheme.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: false,
centerTitle: false,
title: Builder(
@@ -176,12 +149,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
),
actions: [
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
if (kDebugMode || kProfileMode)
IconButton(
icon: const Icon(Icons.science_rounded),
@@ -192,25 +160,13 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
padding: const EdgeInsets.only(right: 12),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
showDialog(context: context, builder: (context) => const CastDialog());
},
icon: Icon(
isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
),
icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded),
),
),
if (showUploadButton)
Padding(
padding: const EdgeInsets.only(right: 20),
child: buildBackupIndicator(),
),
Padding(
padding: const EdgeInsets.only(right: 20),
child: buildProfileIndicator(),
),
if (showUploadButton) Padding(padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator()),
Padding(padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator()),
],
);
}
+7 -30
View File
@@ -28,32 +28,19 @@ class ImmichImage extends StatelessWidget {
// either by using the asset ID or the asset itself
/// [asset] is the Asset to request, or else use [assetId] to get a remote
/// image provider
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
double width = 1080,
double height = 1920,
}) {
static ImageProvider imageProvider({Asset? asset, String? assetId, double width = 1080, double height = 1920}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
}
if (asset == null) {
return ImmichRemoteImageProvider(
assetId: assetId!,
);
return ImmichRemoteImageProvider(assetId: assetId!);
}
if (useLocal(asset)) {
return ImmichLocalImageProvider(
asset: asset,
width: width,
height: height,
);
return ImmichLocalImageProvider(asset: asset, width: width, height: height);
} else {
return ImmichRemoteImageProvider(
assetId: asset.remoteId!,
);
return ImmichRemoteImageProvider(assetId: asset.remoteId!);
}
}
@@ -68,17 +55,11 @@ class ImmichImage extends StatelessWidget {
color: Colors.grey,
width: width,
height: height,
child: const Center(
child: Icon(Icons.no_photography),
),
child: const Center(child: Icon(Icons.no_photography)),
);
}
final imageProviderInstance = ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
);
final imageProviderInstance = ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height);
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
@@ -96,11 +77,7 @@ class ImmichImage extends StatelessWidget {
errorBuilder: (context, error, stackTrace) {
imageProviderInstance.evict();
return Icon(
Icons.image_not_supported_outlined,
size: 32,
color: Colors.red[200],
);
return Icon(Icons.image_not_supported_outlined, size: 32, color: Colors.red[200]);
},
);
}
@@ -5,22 +5,15 @@ import 'package:immich_mobile/widgets/common/immich_logo.dart';
class ImmichLoadingIndicator extends HookWidget {
final double? borderRadius;
const ImmichLoadingIndicator({
super.key,
this.borderRadius,
});
const ImmichLoadingIndicator({super.key, this.borderRadius});
@override
Widget build(BuildContext context) {
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 6),
)
final logoAnimationController = useAnimationController(duration: const Duration(seconds: 6))
..reverse()
..repeat();
final borderAnimationController = useAnimationController(
duration: const Duration(seconds: 6),
)..repeat();
final borderAnimationController = useAnimationController(duration: const Duration(seconds: 6))..repeat();
return Container(
height: 80,
@@ -34,10 +27,7 @@ class ImmichLoadingIndicator extends HookWidget {
animation: borderAnimationController,
builder: (context, child) {
return CustomPaint(
painter: GradientBorderPainter(
animation: borderAnimationController.value,
strokeWidth: 3,
),
painter: GradientBorderPainter(animation: borderAnimationController.value, strokeWidth: 3),
child: child,
);
},
@@ -45,9 +35,7 @@ class ImmichLoadingIndicator extends HookWidget {
padding: const EdgeInsets.all(15),
child: RotationTransition(
turns: logoAnimationController,
child: const ImmichLogo(
heroTag: 'logo',
),
child: const ImmichLogo(heroTag: 'logo'),
),
),
),
@@ -67,10 +55,7 @@ class GradientBorderPainter extends CustomPainter {
const Color(0xFF18C249),
];
GradientBorderPainter({
required this.animation,
required this.strokeWidth,
});
GradientBorderPainter({required this.animation, required this.strokeWidth});
@override
void paint(Canvas canvas, Size size) {
@@ -96,10 +81,7 @@ class GradientBorderPainter extends CustomPainter {
colors.first.withValues(alpha: opacity),
],
// Add evenly distributed stops
stops: List.generate(
colors.length + 1,
(index) => index / colors.length,
),
stops: List.generate(colors.length + 1, (index) => index / colors.length),
tileMode: TileMode.clamp,
// Use transformations to rotate the gradient
transform: GradientRotation(-animation * 2 * 3.14159),
+1 -5
View File
@@ -4,11 +4,7 @@ class ImmichLogo extends StatelessWidget {
final double size;
final dynamic heroTag;
const ImmichLogo({
super.key,
this.size = 100,
this.heroTag,
});
const ImmichLogo({super.key, this.size = 100, this.heroTag});
@override
Widget build(BuildContext context) {
@@ -52,11 +52,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
pinned: pinned,
snap: snap,
expandedHeight: expandedHeight,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: false,
centerTitle: false,
title: title ?? const _ImmichLogoWithText(),
@@ -66,38 +62,21 @@ class ImmichSliverAppBar extends ConsumerWidget {
padding: const EdgeInsets.only(right: 12),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
showDialog(context: context, builder: (context) => const CastDialog());
},
icon: Icon(
isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
),
icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded),
),
),
const _SyncStatusIndicator(),
if (actions != null)
...actions!.map(
(action) => Padding(
padding: const EdgeInsets.only(right: 16),
child: action,
),
),
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
if (kDebugMode || kProfileMode)
IconButton(
icon: const Icon(Icons.science_rounded),
onPressed: () => context.pushRoute(const FeatInDevRoute()),
),
if (showUploadButton)
const Padding(
padding: EdgeInsets.only(right: 20),
child: _BackupIndicator(),
),
const Padding(
padding: EdgeInsets.only(right: 20),
child: _ProfileIndicator(),
),
if (showUploadButton) const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()),
],
),
);
@@ -159,23 +138,12 @@ class _ProfileIndicator extends ConsumerWidget {
const widgetSize = 30.0;
return InkWell(
onTap: () => showDialog(
context: context,
useRootNavigator: false,
builder: (ctx) => const ImmichAppBarDialog(),
),
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Badge(
label: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(widgetSize / 2),
),
child: const Icon(
Icons.info,
color: Color.fromARGB(255, 243, 188, 106),
size: widgetSize / 2,
),
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)),
child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
),
backgroundColor: Colors.transparent,
alignment: Alignment.bottomRight,
@@ -183,17 +151,10 @@ class _ProfileIndicator extends ConsumerWidget {
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
offset: const Offset(-2, -12),
child: user == null
? const Icon(
Icons.face_outlined,
size: widgetSize,
)
? const Icon(Icons.face_outlined, size: widgetSize)
: Semantics(
label: "logged_in_as".tr(namedArgs: {"user": user.name}),
child: UserCircleAvatar(
radius: 17,
size: 31,
user: user,
),
child: UserCircleAvatar(radius: 17, size: 31, user: user),
),
),
);
@@ -218,9 +179,7 @@ class _BackupIndicator extends ConsumerWidget {
height: widgetSize / 2,
decoration: BoxDecoration(
color: badgeBackground,
border: Border.all(
color: context.colorScheme.outline.withValues(alpha: .3),
),
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
borderRadius: BorderRadius.circular(widgetSize / 2),
),
child: indicatorIcon,
@@ -229,11 +188,7 @@ class _BackupIndicator extends ConsumerWidget {
alignment: Alignment.bottomRight,
isLabelVisible: indicatorIcon != null,
offset: const Offset(-2, -12),
child: Icon(
Icons.backup_rounded,
size: widgetSize,
color: context.primaryColor,
),
child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor),
),
);
}
@@ -263,8 +218,9 @@ class _BackupIndicator extends ConsumerWidget {
return Container(
padding: const EdgeInsets.all(3.5),
child: Theme(
data: context.themeData
.copyWith(progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true)),
data: context.themeData.copyWith(
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
),
child: CircularProgressIndicator(
strokeWidth: 2,
strokeCap: StrokeCap.round,
@@ -302,27 +258,13 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with
@override
void initState() {
super.initState();
_rotationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_dismissalController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(_rotationController);
_rotationController = AnimationController(duration: const Duration(seconds: 2), vsync: this);
_dismissalController = AnimationController(duration: const Duration(milliseconds: 300), vsync: this);
_rotationAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(_rotationController);
_dismissalAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _dismissalController,
curve: Curves.easeOutQuart,
),
);
).animate(CurvedAnimation(parent: _dismissalController, curve: Curves.easeOutQuart));
}
@override
@@ -366,11 +308,7 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with
opacity: isSyncing ? 1.0 : _dismissalAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159 * -1, // Rotate counter-clockwise
child: Icon(
Icons.sync,
size: 24,
color: context.primaryColor,
),
child: Icon(Icons.sync, size: 24, color: context.primaryColor),
),
),
),
@@ -13,13 +13,7 @@ import 'package:octo_image/octo_image.dart';
import 'package:immich_mobile/providers/user.provider.dart';
class ImmichThumbnail extends HookConsumerWidget {
const ImmichThumbnail({
this.asset,
this.width = 250,
this.height = 250,
this.fit = BoxFit.cover,
super.key,
});
const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key});
final Asset? asset;
final double width;
@@ -30,35 +24,19 @@ class ImmichThumbnail extends HookConsumerWidget {
/// either by using the asset ID or the asset itself
/// [asset] is the Asset to request, or else use [assetId] to get a remote
/// image provider
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
String? userId,
int thumbnailSize = 256,
}) {
static ImageProvider imageProvider({Asset? asset, String? assetId, String? userId, int thumbnailSize = 256}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
}
if (asset == null) {
return ImmichRemoteThumbnailProvider(
assetId: assetId!,
);
return ImmichRemoteThumbnailProvider(assetId: assetId!);
}
if (ImmichImage.useLocal(asset)) {
return ImmichLocalThumbnailProvider(
asset: asset,
height: thumbnailSize,
width: thumbnailSize,
userId: userId,
);
return ImmichLocalThumbnailProvider(asset: asset, height: thumbnailSize, width: thumbnailSize, userId: userId);
} else {
return ImmichRemoteThumbnailProvider(
assetId: asset.remoteId!,
height: thumbnailSize,
width: thumbnailSize,
);
return ImmichRemoteThumbnailProvider(assetId: asset.remoteId!, height: thumbnailSize, width: thumbnailSize);
}
}
@@ -72,23 +50,13 @@ class ImmichThumbnail extends HookConsumerWidget {
color: Colors.grey,
width: width,
height: height,
child: const Center(
child: Icon(Icons.no_photography),
),
child: const Center(child: Icon(Icons.no_photography)),
);
}
final assetAltText = getAltText(
asset!.exifInfo,
asset!.fileCreatedAt,
asset!.type,
[],
);
final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []);
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(
asset: asset,
userId: userId,
);
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset, userId: userId);
customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) {
thumbnailProviderInstance.evict();
@@ -5,18 +5,12 @@ class ImmichTitleText extends StatelessWidget {
final double fontSize;
final Color? color;
const ImmichTitleText({
super.key,
this.fontSize = 48,
this.color,
});
const ImmichTitleText({super.key, this.fontSize = 48, this.color});
@override
Widget build(BuildContext context) {
return Image(
image: AssetImage(
context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png',
),
image: AssetImage(context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png'),
width: fontSize * 4,
filterQuality: FilterQuality.high,
color: context.primaryColor,
+11 -29
View File
@@ -16,25 +16,16 @@ class ImmichToast {
fToast.init(context);
Color getColor(ToastType type, BuildContext context) => switch (type) {
ToastType.info => context.primaryColor,
ToastType.success => const Color.fromARGB(255, 78, 140, 124),
ToastType.error => const Color.fromARGB(255, 220, 48, 85),
};
ToastType.info => context.primaryColor,
ToastType.success => const Color.fromARGB(255, 78, 140, 124),
ToastType.error => const Color.fromARGB(255, 220, 48, 85),
};
Icon getIcon(ToastType type) => switch (type) {
ToastType.info => Icon(
Icons.info_outline_rounded,
color: context.primaryColor,
),
ToastType.success => const Icon(
Icons.check_circle_rounded,
color: Color.fromARGB(255, 78, 140, 124),
),
ToastType.error => const Icon(
Icons.error_outline_rounded,
color: Color.fromARGB(255, 240, 162, 156),
),
};
ToastType.info => Icon(Icons.info_outline_rounded, color: context.primaryColor),
ToastType.success => const Icon(Icons.check_circle_rounded, color: Color.fromARGB(255, 78, 140, 124)),
ToastType.error => const Icon(Icons.error_outline_rounded, color: Color.fromARGB(255, 240, 162, 156)),
};
fToast.showToast(
child: Container(
@@ -42,26 +33,17 @@ class ImmichToast {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
color: context.colorScheme.surfaceContainer,
border: Border.all(
color: context.colorScheme.outline.withValues(alpha: .5),
width: 1,
),
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .5), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
getIcon(toastType),
const SizedBox(
width: 12.0,
),
const SizedBox(width: 12.0),
Flexible(
child: Text(
msg,
style: TextStyle(
color: getColor(toastType, context),
fontWeight: FontWeight.w600,
fontSize: 14,
),
style: TextStyle(color: getColor(toastType, context), fontWeight: FontWeight.w600, fontSize: 14),
),
),
],
@@ -12,14 +12,10 @@ class LocalAlbumsSliverAppBar extends StatelessWidget {
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: true,
centerTitle: true,
title: Text(
"on_this_device".t(context: context),
),
title: Text("on_this_device".t(context: context)),
);
}
}
+10 -38
View File
@@ -9,16 +9,11 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
Future<LatLng?> showLocationPicker({
required BuildContext context,
LatLng? initialLatLng,
}) {
Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initialLatLng}) {
return showDialog<LatLng?>(
context: context,
useRootNavigator: false,
builder: (ctx) => _LocationPicker(
initialLatLng: initialLatLng,
),
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
);
}
@@ -27,9 +22,7 @@ enum _LocationPickerMode { map, manual }
class _LocationPicker extends HookWidget {
final LatLng? initialLatLng;
const _LocationPicker({
this.initialLatLng,
});
const _LocationPicker({this.initialLatLng});
@override
Widget build(BuildContext context) {
@@ -39,9 +32,7 @@ class _LocationPicker extends HookWidget {
final pickerMode = useState(_LocationPickerMode.map);
Future<void> onMapTap() async {
final newLatLng = await context.pushRoute<LatLng?>(
MapLocationPickerRoute(initialLatLng: latlng),
);
final newLatLng = await context.pushRoute<LatLng?>(MapLocationPickerRoute(initialLatLng: latlng));
if (newLatLng != null) {
latitude.value = newLatLng.latitude;
longitude.value = newLatLng.longitude;
@@ -81,10 +72,7 @@ class _LocationPicker extends HookWidget {
onPressed: () => context.maybePop(latlng),
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
).tr(),
),
],
@@ -129,10 +117,7 @@ class _ManualPickerInput extends HookWidget {
autofocus: false,
decoration: InputDecoration(
labelText: decorationText.tr(),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor),
floatingLabelBehavior: FloatingLabelBehavior.auto,
border: const OutlineInputBorder(),
hintText: hintText.tr(),
@@ -188,10 +173,7 @@ class _ManualPicker extends HookWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_location_dialog_title",
textAlign: TextAlign.center,
).tr(),
const Text("edit_location_dialog_title", textAlign: TextAlign.center).tr(),
const SizedBox(height: 12),
TextButton.icon(
icon: const Text("location_picker_choose_on_map").tr(),
@@ -228,27 +210,17 @@ class _MapPicker extends StatelessWidget {
final Function() onModeSwitch;
final Function() onMapTap;
const _MapPicker({
required this.latlng,
required this.onModeSwitch,
required this.onMapTap,
super.key,
});
const _MapPicker({required this.latlng, required this.onModeSwitch, required this.onMapTap, super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_location_dialog_title",
textAlign: TextAlign.center,
).tr(),
const Text("edit_location_dialog_title", textAlign: TextAlign.center).tr(),
const SizedBox(height: 12),
TextButton.icon(
icon: Text(
"${latlng.latitude.toStringAsFixed(4)}, ${latlng.longitude.toStringAsFixed(4)}",
),
icon: Text("${latlng.latitude.toStringAsFixed(4)}, ${latlng.longitude.toStringAsFixed(4)}"),
label: const Icon(Icons.edit_outlined, size: 16),
onPressed: onModeSwitch,
),
@@ -14,11 +14,7 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class MesmerizingSliverAppBar extends ConsumerStatefulWidget {
const MesmerizingSliverAppBar({
super.key,
required this.title,
this.icon = Icons.camera,
});
const MesmerizingSliverAppBar({super.key, required this.title, this.icon = Icons.camera});
final String title;
final IconData icon;
@@ -62,23 +58,11 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
leading: IconButton(
icon: Icon(
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
color: Color.lerp(
Colors.white,
context.primaryColor,
_scrollProgress,
),
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
shadows: [
_scrollProgress < 0.95
? Shadow(
offset: const Offset(0, 2),
blurRadius: 5,
color: Colors.black.withValues(alpha: 0.5),
)
: const Shadow(
offset: Offset(0, 2),
blurRadius: 0,
color: Colors.transparent,
),
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
],
),
onPressed: () {
@@ -106,11 +90,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
child: scrollProgress > 0.95
? Text(
widget.title,
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 18,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
)
: null,
),
@@ -131,11 +111,7 @@ class _ExpandedBackground extends ConsumerStatefulWidget {
final String title;
final IconData icon;
const _ExpandedBackground({
required this.scrollProgress,
required this.title,
required this.icon,
});
const _ExpandedBackground({required this.scrollProgress, required this.title, required this.icon});
@override
ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState();
@@ -149,20 +125,12 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
void initState() {
super.initState();
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
),
);
).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic));
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
@@ -188,10 +156,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
offset: Offset(0, widget.scrollProgress * 50),
child: Transform.scale(
scale: 1.4 - (widget.scrollProgress * 0.2),
child: _RandomAssetBackground(
timelineService: timelineService,
icon: widget.icon,
),
child: _RandomAssetBackground(timelineService: timelineService, icon: widget.icon),
),
),
Container(
@@ -202,9 +167,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
colors: [
Colors.transparent,
Colors.transparent,
Colors.black.withValues(
alpha: 0.6 + (widget.scrollProgress * 0.2),
),
Colors.black.withValues(alpha: 0.6 + (widget.scrollProgress * 0.2)),
],
stops: const [0.0, 0.65, 1.0],
),
@@ -232,21 +195,12 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 12,
color: Colors.black45,
),
],
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black45)],
),
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: const _ItemCountText(),
),
AnimatedContainer(duration: const Duration(milliseconds: 300), child: const _ItemCountText()),
],
),
),
@@ -280,26 +234,15 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> {
@override
Widget build(BuildContext context) {
final assetCount = ref.watch(
timelineServiceProvider.select((s) => s.totalAssets),
);
final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets));
return Text(
'items_count'.t(
context: context,
args: {"count": assetCount},
),
'items_count'.t(context: context, args: {"count": assetCount}),
style: context.textTheme.labelLarge?.copyWith(
// letterSpacing: 0.2,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
const Shadow(
offset: Offset(0, 1),
blurRadius: 6,
color: Colors.black45,
),
],
shadows: [const Shadow(offset: Offset(0, 1), blurRadius: 6, color: Colors.black45)],
),
);
}
@@ -309,10 +252,7 @@ class _RandomAssetBackground extends StatefulWidget {
final TimelineService timelineService;
final IconData icon;
const _RandomAssetBackground({
required this.timelineService,
required this.icon,
});
const _RandomAssetBackground({required this.timelineService, required this.icon});
@override
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
@@ -332,50 +272,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
void initState() {
super.initState();
_zoomController = AnimationController(
duration: const Duration(seconds: 12),
vsync: this,
);
_zoomController = AnimationController(duration: const Duration(seconds: 12), vsync: this);
_crossFadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_crossFadeController = AnimationController(duration: const Duration(milliseconds: 1200), vsync: this);
_zoomAnimation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(
CurvedAnimation(
parent: _zoomController,
curve: Curves.easeInOut,
),
);
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
_panAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, -0.5),
).animate(
CurvedAnimation(
parent: _zoomController,
curve: Curves.easeInOut,
),
);
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
_crossFadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: _crossFadeController,
curve: Curves.easeInOutCubic,
),
);
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
Future.delayed(
Durations.medium1,
() => _loadFirstAsset(),
);
Future.delayed(Durations.medium1, () => _loadFirstAsset());
}
@override
@@ -465,9 +381,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
}
return AnimatedBuilder(
animation: Listenable.merge(
[_zoomAnimation, _panAnimation, _crossFadeAnimation],
),
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
builder: (context, child) {
return Transform.scale(
scale: _zoomAnimation.value,
@@ -499,11 +413,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Icon(
Icons.error_outline_rounded,
size: 24,
color: Colors.red[300],
),
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
);
},
),
@@ -530,11 +440,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Icon(
Icons.error_outline_rounded,
size: 24,
color: Colors.red[300],
),
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
);
},
),
@@ -63,25 +63,13 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
Color? actionIconColor = Color.lerp(
Colors.white,
context.primaryColor,
_scrollProgress,
);
Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress);
List<Shadow> actionIconShadows = [
if (_scrollProgress < 0.95)
Shadow(
offset: const Offset(0, 2),
blurRadius: 5,
color: Colors.black.withValues(alpha: 0.5),
)
Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
else
const Shadow(
offset: Offset(0, 2),
blurRadius: 0,
color: Colors.transparent,
),
const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
];
return isMultiSelectEnabled
@@ -111,20 +99,12 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
actions: [
if (widget.onToggleAlbumOrder != null)
IconButton(
icon: Icon(
Icons.swap_vert_rounded,
color: actionIconColor,
shadows: actionIconShadows,
),
icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows),
onPressed: widget.onToggleAlbumOrder,
),
if (widget.onShowOptions != null)
IconButton(
icon: Icon(
Icons.more_vert,
color: actionIconColor,
shadows: actionIconShadows,
),
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
onPressed: widget.onShowOptions,
),
],
@@ -149,11 +129,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
child: scrollProgress > 0.95
? Text(
currentAlbum.name,
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 18,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
)
: null,
),
@@ -174,11 +150,7 @@ class _ExpandedBackground extends ConsumerStatefulWidget {
final IconData icon;
final void Function()? onEditTitle;
const _ExpandedBackground({
required this.scrollProgress,
required this.icon,
this.onEditTitle,
});
const _ExpandedBackground({required this.scrollProgress, required this.icon, this.onEditTitle});
@override
ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState();
@@ -192,20 +164,12 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
void initState() {
super.initState();
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
),
);
).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic));
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
@@ -229,9 +193,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
return const SizedBox.shrink();
}
final dateRange = ref.watch(
remoteAlbumDateRangeProvider(currentAlbum.id),
);
final dateRange = ref.watch(remoteAlbumDateRangeProvider(currentAlbum.id));
return Stack(
fit: StackFit.expand,
children: [
@@ -239,18 +201,12 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
offset: Offset(0, widget.scrollProgress * 50),
child: Transform.scale(
scale: 1.4 - (widget.scrollProgress * 0.2),
child: _RandomAssetBackground(
timelineService: timelineService,
icon: widget.icon,
),
child: _RandomAssetBackground(timelineService: timelineService, icon: widget.icon),
),
),
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.scrollProgress * 2.0,
sigmaY: widget.scrollProgress * 2.0,
),
filter: ImageFilter.blur(sigmaX: widget.scrollProgress * 2.0, sigmaY: widget.scrollProgress * 2.0),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -260,9 +216,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
Colors.black.withValues(alpha: 0.05),
Colors.transparent,
Colors.black.withValues(alpha: 0.3),
Colors.black.withValues(
alpha: 0.6 + (widget.scrollProgress * 0.25),
),
Colors.black.withValues(alpha: 0.6 + (widget.scrollProgress * 0.25)),
],
stops: const [0.0, 0.15, 0.55, 1.0],
),
@@ -291,32 +245,17 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
),
style: const TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 12,
color: Colors.black87,
),
],
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
),
),
const Text(
"",
style: TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 12,
color: Colors.black87,
),
],
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: const _ItemCountText(),
),
AnimatedContainer(duration: const Duration(milliseconds: 300), child: const _ItemCountText()),
],
),
GestureDetector(
@@ -333,13 +272,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 12,
color: Colors.black54,
),
],
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black54)],
),
),
),
@@ -349,31 +282,20 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
GestureDetector(
onTap: widget.onEditTitle,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 80,
),
constraints: const BoxConstraints(maxHeight: 80),
child: SingleChildScrollView(
child: Text(
currentAlbum.description,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
shadows: [
Shadow(
offset: Offset(0, 2),
blurRadius: 8,
color: Colors.black54,
),
],
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 8, color: Colors.black54)],
),
),
),
),
),
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: RemoteAlbumSharedUserIcons(),
),
const Padding(padding: EdgeInsets.only(top: 8.0), child: RemoteAlbumSharedUserIcons()),
],
),
),
@@ -407,24 +329,13 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> {
@override
Widget build(BuildContext context) {
final assetCount = ref.watch(
timelineServiceProvider.select((s) => s.totalAssets),
);
final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets));
return Text(
'items_count'.t(
context: context,
args: {"count": assetCount},
),
'items_count'.t(context: context, args: {"count": assetCount}),
style: context.textTheme.labelLarge?.copyWith(
color: Colors.white,
shadows: [
const Shadow(
offset: Offset(0, 2),
blurRadius: 12,
color: Colors.black87,
),
],
shadows: [const Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
),
);
}
@@ -434,10 +345,7 @@ class _RandomAssetBackground extends StatefulWidget {
final TimelineService timelineService;
final IconData icon;
const _RandomAssetBackground({
required this.timelineService,
required this.icon,
});
const _RandomAssetBackground({required this.timelineService, required this.icon});
@override
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
@@ -457,50 +365,26 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
void initState() {
super.initState();
_zoomController = AnimationController(
duration: const Duration(seconds: 12),
vsync: this,
);
_zoomController = AnimationController(duration: const Duration(seconds: 12), vsync: this);
_crossFadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_crossFadeController = AnimationController(duration: const Duration(milliseconds: 1200), vsync: this);
_zoomAnimation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(
CurvedAnimation(
parent: _zoomController,
curve: Curves.easeInOut,
),
);
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
_panAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, -0.5),
).animate(
CurvedAnimation(
parent: _zoomController,
curve: Curves.easeInOut,
),
);
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
_crossFadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: _crossFadeController,
curve: Curves.easeInOutCubic,
),
);
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
Future.delayed(
Durations.medium1,
() => _loadFirstAsset(),
);
Future.delayed(Durations.medium1, () => _loadFirstAsset());
}
@override
@@ -590,9 +474,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
}
return AnimatedBuilder(
animation: Listenable.merge(
[_zoomAnimation, _panAnimation, _crossFadeAnimation],
),
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
builder: (context, child) {
return Transform.scale(
scale: _zoomAnimation.value,
@@ -624,11 +506,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Icon(
Icons.error_outline_rounded,
size: 24,
color: Colors.red[300],
),
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
);
},
),
@@ -655,11 +533,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Icon(
Icons.error_outline_rounded,
size: 24,
color: Colors.red[300],
),
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
);
},
),
@@ -15,11 +15,7 @@ class ScaffoldErrorBody extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"scaffold_body_error_occurred",
style: context.textTheme.displayMedium,
textAlign: TextAlign.center,
).tr(),
Text("scaffold_body_error_occurred", style: context.textTheme.displayMedium, textAlign: TextAlign.center).tr(),
if (withIcon)
Center(
child: Padding(
@@ -34,11 +30,7 @@ class ScaffoldErrorBody extends StatelessWidget {
if (withIcon && errorMsg != null)
Padding(
padding: const EdgeInsets.all(20),
child: Text(
errorMsg!,
style: context.textTheme.displaySmall,
textAlign: TextAlign.center,
),
child: Text(errorMsg!, style: context.textTheme.displaySmall, textAlign: TextAlign.center),
),
],
);
+9 -27
View File
@@ -43,40 +43,22 @@ class SearchField extends StatelessWidget {
contentPadding: contentPadding,
filled: filled,
fillColor: context.primaryColor.withValues(alpha: 0.1),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurfaceSecondary,
),
hintStyle: context.textTheme.bodyLarge?.copyWith(color: context.themeData.colorScheme.onSurfaceSecondary),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide(
color: context.colorScheme.surfaceDim,
),
borderRadius: const BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: context.colorScheme.surfaceDim),
),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide(
color: context.colorScheme.surfaceContainer,
),
borderRadius: const BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: context.colorScheme.surfaceContainer),
),
disabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide(
color: context.colorScheme.surfaceDim,
),
borderRadius: const BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: context.colorScheme.surfaceDim),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide(
color: context.colorScheme.primary.withAlpha(100),
),
borderRadius: const BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: context.colorScheme.primary.withAlpha(100)),
),
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
@@ -7,9 +7,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class SelectionSliverAppBar extends ConsumerStatefulWidget {
const SelectionSliverAppBar({
super.key,
});
const SelectionSliverAppBar({super.key});
@override
ConsumerState<SelectionSliverAppBar> createState() => _SelectionSliverAppBarState();
@@ -18,13 +16,9 @@ class SelectionSliverAppBar extends ConsumerStatefulWidget {
class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
@override
Widget build(BuildContext context) {
final selection = ref.watch(
multiSelectProvider.select((s) => s.selectedAssets),
);
final selection = ref.watch(multiSelectProvider.select((s) => s.selectedAssets));
final toExclude = ref.watch(
multiSelectProvider.select((s) => s.lockedSelectionAssets),
);
final toExclude = ref.watch(multiSelectProvider.select((s) => s.lockedSelectionAssets));
final filteredAssets = selection.where((asset) {
return !toExclude.contains(asset);
@@ -40,9 +34,7 @@ class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
@@ -52,22 +44,13 @@ class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
},
),
centerTitle: true,
title: Text(
"Select {count}".t(
context: context,
args: {
'count': filteredAssets.length.toString(),
},
),
),
title: Text("Select {count}".t(context: context, args: {'count': filteredAssets.length.toString()})),
actions: [
TextButton(
onPressed: () => onDone(filteredAssets),
child: Text(
'done'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.primary,
),
style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary),
),
),
],
+1 -4
View File
@@ -11,10 +11,7 @@ class ShareDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
Container(
margin: const EdgeInsets.only(top: 12),
child: const Text('share_dialog_preparing').tr(),
),
Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()),
],
),
);
@@ -6,21 +6,14 @@ import 'package:octo_image/octo_image.dart';
/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
/// placeholder and [OctoError.icon] as error.
OctoSet blurHashOrPlaceholder(
Uint8List? blurhash, {
BoxFit? fit,
Text? errorMessage,
}) {
OctoSet blurHashOrPlaceholder(Uint8List? blurhash, {BoxFit? fit, Text? errorMessage}) {
return OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
errorBuilder: blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage),
);
}
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
Uint8List? blurhash, {
BoxFit? fit,
}) {
OctoPlaceholderBuilder blurHashPlaceholderBuilder(Uint8List? blurhash, {BoxFit? fit}) {
return (context) => blurhash == null
? const ThumbnailPlaceholder()
: FadeInPlaceholderImage(
@@ -16,13 +16,7 @@ class UserCircleAvatar extends ConsumerWidget {
double size;
bool hasBorder;
UserCircleAvatar({
super.key,
this.radius = 22,
this.size = 44,
this.hasBorder = false,
required this.user,
});
UserCircleAvatar({super.key, this.radius = 22, this.size = 44, this.hasBorder = false, required this.user});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -43,12 +37,7 @@ class UserCircleAvatar extends ConsumerWidget {
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: hasBorder
? Border.all(
color: Colors.grey[500]!,
width: 1,
)
: null,
border: hasBorder ? Border.all(color: Colors.grey[500]!, width: 1) : null,
),
child: CircleAvatar(
backgroundColor: userAvatarColor,
@@ -33,25 +33,13 @@ class ChangePasswordForm extends HookConsumerWidget {
children: [
Text(
'change_password'.tr(),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: context.primaryColor),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
'change_password_form_description'.tr(
namedArgs: {
'name': authState.name,
},
),
style: TextStyle(
fontSize: 14,
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
'change_password_form_description'.tr(namedArgs: {'name': authState.name}),
style: TextStyle(fontSize: 14, color: context.colorScheme.onSurface, fontWeight: FontWeight.w600),
),
),
Form(
@@ -70,8 +58,9 @@ class ChangePasswordForm extends HookConsumerWidget {
passwordController: passwordController,
onPressed: () async {
if (formKey.currentState!.validate()) {
var isSuccess =
await ref.read(authProvider.notifier).changePassword(passwordController.value.text);
var isSuccess = await ref
.read(authProvider.notifier)
.changePassword(passwordController.value.text);
if (isSuccess) {
await ref.read(authProvider.notifier).logout();
@@ -139,11 +128,7 @@ class ConfirmPasswordInput extends StatelessWidget {
final TextEditingController originalController;
final TextEditingController confirmController;
const ConfirmPasswordInput({
super.key,
required this.originalController,
required this.confirmController,
});
const ConfirmPasswordInput({super.key, required this.originalController, required this.confirmController});
String? _validateInput(String? email) {
if (confirmController.value != originalController.value) {
@@ -171,11 +156,7 @@ class ConfirmPasswordInput extends StatelessWidget {
class ChangePasswordButton extends ConsumerWidget {
final TextEditingController passwordController;
final VoidCallback onPressed;
const ChangePasswordButton({
super.key,
required this.passwordController,
required this.onPressed,
});
const ChangePasswordButton({super.key, required this.passwordController, required this.onPressed});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -185,10 +166,7 @@ class ChangePasswordButton extends ConsumerWidget {
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
onPressed: onPressed,
child: Text(
'change_password'.tr(),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
child: Text('change_password'.tr(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
);
}
}
@@ -6,12 +6,7 @@ class EmailInput extends StatelessWidget {
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
const EmailInput({super.key, required this.controller, this.focusNode, this.onSubmit});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
@@ -32,10 +27,7 @@ class EmailInput extends StatelessWidget {
labelText: 'email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
@@ -7,15 +7,7 @@ class LoadingIcon extends StatelessWidget {
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
child: SizedBox(width: 24, height: 24, child: FittedBox(child: CircularProgressIndicator(strokeWidth: 2))),
);
}
}
@@ -5,23 +5,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
const LoginButton({super.key, required this.onPressed});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
label: const Text("login", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
);
}
}
+28 -82
View File
@@ -54,9 +54,7 @@ class LoginForm extends HookConsumerWidget {
final isOauthEnable = useState<bool>(false);
final isPasswordLoginEnable = useState<bool>(false);
final oAuthButtonLabel = useState<String>('OAuth');
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
final logoAnimationController = useAnimationController(duration: const Duration(seconds: 60))..repeat();
final serverInfo = ref.watch(serverInfoProvider);
final warningMessage = useState<String?>(null);
final loginFormKey = GlobalKey<FormState>();
@@ -90,11 +88,7 @@ class LoginForm extends HookConsumerWidget {
// Guard empty URL
if (serverUrl.isEmpty) {
ImmichToast.show(
context: context,
msg: "login_form_server_empty".tr(),
toastType: ToastType.error,
);
ImmichToast.show(context: context, msg: "login_form_server_empty".tr(), toastType: ToastType.error);
}
try {
@@ -148,16 +142,13 @@ class LoginForm extends HookConsumerWidget {
isLoadingServer.value = false;
}
useEffect(
() {
final serverUrl = getServerUrl();
if (serverUrl != null) {
serverEndpointController.text = serverUrl;
}
return null;
},
[],
);
useEffect(() {
final serverUrl = getServerUrl();
if (serverUrl != null) {
serverEndpointController.text = serverUrl;
}
return null;
}, []);
populateTestLoginInfo() {
emailController.text = 'demo@immich.app';
@@ -180,10 +171,7 @@ class LoginForm extends HookConsumerWidget {
invalidateAllApiRepositoryProviders(ref);
try {
final result = await ref.read(authProvider.notifier).login(
emailController.text,
passwordController.text,
);
final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text);
if (result.shouldChangePassword && !result.isAdmin) {
context.pushRoute(const ChangePasswordRoute());
@@ -212,12 +200,7 @@ class LoginForm extends HookConsumerWidget {
String generateRandomString(int length) {
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final random = Random.secure();
return String.fromCharCodes(
Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
),
);
return String.fromCharCodes(Iterable.generate(length, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
}
List<int> randomBytes(int length) {
@@ -273,23 +256,17 @@ class LoginForm extends HookConsumerWidget {
if (oAuthServerUrl != null) {
try {
final loginResponseDto = await oAuthService.oAuthLogin(
oAuthServerUrl,
state,
codeVerifier,
);
final loginResponseDto = await oAuthService.oAuthLogin(oAuthServerUrl, state, codeVerifier);
if (loginResponseDto == null) {
return;
}
log.info(
"Finished OAuth login with response: ${loginResponseDto.userEmail}",
);
log.info("Finished OAuth login with response: ${loginResponseDto.userEmail}");
final isSuccess = await ref.watch(authProvider.notifier).saveAuthInfo(
accessToken: loginResponseDto.accessToken,
);
final isSuccess = await ref
.watch(authProvider.notifier)
.saveAuthInfo(accessToken: loginResponseDto.accessToken);
if (isSuccess) {
isLoading.value = false;
@@ -374,10 +351,7 @@ class LoginForm extends HookConsumerWidget {
),
onPressed: isLoadingServer.value ? null : getServerAuthSettings,
icon: const Icon(Icons.arrow_forward_rounded),
label: const Text(
'next',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
label: const Text('next', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
),
),
],
@@ -401,17 +375,10 @@ class LoginForm extends HookConsumerWidget {
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100,
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
border: Border.all(
color: context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!,
),
),
child: Text(
warningMessage.value!,
textAlign: TextAlign.center,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(color: context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!),
),
child: Text(warningMessage.value!, textAlign: TextAlign.center),
),
);
}
@@ -435,11 +402,7 @@ class LoginForm extends HookConsumerWidget {
onSubmit: passwordFocusNode.requestFocus,
),
const SizedBox(height: 8),
PasswordInput(
controller: passwordController,
focusNode: passwordFocusNode,
onSubmit: login,
),
PasswordInput(controller: passwordController, focusNode: passwordFocusNode, onSubmit: login),
],
// Note: This used to have an AnimatedSwitcher, but was removed
@@ -455,12 +418,8 @@ class LoginForm extends HookConsumerWidget {
if (isOauthEnable.value) ...[
if (isPasswordLoginEnable.value)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Divider(
color: context.isDarkTheme ? Colors.white : Colors.black,
),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black),
),
OAuthLoginButton(
serverEndpointController: serverEndpointController,
@@ -471,10 +430,7 @@ class LoginForm extends HookConsumerWidget {
],
],
),
if (!isOauthEnable.value && !isPasswordLoginEnable.value)
Center(
child: const Text('login_disabled').tr(),
),
if (!isOauthEnable.value && !isPasswordLoginEnable.value) Center(child: const Text('login_disabled').tr()),
const SizedBox(height: 12),
TextButton.icon(
icon: const Icon(Icons.arrow_back),
@@ -498,9 +454,7 @@ class LoginForm extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: constraints.maxHeight / 5,
),
SizedBox(height: constraints.maxHeight / 5),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
@@ -510,24 +464,16 @@ class LoginForm extends HookConsumerWidget {
onLongPress: () => populateTestLoginInfo1(),
child: RotationTransition(
turns: logoAnimationController,
child: const ImmichLogo(
heroTag: 'logo',
),
child: const ImmichLogo(heroTag: 'logo'),
),
),
const Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 16),
child: ImmichTitleText(),
),
const Padding(padding: EdgeInsets.only(top: 8.0, bottom: 16), child: ImmichTitleText()),
],
),
// Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874
Form(
key: loginFormKey,
child: serverSelectionOrLogin,
),
Form(key: loginFormKey, child: serverSelectionOrLogin),
],
),
),
@@ -25,10 +25,7 @@ class OAuthLoginButton extends ConsumerWidget {
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
label: Text(buttonLabel, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
);
}
}
@@ -8,12 +8,7 @@ class PasswordInput extends HookConsumerWidget {
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
const PasswordInput({super.key, required this.controller, this.focusNode, this.onSubmit});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -26,15 +21,10 @@ class PasswordInput extends HookConsumerWidget {
labelText: 'password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value ? Icons.visibility_off_sharp : Icons.visibility_sharp,
),
icon: Icon(isPasswordVisible.value ? Icons.visibility_off_sharp : Icons.visibility_sharp),
),
),
autofillHints: const [AutofillHints.password],
@@ -7,12 +7,7 @@ class ServerEndpointInput extends StatelessWidget {
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
const ServerEndpointInput({super.key, required this.controller, required this.focusNode, this.onSubmit});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
+6 -28
View File
@@ -43,11 +43,7 @@ class PinInput extends StatelessWidget {
final defaultPinTheme = PinTheme(
width: getPinSize().width,
height: getPinSize().height,
textStyle: TextStyle(
fontSize: 24,
color: context.colorScheme.onSurface,
fontFamily: 'Overpass Mono',
),
textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'Overpass Mono'),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(19)),
border: Border.all(color: context.colorScheme.surfaceBright),
@@ -70,34 +66,19 @@ class PinInput extends StatelessWidget {
forceErrorState: hasError ?? false,
autofocus: autoFocus ?? false,
obscureText: obscureText ?? false,
obscuringWidget: Icon(
Icons.vpn_key_rounded,
color: context.primaryColor,
size: 20,
),
separatorBuilder: (index) => const SizedBox(
height: 64,
width: 3,
),
obscuringWidget: Icon(Icons.vpn_key_rounded, color: context.primaryColor, size: 20),
separatorBuilder: (index) => const SizedBox(height: 64, width: 3),
cursor: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.only(bottom: 9),
width: 18,
height: 2,
color: context.primaryColor,
),
Container(margin: const EdgeInsets.only(bottom: 9), width: 18, height: 2, color: context.primaryColor),
],
),
defaultPinTheme: defaultPinTheme,
focusedPinTheme: defaultPinTheme.copyWith(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(19)),
border: Border.all(
color: context.primaryColor.withValues(alpha: 0.5),
width: 2,
),
border: Border.all(color: context.primaryColor.withValues(alpha: 0.5), width: 2),
color: context.colorScheme.surfaceContainerHigh,
),
),
@@ -105,10 +86,7 @@ class PinInput extends StatelessWidget {
decoration: BoxDecoration(
color: context.colorScheme.error.withAlpha(15),
borderRadius: const BorderRadius.all(Radius.circular(19)),
border: Border.all(
color: context.colorScheme.error.withAlpha(100),
width: 2,
),
border: Border.all(color: context.colorScheme.error.withAlpha(100), width: 2),
),
),
pinputAutovalidateMode: PinputAutovalidateMode.onSubmit,
@@ -9,10 +9,7 @@ import 'package:immich_mobile/widgets/forms/pin_input.dart';
class PinRegistrationForm extends HookConsumerWidget {
final Function() onDone;
const PinRegistrationForm({
super.key,
required this.onDone,
});
const PinRegistrationForm({super.key, required this.onDone});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -40,35 +37,25 @@ class PinRegistrationForm extends HookConsumerWidget {
}
try {
await ref.read(authProvider.notifier).setupPinCode(
newPinCodeController.text,
);
await ref.read(authProvider.notifier).setupPinCode(newPinCodeController.text);
onDone();
} catch (error) {
hasError.value = true;
context.showSnackBar(
SnackBar(content: Text(error.toString())),
);
context.showSnackBar(SnackBar(content: Text(error.toString())));
}
}
return Form(
child: Column(
children: [
Icon(
Icons.pin_outlined,
size: 64,
color: context.primaryColor,
),
Icon(Icons.pin_outlined, size: 64, color: context.primaryColor),
const SizedBox(height: 32),
SizedBox(
width: context.width * 0.7,
child: Text(
'setup_pin_code'.tr(),
style: context.textTheme.labelLarge!.copyWith(
fontSize: 24,
),
style: context.textTheme.labelLarge!.copyWith(fontSize: 24),
textAlign: TextAlign.center,
),
),
@@ -76,9 +63,7 @@ class PinRegistrationForm extends HookConsumerWidget {
width: context.width * 0.8,
child: Text(
'new_pin_code_subtitle'.tr(),
style: context.textTheme.bodyLarge!.copyWith(
fontSize: 16,
),
style: context.textTheme.bodyLarge!.copyWith(fontSize: 16),
textAlign: TextAlign.center,
),
),
@@ -113,10 +98,7 @@ class PinRegistrationForm extends HookConsumerWidget {
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: createNewPinCode,
child: Text('create'.tr()),
),
child: ElevatedButton(onPressed: createNewPinCode, child: Text('create'.tr())),
),
],
),
@@ -49,11 +49,7 @@ class PinVerificationForm extends HookConsumerWidget {
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: isVerified.value
? Icon(
successIcon ?? Icons.lock_open_rounded,
size: 64,
color: Colors.green[300],
)
? Icon(successIcon ?? Icons.lock_open_rounded, size: 64, color: Colors.green[300])
: Icon(
icon ?? Icons.lock_outline_rounded,
size: 64,
@@ -65,9 +61,7 @@ class PinVerificationForm extends HookConsumerWidget {
width: context.width * 0.7,
child: Text(
description ?? 'enter_your_pin_code_subtitle'.tr(),
style: context.textTheme.labelLarge!.copyWith(
fontSize: 18,
),
style: context.textTheme.labelLarge!.copyWith(fontSize: 18),
textAlign: TextAlign.center,
),
),
+12 -44
View File
@@ -52,16 +52,12 @@ class _NonSelectionRow extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () => context.maybePop(),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.arrow_back_ios_new_rounded),
),
ElevatedButton(
onPressed: onSettingsPressed,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.more_vert_rounded),
),
],
@@ -78,10 +74,7 @@ class _SelectionRow extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isProcessing = useProcessingOverlay();
Future<void> handleProcessing(
FutureOr<void> Function() action, [
bool reloadMarkers = false,
]) async {
Future<void> handleProcessing(FutureOr<void> Function() action, [bool reloadMarkers = false]) async {
isProcessing.value = true;
await action();
// Reset state
@@ -101,9 +94,7 @@ class _SelectionRow extends HookConsumerWidget {
icon: const Icon(Icons.close_rounded),
label: Text(
'${selectedAssets.value.length}',
style: context.textTheme.titleMedium?.copyWith(
color: context.colorScheme.onPrimary,
),
style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onPrimary),
),
),
),
@@ -112,43 +103,20 @@ class _SelectionRow extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () => handleProcessing(
() => handleShareAssets(
ref,
context,
selectedAssets.value.toList(),
),
),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
onPressed: () => handleProcessing(() => handleShareAssets(ref, context, selectedAssets.value.toList())),
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.ios_share_rounded),
),
ElevatedButton(
onPressed: () => handleProcessing(
() => handleFavoriteAssets(
ref,
context,
selectedAssets.value.toList(),
),
),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
onPressed: () =>
handleProcessing(() => handleFavoriteAssets(ref, context, selectedAssets.value.toList())),
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.favorite),
),
ElevatedButton(
onPressed: () => handleProcessing(
() => handleArchiveAssets(
ref,
context,
selectedAssets.value.toList(),
),
true,
),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
onPressed: () =>
handleProcessing(() => handleArchiveAssets(ref, context, selectedAssets.value.toList()), true),
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.archive),
),
],
+33 -53
View File
@@ -119,7 +119,8 @@ class MapAssetGrid extends HookConsumerWidget {
final rowOffset = renderElement.offset;
// Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset
final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge;
final edgeOffset = (totalOffset - partialOffset) /
final edgeOffset =
(totalOffset - partialOffset) /
// Round the total count to the next multiple of [assetsPerRow]
((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor();
@@ -146,34 +147,32 @@ class MapAssetGrid extends HookConsumerWidget {
// Place it just below the drag handle
heightFactor: 0.87,
child: assetsInBounds.value.isNotEmpty
? ref.watch(assetsTimelineProvider(assetsInBounds.value)).when(
data: (renderList) {
// Cache render list here to use it back during visibleItemsListener
cachedRenderList.value = renderList;
return ValueListenableBuilder(
valueListenable: selectedAssets,
builder: (_, value, __) => ImmichAssetGrid(
shrinkWrap: true,
renderList: renderList,
showDragScroll: false,
assetsPerRow: assetsPerRow,
showMultiSelectIndicator: false,
selectionActive: value.isNotEmpty,
listener: onAssetsSelected,
visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)),
),
);
},
error: (error, stackTrace) {
log.warning(
"Cannot get assets in the current map bounds",
error,
stackTrace,
);
return const SizedBox.shrink();
},
loading: () => const SizedBox.shrink(),
)
? ref
.watch(assetsTimelineProvider(assetsInBounds.value))
.when(
data: (renderList) {
// Cache render list here to use it back during visibleItemsListener
cachedRenderList.value = renderList;
return ValueListenableBuilder(
valueListenable: selectedAssets,
builder: (_, value, __) => ImmichAssetGrid(
shrinkWrap: true,
renderList: renderList,
showDragScroll: false,
assetsPerRow: assetsPerRow,
showMultiSelectIndicator: false,
selectionActive: value.isNotEmpty,
listener: onAssetsSelected,
visibleItemsListener: (pos) => gridScrollThrottler.run(() => handleVisibleItems(pos)),
),
);
},
error: (error, stackTrace) {
log.warning("Cannot get assets in the current map bounds", error, stackTrace);
return const SizedBox.shrink();
},
loading: () => const SizedBox.shrink(),
)
: const _MapNoAssetsInSheet(),
),
),
@@ -194,11 +193,7 @@ class _MapNoAssetsInSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
const image = Image(
height: 150,
width: 150,
image: AssetImage('assets/lighthouse.png'),
);
const image = Image(height: 150, width: 150, image: AssetImage('assets/lighthouse.png'));
return Center(
child: ListView(
@@ -206,21 +201,12 @@ class _MapNoAssetsInSheet extends StatelessWidget {
children: [
context.isDarkTheme
? const InvertionFilter(
child: SaturationFilter(
saturation: -1,
child: BrightnessFilter(
brightness: -5,
child: image,
),
),
child: SaturationFilter(saturation: -1, child: BrightnessFilter(brightness: -5, child: image)),
)
: image,
const SizedBox(height: 20),
Center(
child: Text(
"map_zoom_to_see_photos".tr(),
style: context.textTheme.displayLarge?.copyWith(fontSize: 18),
),
child: Text("map_zoom_to_see_photos".tr(), style: context.textTheme.displayLarge?.copyWith(fontSize: 18)),
),
],
),
@@ -254,10 +240,7 @@ class _MapSheetDragRegion extends StatelessWidget {
margin: EdgeInsets.zero,
shape: context.isMobile
? const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)),
)
: const BeveledRectangleBorder(),
elevation: 0.0,
@@ -291,10 +274,7 @@ class _MapSheetDragRegion extends StatelessWidget {
right: 18,
top: 24,
child: IconButton(
icon: Icon(
Icons.map_outlined,
color: context.textTheme.displayLarge?.color,
),
icon: Icon(Icons.map_outlined, color: context.textTheme.displayLarge?.color),
iconSize: 24,
tooltip: 'Zoom to bounds',
onPressed: () => onZoomToAsset?.call(value!),

Some files were not shown because too many files have changed in this diff Show More