mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
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:
parent
9b3718120b
commit
e52b9d15b5
643 changed files with 32561 additions and 35292 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) ...[
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) ...[
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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))],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue