mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): Explore favorites, recently added, videos, and motion photos (#2076)
* Added placeholder for search explore * refactor immich asset grid to use ref and provider * all videos page * got favorites, recently added, videos, and motion videos all using the immich grid * Fixed issue with hero animations * theming * localization * delete empty file * style text * Styling icons * more styling --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
d2600e0ddd
commit
501b96baf7
23 changed files with 1013 additions and 524 deletions
|
|
@ -1,300 +1,104 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'asset_grid_data_structure.dart';
|
||||
import 'group_divider_title.dart';
|
||||
import 'disable_multi_select_button.dart';
|
||||
import 'draggable_scrollbar_custom.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
typedef ImmichAssetGridSelectionListener = void Function(
|
||||
bool,
|
||||
Set<Asset>,
|
||||
);
|
||||
|
||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ItemPositionsListener _itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
bool _scrolling = false;
|
||||
final Set<int> _selectedAssets = HashSet();
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return _selectedAssets
|
||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _callSelectionListener(bool selectionActive) {
|
||||
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||
}
|
||||
|
||||
void _selectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
for (var e in assets) {
|
||||
_selectedAssets.add(e.id);
|
||||
}
|
||||
_callSelectionListener(true);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
for (var e in assets) {
|
||||
_selectedAssets.remove(e.id);
|
||||
}
|
||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAll() {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
});
|
||||
|
||||
_callSelectionListener(false);
|
||||
}
|
||||
|
||||
bool _allAssetsSelected(List<Asset> assets) {
|
||||
return widget.selectionActive &&
|
||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||
}
|
||||
|
||||
Widget _buildThumbnailOrPlaceholder(
|
||||
Asset asset,
|
||||
bool placeholder,
|
||||
) {
|
||||
if (placeholder) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
return ThumbnailImage(
|
||||
asset: asset,
|
||||
assetList: widget.allAssets,
|
||||
multiselectEnabled: widget.selectionActive,
|
||||
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
||||
onSelect: () => _selectAssets([asset]),
|
||||
onDeselect: () => _deselectAssets([asset]),
|
||||
useGrayBoxPlaceholder: true,
|
||||
showStorageIndicator: widget.showStorageIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetRow(
|
||||
BuildContext context,
|
||||
RenderAssetGridRow row,
|
||||
bool scrolling,
|
||||
) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = constraints.maxWidth / widget.assetsPerRow -
|
||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||
return Row(
|
||||
key: Key("asset-row-${row.assets.first.id}"),
|
||||
children: row.assets.mapIndexed((int index, Asset asset) {
|
||||
bool last = asset.id == row.assets.last.id;
|
||||
|
||||
return Container(
|
||||
key: Key("asset-${asset.id}"),
|
||||
width: size * row.widthDistribution[index],
|
||||
height: size,
|
||||
margin: EdgeInsets.only(
|
||||
top: widget.margin,
|
||||
right: last ? 0.0 : widget.margin,
|
||||
),
|
||||
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(
|
||||
BuildContext context,
|
||||
String title,
|
||||
List<Asset> assets,
|
||||
) {
|
||||
return GroupDividerTitle(
|
||||
text: title,
|
||||
multiselectEnabled: widget.selectionActive,
|
||||
onSelect: () => _selectAssets(assets),
|
||||
onDeselect: () => _deselectAssets(assets),
|
||||
selected: _allAssetsSelected(assets),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||
return Padding(
|
||||
key: Key("month-$title"),
|
||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.displayLarge?.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(BuildContext c, int position) {
|
||||
final item = widget.renderList.elements[position];
|
||||
|
||||
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||
return _buildMonthTitle(c, item.title!);
|
||||
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
||||
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
||||
}
|
||||
|
||||
return const Text("Invalid widget type!");
|
||||
}
|
||||
|
||||
Text _labelBuilder(int pos) {
|
||||
final date = widget.renderList.elements[pos].date;
|
||||
return Text(
|
||||
DateFormat.yMMMM().format(date),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultiSelectIndicator() {
|
||||
return DisableMultiSelectButton(
|
||||
onPressed: () => _deselectAll(),
|
||||
selectedItemCount: _selectedAssets.length,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetGrid() {
|
||||
final useDragScrolling = widget.allAssets.length >= 20;
|
||||
|
||||
void dragScrolling(bool active) {
|
||||
setState(() {
|
||||
_scrolling = active;
|
||||
});
|
||||
}
|
||||
|
||||
final listWidget = ScrollablePositionedList.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 220,
|
||||
),
|
||||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
itemScrollController: _itemScrollController,
|
||||
itemCount: widget.renderList.elements.length,
|
||||
addRepaintBoundaries: true,
|
||||
);
|
||||
|
||||
if (!useDragScrolling) {
|
||||
return listWidget;
|
||||
}
|
||||
|
||||
return DraggableScrollbar.semicircle(
|
||||
scrollStateListener: dragScrolling,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
controller: _itemScrollController,
|
||||
backgroundColor: Theme.of(context).hintColor,
|
||||
labelTextBuilder: _labelBuilder,
|
||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||
child: listWidget,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ImmichAssetGrid oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!widget.selectionActive) {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||
_deselectAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollToTop() {
|
||||
// for some reason, this is necessary as well in order
|
||||
// to correctly reposition the drag thumb scroll bar
|
||||
_itemScrollController.jumpTo(
|
||||
index: 0,
|
||||
);
|
||||
_itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichAssetGrid extends StatefulWidget {
|
||||
final RenderList renderList;
|
||||
final int assetsPerRow;
|
||||
class ImmichAssetGrid extends HookConsumerWidget {
|
||||
final int? assetsPerRow;
|
||||
final double margin;
|
||||
final bool showStorageIndicator;
|
||||
final bool? showStorageIndicator;
|
||||
final ImmichAssetGridSelectionListener? listener;
|
||||
final bool selectionActive;
|
||||
final List<Asset> allAssets;
|
||||
final List<Asset> assets;
|
||||
final RenderList? renderList;
|
||||
|
||||
const ImmichAssetGrid({
|
||||
super.key,
|
||||
required this.renderList,
|
||||
required this.allAssets,
|
||||
required this.assetsPerRow,
|
||||
required this.showStorageIndicator,
|
||||
required this.assets,
|
||||
this.renderList,
|
||||
this.assetsPerRow,
|
||||
this.showStorageIndicator,
|
||||
this.listener,
|
||||
this.margin = 5.0,
|
||||
this.selectionActive = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ImmichAssetGridState();
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var settings = ref.watch(appSettingsServiceProvider);
|
||||
final renderListFuture = ref.watch(renderListProvider(assets));
|
||||
|
||||
// Needs to suppress hero animations when navigating to this widget
|
||||
final enableHeroAnimations = useState(false);
|
||||
|
||||
// Wait for transition to complete, then re-enable
|
||||
ModalRoute.of(context)?.animation?.addListener(() {
|
||||
// If we've already enabled, we are done
|
||||
if (enableHeroAnimations.value) {
|
||||
return;
|
||||
}
|
||||
final animation = ModalRoute.of(context)?.animation;
|
||||
if (animation != null) {
|
||||
// When the animation is complete, re-enable hero animations
|
||||
enableHeroAnimations.value = animation.isCompleted;
|
||||
}
|
||||
});
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
enableHeroAnimations.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (renderList != null) {
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: HeroMode(
|
||||
enabled: enableHeroAnimations.value,
|
||||
child: ImmichAssetGridView(
|
||||
allAssets: assets,
|
||||
assetsPerRow: assetsPerRow
|
||||
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
listener: listener,
|
||||
showStorageIndicator: showStorageIndicator
|
||||
?? settings.getSetting(AppSettingsEnum.storageIndicator),
|
||||
renderList: renderList!,
|
||||
margin: margin,
|
||||
selectionActive: selectionActive,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return renderListFuture.when(
|
||||
data: (renderList) =>
|
||||
WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: HeroMode(
|
||||
enabled: enableHeroAnimations.value,
|
||||
child: ImmichAssetGridView(
|
||||
allAssets: assets,
|
||||
assetsPerRow: assetsPerRow
|
||||
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
listener: listener,
|
||||
showStorageIndicator: showStorageIndicator
|
||||
?? settings.getSetting(AppSettingsEnum.storageIndicator),
|
||||
renderList: renderList,
|
||||
margin: margin,
|
||||
selectionActive: selectionActive,
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (err, stack) =>
|
||||
Center(child: Text("$err")),
|
||||
loading: () => const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'asset_grid_data_structure.dart';
|
||||
import 'group_divider_title.dart';
|
||||
import 'disable_multi_select_button.dart';
|
||||
import 'draggable_scrollbar_custom.dart';
|
||||
|
||||
typedef ImmichAssetGridSelectionListener = void Function(
|
||||
bool,
|
||||
Set<Asset>,
|
||||
);
|
||||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ItemPositionsListener _itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
bool _scrolling = false;
|
||||
final Set<int> _selectedAssets = HashSet();
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return _selectedAssets
|
||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _callSelectionListener(bool selectionActive) {
|
||||
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||
}
|
||||
|
||||
void _selectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
for (var e in assets) {
|
||||
_selectedAssets.add(e.id);
|
||||
}
|
||||
_callSelectionListener(true);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
for (var e in assets) {
|
||||
_selectedAssets.remove(e.id);
|
||||
}
|
||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAll() {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
});
|
||||
|
||||
_callSelectionListener(false);
|
||||
}
|
||||
|
||||
bool _allAssetsSelected(List<Asset> assets) {
|
||||
return widget.selectionActive &&
|
||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||
}
|
||||
|
||||
Widget _buildThumbnailOrPlaceholder(
|
||||
Asset asset,
|
||||
bool placeholder,
|
||||
) {
|
||||
if (placeholder) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
return ThumbnailImage(
|
||||
asset: asset,
|
||||
assetList: widget.allAssets,
|
||||
multiselectEnabled: widget.selectionActive,
|
||||
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
||||
onSelect: () => _selectAssets([asset]),
|
||||
onDeselect: () => _deselectAssets([asset]),
|
||||
useGrayBoxPlaceholder: true,
|
||||
showStorageIndicator: widget.showStorageIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetRow(
|
||||
BuildContext context,
|
||||
RenderAssetGridRow row,
|
||||
bool scrolling,
|
||||
) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = constraints.maxWidth / widget.assetsPerRow -
|
||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||
return Row(
|
||||
key: Key("asset-row-${row.assets.first.id}"),
|
||||
children: row.assets.mapIndexed((int index, Asset asset) {
|
||||
bool last = asset.id == row.assets.last.id;
|
||||
|
||||
return Container(
|
||||
key: Key("asset-${asset.id}"),
|
||||
width: size * row.widthDistribution[index],
|
||||
height: size,
|
||||
margin: EdgeInsets.only(
|
||||
top: widget.margin,
|
||||
right: last ? 0.0 : widget.margin,
|
||||
),
|
||||
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle(
|
||||
BuildContext context,
|
||||
String title,
|
||||
List<Asset> assets,
|
||||
) {
|
||||
return GroupDividerTitle(
|
||||
text: title,
|
||||
multiselectEnabled: widget.selectionActive,
|
||||
onSelect: () => _selectAssets(assets),
|
||||
onDeselect: () => _deselectAssets(assets),
|
||||
selected: _allAssetsSelected(assets),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||
return Padding(
|
||||
key: Key("month-$title"),
|
||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.displayLarge?.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(BuildContext c, int position) {
|
||||
final item = widget.renderList.elements[position];
|
||||
|
||||
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||
return _buildMonthTitle(c, item.title!);
|
||||
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
||||
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
||||
}
|
||||
|
||||
return const Text("Invalid widget type!");
|
||||
}
|
||||
|
||||
Text _labelBuilder(int pos) {
|
||||
final date = widget.renderList.elements[pos].date;
|
||||
return Text(
|
||||
DateFormat.yMMMM().format(date),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultiSelectIndicator() {
|
||||
return DisableMultiSelectButton(
|
||||
onPressed: () => _deselectAll(),
|
||||
selectedItemCount: _selectedAssets.length,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetGrid() {
|
||||
final useDragScrolling = widget.allAssets.length >= 20;
|
||||
|
||||
void dragScrolling(bool active) {
|
||||
setState(() {
|
||||
_scrolling = active;
|
||||
});
|
||||
}
|
||||
|
||||
final listWidget = ScrollablePositionedList.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 220,
|
||||
),
|
||||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
itemScrollController: _itemScrollController,
|
||||
itemCount: widget.renderList.elements.length,
|
||||
addRepaintBoundaries: true,
|
||||
);
|
||||
|
||||
if (!useDragScrolling) {
|
||||
return listWidget;
|
||||
}
|
||||
|
||||
return DraggableScrollbar.semicircle(
|
||||
scrollStateListener: dragScrolling,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
controller: _itemScrollController,
|
||||
backgroundColor: Theme.of(context).hintColor,
|
||||
labelTextBuilder: _labelBuilder,
|
||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||
child: listWidget,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ImmichAssetGridView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!widget.selectionActive) {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> onWillPop() async {
|
||||
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||
_deselectAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollToTop() {
|
||||
// for some reason, this is necessary as well in order
|
||||
// to correctly reposition the drag thumb scroll bar
|
||||
_itemScrollController.jumpTo(
|
||||
index: 0,
|
||||
);
|
||||
_itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: onWillPop,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichAssetGridView extends StatefulWidget {
|
||||
final RenderList renderList;
|
||||
final int assetsPerRow;
|
||||
final double margin;
|
||||
final bool showStorageIndicator;
|
||||
final ImmichAssetGridSelectionListener? listener;
|
||||
final bool selectionActive;
|
||||
final List<Asset> allAssets;
|
||||
|
||||
const ImmichAssetGridView({
|
||||
super.key,
|
||||
required this.renderList,
|
||||
required this.allAssets,
|
||||
required this.assetsPerRow,
|
||||
required this.showStorageIndicator,
|
||||
this.listener,
|
||||
this.margin = 5.0,
|
||||
this.selectionActive = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return ImmichAssetGridViewState();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue