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:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user