chore: bump dart sdk to 3.8 (#20355)

* chore: bump dart sdk to 3.8

* chore: make build

* make pigeon

* chore: format files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-07-29 00:34:03 +05:30 committed by GitHub
parent 9b3718120b
commit e52b9d15b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
643 changed files with 32561 additions and 35292 deletions

View file

@ -27,10 +27,7 @@ class ArchiveActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'archive_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -39,14 +39,10 @@ class BaseActionButton extends StatelessWidget {
}
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
constraints: BoxConstraints(maxWidth: maxWidth),
child: MaterialButton(
padding: const EdgeInsets.all(10),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
textColor: textColor,
onPressed: onPressed,
onLongPress: onLongPressed,
@ -59,10 +55,7 @@ class BaseActionButton extends StatelessWidget {
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w400,
),
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400),
maxLines: 3,
textAlign: TextAlign.center,
softWrap: true,

View file

@ -20,10 +20,7 @@ class CastActionButton extends ConsumerWidget {
iconColor: isCasting ? context.primaryColor : null, // null = default color
label: "cast".t(context: context),
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
showDialog(context: context, builder: (context) => const CastDialog());
},
menuItem: menuItem,
);

View file

@ -40,9 +40,7 @@ class DeleteActionButton extends ConsumerWidget {
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'confirm'.t(context: context),
style: TextStyle(
color: context.colorScheme.error,
),
style: TextStyle(color: context.colorScheme.error),
),
),
],
@ -58,10 +56,7 @@ class DeleteActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'delete_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -33,10 +33,7 @@ class DeleteLocalActionButton extends ConsumerWidget {
return;
}
final successMessage = 'delete_local_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -43,17 +43,10 @@ class DeleteTrashActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton.icon(
icon: Icon(
Icons.delete_forever,
color: Colors.red[400],
),
icon: Icon(Icons.delete_forever, color: Colors.red[400]),
label: Text(
"delete".t(context: context),
style: TextStyle(
fontSize: 14,
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold),
),
onPressed: () => _onTap(context, ref),
);

View file

@ -25,10 +25,7 @@ class EditLocationActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'edit_location_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'edit_location_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -12,11 +12,7 @@ class FavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool menuItem;
const FavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
const FavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@ -31,10 +27,7 @@ class FavoriteActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'favorite_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -12,11 +12,7 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
final String albumId;
final ActionSource source;
const RemoveFromAlbumActionButton({
super.key,
required this.albumId,
required this.source,
});
const RemoveFromAlbumActionButton({super.key, required this.albumId, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {

View file

@ -20,10 +20,7 @@ class RestoreTrashActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).restoreTrash(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'assets_restored_count'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(
@ -38,16 +35,8 @@ class RestoreTrashActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton.icon(
icon: const Icon(
Icons.history_rounded,
),
label: Text(
'restore'.t(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
icon: const Icon(Icons.history_rounded),
label: Text('restore'.t(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
onPressed: () => _onTap(context, ref),
);
}

View file

@ -27,10 +27,7 @@ class StackActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).stack(user.id, source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'stack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -30,10 +30,7 @@ class TrashActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'trash_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -21,10 +21,7 @@ class UnArchiveActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).unArchive(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unarchive_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -12,11 +12,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool menuItem;
const UnFavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@ -31,10 +27,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unfavorite_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -21,10 +21,7 @@ class UnStackActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).unStack(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unstack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -20,10 +20,7 @@ class UploadActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).upload(source);
final successMessage = 'upload_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'upload_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View file

@ -27,10 +27,7 @@ typedef AlbumSelectorCallback = void Function(RemoteAlbum album);
class AlbumSelector extends ConsumerStatefulWidget {
final AlbumSelectorCallback onAlbumSelected;
const AlbumSelector({
super.key,
required this.onAlbumSelected,
});
const AlbumSelector({super.key, required this.onAlbumSelected});
@override
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
@ -113,21 +110,10 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(
isGrid: isGrid,
onToggleViewMode: toggleViewMode,
),
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
isGrid
? _AlbumGrid(
albums: albums,
userId: userId,
onAlbumSelected: widget.onAlbumSelected,
)
: _AlbumList(
albums: albums,
userId: userId,
onAlbumSelected: widget.onAlbumSelected,
),
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
],
);
}
@ -151,18 +137,12 @@ class _SortButtonState extends ConsumerState<_SortButton> {
setState(() {
albumSortIsReverse = !albumSortIsReverse;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
} else {
setState(() {
albumSortOption = sortMode;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
}
}
@ -172,15 +152,9 @@ class _SortButtonState extends ConsumerState<_SortButton> {
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.all(4)),
),
consumeOutsideTap: true,
menuChildren: RemoteAlbumSortMode.values
@ -188,33 +162,27 @@ class _SortButtonState extends ConsumerState<_SortButton> {
(sortMode) => MenuItemButton(
leadingIcon: albumSortOption == sortMode
? albumSortIsReverse
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: const Icon(Icons.abc, color: Colors.transparent),
onPressed: () => onMenuTapped(sortMode),
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.fromLTRB(16, 16, 32, 16),
),
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)),
backgroundColor: WidgetStateProperty.all(
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
),
child: Text(
@ -243,12 +211,8 @@ class _SortButtonState extends ConsumerState<_SortButton> {
Padding(
padding: const EdgeInsets.only(right: 5),
child: albumSortIsReverse
? const Icon(
Icons.keyboard_arrow_down,
)
: const Icon(
Icons.keyboard_arrow_up_rounded,
),
? const Icon(Icons.keyboard_arrow_down)
: const Icon(Icons.keyboard_arrow_up_rounded),
),
Text(
albumSortOption.key.t(context: context),
@ -287,13 +251,8 @@ class _SearchBar extends StatelessWidget {
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(0),
width: 0,
),
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0),
borderRadius: const BorderRadius.all(Radius.circular(24)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withValues(alpha: 0.075),
@ -311,10 +270,7 @@ class _SearchBar extends StatelessWidget {
hintText: 'search_albums'.tr(),
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: onClearSearch,
)
? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClearSearch)
: null,
controller: searchController,
onChanged: (_) => onSearch(searchController.text, filterMode),
@ -362,10 +318,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
isSelected: filterMode == QuickFilterMode.sharedWithMe,
onTap: () {
onChangeFilter(QuickFilterMode.sharedWithMe);
onSearch(
searchController.text,
QuickFilterMode.sharedWithMe,
);
onSearch(searchController.text, QuickFilterMode.sharedWithMe);
},
),
_QuickFilterButton(
@ -373,10 +326,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
isSelected: filterMode == QuickFilterMode.myAlbums,
onTap: () {
onChangeFilter(QuickFilterMode.myAlbums);
onSearch(
searchController.text,
QuickFilterMode.myAlbums,
);
onSearch(searchController.text, QuickFilterMode.myAlbums);
},
),
],
@ -387,11 +337,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
}
class _QuickFilterButton extends StatelessWidget {
const _QuickFilterButton({
required this.isSelected,
required this.onTap,
required this.label,
});
const _QuickFilterButton({required this.isSelected, required this.onTap, required this.label});
final bool isSelected;
final VoidCallback onTap;
@ -402,18 +348,11 @@ class _QuickFilterButton extends StatelessWidget {
return TextButton(
onPressed: onTap,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
isSelected ? context.colorScheme.primary : Colors.transparent,
),
backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1),
),
),
),
@ -429,10 +368,7 @@ class _QuickFilterButton extends StatelessWidget {
}
class _QuickSortAndViewMode extends StatelessWidget {
const _QuickSortAndViewMode({
required this.isGrid,
required this.onToggleViewMode,
});
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
final bool isGrid;
final VoidCallback onToggleViewMode;
@ -447,10 +383,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
children: [
const _SortButton(),
IconButton(
icon: Icon(
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
size: 24,
),
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
onPressed: onToggleViewMode,
),
],
@ -461,11 +394,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
}
class _AlbumList extends ConsumerWidget {
const _AlbumList({
required this.albums,
required this.userId,
required this.onAlbumSelected,
});
const _AlbumList({required this.albums, required this.userId, required this.onAlbumSelected});
final List<RemoteAlbum> albums;
final String? userId;
@ -476,10 +405,7 @@ class _AlbumList extends ConsumerWidget {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
),
);
}
@ -491,51 +417,25 @@ class _AlbumList extends ConsumerWidget {
final album = albums[index];
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
),
padding: const EdgeInsets.only(bottom: 8.0),
child: LargeLeadingTile(
title: Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onTap: () => onAlbumSelected(album),
leadingPadding: const EdgeInsets.only(
right: 16,
),
leadingPadding: const EdgeInsets.only(right: 16),
leading: album.thumbnailAssetId != null
? ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
child: SizedBox(
width: 80,
height: 80,
child: Thumbnail(
remoteId: album.thumbnailAssetId,
),
),
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: SizedBox(width: 80, height: 80, child: Thumbnail(remoteId: album.thumbnailAssetId)),
)
: SizedBox(
width: 80,
@ -544,16 +444,9 @@ class _AlbumList extends ConsumerWidget {
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: context.colorScheme.outline.withAlpha(50),
width: 1,
),
),
child: const Icon(
Icons.photo_album_rounded,
size: 24,
color: Colors.grey,
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
),
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
),
),
),
@ -566,11 +459,7 @@ class _AlbumList extends ConsumerWidget {
}
class _AlbumGrid extends StatelessWidget {
const _AlbumGrid({
required this.albums,
required this.userId,
required this.onAlbumSelected,
});
const _AlbumGrid({required this.albums, required this.userId, required this.onAlbumSelected});
final List<RemoteAlbum> albums;
final String? userId;
@ -581,10 +470,7 @@ class _AlbumGrid extends StatelessWidget {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
),
);
}
@ -598,28 +484,17 @@ class _AlbumGrid extends StatelessWidget {
crossAxisSpacing: 4,
childAspectRatio: .7,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final album = albums[index];
return _GridAlbumCard(
album: album,
userId: userId,
onAlbumSelected: onAlbumSelected,
);
},
childCount: albums.length,
),
delegate: SliverChildBuilderDelegate((context, index) {
final album = albums[index];
return _GridAlbumCard(album: album, userId: userId, onAlbumSelected: onAlbumSelected);
}, childCount: albums.length),
),
);
}
}
class _GridAlbumCard extends ConsumerWidget {
const _GridAlbumCard({
required this.album,
required this.userId,
required this.onAlbumSelected,
});
const _GridAlbumCard({required this.album, required this.userId, required this.onAlbumSelected});
final RemoteAlbum album;
final String? userId;
@ -633,13 +508,8 @@ class _GridAlbumCard extends ConsumerWidget {
elevation: 0,
color: context.colorScheme.surfaceBright,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(16)),
side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -647,22 +517,14 @@ class _GridAlbumCard extends ConsumerWidget {
Expanded(
flex: 2,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(15),
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
child: SizedBox(
width: double.infinity,
child: album.thumbnailAssetId != null
? Thumbnail(
remoteId: album.thumbnailAssetId,
)
? Thumbnail(remoteId: album.thumbnailAssetId)
: Container(
color: context.colorScheme.surfaceContainerHighest,
child: const Icon(
Icons.photo_album_rounded,
size: 40,
color: Colors.grey,
),
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
),
),
),
@ -679,27 +541,13 @@ class _GridAlbumCard extends ConsumerWidget {
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
],
),
@ -718,17 +566,15 @@ class AddToAlbumHeader extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> onCreateAlbum() async {
final newAlbum = await ref.read(remoteAlbumProvider.notifier).createAlbum(
final newAlbum = await ref
.read(remoteAlbumProvider.notifier)
.createAlbum(
title: "Untitled Album",
assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
);
if (newAlbum == null) {
ImmichToast.show(
context: context,
toastType: ToastType.error,
msg: 'errors.failed_to_create_album'.tr(),
);
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
return;
}
@ -736,38 +582,23 @@ class AddToAlbumHeader extends ConsumerWidget {
}
return SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
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(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
), // remove internal padding
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding
minimumSize: const Size(0, 0), // allow shrinking
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height
),
onPressed: onCreateAlbum,
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(),
),
],

