mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(mobile): Improve timeline performance on mobile - experimental (#710)
This commit is contained in:
		
							parent
							
								
									8ede738396
								
							
						
					
					
						commit
						28bf497a0b
					
				@ -165,5 +165,10 @@
 | 
				
			|||||||
  "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
 | 
					  "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
 | 
				
			||||||
  "version_announcement_overlay_text_2": "please take your time to visit the ",
 | 
					  "version_announcement_overlay_text_2": "please take your time to visit the ",
 | 
				
			||||||
  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
					  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
				
			||||||
  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
 | 
					  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
 | 
				
			||||||
 | 
					  "experimental_settings_title": "Experimental",
 | 
				
			||||||
 | 
					  "experimental_settings_subtitle": "Use at your own risk!",
 | 
				
			||||||
 | 
					  "experimental_settings_new_asset_list_title": "Enable experimental photo grid",
 | 
				
			||||||
 | 
					  "experimental_settings_new_asset_list_subtitle": "Work in progress",
 | 
				
			||||||
 | 
					  "settings_require_restart": "Please restart Immich to apply this setting"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum RenderAssetGridElementType {
 | 
				
			||||||
 | 
					  assetRow,
 | 
				
			||||||
 | 
					  dayTitle,
 | 
				
			||||||
 | 
					  monthTitle;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RenderAssetGridRow {
 | 
				
			||||||
 | 
					  final List<AssetResponseDto> assets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RenderAssetGridRow(this.assets);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RenderAssetGridElement {
 | 
				
			||||||
 | 
					  final RenderAssetGridElementType type;
 | 
				
			||||||
 | 
					  final RenderAssetGridRow? assetRow;
 | 
				
			||||||
 | 
					  final String? title;
 | 
				
			||||||
 | 
					  final int? month;
 | 
				
			||||||
 | 
					  final int? year;
 | 
				
			||||||
 | 
					  final List<AssetResponseDto>? relatedAssetList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RenderAssetGridElement(
 | 
				
			||||||
 | 
					    this.type, {
 | 
				
			||||||
 | 
					    this.assetRow,
 | 
				
			||||||
 | 
					    this.title,
 | 
				
			||||||
 | 
					    this.month,
 | 
				
			||||||
 | 
					    this.year,
 | 
				
			||||||
 | 
					    this.relatedAssetList,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final renderListProvider = StateProvider((ref) {
 | 
				
			||||||
 | 
					  var assetGroups = ref.watch(assetGroupByDateTimeProvider);
 | 
				
			||||||
 | 
					  var settings = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<RenderAssetGridElement> elements = [];
 | 
				
			||||||
 | 
					  DateTime? lastDate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  assetGroups.forEach((groupName, assets) {
 | 
				
			||||||
 | 
					    final date = DateTime.parse(groupName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (lastDate == null || lastDate!.month != date.month) {
 | 
				
			||||||
 | 
					      elements.add(
 | 
				
			||||||
 | 
					        RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
 | 
				
			||||||
 | 
					            title: groupName, month: date.month, year: date.year),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add group title
 | 
				
			||||||
 | 
					    elements.add(
 | 
				
			||||||
 | 
					      RenderAssetGridElement(
 | 
				
			||||||
 | 
					        RenderAssetGridElementType.dayTitle,
 | 
				
			||||||
 | 
					        title: groupName,
 | 
				
			||||||
 | 
					        month: date.month,
 | 
				
			||||||
 | 
					        year: date.year,
 | 
				
			||||||
 | 
					        relatedAssetList: assets,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add rows
 | 
				
			||||||
 | 
					    int cursor = 0;
 | 
				
			||||||
 | 
					    while (cursor < assets.length) {
 | 
				
			||||||
 | 
					      int rowElements = min(assets.length - cursor, assetsPerRow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final rowElement = RenderAssetGridElement(
 | 
				
			||||||
 | 
					        RenderAssetGridElementType.assetRow,
 | 
				
			||||||
 | 
					        month: date.month,
 | 
				
			||||||
 | 
					        year: date.year,
 | 
				
			||||||
 | 
					        assetRow: RenderAssetGridRow(
 | 
				
			||||||
 | 
					          assets.sublist(cursor, cursor + rowElements),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      elements.add(rowElement);
 | 
				
			||||||
 | 
					      cursor += rowElements;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lastDate = date;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return elements;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										107
									
								
								mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DailyTitleText extends ConsumerWidget {
 | 
				
			||||||
 | 
					  const DailyTitleText({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.isoDate,
 | 
				
			||||||
 | 
					    required this.assetGroup,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String isoDate;
 | 
				
			||||||
 | 
					  final List<AssetResponseDto> assetGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    var currentYear = DateTime.now().year;
 | 
				
			||||||
 | 
					    var groupYear = DateTime.parse(isoDate).year;
 | 
				
			||||||
 | 
					    var formatDateTemplate = currentYear == groupYear
 | 
				
			||||||
 | 
					        ? "daily_title_text_date".tr()
 | 
				
			||||||
 | 
					        : "daily_title_text_date_year".tr();
 | 
				
			||||||
 | 
					    var dateText =
 | 
				
			||||||
 | 
					        DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
 | 
				
			||||||
 | 
					    var isMultiSelectEnable =
 | 
				
			||||||
 | 
					        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
				
			||||||
 | 
					    var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
 | 
				
			||||||
 | 
					    var selectedItems = ref.watch(homePageStateProvider).selectedItems;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void _handleTitleIconClick() {
 | 
				
			||||||
 | 
					      if (isMultiSelectEnable &&
 | 
				
			||||||
 | 
					          selectedDateGroup.contains(dateText) &&
 | 
				
			||||||
 | 
					          selectedDateGroup.length == 1 &&
 | 
				
			||||||
 | 
					          selectedItems.length <= assetGroup.length) {
 | 
				
			||||||
 | 
					        // Multi select is active - click again on the icon while it is the only active group -> disable multi select
 | 
				
			||||||
 | 
					        ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
				
			||||||
 | 
					      } else if (isMultiSelectEnable &&
 | 
				
			||||||
 | 
					          selectedDateGroup.contains(dateText) &&
 | 
				
			||||||
 | 
					          selectedItems.length != assetGroup.length) {
 | 
				
			||||||
 | 
					        // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeMultipleSelectedItem(assetGroup);
 | 
				
			||||||
 | 
					      } else if (isMultiSelectEnable &&
 | 
				
			||||||
 | 
					          selectedDateGroup.contains(dateText) &&
 | 
				
			||||||
 | 
					          selectedDateGroup.length > 1) {
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeMultipleSelectedItem(assetGroup);
 | 
				
			||||||
 | 
					      } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addMultipleSelectedItems(assetGroup);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .enableMultiSelect(assetGroup.toSet());
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Padding(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.only(
 | 
				
			||||||
 | 
					        top: 29.0,
 | 
				
			||||||
 | 
					        bottom: 29.0,
 | 
				
			||||||
 | 
					        left: 12.0,
 | 
				
			||||||
 | 
					        right: 12.0,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            dateText,
 | 
				
			||||||
 | 
					            style: const TextStyle(
 | 
				
			||||||
 | 
					              fontSize: 14,
 | 
				
			||||||
 | 
					              fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          const Spacer(),
 | 
				
			||||||
 | 
					          GestureDetector(
 | 
				
			||||||
 | 
					            onTap: _handleTitleIconClick,
 | 
				
			||||||
 | 
					            child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
 | 
				
			||||||
 | 
					                ? Icon(
 | 
				
			||||||
 | 
					                    Icons.check_circle_rounded,
 | 
				
			||||||
 | 
					                    color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                : const Icon(
 | 
				
			||||||
 | 
					                    Icons.check_circle_outline_rounded,
 | 
				
			||||||
 | 
					                    color: Colors.grey,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,536 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Build the Scroll Thumb and label using the current configuration
 | 
				
			||||||
 | 
					typedef ScrollThumbBuilder = Widget Function(
 | 
				
			||||||
 | 
					  Color backgroundColor,
 | 
				
			||||||
 | 
					  Animation<double> thumbAnimation,
 | 
				
			||||||
 | 
					  Animation<double> labelAnimation,
 | 
				
			||||||
 | 
					  double height, {
 | 
				
			||||||
 | 
					  Text? labelText,
 | 
				
			||||||
 | 
					  BoxConstraints? labelConstraints,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Build a Text widget using the current scroll offset
 | 
				
			||||||
 | 
					typedef LabelTextBuilder = Text Function(int item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
 | 
				
			||||||
 | 
					/// for quick navigation of the BoxScrollView.
 | 
				
			||||||
 | 
					class DraggableScrollbar extends StatefulWidget {
 | 
				
			||||||
 | 
					  /// The view that will be scrolled with the scroll thumb
 | 
				
			||||||
 | 
					  final ScrollablePositionedList child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ItemPositionsListener itemPositionsListener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A function that builds a thumb using the current configuration
 | 
				
			||||||
 | 
					  final ScrollThumbBuilder scrollThumbBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The height of the scroll thumb
 | 
				
			||||||
 | 
					  final double heightScrollThumb;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The background color of the label and thumb
 | 
				
			||||||
 | 
					  final Color backgroundColor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The amount of padding that should surround the thumb
 | 
				
			||||||
 | 
					  final EdgeInsetsGeometry? padding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Determines how quickly the scrollbar will animate in and out
 | 
				
			||||||
 | 
					  final Duration scrollbarAnimationDuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// How long should the thumb be visible before fading out
 | 
				
			||||||
 | 
					  final Duration scrollbarTimeToFade;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Build a Text widget from the current offset in the BoxScrollView
 | 
				
			||||||
 | 
					  final LabelTextBuilder? labelTextBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Determines box constraints for Container displaying label
 | 
				
			||||||
 | 
					  final BoxConstraints? labelConstraints;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The ScrollController for the BoxScrollView
 | 
				
			||||||
 | 
					  final ItemScrollController controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder]
 | 
				
			||||||
 | 
					  final bool alwaysVisibleScrollThumb;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Function(bool scrolling) scrollStateListener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  DraggableScrollbar.semicircle({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    Key? scrollThumbKey,
 | 
				
			||||||
 | 
					    this.alwaysVisibleScrollThumb = false,
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					    required this.controller,
 | 
				
			||||||
 | 
					    required this.itemPositionsListener,
 | 
				
			||||||
 | 
					    required this.scrollStateListener,
 | 
				
			||||||
 | 
					    this.heightScrollThumb = 48.0,
 | 
				
			||||||
 | 
					    this.backgroundColor = Colors.white,
 | 
				
			||||||
 | 
					    this.padding,
 | 
				
			||||||
 | 
					    this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
 | 
				
			||||||
 | 
					    this.scrollbarTimeToFade = const Duration(milliseconds: 600),
 | 
				
			||||||
 | 
					    this.labelTextBuilder,
 | 
				
			||||||
 | 
					    this.labelConstraints,
 | 
				
			||||||
 | 
					  })  : assert(child.scrollDirection == Axis.vertical),
 | 
				
			||||||
 | 
					        scrollThumbBuilder = _thumbSemicircleBuilder(
 | 
				
			||||||
 | 
					          heightScrollThumb * 0.6,
 | 
				
			||||||
 | 
					          scrollThumbKey,
 | 
				
			||||||
 | 
					          alwaysVisibleScrollThumb,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  DraggableScrollbarState createState() => DraggableScrollbarState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static buildScrollThumbAndLabel({
 | 
				
			||||||
 | 
					    required Widget scrollThumb,
 | 
				
			||||||
 | 
					    required Color backgroundColor,
 | 
				
			||||||
 | 
					    required Animation<double>? thumbAnimation,
 | 
				
			||||||
 | 
					    required Animation<double>? labelAnimation,
 | 
				
			||||||
 | 
					    required Text? labelText,
 | 
				
			||||||
 | 
					    required BoxConstraints? labelConstraints,
 | 
				
			||||||
 | 
					    required bool alwaysVisibleScrollThumb,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    var scrollThumbAndLabel = labelText == null
 | 
				
			||||||
 | 
					        ? scrollThumb
 | 
				
			||||||
 | 
					        : Row(
 | 
				
			||||||
 | 
					            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					            mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              ScrollLabel(
 | 
				
			||||||
 | 
					                animation: labelAnimation,
 | 
				
			||||||
 | 
					                backgroundColor: backgroundColor,
 | 
				
			||||||
 | 
					                constraints: labelConstraints,
 | 
				
			||||||
 | 
					                child: labelText,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              scrollThumb,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (alwaysVisibleScrollThumb) {
 | 
				
			||||||
 | 
					      return scrollThumbAndLabel;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return SlideFadeTransition(
 | 
				
			||||||
 | 
					      animation: thumbAnimation!,
 | 
				
			||||||
 | 
					      child: scrollThumbAndLabel,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static ScrollThumbBuilder _thumbSemicircleBuilder(
 | 
				
			||||||
 | 
					    double width,
 | 
				
			||||||
 | 
					    Key? scrollThumbKey,
 | 
				
			||||||
 | 
					    bool alwaysVisibleScrollThumb,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      Color backgroundColor,
 | 
				
			||||||
 | 
					      Animation<double> thumbAnimation,
 | 
				
			||||||
 | 
					      Animation<double> labelAnimation,
 | 
				
			||||||
 | 
					      double height, {
 | 
				
			||||||
 | 
					      Text? labelText,
 | 
				
			||||||
 | 
					      BoxConstraints? labelConstraints,
 | 
				
			||||||
 | 
					    }) {
 | 
				
			||||||
 | 
					      final scrollThumb = CustomPaint(
 | 
				
			||||||
 | 
					        key: scrollThumbKey,
 | 
				
			||||||
 | 
					        foregroundPainter: ArrowCustomPainter(Colors.white),
 | 
				
			||||||
 | 
					        child: Material(
 | 
				
			||||||
 | 
					          elevation: 4.0,
 | 
				
			||||||
 | 
					          color: backgroundColor,
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.only(
 | 
				
			||||||
 | 
					            topLeft: Radius.circular(height),
 | 
				
			||||||
 | 
					            bottomLeft: Radius.circular(height),
 | 
				
			||||||
 | 
					            topRight: const Radius.circular(4.0),
 | 
				
			||||||
 | 
					            bottomRight: const Radius.circular(4.0),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          child: Container(
 | 
				
			||||||
 | 
					            constraints: BoxConstraints.tight(Size(width, height)),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return buildScrollThumbAndLabel(
 | 
				
			||||||
 | 
					        scrollThumb: scrollThumb,
 | 
				
			||||||
 | 
					        backgroundColor: backgroundColor,
 | 
				
			||||||
 | 
					        thumbAnimation: thumbAnimation,
 | 
				
			||||||
 | 
					        labelAnimation: labelAnimation,
 | 
				
			||||||
 | 
					        labelText: labelText,
 | 
				
			||||||
 | 
					        labelConstraints: labelConstraints,
 | 
				
			||||||
 | 
					        alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScrollLabel extends StatelessWidget {
 | 
				
			||||||
 | 
					  final Animation<double>? animation;
 | 
				
			||||||
 | 
					  final Color backgroundColor;
 | 
				
			||||||
 | 
					  final Text child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final BoxConstraints? constraints;
 | 
				
			||||||
 | 
					  static const BoxConstraints _defaultConstraints =
 | 
				
			||||||
 | 
					      BoxConstraints.tightFor(width: 72.0, height: 28.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ScrollLabel({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					    required this.animation,
 | 
				
			||||||
 | 
					    required this.backgroundColor,
 | 
				
			||||||
 | 
					    this.constraints = _defaultConstraints,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return FadeTransition(
 | 
				
			||||||
 | 
					      opacity: animation!,
 | 
				
			||||||
 | 
					      child: Container(
 | 
				
			||||||
 | 
					        margin: const EdgeInsets.only(right: 12.0),
 | 
				
			||||||
 | 
					        child: Material(
 | 
				
			||||||
 | 
					          elevation: 4.0,
 | 
				
			||||||
 | 
					          color: backgroundColor,
 | 
				
			||||||
 | 
					          borderRadius: const BorderRadius.all(Radius.circular(16.0)),
 | 
				
			||||||
 | 
					          child: Container(
 | 
				
			||||||
 | 
					            constraints: constraints ?? _defaultConstraints,
 | 
				
			||||||
 | 
					            alignment: Alignment.center,
 | 
				
			||||||
 | 
					            child: child,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DraggableScrollbarState extends State<DraggableScrollbar>
 | 
				
			||||||
 | 
					    with TickerProviderStateMixin {
 | 
				
			||||||
 | 
					  late double _barOffset;
 | 
				
			||||||
 | 
					  late bool _isDragInProcess;
 | 
				
			||||||
 | 
					  late int _currentItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late AnimationController _thumbAnimationController;
 | 
				
			||||||
 | 
					  late Animation<double> _thumbAnimation;
 | 
				
			||||||
 | 
					  late AnimationController _labelAnimationController;
 | 
				
			||||||
 | 
					  late Animation<double> _labelAnimation;
 | 
				
			||||||
 | 
					  Timer? _fadeoutTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _barOffset = 0.0;
 | 
				
			||||||
 | 
					    _isDragInProcess = false;
 | 
				
			||||||
 | 
					    _currentItem = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _thumbAnimationController = AnimationController(
 | 
				
			||||||
 | 
					      vsync: this,
 | 
				
			||||||
 | 
					      duration: widget.scrollbarAnimationDuration,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _thumbAnimation = CurvedAnimation(
 | 
				
			||||||
 | 
					      parent: _thumbAnimationController,
 | 
				
			||||||
 | 
					      curve: Curves.fastOutSlowIn,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _labelAnimationController = AnimationController(
 | 
				
			||||||
 | 
					      vsync: this,
 | 
				
			||||||
 | 
					      duration: widget.scrollbarAnimationDuration,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _labelAnimation = CurvedAnimation(
 | 
				
			||||||
 | 
					      parent: _labelAnimationController,
 | 
				
			||||||
 | 
					      curve: Curves.fastOutSlowIn,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _thumbAnimationController.dispose();
 | 
				
			||||||
 | 
					    _labelAnimationController.dispose();
 | 
				
			||||||
 | 
					    _fadeoutTimer?.cancel();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get barMaxScrollExtent =>
 | 
				
			||||||
 | 
					      (context.size?.height ?? 0) - widget.heightScrollThumb;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get barMinScrollExtent => 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get maxItemCount => widget.child.itemCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    Text? labelText;
 | 
				
			||||||
 | 
					    if (widget.labelTextBuilder != null && _isDragInProcess) {
 | 
				
			||||||
 | 
					      int numberOfItems = widget.child.itemCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      labelText = widget.labelTextBuilder!(_currentItem);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return LayoutBuilder(
 | 
				
			||||||
 | 
					      builder: (BuildContext context, BoxConstraints constraints) {
 | 
				
			||||||
 | 
					        //print("LayoutBuilder constraints=$constraints");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NotificationListener<ScrollNotification>(
 | 
				
			||||||
 | 
					          onNotification: (ScrollNotification notification) {
 | 
				
			||||||
 | 
					            changePosition(notification);
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          child: Stack(
 | 
				
			||||||
 | 
					            children: <Widget>[
 | 
				
			||||||
 | 
					              RepaintBoundary(
 | 
				
			||||||
 | 
					                child: widget.child,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              RepaintBoundary(
 | 
				
			||||||
 | 
					                child: GestureDetector(
 | 
				
			||||||
 | 
					                  onVerticalDragStart: _onVerticalDragStart,
 | 
				
			||||||
 | 
					                  onVerticalDragUpdate: _onVerticalDragUpdate,
 | 
				
			||||||
 | 
					                  onVerticalDragEnd: _onVerticalDragEnd,
 | 
				
			||||||
 | 
					                  child: Container(
 | 
				
			||||||
 | 
					                    alignment: Alignment.topRight,
 | 
				
			||||||
 | 
					                    margin: EdgeInsets.only(top: _barOffset),
 | 
				
			||||||
 | 
					                    padding: widget.padding,
 | 
				
			||||||
 | 
					                    child: widget.scrollThumbBuilder(
 | 
				
			||||||
 | 
					                      widget.backgroundColor,
 | 
				
			||||||
 | 
					                      _thumbAnimation,
 | 
				
			||||||
 | 
					                      _labelAnimation,
 | 
				
			||||||
 | 
					                      widget.heightScrollThumb,
 | 
				
			||||||
 | 
					                      labelText: labelText,
 | 
				
			||||||
 | 
					                      labelConstraints: widget.labelConstraints,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // scroll bar has received notification that it's view was scrolled
 | 
				
			||||||
 | 
					  // so it should also changes his position
 | 
				
			||||||
 | 
					  // but only if it isn't dragged
 | 
				
			||||||
 | 
					  changePosition(ScrollNotification notification) {
 | 
				
			||||||
 | 
					    if (_isDragInProcess) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      int firstItemIndex =
 | 
				
			||||||
 | 
					          widget.itemPositionsListener.itemPositions.value.first.index;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (notification is ScrollUpdateNotification) {
 | 
				
			||||||
 | 
					        _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (_barOffset < barMinScrollExtent) {
 | 
				
			||||||
 | 
					          _barOffset = barMinScrollExtent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (_barOffset > barMaxScrollExtent) {
 | 
				
			||||||
 | 
					          _barOffset = barMaxScrollExtent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (notification is ScrollUpdateNotification ||
 | 
				
			||||||
 | 
					          notification is OverscrollNotification) {
 | 
				
			||||||
 | 
					        if (_thumbAnimationController.status != AnimationStatus.forward) {
 | 
				
			||||||
 | 
					          _thumbAnimationController.forward();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (itemPos < maxItemCount) {
 | 
				
			||||||
 | 
					          _currentItem = itemPos;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _fadeoutTimer?.cancel();
 | 
				
			||||||
 | 
					        _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
 | 
				
			||||||
 | 
					          _thumbAnimationController.reverse();
 | 
				
			||||||
 | 
					          _labelAnimationController.reverse();
 | 
				
			||||||
 | 
					          _fadeoutTimer = null;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onVerticalDragStart(DragStartDetails details) {
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _isDragInProcess = true;
 | 
				
			||||||
 | 
					      _labelAnimationController.forward();
 | 
				
			||||||
 | 
					      _fadeoutTimer?.cancel();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    widget.scrollStateListener(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get itemPos {
 | 
				
			||||||
 | 
					    int numberOfItems = widget.child.itemCount;
 | 
				
			||||||
 | 
					    return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _jumpToBarPos() {
 | 
				
			||||||
 | 
					    if (itemPos > maxItemCount - 1) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _currentItem = itemPos;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    widget.controller.jumpTo(
 | 
				
			||||||
 | 
					      index: itemPos,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Timer? dragHaltTimer;
 | 
				
			||||||
 | 
					  int lastTimerPos = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onVerticalDragUpdate(DragUpdateDetails details) {
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      if (_thumbAnimationController.status != AnimationStatus.forward) {
 | 
				
			||||||
 | 
					        _thumbAnimationController.forward();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (_isDragInProcess) {
 | 
				
			||||||
 | 
					        _barOffset += details.delta.dy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (_barOffset < barMinScrollExtent) {
 | 
				
			||||||
 | 
					          _barOffset = barMinScrollExtent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (_barOffset > barMaxScrollExtent) {
 | 
				
			||||||
 | 
					          _barOffset = barMaxScrollExtent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (itemPos != lastTimerPos) {
 | 
				
			||||||
 | 
					          lastTimerPos = itemPos;
 | 
				
			||||||
 | 
					          dragHaltTimer?.cancel();
 | 
				
			||||||
 | 
					          widget.scrollStateListener(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          dragHaltTimer = Timer(
 | 
				
			||||||
 | 
					            const Duration(milliseconds: 200),
 | 
				
			||||||
 | 
					                () {
 | 
				
			||||||
 | 
					              widget.scrollStateListener(false);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _jumpToBarPos();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onVerticalDragEnd(DragEndDetails details) {
 | 
				
			||||||
 | 
					    _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
 | 
				
			||||||
 | 
					      _thumbAnimationController.reverse();
 | 
				
			||||||
 | 
					      _labelAnimationController.reverse();
 | 
				
			||||||
 | 
					      _fadeoutTimer = null;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _jumpToBarPos();
 | 
				
			||||||
 | 
					      _isDragInProcess = false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    widget.scrollStateListener(false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Draws 2 triangles like arrow up and arrow down
 | 
				
			||||||
 | 
					class ArrowCustomPainter extends CustomPainter {
 | 
				
			||||||
 | 
					  Color color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ArrowCustomPainter(this.color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void paint(Canvas canvas, Size size) {
 | 
				
			||||||
 | 
					    final paint = Paint()..color = color;
 | 
				
			||||||
 | 
					    const width = 12.0;
 | 
				
			||||||
 | 
					    const height = 8.0;
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Path _trianglePath(Offset o, double width, double height, bool isUp) {
 | 
				
			||||||
 | 
					    return Path()
 | 
				
			||||||
 | 
					      ..moveTo(o.dx, o.dy)
 | 
				
			||||||
 | 
					      ..lineTo(o.dx + width, o.dy)
 | 
				
			||||||
 | 
					      ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height)
 | 
				
			||||||
 | 
					      ..close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					///This cut 2 lines in arrow shape
 | 
				
			||||||
 | 
					class ArrowClipper extends CustomClipper<Path> {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Path getClip(Size size) {
 | 
				
			||||||
 | 
					    Path path = Path();
 | 
				
			||||||
 | 
					    path.lineTo(0.0, size.height);
 | 
				
			||||||
 | 
					    path.lineTo(size.width, size.height);
 | 
				
			||||||
 | 
					    path.lineTo(size.width, 0.0);
 | 
				
			||||||
 | 
					    path.lineTo(0.0, 0.0);
 | 
				
			||||||
 | 
					    path.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    double arrowWidth = 8.0;
 | 
				
			||||||
 | 
					    double startPointX = (size.width - arrowWidth) / 2;
 | 
				
			||||||
 | 
					    double startPointY = size.height / 2 - arrowWidth / 2;
 | 
				
			||||||
 | 
					    path.moveTo(startPointX, startPointY);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX + arrowWidth, startPointY);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
 | 
				
			||||||
 | 
					    path.lineTo(
 | 
				
			||||||
 | 
					      startPointX + arrowWidth / 2,
 | 
				
			||||||
 | 
					      startPointY - arrowWidth / 2 + 1.0,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    path.lineTo(startPointX, startPointY + 1.0);
 | 
				
			||||||
 | 
					    path.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    startPointY = size.height / 2 + arrowWidth / 2;
 | 
				
			||||||
 | 
					    path.moveTo(startPointX + arrowWidth, startPointY);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX, startPointY);
 | 
				
			||||||
 | 
					    path.lineTo(startPointX, startPointY - 1.0);
 | 
				
			||||||
 | 
					    path.lineTo(
 | 
				
			||||||
 | 
					      startPointX + arrowWidth / 2,
 | 
				
			||||||
 | 
					      startPointY + arrowWidth / 2 - 1.0,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
 | 
				
			||||||
 | 
					    path.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return path;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SlideFadeTransition extends StatelessWidget {
 | 
				
			||||||
 | 
					  final Animation<double> animation;
 | 
				
			||||||
 | 
					  final Widget child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const SlideFadeTransition({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.animation,
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AnimatedBuilder(
 | 
				
			||||||
 | 
					      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,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										166
									
								
								mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/src/widgets/framework.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../thumbnail_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImmichAssetGrid extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final ItemScrollController _itemScrollController = ItemScrollController();
 | 
				
			||||||
 | 
					  final ItemPositionsListener _itemPositionsListener =
 | 
				
			||||||
 | 
					      ItemPositionsListener.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final List<RenderAssetGridElement> renderList;
 | 
				
			||||||
 | 
					  final int assetsPerRow;
 | 
				
			||||||
 | 
					  final double margin;
 | 
				
			||||||
 | 
					  final bool showStorageIndicator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImmichAssetGrid({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.renderList,
 | 
				
			||||||
 | 
					    required this.assetsPerRow,
 | 
				
			||||||
 | 
					    required this.showStorageIndicator,
 | 
				
			||||||
 | 
					    this.margin = 5.0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AssetResponseDto> get _assets {
 | 
				
			||||||
 | 
					    return renderList
 | 
				
			||||||
 | 
					        .map((e) {
 | 
				
			||||||
 | 
					          if (e.type == RenderAssetGridElementType.assetRow) {
 | 
				
			||||||
 | 
					            return e.assetRow!.assets;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return List<AssetResponseDto>.empty();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .flattened
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double _getItemSize(BuildContext context) {
 | 
				
			||||||
 | 
					    return MediaQuery.of(context).size.width / assetsPerRow -
 | 
				
			||||||
 | 
					        margin * (assetsPerRow - 1) / assetsPerRow;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildThumbnailOrPlaceholder(
 | 
				
			||||||
 | 
					      AssetResponseDto asset, bool placeholder) {
 | 
				
			||||||
 | 
					    if (placeholder) {
 | 
				
			||||||
 | 
					      return const DecoratedBox(
 | 
				
			||||||
 | 
					        decoration: BoxDecoration(color: Colors.grey),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return ThumbnailImage(
 | 
				
			||||||
 | 
					      asset: asset,
 | 
				
			||||||
 | 
					      assetList: _assets,
 | 
				
			||||||
 | 
					      showStorageIndicator: showStorageIndicator,
 | 
				
			||||||
 | 
					      useGrayBoxPlaceholder: true,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildAssetRow(
 | 
				
			||||||
 | 
					      BuildContext context, RenderAssetGridRow row, bool scrolling) {
 | 
				
			||||||
 | 
					    double size = _getItemSize(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Row(
 | 
				
			||||||
 | 
					      key: Key("asset-row-${row.assets.first.id}"),
 | 
				
			||||||
 | 
					      children: row.assets.map((AssetResponseDto asset) {
 | 
				
			||||||
 | 
					        bool last = asset == row.assets.last;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Container(
 | 
				
			||||||
 | 
					          key: Key("asset-${asset.id}"),
 | 
				
			||||||
 | 
					          width: size,
 | 
				
			||||||
 | 
					          height: size,
 | 
				
			||||||
 | 
					          margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
 | 
				
			||||||
 | 
					          child: _buildThumbnailOrPlaceholder(asset, scrolling),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }).toList(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildTitle(
 | 
				
			||||||
 | 
					      BuildContext context, String title, List<AssetResponseDto> assets) {
 | 
				
			||||||
 | 
					    return DailyTitleText(
 | 
				
			||||||
 | 
					      isoDate: title,
 | 
				
			||||||
 | 
					      assetGroup: assets,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildMonthTitle(BuildContext context, String title) {
 | 
				
			||||||
 | 
					    var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
 | 
				
			||||||
 | 
					        .format(DateTime.parse(title));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Padding(
 | 
				
			||||||
 | 
					      key: Key("month-$title"),
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.only(left: 12.0, top: 32),
 | 
				
			||||||
 | 
					      child: Text(
 | 
				
			||||||
 | 
					        monthTitleText,
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontSize: 26,
 | 
				
			||||||
 | 
					          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					          color: Theme.of(context).textTheme.headline1?.color,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
 | 
				
			||||||
 | 
					    final item = renderList[position];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item.type == RenderAssetGridElementType.dayTitle) {
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					    return Text(
 | 
				
			||||||
 | 
					      "${renderList[pos].month} / ${renderList[pos].year}",
 | 
				
			||||||
 | 
					      style: const TextStyle(
 | 
				
			||||||
 | 
					        color: Colors.white,
 | 
				
			||||||
 | 
					        fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final scrolling = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void dragScrolling(bool active) {
 | 
				
			||||||
 | 
					      scrolling.value = active;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget itemBuilder(BuildContext c, int position) {
 | 
				
			||||||
 | 
					      return _itemBuilder(c, position, scrolling.value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return DraggableScrollbar.semicircle(
 | 
				
			||||||
 | 
					        scrollStateListener: dragScrolling,
 | 
				
			||||||
 | 
					        itemPositionsListener: _itemPositionsListener,
 | 
				
			||||||
 | 
					        controller: _itemScrollController,
 | 
				
			||||||
 | 
					        backgroundColor: Theme.of(context).hintColor,
 | 
				
			||||||
 | 
					        labelTextBuilder: _labelBuilder,
 | 
				
			||||||
 | 
					        scrollbarAnimationDuration: const Duration(seconds: 1),
 | 
				
			||||||
 | 
					        scrollbarTimeToFade: const Duration(seconds: 4),
 | 
				
			||||||
 | 
					        child: ScrollablePositionedList.builder(
 | 
				
			||||||
 | 
					          itemBuilder: itemBuilder,
 | 
				
			||||||
 | 
					          itemPositionsListener: _itemPositionsListener,
 | 
				
			||||||
 | 
					          itemScrollController: _itemScrollController,
 | 
				
			||||||
 | 
					          itemCount: renderList.length,
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -33,35 +33,11 @@ class ImageGrid extends ConsumerWidget {
 | 
				
			|||||||
          var assetType = assetGroup[index].type;
 | 
					          var assetType = assetGroup[index].type;
 | 
				
			||||||
          return GestureDetector(
 | 
					          return GestureDetector(
 | 
				
			||||||
            onTap: () {},
 | 
					            onTap: () {},
 | 
				
			||||||
            child: Stack(
 | 
					            child: ThumbnailImage(
 | 
				
			||||||
              children: [
 | 
					 | 
				
			||||||
                ThumbnailImage(
 | 
					 | 
				
			||||||
              asset: assetGroup[index],
 | 
					              asset: assetGroup[index],
 | 
				
			||||||
              assetList: sortedAssetGroup,
 | 
					              assetList: sortedAssetGroup,
 | 
				
			||||||
              showStorageIndicator: showStorageIndicator,
 | 
					              showStorageIndicator: showStorageIndicator,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
                if (assetType != AssetTypeEnum.IMAGE)
 | 
					 | 
				
			||||||
                  Positioned(
 | 
					 | 
				
			||||||
                    top: 5,
 | 
					 | 
				
			||||||
                    right: 5,
 | 
					 | 
				
			||||||
                    child: Row(
 | 
					 | 
				
			||||||
                      children: [
 | 
					 | 
				
			||||||
                        Text(
 | 
					 | 
				
			||||||
                          assetGroup[index].duration.toString().substring(0, 7),
 | 
					 | 
				
			||||||
                          style: const TextStyle(
 | 
					 | 
				
			||||||
                            color: Colors.white,
 | 
					 | 
				
			||||||
                            fontSize: 10,
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                        const Icon(
 | 
					 | 
				
			||||||
                          Icons.play_circle_outline_rounded,
 | 
					 | 
				
			||||||
                          color: Colors.white,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ],
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        childCount: assetGroup.length,
 | 
					        childCount: assetGroup.length,
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,14 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
  final AssetResponseDto asset;
 | 
					  final AssetResponseDto asset;
 | 
				
			||||||
  final List<AssetResponseDto> assetList;
 | 
					  final List<AssetResponseDto> assetList;
 | 
				
			||||||
  final bool showStorageIndicator;
 | 
					  final bool showStorageIndicator;
 | 
				
			||||||
 | 
					  final bool useGrayBoxPlaceholder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ThumbnailImage({
 | 
					  const ThumbnailImage({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
    required this.asset,
 | 
					    required this.asset,
 | 
				
			||||||
    required this.assetList,
 | 
					    required this.assetList,
 | 
				
			||||||
    this.showStorageIndicator = true,
 | 
					    this.showStorageIndicator = true,
 | 
				
			||||||
 | 
					    this.useGrayBoxPlaceholder = false,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -102,13 +104,19 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
					                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
					                fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
                progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
					                progressIndicatorBuilder: (context, url, downloadProgress) {
 | 
				
			||||||
                    Transform.scale(
 | 
					                  if (useGrayBoxPlaceholder) {
 | 
				
			||||||
 | 
					                    return const DecoratedBox(
 | 
				
			||||||
 | 
					                      decoration: BoxDecoration(color: Colors.grey),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  return Transform.scale(
 | 
				
			||||||
                    scale: 0.2,
 | 
					                    scale: 0.2,
 | 
				
			||||||
                    child: CircularProgressIndicator(
 | 
					                    child: CircularProgressIndicator(
 | 
				
			||||||
                      value: downloadProgress.progress,
 | 
					                      value: downloadProgress.progress,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                errorWidget: (context, url, error) {
 | 
					                errorWidget: (context, url, error) {
 | 
				
			||||||
                  debugPrint("Error getting thumbnail $url = $error");
 | 
					                  debugPrint("Error getting thumbnail $url = $error");
 | 
				
			||||||
                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
					                  CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
 | 
				
			||||||
@ -139,7 +147,27 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                  color: Colors.white,
 | 
					                  color: Colors.white,
 | 
				
			||||||
                  size: 18,
 | 
					                  size: 18,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              )
 | 
					              ),
 | 
				
			||||||
 | 
					            if (asset.type != AssetTypeEnum.IMAGE)
 | 
				
			||||||
 | 
					              Positioned(
 | 
				
			||||||
 | 
					                top: 5,
 | 
				
			||||||
 | 
					                right: 5,
 | 
				
			||||||
 | 
					                child: Row(
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      asset.duration.toString().substring(0, 7),
 | 
				
			||||||
 | 
					                      style: const TextStyle(
 | 
				
			||||||
 | 
					                        color: Colors.white,
 | 
				
			||||||
 | 
					                        fontSize: 10,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const Icon(
 | 
				
			||||||
 | 
					                      Icons.play_circle_outline_rounded,
 | 
				
			||||||
 | 
					                      color: Colors.white,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,14 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
 | 
				
			||||||
@ -25,6 +27,8 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
					    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var renderList = ref.watch(renderListProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ScrollController scrollController = useScrollController();
 | 
					    ScrollController scrollController = useScrollController();
 | 
				
			||||||
    var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
 | 
					    var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
 | 
				
			||||||
    List<Widget> imageGridGroup = [];
 | 
					    List<Widget> imageGridGroup = [];
 | 
				
			||||||
@ -120,6 +124,31 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
              );
 | 
					              );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _buildAssetGrid() {
 | 
				
			||||||
 | 
					        if (appSettingService
 | 
				
			||||||
 | 
					            .getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
 | 
				
			||||||
 | 
					          return ImmichAssetGrid(
 | 
				
			||||||
 | 
					              renderList: renderList,
 | 
				
			||||||
 | 
					              assetsPerRow:
 | 
				
			||||||
 | 
					              appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
 | 
				
			||||||
 | 
					              showStorageIndicator: appSettingService
 | 
				
			||||||
 | 
					                  .getSetting(AppSettingsEnum.storageIndicator),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return DraggableScrollbar.semicircle(
 | 
				
			||||||
 | 
					            backgroundColor: Theme.of(context).hintColor,
 | 
				
			||||||
 | 
					            controller: scrollController,
 | 
				
			||||||
 | 
					            heightScrollThumb: 48.0,
 | 
				
			||||||
 | 
					            child: CustomScrollView(
 | 
				
			||||||
 | 
					              controller: scrollController,
 | 
				
			||||||
 | 
					              slivers: [
 | 
				
			||||||
 | 
					                ...imageGridGroup,
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return SafeArea(
 | 
					      return SafeArea(
 | 
				
			||||||
        bottom: !isMultiSelectEnable,
 | 
					        bottom: !isMultiSelectEnable,
 | 
				
			||||||
        top: !isMultiSelectEnable,
 | 
					        top: !isMultiSelectEnable,
 | 
				
			||||||
@ -132,17 +161,7 @@ class HomePage extends HookConsumerWidget {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
 | 
					              padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
 | 
				
			||||||
              child: DraggableScrollbar.semicircle(
 | 
					              child: _buildAssetGrid(),
 | 
				
			||||||
                backgroundColor: Theme.of(context).hintColor,
 | 
					 | 
				
			||||||
                controller: scrollController,
 | 
					 | 
				
			||||||
                heightScrollThumb: 48.0,
 | 
					 | 
				
			||||||
                child: CustomScrollView(
 | 
					 | 
				
			||||||
                  controller: scrollController,
 | 
					 | 
				
			||||||
                  slivers: [
 | 
					 | 
				
			||||||
                    ...imageGridGroup,
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            if (isMultiSelectEnable) ...[
 | 
					            if (isMultiSelectEnable) ...[
 | 
				
			||||||
              _buildSelectedItemCountIndicator(),
 | 
					              _buildSelectedItemCountIndicator(),
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,8 @@ enum AppSettingsEnum<T> {
 | 
				
			|||||||
  storageIndicator<bool>("storageIndicator", true),
 | 
					  storageIndicator<bool>("storageIndicator", true),
 | 
				
			||||||
  thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
 | 
					  thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
 | 
				
			||||||
  imageCacheSize<int>("imageCacheSize", 350),
 | 
					  imageCacheSize<int>("imageCacheSize", 350),
 | 
				
			||||||
  albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200);
 | 
					  albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200),
 | 
				
			||||||
 | 
					  useExperimentalAssetGrid<bool>("useExperimentalAssetGrid", false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AppSettingsEnum(this.hiveKey, this.defaultValue);
 | 
					  const AppSettingsEnum(this.hiveKey, this.defaultValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.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/ui/immich_toast.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExperimentalSettings extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const ExperimentalSettings({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final useExperimentalAssetGrid = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(
 | 
				
			||||||
 | 
					      () {
 | 
				
			||||||
 | 
					        useExperimentalAssetGrid.value = appSettingService
 | 
				
			||||||
 | 
					            .getSetting(AppSettingsEnum.useExperimentalAssetGrid);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void changeUseExperimentalAssetGrid(bool status) {
 | 
				
			||||||
 | 
					      useExperimentalAssetGrid.value = status;
 | 
				
			||||||
 | 
					      appSettingService.setSetting(
 | 
				
			||||||
 | 
					        AppSettingsEnum.useExperimentalAssetGrid,
 | 
				
			||||||
 | 
					        status,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ImmichToast.show(
 | 
				
			||||||
 | 
					        context: context,
 | 
				
			||||||
 | 
					        msg: "settings_require_restart".tr(),
 | 
				
			||||||
 | 
					        gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ExpansionTile(
 | 
				
			||||||
 | 
					      textColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					      title: const Text(
 | 
				
			||||||
 | 
					        'experimental_settings_title',
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ).tr(),
 | 
				
			||||||
 | 
					      subtitle: const Text(
 | 
				
			||||||
 | 
					        'experimental_settings_subtitle',
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontSize: 13,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ).tr(),
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        SwitchListTile.adaptive(
 | 
				
			||||||
 | 
					          activeColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					          title: const Text(
 | 
				
			||||||
 | 
					            "experimental_settings_new_asset_list_title",
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              fontSize: 12,
 | 
				
			||||||
 | 
					              fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ).tr(),
 | 
				
			||||||
 | 
					          subtitle: const Text(
 | 
				
			||||||
 | 
					            "experimental_settings_new_asset_list_subtitle",
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              fontSize: 12,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ).tr(),
 | 
				
			||||||
 | 
					          value: useExperimentalAssetGrid.value,
 | 
				
			||||||
 | 
					          onChanged: changeUseExperimentalAssetGrid,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/settings/ui/experimental_settings/experimental_settings.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
 | 
				
			||||||
@ -42,6 +43,7 @@ class SettingsPage extends HookConsumerWidget {
 | 
				
			|||||||
              const ThemeSetting(),
 | 
					              const ThemeSetting(),
 | 
				
			||||||
              const AssetListSettings(),
 | 
					              const AssetListSettings(),
 | 
				
			||||||
              if (Platform.isAndroid) const NotificationSetting(),
 | 
					              if (Platform.isAndroid) const NotificationSetting(),
 | 
				
			||||||
 | 
					              const ExperimentalSettings(),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ).toList(),
 | 
					          ).toList(),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ class ImmichToast {
 | 
				
			|||||||
    ToastType toastType = ToastType.info,
 | 
					    ToastType toastType = ToastType.info,
 | 
				
			||||||
    ToastGravity gravity = ToastGravity.TOP,
 | 
					    ToastGravity gravity = ToastGravity.TOP,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
 | 
					    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 | 
				
			||||||
    final fToast = FToast();
 | 
					    final fToast = FToast();
 | 
				
			||||||
    fToast.init(context);
 | 
					    fToast.init(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -49,7 +50,7 @@ class ImmichToast {
 | 
				
			|||||||
        padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
 | 
					        padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
 | 
				
			||||||
        decoration: BoxDecoration(
 | 
					        decoration: BoxDecoration(
 | 
				
			||||||
          borderRadius: BorderRadius.circular(5.0),
 | 
					          borderRadius: BorderRadius.circular(5.0),
 | 
				
			||||||
          color: Colors.grey[50],
 | 
					          color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
 | 
				
			||||||
          border: Border.all(
 | 
					          border: Border.all(
 | 
				
			||||||
            color: Colors.black12,
 | 
					            color: Colors.black12,
 | 
				
			||||||
            width: 1,
 | 
					            width: 1,
 | 
				
			||||||
 | 
				
			|||||||
@ -868,6 +868,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.27.3"
 | 
					    version: "0.27.3"
 | 
				
			||||||
 | 
					  scrollable_positioned_list:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: scrollable_positioned_list
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.3.4"
 | 
				
			||||||
  share_plus:
 | 
					  share_plus:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ dependencies:
 | 
				
			|||||||
  easy_localization: ^3.0.1
 | 
					  easy_localization: ^3.0.1
 | 
				
			||||||
  share_plus: ^4.0.10
 | 
					  share_plus: ^4.0.10
 | 
				
			||||||
  flutter_displaymode: ^0.4.0
 | 
					  flutter_displaymode: ^0.4.0
 | 
				
			||||||
 | 
					  scrollable_positioned_list: ^0.3.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  path: ^1.8.1
 | 
					  path: ^1.8.1
 | 
				
			||||||
  path_provider: ^2.0.11
 | 
					  path_provider: ^2.0.11
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user