|
@@ -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';
|
|
|
-
|
|
|
-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();
|
|
|
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
|
|
|
|
- Set<Asset> _getSelectedAssets() {
|
|
|
- return _selectedAssets
|
|
|
- .map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
|
|
- .whereNotNull()
|
|
|
- .toSet();
|
|
|
- }
|
|
|
+class ImmichAssetGrid extends HookConsumerWidget {
|
|
|
+ final int? assetsPerRow;
|
|
|
+ final double margin;
|
|
|
+ final bool? showStorageIndicator;
|
|
|
+ final ImmichAssetGridSelectionListener? listener;
|
|
|
+ final bool selectionActive;
|
|
|
+ final List<Asset> assets;
|
|
|
+ final RenderList? renderList;
|
|
|
|
|
|
- void _callSelectionListener(bool selectionActive) {
|
|
|
- widget.listener?.call(selectionActive, _getSelectedAssets());
|
|
|
- }
|
|
|
+ const ImmichAssetGrid({
|
|
|
+ super.key,
|
|
|
+ required this.assets,
|
|
|
+ this.renderList,
|
|
|
+ this.assetsPerRow,
|
|
|
+ this.showStorageIndicator,
|
|
|
+ this.listener,
|
|
|
+ this.margin = 5.0,
|
|
|
+ this.selectionActive = false,
|
|
|
+ });
|
|
|
|
|
|
- void _selectAssets(List<Asset> assets) {
|
|
|
- setState(() {
|
|
|
- for (var e in assets) {
|
|
|
- _selectedAssets.add(e.id);
|
|
|
+ @override
|
|
|
+ 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;
|
|
|
}
|
|
|
- _callSelectionListener(true);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- void _deselectAssets(List<Asset> assets) {
|
|
|
- setState(() {
|
|
|
- for (var e in assets) {
|
|
|
- _selectedAssets.remove(e.id);
|
|
|
+ final animation = ModalRoute.of(context)?.animation;
|
|
|
+ if (animation != null) {
|
|
|
+ // When the animation is complete, re-enable hero animations
|
|
|
+ enableHeroAnimations.value = animation.isCompleted;
|
|
|
}
|
|
|
- _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),
|
|
|
- );
|
|
|
+ Future<bool> onWillPop() async {
|
|
|
+ enableHeroAnimations.value = false;
|
|
|
+ return true;
|
|
|
}
|
|
|
- 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,
|
|
|
+ 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,
|
|
|
+ ),
|
|
|
),
|
|
|
- ),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- 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(),
|
|
|
- ],
|
|
|
+ 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(),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-class ImmichAssetGrid 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 ImmichAssetGrid({
|
|
|
- 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 ImmichAssetGridState();
|
|
|
- }
|
|
|
-}
|