View file

@ -13,7 +13,5 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
}
}
final stackChildrenNotifier =
AsyncNotifierProvider.autoDispose.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(
StackChildrenNotifier.new,
);
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(StackChildrenNotifier.new);

View file

@ -11,9 +11,7 @@ class AssetStackRow extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
@ -27,11 +25,10 @@ class AssetStackRow extends ConsumerWidget {
child: AnimatedOpacity(
opacity: opacity / 255,
duration: Durations.short2,
child: ref.watch(stackChildrenNotifier(asset)).when(
data: (state) => SizedBox.square(
dimension: 80,
child: _StackList(stack: state),
),
child: ref
.watch(stackChildrenNotifier(asset))
.when(
data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
@ -49,11 +46,7 @@ class _StackList extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(
left: 5,
right: 5,
bottom: 30,
),
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
itemCount: stack.length,
itemBuilder: (ctx, index) {
final asset = stack[index];
@ -71,9 +64,7 @@ class _StackList extends ConsumerWidget {
? const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(color: Colors.white, width: 2),
),
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
)
: const BoxDecoration(
color: Colors.white,
@ -87,10 +78,7 @@ class _StackList extends ConsumerWidget {
children: [
Image(
fit: BoxFit.cover,
image: getThumbnailImageProvider(
remoteId: asset.id,
size: const Size.square(60),
),
image: getThumbnailImageProvider(remoteId: asset.id, size: const Size.square(60)),
),
if (asset.isVideo)
const Icon(
@ -98,11 +86,7 @@ class _StackList extends ConsumerWidget {
color: Colors.white,
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0)),
],
),
],

View file

@ -36,12 +36,7 @@ class AssetViewerPage extends StatelessWidget {
final TimelineService timelineService;
final int? heroOffset;
const AssetViewerPage({
super.key,
required this.initialIndex,
required this.timelineService,
this.heroOffset,
});
const AssetViewerPage({super.key, required this.initialIndex, required this.timelineService, this.heroOffset});
@override
Widget build(BuildContext context) {
@ -59,12 +54,7 @@ class AssetViewer extends ConsumerStatefulWidget {
final Platform? platform;
final int? heroOffset;
const AssetViewer({
super.key,
required this.initialIndex,
this.platform,
this.heroOffset,
});
const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset});
@override
ConsumerState createState() => _AssetViewerState();
@ -162,11 +152,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
context,
onError: (_, __) {},
),
precacheImage(
getFullImageProvider(asset, size: screenSize),
context,
onError: (_, __) {},
),
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
]),
);
}
@ -222,9 +208,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
duration: const Duration(seconds: 2),
content: Text(
"local_asset_cast_failed".tr(),
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
@ -262,7 +246,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
viewController = controller;
dragDownPosition = details.localPosition;
initialPhotoViewState = controller.value;
final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
final isZoomed =
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
scaleStateController.scaleState == PhotoViewScaleState.covering;
if (!showingBottomSheet && isZoomed) {
blockGestures = true;
@ -350,10 +335,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final backgroundOpacity = (255 * (1.0 - (scaleReduction / dragRatio))).round();
viewController?.updateMultiple(
position: initialPhotoViewState.position + delta,
scale: updatedScale,
);
viewController?.updateMultiple(position: initialPhotoViewState.position + delta, scale: updatedScale);
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
}
@ -450,32 +432,21 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
});
}
void _openBottomSheet(
BuildContext ctx, {
double extent = _kBottomSheetMinimumExtent,
}) {
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) {
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
initialScale = viewController?.scale;
viewController?.updateMultiple(scale: _getScaleForBottomSheet);
previousExtent = _kBottomSheetMinimumExtent;
sheetCloseController = showBottomSheet(
context: ctx,
sheetAnimationStyle: const AnimationStyle(
duration: Durations.short4,
reverseDuration: Durations.short2,
),
sheetAnimationStyle: const AnimationStyle(duration: Durations.short4, reverseDuration: Durations.short2),
constraints: const BoxConstraints(maxWidth: double.infinity),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
builder: (_) {
return NotificationListener<Notification>(
onNotification: _onNotification,
child: AssetDetailBottomSheet(
controller: bottomSheetController,
initialChildSize: extent,
),
child: AssetDetailBottomSheet(controller: bottomSheetController, initialChildSize: extent),
);
},
);
@ -496,18 +467,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
return;
}
isSnapping = true;
bottomSheetController.animateTo(
_kBottomSheetSnapExtent,
duration: Durations.short3,
curve: Curves.easeOut,
);
bottomSheetController.animateTo(_kBottomSheetSnapExtent, duration: Durations.short3, curve: Curves.easeOut);
}
Widget _placeholderBuilder(
BuildContext ctx,
ImageChunkEvent? progress,
int index,
) {
Widget _placeholderBuilder(BuildContext ctx, ImageChunkEvent? progress, int index) {
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) {
@ -517,14 +480,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
width: double.infinity,
height: double.infinity,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: Size(
ctx.width,
ctx.height,
),
),
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
);
}
@ -574,11 +530,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
width: ctx.width,
height: ctx.height,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: size,
),
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
),
);
}
@ -662,8 +614,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
pageController: pageController,
scrollPhysics: platform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
,
: const FastClampingScrollPhysics(), // Use heavy physics for Android
itemCount: totalAssets,
onPageChanged: _onPageChanged,
onPageBuild: _onPageBuild,
@ -678,10 +629,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AssetStackRow(),
if (!isInLockedView) const ViewerBottomBar(),
],
children: [const AssetStackRow(), if (!isInLockedView) const ViewerBottomBar()],
),
),
);

View file

@ -79,17 +79,11 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
}
void setOpacity(int opacity) {
state = state.copyWith(
backgroundOpacity: opacity,
showingControls: opacity == 255 ? true : state.showingControls,
);
state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls);
}
void setBottomSheet(bool showing) {
state = state.copyWith(
showingBottomSheet: showing,
showingControls: showing ? true : state.showingControls,
);
state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls);
if (showing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
}

View file

@ -25,12 +25,8 @@ class ViewerBottomBar extends ConsumerWidget {
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isSheetOpen = ref.watch(
assetViewerProvider.select((s) => s.showingBottomSheet),
);
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
@ -42,13 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer),
asset.isLocalOnly
? const DeleteLocalActionButton(
source: ActionSource.viewer,
)
: const DeleteActionButton(
source: ActionSource.viewer,
showConfirmation: true,
),
? const DeleteLocalActionButton(source: ActionSource.viewer)
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
];
return IgnorePointer(
@ -64,9 +55,7 @@ class ViewerBottomBar extends ConsumerWidget {
data: context.themeData.copyWith(
iconTheme: const IconThemeData(size: 22, color: Colors.white),
textTheme: context.themeData.textTheme.copyWith(
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(
color: Colors.white,
),
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(color: Colors.white),
),
),
child: Container(
@ -77,10 +66,7 @@ class ViewerBottomBar extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (asset.isVideo) const VideoControls(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: actions,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
],
),
),

View file

@ -29,11 +29,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final DraggableScrollableController? controller;
final double initialChildSize;
const AssetDetailBottomSheet({
this.controller,
this.initialChildSize = 0.35,
super.key,
});
const AssetDetailBottomSheet({this.controller, this.initialChildSize = 0.35, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -42,9 +38,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
return const SizedBox.shrink();
}
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final isInLockedView = ref.watch(inLockedViewProvider);
@ -58,9 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
? const TrashActionButton(source: ActionSource.viewer)
: const DeletePermanentActionButton(source: ActionSource.viewer),
const DeleteActionButton(source: ActionSource.viewer),
const MoveToLockFolderActionButton(
source: ActionSource.viewer,
),
const MoveToLockFolderActionButton(source: ActionSource.viewer),
],
if (asset.storage == AssetState.local) ...[
const DeleteLocalActionButton(source: ActionSource.viewer),
@ -153,9 +145,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
// Asset Date and Time
_SheetTile(
title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SheetLocationDetails(),
// Details header
@ -185,11 +175,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
_SheetTile(
title: cameraTitle,
titleStyle: context.textTheme.labelLarge,
leading: Icon(
Icons.camera_outlined,
size: 24,
color: context.textTheme.labelLarge?.color,
),
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getCameraInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
@ -207,13 +193,7 @@ class _SheetTile extends StatelessWidget {
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
const _SheetTile({
required this.title,
this.titleStyle,
this.leading,
this.subtitle,
this.subtitleStyle,
});
const _SheetTile({required this.title, this.titleStyle, this.leading, this.subtitle, this.subtitleStyle});
@override
Widget build(BuildContext context) {

View file

@ -38,20 +38,13 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
_mapController = controller;
}
void _onExifChanged(
AsyncValue<ExifInfo?>? previous,
AsyncValue<ExifInfo?> current,
) {
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
asset = ref.read(currentAssetNotifier);
setState(() {
exifInfo = current.valueOrNull;
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
if (exifInfo != null && hasCoordinates) {
_mapController?.moveCamera(
CameraUpdate.newLatLng(
LatLng(exifInfo!.latitude!, exifInfo!.longitude!),
),
);
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exifInfo!.latitude!, exifInfo!.longitude!)));
}
});
}
@ -59,11 +52,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
@override
void initState() {
super.initState();
ref.listenManual(
currentAssetExifProvider,
_onExifChanged,
fireImmediately: true,
);
ref.listenManual(currentAssetExifProvider, _onExifChanged, fireImmediately: true);
}
@override
@ -80,10 +69,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
final coordinates = "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
return Padding(
padding: EdgeInsets.symmetric(
vertical: 16.0,
horizontal: context.isMobile ? 16.0 : 56.0,
),
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: context.isMobile ? 16.0 : 56.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -97,25 +83,16 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
),
),
),
ExifMap(
exifInfo: exifInfo!,
markerId: remoteId,
onMapCreated: _onMapCreated,
),
ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated),
const SizedBox(height: 15),
if (locationName != null)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
locationName,
style: context.textTheme.labelLarge,
),
child: Text(locationName, style: context.textTheme.labelLarge),
),
Text(
coordinates,
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)),
),
],
),

View file

@ -36,25 +36,18 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final showViewInTimelineButton = previousRouteName != TabShellRoute.name && previousRouteName != null;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
opacity = 0;
}
final isCasting = ref.watch(
castProvider.select((c) => c.isCasting),
);
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected));
final actions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
if (isCasting || (asset.hasRemote && websocketConnected)) const CastActionButton(menuItem: true),
if (showViewInTimelineButton)
IconButton(
onPressed: () async {
@ -68,19 +61,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(
source: ActionSource.viewer,
menuItem: true,
),
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(),
];
final lockedViewActions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
if (isCasting || (asset.hasRemote && websocketConnected)) const CastActionButton(menuItem: true),
const _KebabMenu(),
];
@ -98,8 +85,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
actions: isShowingSheet
? null
: isInLockedView
? lockedViewActions
: actions,
? lockedViewActions
: actions,
),
),
);

View file

@ -27,10 +27,7 @@ import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
bool _isCurrentAsset(
BaseAsset asset,
BaseAsset? currentAsset,
) {
bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) {
if (asset is RemoteAsset) {
return switch (currentAsset) {
RemoteAsset remoteAsset => remoteAsset.id == asset.id,
@ -98,10 +95,7 @@ class NativeVideoViewer extends HookConsumerWidget {
throw Exception('No file found for the video');
}
final source = await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
return source;
}
@ -122,31 +116,24 @@ class NativeVideoViewer extends HookConsumerWidget {
);
return source;
} catch (error) {
log.severe(
'Error creating video source for asset ${asset.name}: $error',
);
log.severe('Error creating video source for asset ${asset.name}: $error');
return null;
}
}
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
final aspectRatio = useState<double?>(null);
useMemoized(
() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
useMemoized(() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe(
'Error getting aspect ratio for asset ${asset.name}: $error',
);
}
},
[asset.heroTag],
);
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe('Error getting aspect ratio for asset ${asset.name}: $error');
}
}, [asset.heroTag]);
void checkIfBuffering() {
if (!context.mounted) {
@ -156,8 +143,9 @@ class NativeVideoViewer extends HookConsumerWidget {
final videoPlayback = ref.read(videoPlaybackValueProvider);
if ((isBuffering.value || videoPlayback.state == VideoPlaybackState.initializing) &&
videoPlayback.state != VideoPlaybackState.buffering) {
ref.read(videoPlaybackValueProvider.notifier).value =
videoPlayback.copyWith(state: VideoPlaybackState.buffering);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback.copyWith(
state: VideoPlaybackState.buffering,
);
}
}
@ -345,48 +333,42 @@ class NativeVideoViewer extends HookConsumerWidget {
// This delay seems like a hacky way to resolve underlying bugs in video
// playback, but other resolutions failed thus far
Timer(
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor), () {
if (!context.mounted) {
return;
}
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
});
});
useEffect(
() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value
? null
: Timer(
const Duration(milliseconds: 300),
() => isVisible.value = true,
);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor),
() {
if (!context.mounted) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
},
const [],
);
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
},
);
});
useEffect(() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
}, const []);
useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
@ -416,12 +398,7 @@ class NativeVideoViewer extends HookConsumerWidget {
child: AspectRatio(
key: ValueKey(asset),
aspectRatio: aspectRatio.value!,
child: isCurrent
? NativeVideoPlayerView(
key: ValueKey(asset),
onViewReady: initController,
)
: null,
child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null,
),
),
),

View file

@ -13,16 +13,11 @@ import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
class VideoViewerControls extends HookConsumerWidget {
final Duration hideTimerDuration;
const VideoViewerControls({
super.key,
this.hideTimerDuration = const Duration(seconds: 5),
});
const VideoViewerControls({super.key, this.hideTimerDuration = const Duration(seconds: 5)});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetIsVideo = ref.watch(
currentAssetNotifier.select((asset) => asset != null && asset.isVideo),
);
final assetIsVideo = ref.watch(currentAssetNotifier.select((asset) => asset != null && asset.isVideo));
bool showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
final showBottomSheet = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
if (showBottomSheet) {
@ -33,20 +28,17 @@ class VideoViewerControls extends HookConsumerWidget {
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(assetViewerProvider.notifier).setControls(false);
}
},
);
// Do not hide on paused
if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
});
final showBuffering = state == VideoPlaybackState.buffering && !cast.isCasting;
/// Shows the controls and starts the timer to hide them
@ -97,11 +89,7 @@ class VideoViewerControls 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(assetViewerProvider.notifier).setControls(false),

View file

@ -11,11 +11,7 @@ class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart;
final VoidCallback onStop;
const BackupToggleButton({
super.key,
required this.onStart,
required this.onStop,
});
const BackupToggleButton({super.key, required this.onStart, required this.onStop});
@override
ConsumerState<BackupToggleButton> createState() => BackupToggleButtonState();
@ -29,17 +25,12 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 8),
vsync: this,
);
_animationController = AnimationController(duration: const Duration(seconds: 8), vsync: this);
_gradientAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_gradientAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
}
@ -66,21 +57,13 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
@override
Widget build(BuildContext context) {
final enqueueCount = ref.watch(
driftBackupProvider.select((state) => state.enqueueCount),
);
final enqueueCount = ref.watch(driftBackupProvider.select((state) => state.enqueueCount));
final enqueueTotalCount = ref.watch(
driftBackupProvider.select((state) => state.enqueueTotalCount),
);
final enqueueTotalCount = ref.watch(driftBackupProvider.select((state) => state.enqueueTotalCount));
final isCanceling = ref.watch(
driftBackupProvider.select((state) => state.isCanceling),
);
final isCanceling = ref.watch(driftBackupProvider.select((state) => state.isCanceling));
final uploadTasks = ref.watch(
driftBackupProvider.select((state) => state.uploadItems),
);
final uploadTasks = ref.watch(driftBackupProvider.select((state) => state.uploadItems));
final isUploading = uploadTasks.isNotEmpty;
@ -116,11 +99,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: context.primaryColor.withValues(alpha: 0.1),
blurRadius: 12,
offset: const Offset(0, 2),
),
BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 12, offset: const Offset(0, 2)),
],
),
child: Container(
@ -151,18 +130,8 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
),
),
child: isUploading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
Icons.cloud_upload_outlined,
color: context.primaryColor,
size: 24,
),
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: Icon(Icons.cloud_upload_outlined, color: context.primaryColor, size: 24),
),
const SizedBox(width: 16),
Expanded(
@ -185,10 +154,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
Text(
"queue_status".t(
context: context,
args: {
'count': enqueueCount.toString(),
'total': enqueueTotalCount.toString(),
},
args: {'count': enqueueCount.toString(), 'total': enqueueTotalCount.toString()},
),
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
@ -197,10 +163,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
if (isCanceling)
Row(
children: [
Text(
"canceling".t(),
style: context.textTheme.labelLarge,
),
Text("canceling".t(), style: context.textTheme.labelLarge),
const SizedBox(width: 4),
SizedBox(
width: 18,
@ -215,10 +178,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
],
),
),
Switch.adaptive(
value: _isEnabled,
onChanged: (value) => isCanceling ? null : _onToggle(value),
),
Switch.adaptive(value: _isEnabled, onChanged: (value) => isCanceling ? null : _onToggle(value)),
],
),
),

View file

@ -24,9 +24,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@ -41,14 +39,10 @@ class ArchiveBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[

View file

@ -73,9 +73,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
borderOnForeground: false,
clipBehavior: Clip.antiAlias,
elevation: 6.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
margin: const EdgeInsets.symmetric(horizontal: 0),
child: CustomScrollView(
controller: scrollController,
@ -89,11 +87,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
if (widget.actions.isNotEmpty)
SizedBox(
height: 115,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: widget.actions,
),
child: ListView(shrinkWrap: true, scrollDirection: Axis.horizontal, children: widget.actions),
),
if (widget.actions.isNotEmpty) ...[
const Divider(indent: 16, endIndent: 16),

View file

@ -24,9 +24,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@ -41,14 +39,10 @@ class FavoriteBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[

View file

@ -31,9 +31,7 @@ class GeneralBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets;
@ -41,24 +39,19 @@ class GeneralBottomSheet extends ConsumerWidget {
return;
}
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(
album.id,
selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
);
final addedCount = await ref
.read(remoteAlbumProvider.notifier)
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
if (addedCount != selectedAssets.length) {
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}),
);
}
@ -78,18 +71,14 @@ class GeneralBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const DeleteActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal || multiselect.hasMerged) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
],
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
@ -99,9 +88,7 @@ class GeneralBottomSheet extends ConsumerWidget {
],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(
onAlbumSelected: addAssetsToAlbum,
),
AlbumSelector(onAlbumSelected: addAssetsToAlbum),
],
);
}

View file

@ -27,9 +27,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@ -44,24 +42,17 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline),
],
RemoveFromAlbumActionButton(
source: ActionSource.timeline,
albumId: album.id,
),
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
],
);
}

View file

@ -5,20 +5,12 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
ImageProvider getFullImageProvider(
BaseAsset asset, {
Size size = const Size(1080, 1920),
}) {
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
// Create new provider and cache it
final ImageProvider provider;
if (_shouldUseLocalAsset(asset)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
provider = LocalFullImageProvider(
id: id,
name: asset.name,
size: size,
type: asset.type,
);
provider = LocalFullImageProvider(id: id, name: asset.name, size: size, type: asset.type);
} else {
final String assetId;
if (asset is LocalAsset && asset.hasRemote) {
@ -34,15 +26,8 @@ ImageProvider getFullImageProvider(
return provider;
}
ImageProvider getThumbnailImageProvider({
BaseAsset? asset,
String? remoteId,
Size size = const Size.square(256),
}) {
assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = const Size.square(256)}) {
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
if (remoteId != null) {
return RemoteThumbProvider(assetId: remoteId);
@ -50,12 +35,7 @@ ImageProvider getThumbnailImageProvider({
if (_shouldUseLocalAsset(asset!)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
return LocalThumbProvider(
id: id,
updatedAt: asset.updatedAt,
name: asset.name,
size: size,
);
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, name: asset.name, size: size);
}
final String assetId;

View file

@ -5,10 +5,7 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
class LocalAlbumThumbnail extends ConsumerWidget {
const LocalAlbumThumbnail({
super.key,
required this.albumId,
});
const LocalAlbumThumbnail({super.key, required this.albumId});
final String albumId;
@override
@ -21,34 +18,21 @@ class LocalAlbumThumbnail extends ConsumerWidget {
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: context.colorScheme.outline.withAlpha(50),
width: 1,
),
),
child: Icon(
Icons.collections,
size: 24,
color: context.primaryColor,
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
),
child: Icon(Icons.collections, size: 24, color: context.primaryColor),
);
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Thumbnail(
asset: data,
),
child: Thumbnail(asset: data),
);
},
error: (error, stack) {
return const Icon(Icons.error, size: 24);
},
loading: () => const SizedBox(
width: 24,
height: 24,
child: Center(child: CircularProgressIndicator()),
),
loading: () => const SizedBox(width: 24, height: 24, child: Center(child: CircularProgressIndicator())),
);
}
}

View file

@ -39,10 +39,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
}
@override
ImageStreamCompleter loadImage(
LocalThumbProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(LocalThumbProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode),
@ -57,11 +54,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
);
}
Future<Codec> _codec(
LocalThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
) async {
Future<Codec> _codec(LocalThumbProvider key, CacheManager cache, ImageDecoderCallback decode) async {
final cacheKey = '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}';
final fileFromCache = await cache.getFileFromCache(cacheKey);
@ -75,9 +68,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbnailBytes == null) {
PaintingBinding.instance.imageCache.evict(key);
throw StateError(
"Loading thumb for local photo ${key.name} failed",
);
throw StateError("Loading thumb for local photo ${key.name} failed");
}
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
@ -107,12 +98,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
final Size size;
final AssetType type;
const LocalFullImageProvider({
required this.id,
required this.name,
required this.size,
required this.type,
});
const LocalFullImageProvider({required this.id, required this.name, required this.size, required this.type});
@override
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
@ -120,10 +106,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
@override
ImageStreamCompleter loadImage(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
return MultiImageStreamCompleter(
codec: _codec(key, decode),
scale: 1.0,
@ -134,10 +117,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
// Streams in each stage of the image as we ask for it
Stream<Codec> _codec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
Stream<Codec> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
try {
switch (key.type) {
case AssetType.image:
@ -156,16 +136,11 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
} catch (error, stack) {
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack);
throw const ImageLoadingException(
'Could not load image from local storage',
);
throw const ImageLoadingException('Could not load image from local storage');
}
}
Future<Codec?> _getThumbnailCodec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async {
Future<Codec?> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbBytes == null) {
return null;
@ -174,10 +149,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
return decode(buffer);
}
Stream<Codec> _decodeProgressive(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
Stream<Codec> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
final file = await _storageRepository.getFileForAsset(key.id);
if (file == null) {
throw StateError("Opening file for asset ${key.name} failed");

View file

@ -15,10 +15,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteThumbProvider({
required this.assetId,
this.cacheManager,
});
const RemoteThumbProvider({required this.assetId, this.cacheManager});
@override
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
@ -26,10 +23,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
}
@override
ImageStreamCompleter loadImage(
RemoteThumbProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkController = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
@ -49,9 +43,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkController,
) async {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
);
final preview = getThumbnailUrlForRemoteId(key.assetId);
return ImageLoader.loadImageFromCache(
preview,
@ -79,10 +71,7 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteFullImageProvider({
required this.assetId,
this.cacheManager,
});
const RemoteFullImageProvider({required this.assetId, this.cacheManager});
@override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
@ -90,10 +79,7 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
}
@override
ImageStreamCompleter loadImage(
RemoteFullImageProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(

View file

@ -8,9 +8,7 @@ import 'package:thumbhash/thumbhash.dart';
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
final String thumbHash;
const ThumbHashProvider({
required this.thumbHash,
});
const ThumbHashProvider({required this.thumbHash});
@override
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
@ -18,20 +16,11 @@ class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
}
@override
ImageStreamCompleter loadImage(
ThumbHashProvider key,
ImageDecoderCallback decode,
) {
return MultiFrameImageStreamCompleter(
codec: _loadCodec(key, decode),
scale: 1.0,
);
ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(codec: _loadCodec(key, decode), scale: 1.0);
}
Future<Codec> _loadCodec(
ThumbHashProvider key,
ImageDecoderCallback decode,
) async {
Future<Codec> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) async {
final image = thumbHashToRGBA(base64Decode(key.thumbHash));
return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image)));
}

View file

@ -8,16 +8,8 @@ import 'package:logging/logging.dart';
import 'package:octo_image/octo_image.dart';
class Thumbnail extends StatelessWidget {
const Thumbnail({
this.asset,
this.remoteId,
this.size = const Size.square(256),
this.fit = BoxFit.cover,
super.key,
}) : assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
const Thumbnail({this.asset, this.remoteId, this.size = const Size.square(256), this.fit = BoxFit.cover, super.key})
: assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
final BaseAsset? asset;
final String? remoteId;
@ -33,12 +25,7 @@ class Thumbnail extends StatelessWidget {
image: provider,
octoSet: OctoSet(
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash, fit: fit),
errorBuilder: _blurHashErrorBuilder(
thumbHash,
provider: provider,
fit: fit,
asset: asset,
),
errorBuilder: _blurHashErrorBuilder(thumbHash, provider: provider, fit: fit, asset: asset),
),
fadeOutDuration: const Duration(milliseconds: 100),
fadeInDuration: Duration.zero,
@ -50,10 +37,7 @@ class Thumbnail extends StatelessWidget {
}
}
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
String? thumbHash, {
BoxFit? fit,
}) {
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
return (context) => thumbHash == null
? const ThumbnailPlaceholder()
: FadeInPlaceholderImage(
@ -63,12 +47,7 @@ OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
);
}
OctoErrorBuilder _blurHashErrorBuilder(
String? blurhash, {
BaseAsset? asset,
ImageProvider? provider,
BoxFit? fit,
}) =>
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) =>
(context, e, s) {
Logger("ImThumbnail").warning("Error loading thumbnail for ${asset?.name}", e, s);
provider?.evict();
@ -76,10 +55,7 @@ OctoErrorBuilder _blurHashErrorBuilder(
alignment: Alignment.center,
children: [
_blurHashPlaceholderBuilder(blurhash, fit: fit)(context),
const Opacity(
opacity: 0.75,
child: Icon(Icons.error_outline_rounded),
),
const Opacity(opacity: 0.75, child: Icon(Icons.error_outline_rounded)),
],
);
};

View file

@ -30,29 +30,25 @@ class ThumbnailTile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
final assetContainerColor =
context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) : context.primaryColor.lighten(amount: 0.75);
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.4)
: context.primaryColor.lighten(amount: 0.75);
final isSelected = ref.watch(
multiSelectProvider.select(
(multiselect) => multiselect.selectedAssets.contains(asset),
),
multiSelectProvider.select((multiselect) => multiselect.selectedAssets.contains(asset)),
);
final borderStyle = lockSelection
? BoxDecoration(
color: context.colorScheme.surfaceContainerHighest,
border: Border.all(
color: context.colorScheme.surfaceContainerHighest,
width: 6,
),
border: Border.all(color: context.colorScheme.surfaceContainerHighest, width: 6),
)
: isSelected
? BoxDecoration(
color: assetContainerColor,
border: Border.all(color: assetContainerColor, width: 6),
)
: const BoxDecoration();
? BoxDecoration(
color: assetContainerColor,
border: Border.all(color: assetContainerColor, width: 6),
)
: const BoxDecoration();
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
@ -63,28 +59,22 @@ class ThumbnailTile extends ConsumerWidget {
curve: Curves.decelerate,
decoration: borderStyle,
child: ClipRRect(
borderRadius:
isSelected || lockSelection ? const BorderRadius.all(Radius.circular(15.0)) : BorderRadius.zero,
borderRadius: isSelected || lockSelection
? const BorderRadius.all(Radius.circular(15.0))
: BorderRadius.zero,
child: Stack(
children: [
Positioned.fill(
child: Hero(
tag: '${asset.heroTag}_$heroIndex',
child: Thumbnail(
asset: asset,
fit: fit,
size: size,
),
child: Thumbnail(asset: asset, fit: fit, size: size),
),
),
if (hasStack)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(
right: 10.0,
top: asset.isVideo ? 24.0 : 6.0,
),
padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0),
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
),
),
@ -99,26 +89,26 @@ class ThumbnailTile extends ConsumerWidget {
if (showStorageIndicator)
switch (asset.storage) {
AssetState.local => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_off_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_off_outlined),
),
),
AssetState.remote => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_outlined),
),
),
AssetState.merged => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_done_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_done_outlined),
),
),
},
if (asset.isFavorite)
const Align(
@ -154,41 +144,22 @@ class _SelectionIndicator extends StatelessWidget {
final bool isLocked;
final Color? color;
const _SelectionIndicator({
required this.isSelected,
required this.isLocked,
this.color,
});
const _SelectionIndicator({required this.isSelected, required this.isLocked, this.color});
@override
Widget build(BuildContext context) {
if (isLocked) {
return DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: const Icon(
Icons.check_circle_rounded,
color: Colors.grey,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: const Icon(Icons.check_circle_rounded, color: Colors.grey),
);
} else if (isSelected) {
return DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: Icon(Icons.check_circle_rounded, color: context.primaryColor),
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
return const Icon(Icons.circle_outlined, color: Colors.white);
}
}
}
@ -212,12 +183,7 @@ class _VideoIndicator extends StatelessWidget {
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
),
],
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6))],
),
),
const _TileOverlayIcon(Icons.play_circle_outline_rounded),
@ -237,13 +203,7 @@ class _TileOverlayIcon extends StatelessWidget {
icon,
color: Colors.white,
size: 16,
shadows: [
const Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
);
}
}

View file

@ -11,11 +11,7 @@ import 'package:immich_mobile/routing/router.dart';
class DriftMemoryBottomInfo extends StatelessWidget {
final DriftMemory memory;
final String title;
const DriftMemoryBottomInfo({
super.key,
required this.memory,
required this.title,
});
const DriftMemoryBottomInfo({super.key, required this.memory, required this.title});
@override
Widget build(BuildContext context) {
@ -23,47 +19,39 @@ class DriftMemoryBottomInfo extends StatelessWidget {
final fileCreatedDate = memory.assets.first.createdAt;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: Colors.grey[400],
fontSize: 13.0,
fontWeight: FontWeight.w500,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500),
),
),
Text(
df.format(fileCreatedDate),
style: const TextStyle(
color: Colors.white,
fontSize: 15.0,
fontWeight: FontWeight.w500,
Text(
df.format(fileCreatedDate),
style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500),
),
),
],
),
Tooltip(
message: 'view_in_timeline'.tr(),
child: MaterialButton(
minWidth: 0,
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.open_in_new,
color: Colors.white,
],
),
Tooltip(
message: 'view_in_timeline'.tr(),
child: MaterialButton(
minWidth: 0,
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(Icons.open_in_new, color: Colors.white),
),
),
),
]),
],
),
);
}
}

View file

@ -29,17 +29,12 @@ class DriftMemoryCard extends StatelessWidget {
color: Colors.black,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
side: BorderSide(
color: Colors.black,
width: 1.0,
),
side: BorderSide(color: Colors.black, width: 1.0),
),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
SizedBox.expand(
child: _BlurredBackdrop(asset: asset),
),
SizedBox.expand(child: _BlurredBackdrop(asset: asset)),
LayoutBuilder(
builder: (context, constraints) {
// Determine the fit using the aspect ratio
@ -55,11 +50,7 @@ class DriftMemoryCard extends StatelessWidget {
}
if (asset.isImage) {
return FullImage(
asset,
fit: fit,
size: const Size(double.infinity, double.infinity),
);
return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity));
} else {
return SizedBox(
width: context.width,
@ -69,11 +60,7 @@ class DriftMemoryCard extends StatelessWidget {
asset: asset,
showControls: false,
playbackDelayFactor: 2,
image: FullImage(
asset,
size: Size(context.width, context.height),
fit: BoxFit.contain,
),
image: FullImage(asset, size: Size(context.width, context.height), fit: BoxFit.contain),
),
);
}
@ -85,10 +72,7 @@ class DriftMemoryCard extends StatelessWidget {
bottom: 18.0,
child: Text(
title,
style: context.textTheme.headlineMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
),
style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500),
),
),
],
@ -109,16 +93,9 @@ class _BlurredBackdrop extends HookWidget {
// Use a nice cheap blur hash image decoration
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(
blurhash,
),
fit: BoxFit.cover,
),
),
child: Container(
color: Colors.black.withValues(alpha: 0.2),
image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
);
} else {
// Fall back to using a more expensive image filtered
@ -129,16 +106,11 @@ class _BlurredBackdrop extends HookWidget {
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: getFullImageProvider(
asset,
size: Size(context.width, context.height),
),
image: getFullImageProvider(asset, size: Size(context.width, context.height)),
fit: BoxFit.cover,
),
),
child: Container(
color: Colors.black.withValues(alpha: 0.2),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
),
);
}

View file

@ -17,17 +17,13 @@ class DriftMemoryLane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
),
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView(
itemExtent: 145.0,
shrinkExtent: 1.0,
elevation: 2,
backgroundColor: Colors.black,
overlayColor: WidgetStateProperty.all(
Colors.white.withValues(alpha: 0.1),
),
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
onTap: (index) {
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
@ -40,12 +36,7 @@ class DriftMemoryLane extends ConsumerWidget {
}
}
context.pushRoute(
DriftMemoryRoute(
memories: memories,
memoryIndex: index,
),
);
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
},
children: memories.map((memory) => DriftMemoryCard(memory: memory)).toList(),
),
@ -54,53 +45,33 @@ class DriftMemoryLane extends ConsumerWidget {
}
class DriftMemoryCard extends ConsumerWidget {
const DriftMemoryCard({
super.key,
required this.memory,
});
const DriftMemoryCard({super.key, required this.memory});
final DriftMemory memory;
@override
Widget build(BuildContext context, WidgetRef ref) {
final yearsAgo = DateTime.now().year - memory.data.year;
final title = 'years_ago'.t(
context: context,
args: {
'years': yearsAgo.toString(),
},
);
final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()});
return Center(
child: Stack(
children: [
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.2),
BlendMode.darken,
),
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
child: SizedBox(
width: 205,
height: 200,
child: Thumbnail(
remoteId: memory.assets[0].id,
fit: BoxFit.cover,
),
child: Thumbnail(remoteId: memory.assets[0].id, fit: BoxFit.cover),
),
),
Positioned(
bottom: 16,
left: 16,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 114,
),
constraints: const BoxConstraints(maxWidth: 114),
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white,
fontSize: 15,
),
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15),
),
),
),

View file

@ -25,9 +25,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.w600,
);
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600);
return SafeArea(
child: Padding(
@ -38,72 +36,46 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
if (onEditAlbum != null)
ListTile(
leading: const Icon(Icons.edit),
title: Text(
'edit_album'.t(context: context),
style: textStyle,
),
title: Text('edit_album'.t(context: context), style: textStyle),
onTap: onEditAlbum,
),
if (onAddPhotos != null)
ListTile(
leading: const Icon(Icons.add_a_photo),
title: Text(
'add_photos'.t(context: context),
style: textStyle,
),
title: Text('add_photos'.t(context: context), style: textStyle),
onTap: onAddPhotos,
),
if (onAddUsers != null)
ListTile(
leading: const Icon(Icons.group_add),
title: Text(
'album_viewer_page_share_add_users'.t(context: context),
style: textStyle,
),
title: Text('album_viewer_page_share_add_users'.t(context: context), style: textStyle),
onTap: onAddUsers,
),
if (onLeaveAlbum != null)
ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: Text(
'leave_album'.t(context: context),
style: textStyle,
),
title: Text('leave_album'.t(context: context), style: textStyle),
onTap: onLeaveAlbum,
),
if (onToggleAlbumOrder != null)
ListTile(
leading: const Icon(Icons.swap_vert_rounded),
title: Text(
'change_display_order'.t(context: context),
style: textStyle,
),
title: Text('change_display_order'.t(context: context), style: textStyle),
onTap: onToggleAlbumOrder,
),
if (onCreateSharedLink != null)
ListTile(
leading: const Icon(Icons.link),
title: Text(
'create_shared_link'.t(context: context),
style: textStyle,
),
title: Text('create_shared_link'.t(context: context), style: textStyle),
onTap: onCreateSharedLink,
),
if (onDeleteAlbum != null) ...[
const Divider(
indent: 16,
endIndent: 16,
),
const Divider(indent: 16, endIndent: 16),
ListTile(
leading: Icon(
Icons.delete,
color: context.isDarkTheme ? Colors.red[400] : Colors.red[800],
),
leading: Icon(Icons.delete, color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]),
title: Text(
'delete_album'.t(context: context),
style: textStyle.copyWith(
color: context.isDarkTheme ? Colors.red[400] : Colors.red[800],
),
style: textStyle.copyWith(color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]),
),
onTap: onDeleteAlbum,
),

View file

@ -16,11 +16,7 @@ class FixedTimelineRow extends MultiChildRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
return RenderFixedRow(
dimension: dimension,
spacing: spacing,
textDirection: textDirection,
);
return RenderFixedRow(dimension: dimension, spacing: spacing, textDirection: textDirection);
}
@override
@ -50,9 +46,9 @@ class RenderFixedRow extends RenderBox
required double dimension,
required double spacing,
required TextDirection textDirection,
}) : _dimension = dimension,
_spacing = spacing,
_textDirection = textDirection {
}) : _dimension = dimension,
_spacing = spacing,
_textDirection = textDirection {
addAll(children);
}

View file

@ -33,8 +33,8 @@ class FixedSegment extends Segment {
required super.headerExtent,
required super.spacing,
required super.header,
}) : assert(tileHeight != 0),
mainAxisExtend = tileHeight + spacing;
}) : assert(tileHeight != 0),
mainAxisExtend = tileHeight + spacing;
@override
double indexToLayoutOffset(int index) {
@ -64,12 +64,7 @@ class FixedSegment extends Segment {
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
if (index == firstIndex) {
return TimelineHeader(
bucket: bucket,
header: header,
height: headerExtent,
assetOffset: firstAssetIndex,
);
return TimelineHeader(bucket: bucket, header: header, height: headerExtent, assetOffset: firstAssetIndex);
}
return _FixedSegmentRow(
@ -104,10 +99,7 @@ class _FixedSegmentRow extends ConsumerWidget {
}
if (timelineService.hasRange(assetIndex, assetCount)) {
return _buildAssetRow(
context,
timelineService.getAssets(assetIndex, assetCount),
);
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount));
}
return FutureBuilder<List<BaseAsset>>(
@ -122,12 +114,7 @@ class _FixedSegmentRow extends ConsumerWidget {
}
Widget _buildPlaceholder(BuildContext context) {
return SegmentBuilder.buildPlaceholder(
context,
assetCount,
size: Size.square(tileHeight),
spacing: spacing,
);
return SegmentBuilder.buildPlaceholder(context, assetCount, size: Size.square(tileHeight), spacing: spacing);
}
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) {
@ -137,11 +124,7 @@ class _FixedSegmentRow extends ConsumerWidget {
textDirection: Directionality.of(context),
children: [
for (int i = 0; i < assets.length; i++)
_AssetTileWidget(
key: ValueKey(assets[i].heroTag),
asset: assets[i],
assetIndex: assetIndex + i,
),
_AssetTileWidget(key: ValueKey(assets[i].heroTag), asset: assets[i], assetIndex: assetIndex + i),
],
);
}
@ -151,19 +134,9 @@ class _AssetTileWidget extends ConsumerWidget {
final BaseAsset asset;
final int assetIndex;
const _AssetTileWidget({
super.key,
required this.asset,
required this.assetIndex,
});
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
Future _handleOnTap(
BuildContext ctx,
WidgetRef ref,
int assetIndex,
BaseAsset asset,
int? heroOffset,
) async {
Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async {
final multiSelectState = ref.read(multiSelectProvider);
if (multiSelectState.forceEnable || multiSelectState.isEnabled) {
@ -192,11 +165,7 @@ class _AssetTileWidget extends ConsumerWidget {
}
bool _getLockSelectionStatus(WidgetRef ref) {
final lockSelectionAssets = ref.read(
multiSelectProvider.select(
(state) => state.lockedSelectionAssets,
),
);
final lockSelectionAssets = ref.read(multiSelectProvider.select((state) => state.lockedSelectionAssets));
if (lockSelectionAssets.isEmpty) {
return false;
@ -210,9 +179,7 @@ class _AssetTileWidget extends ConsumerWidget {
final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
final lockSelection = _getLockSelectionStatus(ref);
final showStorageIndicator = ref.watch(
timelineArgsProvider.select((args) => args.showStorageIndicator),
);
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
return RepaintBoundary(
child: GestureDetector(

View file

@ -35,8 +35,7 @@ class FixedSegmentBuilder extends SegmentBuilder {
final timelineHeader = switch (groupBy) {
GroupAssetsBy.month => HeaderType.month,
GroupAssetsBy.day ||
GroupAssetsBy.auto =>
GroupAssetsBy.day || GroupAssetsBy.auto =>
bucket is TimeBucket && bucket.date.month != previousDate?.month ? HeaderType.monthAndDay : HeaderType.day,
GroupAssetsBy.none => HeaderType.none,
};

View file

@ -47,11 +47,7 @@ class TimelineHeader extends StatelessWidget {
final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay;
return Padding(
padding: EdgeInsets.only(
top: isMonthHeader ? 8.0 : 0.0,
left: 12.0,
right: 12.0,
),
padding: EdgeInsets.only(top: isMonthHeader ? 8.0 : 0.0, left: 12.0, right: 12.0),
child: SizedBox(
height: height,
child: Column(
@ -61,32 +57,17 @@ class TimelineHeader extends StatelessWidget {
if (isMonthHeader)
Row(
children: [
Text(
_formatMonth(context, date),
style: context.textTheme.labelLarge?.copyWith(fontSize: 24),
),
Text(_formatMonth(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 24)),
const Spacer(),
if (header != HeaderType.monthAndDay)
_BulkSelectIconButton(
bucket: bucket,
assetOffset: assetOffset,
),
if (header != HeaderType.monthAndDay) _BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset),
],
),
if (isDayHeader)
Row(
children: [
Text(
_formatDay(context, date),
style: context.textTheme.labelLarge?.copyWith(
fontSize: 15,
),
),
Text(_formatDay(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 15)),
const Spacer(),
_BulkSelectIconButton(
bucket: bucket,
assetOffset: assetOffset,
),
_BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset),
],
),
],
@ -100,10 +81,7 @@ class _BulkSelectIconButton extends ConsumerWidget {
final Bucket bucket;
final int assetOffset;
const _BulkSelectIconButton({
required this.bucket,
required this.assetOffset,
});
const _BulkSelectIconButton({required this.bucket, required this.assetOffset});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -118,23 +96,12 @@ class _BulkSelectIconButton extends ConsumerWidget {
return IconButton(
onPressed: () {
ref.read(multiSelectProvider.notifier).toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
icon: isAllSelected
? Icon(
Icons.check_circle_rounded,
size: 26,
color: context.primaryColor,
)
: Icon(
Icons.check_circle_outline_rounded,
size: 26,
color: context.colorScheme.onSurfaceSecondary,
),
? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor)
: Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary),
);
}
}

View file

@ -43,10 +43,7 @@ class Scrubber extends ConsumerStatefulWidget {
ConsumerState createState() => ScrubberState();
}
List<_Segment> _buildSegments({
required List<Segment> layoutSegments,
required double timelineHeight,
}) {
List<_Segment> _buildSegments({required List<Segment> layoutSegments, required double timelineHeight}) {
const double offsetThreshold = 20.0;
final segments = <_Segment>[];
@ -66,14 +63,7 @@ List<_Segment> _buildSegments({
final showSegment = lastOffset + offsetThreshold <= startOffset && (lastDate == null || date.year != lastDate.year);
segments.add(
_Segment(
date: date,
startOffset: startOffset,
scrollLabel: label,
showSegment: showSegment,
),
);
segments.add(_Segment(date: date, startOffset: startOffset, scrollLabel: label, showSegment: showSegment));
lastDate = date;
if (showSegment) {
lastOffset = startOffset;
@ -109,27 +99,12 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
void initState() {
super.initState();
_isDragging = false;
_segments = _buildSegments(
layoutSegments: widget.layoutSegments,
timelineHeight: _scrubberHeight,
);
_thumbAnimationController = AnimationController(
vsync: this,
duration: kTimelineScrubberFadeInDuration,
);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastEaseInToSlowEaseOut,
);
_labelAnimationController = AnimationController(
vsync: this,
duration: kTimelineScrubberFadeInDuration,
);
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@override
@ -143,10 +118,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
super.didUpdateWidget(oldWidget);
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
_segments = _buildSegments(
layoutSegments: widget.layoutSegments,
timelineHeight: _scrubberHeight,
);
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
}
}
@ -276,12 +248,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}
int _findLayoutSegmentIndex(_Segment segment) {
return widget.layoutSegments.indexWhere(
(layoutSegment) {
final bucket = layoutSegment.bucket as TimeBucket;
return bucket.date.year == segment.date.year && bucket.date.month == segment.date.month;
},
);
return widget.layoutSegments.indexWhere((layoutSegment) {
final bucket = layoutSegment.bucket as TimeBucket;
return bucket.date.year == segment.date.year && bucket.date.month == segment.date.month;
});
}
void _scrollToLayoutSegment(int layoutSegmentIndex) {
@ -311,19 +281,13 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (_scrollController.hasClients == true) {
// Cache to avoid multiple calls to [_currentOffset]
final scrollOffset = _currentOffset;
final labelText = _segments
.lastWhereOrNull(
(segment) => segment.startOffset <= scrollOffset,
)
?.scrollLabel ??
final labelText =
_segments.lastWhereOrNull((segment) => segment.startOffset <= scrollOffset)?.scrollLabel ??
_segments.firstOrNull?.scrollLabel;
label = labelText != null
? Text(
labelText,
style: ctx.textTheme.bodyLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
style: ctx.textTheme.bodyLarge?.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
)
: null;
}
@ -351,11 +315,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
),
child: _Scrubber(thumbAnimation: _thumbAnimation, labelAnimation: _labelAnimation, label: label),
),
),
),
@ -370,12 +330,7 @@ class _SegmentsLayer extends StatelessWidget {
final double topPadding;
final bool isDragging;
const _SegmentsLayer({
super.key,
required this.segments,
required this.topPadding,
required this.isDragging,
});
const _SegmentsLayer({super.key, required this.segments, required this.topPadding, required this.isDragging});
@override
Widget build(BuildContext context) {
@ -389,9 +344,7 @@ class _SegmentsLayer extends StatelessWidget {
key: ValueKey('segment_${segment.date.millisecondsSinceEpoch}'),
top: topPadding + segment.startOffset,
end: 100,
child: RepaintBoundary(
child: _SegmentWidget(segment),
),
child: RepaintBoundary(child: _SegmentWidget(segment)),
),
)
.toList(),
@ -419,10 +372,7 @@ class _SegmentWidget extends StatelessWidget {
alignment: Alignment.center,
child: Text(
_segment.date.year.toString(),
style: context.textTheme.labelMedium?.copyWith(
fontFamily: "OverpassMono",
fontWeight: FontWeight.w600,
),
style: context.textTheme.labelMedium?.copyWith(fontFamily: "OverpassMono", fontWeight: FontWeight.w600),
),
),
),
@ -436,11 +386,7 @@ class _ScrollLabel extends StatelessWidget {
final Color backgroundColor;
final Animation<double> animation;
const _ScrollLabel({
required this.label,
required this.backgroundColor,
required this.animation,
});
const _ScrollLabel({required this.label, required this.backgroundColor, required this.animation});
@override
Widget build(BuildContext context) {
@ -471,16 +417,13 @@ class _Scrubber extends StatelessWidget {
final Animation<double> thumbAnimation;
final Animation<double> labelAnimation;
const _Scrubber({
this.label,
required this.thumbAnimation,
required this.labelAnimation,
});
const _Scrubber({this.label, required this.thumbAnimation, required this.labelAnimation});
@override
Widget build(BuildContext context) {
final backgroundColor =
context.isDarkTheme ? context.colorScheme.primary.darken(amount: .5) : context.colorScheme.primary;
final backgroundColor = context.isDarkTheme
? context.colorScheme.primary.darken(amount: .5)
: context.colorScheme.primary;
return _SlideFadeTransition(
animation: thumbAnimation,
@ -488,12 +431,7 @@ class _Scrubber extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (label != null)
_ScrollLabel(
label: label!,
backgroundColor: backgroundColor,
animation: labelAnimation,
),
if (label != null) _ScrollLabel(label: label!, backgroundColor: backgroundColor, animation: labelAnimation),
_CircularThumb(backgroundColor),
],
),
@ -519,9 +457,7 @@ class _CircularThumb extends StatelessWidget {
topRight: Radius.circular(4.0),
bottomRight: Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(const Size(48.0 * 0.6, 48.0)),
),
child: Container(constraints: BoxConstraints.tight(const Size(48.0 * 0.6, 48.0))),
),
);
}
@ -543,14 +479,8 @@ class _ArrowPainter 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) {
@ -566,11 +496,9 @@ class _SlideFadeTransition extends StatelessWidget {
final Animation<double> _animation;
final Widget _child;
const _SlideFadeTransition({
required Animation<double> animation,
required Widget child,
}) : _animation = animation,
_child = child;
const _SlideFadeTransition({required Animation<double> animation, required Widget child})
: _animation = animation,
_child = child;
@override
Widget build(BuildContext context) {
@ -578,14 +506,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),
),
);
}
@ -597,19 +519,9 @@ class _Segment {
final String scrollLabel;
final bool showSegment;
const _Segment({
required this.date,
required this.startOffset,
required this.scrollLabel,
this.showSegment = false,
});
const _Segment({required this.date, required this.startOffset, required this.scrollLabel, this.showSegment = false});
_Segment copyWith({
DateTime? date,
double? startOffset,
String? scrollLabel,
bool? showSegment,
}) {
_Segment copyWith({DateTime? date, double? startOffset, String? scrollLabel, bool? showSegment}) {
return _Segment(
date: date ?? this.date,
startOffset: startOffset ?? this.startOffset,

View file

@ -37,8 +37,8 @@ abstract class Segment {
required this.headerExtent,
required this.spacing,
required this.header,
}) : gridIndex = firstIndex + 1,
gridOffset = startOffset + headerExtent + spacing;
}) : gridIndex = firstIndex + 1,
gridOffset = startOffset + headerExtent + spacing;
bool containsIndex(int index) => firstIndex <= index && index <= lastIndex;

View file

@ -9,34 +9,26 @@ abstract class SegmentBuilder {
final double spacing;
final GroupAssetsBy groupBy;
const SegmentBuilder({
required this.buckets,
this.spacing = kTimelineSpacing,
this.groupBy = GroupAssetsBy.day,
});
const SegmentBuilder({required this.buckets, this.spacing = kTimelineSpacing, this.groupBy = GroupAssetsBy.day});
static double headerExtent(HeaderType header) => switch (header) {
HeaderType.month => kTimelineHeaderExtent,
HeaderType.day => kTimelineHeaderExtent * 0.90,
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
HeaderType.none => 0.0,
};
HeaderType.month => kTimelineHeaderExtent,
HeaderType.day => kTimelineHeaderExtent * 0.90,
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
HeaderType.none => 0.0,
};
static Widget buildPlaceholder(
BuildContext context,
int count, {
Size size = const Size.square(kTimelineFixedTileExtent),
double spacing = kTimelineSpacing,
}) =>
RepaintBoundary(
child: FixedTimelineRow(
dimension: size.height,
spacing: spacing,
textDirection: Directionality.of(context),
children: List.generate(
count,
(_) => ThumbnailPlaceholder(width: size.width, height: size.height),
),
),
);
}) => RepaintBoundary(
child: FixedTimelineRow(
dimension: size.height,
spacing: spacing,
textDirection: Directionality.of(context),
children: List.generate(count, (_) => ThumbnailPlaceholder(width: size.width, height: size.height)),
),
);
}

View file

@ -54,10 +54,7 @@ class TimelineState {
final bool isScrubbing;
final bool isScrolling;
const TimelineState({
this.isScrubbing = false,
this.isScrolling = false,
});
const TimelineState({this.isScrubbing = false, this.isScrolling = false});
bool get isInteracting => isScrubbing || isScrolling;
@ -70,10 +67,7 @@ class TimelineState {
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
return TimelineState(
isScrubbing: isScrubbing ?? this.isScrubbing,
isScrolling: isScrolling ?? this.isScrolling,
);
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling);
}
}
@ -89,38 +83,30 @@ class TimelineStateNotifier extends Notifier<TimelineState> {
}
@override
TimelineState build() => const TimelineState(
isScrubbing: false,
isScrolling: false,
);
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
}
// This provider watches the buckets from the timeline service & args and serves the segments.
// It should be used only after the timeline service and timeline args provider is overridden
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>(
(ref) async* {
final args = ref.watch(timelineArgsProvider);
final columnCount = args.columnCount;
final spacing = args.spacing;
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref) async* {
final args = ref.watch(timelineArgsProvider);
final columnCount = args.columnCount;
final spacing = args.spacing;
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
return FixedSegmentBuilder(
buckets: buckets,
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
).generate();
});
},
dependencies: [timelineServiceProvider, timelineArgsProvider],
);
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
return FixedSegmentBuilder(
buckets: buckets,
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
).generate();
});
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(
TimelineStateNotifier.new,
);
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(TimelineStateNotifier.new);

View file

@ -29,11 +29,7 @@ class Timeline extends StatelessWidget {
this.topSliverWidgetHeight,
this.showStorageIndicator = false,
this.withStack = false,
this.appBar = const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
this.bottomSheet = const GeneralBottomSheet(),
this.groupBy,
});
@ -57,9 +53,7 @@ class Timeline extends StatelessWidget {
(ref) => TimelineArgs(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
columnCount: ref.watch(
settingsProvider.select((s) => s.get(Setting.tilesPerRow)),
),
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
showStorageIndicator: showStorageIndicator,
withStack: withStack,
groupBy: groupBy,
@ -79,12 +73,7 @@ class Timeline extends StatelessWidget {
}
class _SliverTimeline extends ConsumerStatefulWidget {
const _SliverTimeline({
this.topSliverWidget,
this.topSliverWidgetHeight,
this.appBar,
this.bottomSheet,
});
const _SliverTimeline({this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, this.bottomSheet});
final Widget? topSliverWidget;
final double? topSliverWidgetHeight;
@ -108,11 +97,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
void _onEvent(Event event) {
switch (event) {
case ScrollToTopEvent():
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
_scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
case ScrollToDateEvent scrollToDateEvent:
_scrollToDate(scrollToDateEvent.date);
case TimelineReloadEvent():
@ -143,7 +128,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
});
// If exact date not found, try to find the closest month
final fallbackSegment = targetSegment ??
final fallbackSegment =
targetSegment ??
segments.firstWhereOrNull((segment) {
if (segment.bucket is TimeBucket) {
final segmentDate = (segment.bucket as TimeBucket).date;
@ -168,9 +154,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
Widget build(BuildContext _) {
final asyncSegments = ref.watch(timelineSegmentProvider);
final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
final isSelectionMode = ref.watch(
multiSelectProvider.select((s) => s.forceEnable),
);
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
return asyncSegments.widgetWhen(
onData: (segments) {
@ -211,42 +195,26 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
addRepaintBoundaries: false,
),
),
const SliverPadding(
padding: EdgeInsets.only(
bottom: scrubberBottomPadding,
),
),
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
],
),
),
if (!isSelectionMode) ...[
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
final isMultiSelectEnabled = consumerRef.watch(multiSelectProvider.select((s) => s.isEnabled));
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
),
child: const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
),
if (widget.bottomSheet != null)
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
final isMultiSelectEnabled = consumerRef.watch(multiSelectProvider.select((s) => s.isEnabled));
if (isMultiSelectEnabled) {
return child!;
@ -267,22 +235,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
final List<Segment> _segments;
const _SliverSegmentedList({
required List<Segment> segments,
required super.delegate,
}) : _segments = segments;
const _SliverSegmentedList({required List<Segment> segments, required super.delegate}) : _segments = segments;
@override
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) => _RenderSliverTimelineBoxAdaptor(
childManager: context as SliverMultiBoxAdaptorElement,
segments: _segments,
);
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
_RenderSliverTimelineBoxAdaptor(childManager: context as SliverMultiBoxAdaptorElement, segments: _segments);
@override
void updateRenderObject(
BuildContext context,
_RenderSliverTimelineBoxAdaptor renderObject,
) {
void updateRenderObject(BuildContext context, _RenderSliverTimelineBoxAdaptor renderObject) {
renderObject.segments = _segments;
}
}
@ -299,10 +259,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
markNeedsLayout();
}
_RenderSliverTimelineBoxAdaptor({
required super.childManager,
required List<Segment> segments,
}) : _segments = segments;
_RenderSliverTimelineBoxAdaptor({required super.childManager, required List<Segment> segments})
: _segments = segments;
int getMinChildIndexForScrollOffset(double offset) =>
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
@ -335,16 +293,18 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
final int firstRequiredChildIndex = getMinChildIndexForScrollOffset(scrollOffset);
// Find the index of the last child that should be visible or in the trailing cache area.
final int? lastRequiredChildIndex =
targetScrollOffset.isFinite ? getMaxChildIndexForScrollOffset(targetScrollOffset) : null;
final int? lastRequiredChildIndex = targetScrollOffset.isFinite
? getMaxChildIndexForScrollOffset(targetScrollOffset)
: null;
// Remove children that are no longer visible or within the cache area.
if (firstChild == null) {
collectGarbage(0, 0);
} else {
final int leadingChildrenToRemove = calculateLeadingGarbage(firstIndex: firstRequiredChildIndex);
final int trailingChildrenToRemove =
lastRequiredChildIndex == null ? 0 : calculateTrailingGarbage(lastIndex: lastRequiredChildIndex);
final int trailingChildrenToRemove = lastRequiredChildIndex == null
? 0
: calculateTrailingGarbage(lastIndex: lastRequiredChildIndex);
collectGarbage(leadingChildrenToRemove, trailingChildrenToRemove);
}
@ -352,10 +312,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// try to add the first child needed for the current scroll offset.
if (firstChild == null) {
final double firstChildLayoutOffset = indexToLayoutOffset(firstRequiredChildIndex);
final bool childAdded = addInitialChild(
index: firstRequiredChildIndex,
layoutOffset: firstChildLayoutOffset,
);
final bool childAdded = addInitialChild(index: firstRequiredChildIndex, layoutOffset: firstChildLayoutOffset);
if (!childAdded) {
// There are either no children, or we are past the end of all our children.
@ -408,16 +365,15 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// until we reach the [lastRequiredChildIndex] or run out of children.
double calculatedMaxScrollOffset = double.infinity;
for (int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1;
lastRequiredChildIndex == null || currentIndex <= lastRequiredChildIndex;
++currentIndex) {
for (
int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1;
lastRequiredChildIndex == null || currentIndex <= lastRequiredChildIndex;
++currentIndex
) {
RenderBox? child = childAfter(mostRecentlyLaidOutChild!);
if (child == null || indexOf(child) != currentIndex) {
child = insertAndLayoutChild(
childConstraints,
after: mostRecentlyLaidOutChild,
);
child = insertAndLayoutChild(childConstraints, after: mostRecentlyLaidOutChild);
if (child == null) {
final Segment? segment = _segments.findByIndex(currentIndex) ?? _segments.lastOrNull;
calculatedMaxScrollOffset = segment?.indexToLayoutOffset(currentIndex) ?? computeMaxScrollOffset();
@ -443,30 +399,18 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
);
assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(indexOf(firstChild!) == firstRequiredChildIndex);
assert(
lastRequiredChildIndex == null || lastLaidOutChildIndex <= lastRequiredChildIndex,
);
assert(lastRequiredChildIndex == null || lastLaidOutChildIndex <= lastRequiredChildIndex);
calculatedMaxScrollOffset = math.min(
calculatedMaxScrollOffset,
estimateMaxScrollOffset(),
);
calculatedMaxScrollOffset = math.min(calculatedMaxScrollOffset, estimateMaxScrollOffset());
final double paintExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
final double paintExtent = calculatePaintOffset(constraints, from: leadingScrollOffset, to: trailingScrollOffset);
final double cacheExtent = calculateCacheOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
final double cacheExtent = calculateCacheOffset(constraints, from: leadingScrollOffset, to: trailingScrollOffset);
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
final int? targetLastIndexForPaint =
targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null;
final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite
? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint)
: null;
final maxPaintExtent = math.max(paintExtent, calculatedMaxScrollOffset);
@ -477,7 +421,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// Indicates if there's content scrolled off-screen.
// This is true if the last child needed for painting is actually laid out,
// or if the first child is partially visible.
hasVisualOverflow: (targetLastIndexForPaint != null && lastLaidOutChildIndex >= targetLastIndexForPaint) ||
hasVisualOverflow:
(targetLastIndexForPaint != null && lastLaidOutChildIndex >= targetLastIndexForPaint) ||
constraints.scrollOffset > 0.0,
cacheExtent: cacheExtent,
);
@ -500,16 +445,10 @@ class _MultiSelectStatusButton extends ConsumerWidget {
final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
return ElevatedButton.icon(
onPressed: () => ref.read(multiSelectProvider.notifier).reset(),
icon: Icon(
Icons.close_rounded,
color: context.colorScheme.onPrimary,
),
icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary),
label: Text(
selectCount.toString(),
style: context.textTheme.titleMedium?.copyWith(
height: 2.5,
color: context.colorScheme.onPrimary,
),
style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary),
),
);
}