Merge branch 'main' of github.com:immich-app/immich into dev/mobile-cosmetic-improvement

This commit is contained in:
Alex Tran 2022-10-14 15:58:26 -05:00
commit e38166837d
25 changed files with 933 additions and 1131 deletions

View File

@ -171,8 +171,5 @@
"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_title": "Experimental",
"experimental_settings_subtitle": "Use at your own risk!", "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"
} }

View File

@ -1,47 +0,0 @@
import 'package:collection/collection.dart';
import 'package:openapi/api.dart';
class HomePageState {
final bool isMultiSelectEnable;
final Set<AssetResponseDto> selectedItems;
final Set<String> selectedDateGroup;
HomePageState({
required this.isMultiSelectEnable,
required this.selectedItems,
required this.selectedDateGroup,
});
HomePageState copyWith({
bool? isMultiSelectEnable,
Set<AssetResponseDto>? selectedItems,
Set<String>? selectedDateGroup,
}) {
return HomePageState(
isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable,
selectedItems: selectedItems ?? this.selectedItems,
selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup,
);
}
@override
String toString() =>
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final setEquals = const DeepCollectionEquality().equals;
return other is HomePageState &&
other.isMultiSelectEnable == isMultiSelectEnable &&
setEquals(other.selectedItems, selectedItems) &&
setEquals(other.selectedDateGroup, selectedDateGroup);
}
@override
int get hashCode =>
isMultiSelectEnable.hashCode ^
selectedItems.hashCode ^
selectedDateGroup.hashCode;
}

View File

@ -1,95 +1,14 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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 DateTime date;
final List<AssetResponseDto>? relatedAssetList;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
});
}
final renderListProvider = StateProvider((ref) { final renderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(assetGroupByDateTimeProvider); var assetGroups = ref.watch(assetGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow); final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
List<RenderAssetGridElement> elements = []; return assetGroupsToRenderList(assetGroups, assetsPerRow);
DateTime? lastDate;
assetGroups.forEach((groupName, assets) {
try {
final date = DateTime.parse(groupName);
if (lastDate == null || lastDate!.month != date.month) {
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
title: groupName,
date: date,
),
);
}
// Add group title
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.dayTitle,
title: groupName,
date: date,
relatedAssetList: assets,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
} catch (e) {
debugPrint(e.toString());
}
});
return elements;
}); });

View File

@ -1,90 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart';
class HomePageStateNotifier extends StateNotifier<HomePageState> {
final ShareService _shareService;
HomePageStateNotifier(this._shareService)
: super(
HomePageState(
isMultiSelectEnable: false,
selectedItems: {},
selectedDateGroup: {},
),
);
void addSelectedDateGroup(String dateGroupTitle) {
state = state.copyWith(
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
);
}
void removeSelectedDateGroup(String dateGroupTitle) {
var currentDateGroup = state.selectedDateGroup;
currentDateGroup.removeWhere((e) => e == dateGroupTitle);
state = state.copyWith(selectedDateGroup: currentDateGroup);
}
void enableMultiSelect(Set<AssetResponseDto> selectedItems) {
state =
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
}
void disableMultiSelect() {
state = state.copyWith(
isMultiSelectEnable: false,
selectedItems: {},
selectedDateGroup: {},
);
}
void addSingleSelectedItem(AssetResponseDto asset) {
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
}
void addMultipleSelectedItems(List<AssetResponseDto> assets) {
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
}
void removeSingleSelectedItem(AssetResponseDto asset) {
Set<AssetResponseDto> currentList = state.selectedItems;
currentList.removeWhere((e) => e.id == asset.id);
state = state.copyWith(selectedItems: currentList);
}
void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
Set<AssetResponseDto> currentList = state.selectedItems;
for (AssetResponseDto asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedItems: currentList);
}
void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService
.shareAssets(assets)
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
}
final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
);

View File

@ -0,0 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final multiselectProvider = StateProvider((ref) {
return false;
});

View File

@ -0,0 +1,103 @@
import 'dart:math';
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 DateTime date;
final List<AssetResponseDto>? relatedAssetList;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
});
}
List<RenderAssetGridElement> assetsToRenderList(
List<AssetResponseDto> assets, int assetsPerRow) {
List<RenderAssetGridElement> elements = [];
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final date = DateTime.parse(assets[cursor].createdAt);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
return elements;
}
List<RenderAssetGridElement> assetGroupsToRenderList(
Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
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, date: date),
);
}
// Add group title
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.dayTitle,
title: groupName,
date: date,
relatedAssetList: assets,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
});
return elements;
}

View File

@ -0,0 +1,72 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class DailyTitleText extends ConsumerWidget {
const DailyTitleText({
Key? key,
required this.isoDate,
required this.multiselectEnabled,
required this.onSelect,
required this.onDeselect,
required this.selected,
}) : super(key: key);
final String isoDate;
final bool multiselectEnabled;
final Function onSelect;
final Function onDeselect;
final bool selected;
@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));
void handleTitleIconClick() {
if (selected) {
onDeselect();
} else {
onSelect();
}
}
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: multiselectEnabled && selected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.grey,
),
)
],
),
);
}
}

View File

@ -1,40 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
class DisableMultiSelectButton extends ConsumerWidget { class DisableMultiSelectButton extends ConsumerWidget {
const DisableMultiSelectButton({ const DisableMultiSelectButton({
Key? key, Key? key,
required this.onPressed, required this.onPressed,
required this.selectedItemCount, required this.selectedItemCount,
}) : super(key: key); }) : super(key: key);
final Function onPressed; final Function onPressed;
final int selectedItemCount; final int selectedItemCount;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Padding(
top: 10, padding: const EdgeInsets.only(left: 16.0, top: 15),
left: 0, child: Padding(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0),
padding: const EdgeInsets.only(left: 16.0, top: 46), child: ElevatedButton.icon(
child: Padding( onPressed: () {
padding: const EdgeInsets.symmetric(horizontal: 4.0), onPressed();
child: ElevatedButton.icon( },
onPressed: () { icon: const Icon(Icons.close_rounded),
onPressed(); label: Text(
}, '$selectedItemCount',
icon: const Icon(Icons.close_rounded), style: const TextStyle(
label: Text( fontWeight: FontWeight.w600,
'$selectedItemCount', fontSize: 18,
style: const TextStyle( ),
fontWeight: FontWeight.w600, ),
fontSize: 18, ),
), ),
), );
), }
), }
),
);
}
}

View File

@ -0,0 +1,274 @@
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/home/ui/asset_grid/thumbnail_image.dart';
import 'package:openapi/api.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart';
import 'daily_title_text.dart';
import 'disable_multi_select_button.dart';
import 'draggable_scrollbar_custom.dart';
typedef ImmichAssetGridSelectionListener = void Function(
bool,
Set<AssetResponseDto>,
);
class ImmichAssetGridState extends State<ImmichAssetGrid> {
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create();
bool _scrolling = false;
final Set<String> _selectedAssets = HashSet();
List<AssetResponseDto> get _assets {
return widget.renderList
.map((e) {
if (e.type == RenderAssetGridElementType.assetRow) {
return e.assetRow!.assets;
} else {
return List<AssetResponseDto>.empty();
}
})
.flattened
.toList();
}
Set<AssetResponseDto> _getSelectedAssets() {
return _selectedAssets
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
.whereNotNull()
.toSet();
}
void _callSelectionListener(bool selectionActive) {
widget.listener?.call(selectionActive, _getSelectedAssets());
}
void _selectAssets(List<AssetResponseDto> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.add(e.id);
}
_callSelectionListener(true);
});
}
void _deselectAssets(List<AssetResponseDto> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.remove(e.id);
}
_callSelectionListener(_selectedAssets.isNotEmpty);
});
}
void _deselectAll() {
setState(() {
_selectedAssets.clear();
});
_callSelectionListener(false);
}
bool _allAssetsSelected(List<AssetResponseDto> assets) {
return widget.selectionActive &&
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
}
double _getItemSize(BuildContext context) {
return MediaQuery.of(context).size.width / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
}
Widget _buildThumbnailOrPlaceholder(
AssetResponseDto asset,
bool placeholder,
) {
if (placeholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
return ThumbnailImage(
asset: asset,
assetList: _assets,
multiselectEnabled: widget.selectionActive,
isSelected: _selectedAssets.contains(asset.id),
onSelect: () => _selectAssets([asset]),
onDeselect: () => _deselectAssets([asset]),
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
);
}
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: widget.margin,
right: last ? 0.0 : widget.margin,
),
child: _buildThumbnailOrPlaceholder(asset, scrolling),
);
}).toList(),
);
}
Widget _buildTitle(
BuildContext context,
String title,
List<AssetResponseDto> assets,
) {
return DailyTitleText(
isoDate: title,
multiselectEnabled: widget.selectionActive,
onSelect: () => _selectAssets(assets),
onDeselect: () => _deselectAssets(assets),
selected: _allAssetsSelected(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) {
final item = widget.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) {
final date = widget.renderList[pos].date;
return Text(
DateFormat.yMMMd().format(date),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildMultiSelectIndicator() {
return DisableMultiSelectButton(
onPressed: () => _deselectAll(),
selectedItemCount: _selectedAssets.length,
);
}
Widget _buildAssetGrid() {
final useDragScrolling = _assets.length >= 20;
void dragScrolling(bool active) {
setState(() {
_scrolling = active;
});
}
final listWidget = ScrollablePositionedList.builder(
itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemCount: widget.renderList.length,
);
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();
});
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
_buildAssetGrid(),
if (widget.selectionActive) _buildMultiSelectIndicator(),
],
);
}
}
class ImmichAssetGrid extends StatefulWidget {
final List<RenderAssetGridElement> renderList;
final int assetsPerRow;
final double margin;
final bool showStorageIndicator;
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
const ImmichAssetGrid({
super.key,
required this.renderList,
required this.assetsPerRow,
required this.showStorageIndicator,
this.listener,
this.margin = 5.0,
this.selectionActive = false,
});
@override
State<StatefulWidget> createState() {
return ImmichAssetGridState();
}
}

View File

@ -1,176 +1,172 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart';
import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget {
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;
final bool useGrayBoxPlaceholder; final bool isSelected;
final bool multiselectEnabled;
const ThumbnailImage({ final Function? onSelect;
Key? key, final Function? onDeselect;
required this.asset,
required this.assetList, const ThumbnailImage({
this.showStorageIndicator = true, Key? key,
this.useGrayBoxPlaceholder = false, required this.asset,
}) : super(key: key); required this.assetList,
this.showStorageIndicator = true,
@override this.useGrayBoxPlaceholder = false,
Widget build(BuildContext context, WidgetRef ref) { this.isSelected = false,
var box = Hive.box(userInfoBox); this.multiselectEnabled = false,
var thumbnailRequestUrl = getThumbnailUrl(asset); this.onDeselect,
var selectedAsset = ref.watch(homePageStateProvider).selectedItems; this.onSelect,
var isMultiSelectEnable = }) : super(key: key);
ref.watch(homePageStateProvider).isMultiSelectEnable;
var deviceId = ref.watch(authenticationProvider).deviceId; @override
Widget build(BuildContext context, WidgetRef ref) {
Widget buildSelectionIcon(AssetResponseDto asset) { var box = Hive.box(userInfoBox);
if (selectedAsset.contains(asset)) { var thumbnailRequestUrl = getThumbnailUrl(asset);
return Icon( var deviceId = ref.watch(authenticationProvider).deviceId;
Icons.check_circle,
color: Theme.of(context).primaryColor,
); Widget buildSelectionIcon(AssetResponseDto asset) {
} else { if (isSelected) {
return const Icon( return Icon(
Icons.circle_outlined, Icons.check_circle,
color: Colors.white, color: Theme.of(context).primaryColor,
); );
} } else {
} return const Icon(
Icons.circle_outlined,
return GestureDetector( color: Colors.white,
onTap: () { );
if (isMultiSelectEnable && }
selectedAsset.contains(asset) && }
selectedAsset.length == 1) {
ref.watch(homePageStateProvider.notifier).disableMultiSelect(); return GestureDetector(
} else if (isMultiSelectEnable && onTap: () {
selectedAsset.contains(asset) && if (multiselectEnabled) {
selectedAsset.length > 1) { if (isSelected) {
ref onDeselect?.call();
.watch(homePageStateProvider.notifier) } else {
.removeSingleSelectedItem(asset); onSelect?.call();
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) { }
ref } else {
.watch(homePageStateProvider.notifier) AutoRouter.of(context).push(
.addSingleSelectedItem(asset); GalleryViewerRoute(
} else { assetList: assetList,
AutoRouter.of(context).push( asset: asset,
GalleryViewerRoute( ),
assetList: assetList, );
asset: asset, }
), },
); onLongPress: () {
} onSelect?.call();
}, HapticFeedback.heavyImpact();
onLongPress: () { },
// Enable multi select function child: Hero(
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); tag: asset.id,
HapticFeedback.heavyImpact(); child: Stack(
}, children: [
child: Hero( Container(
tag: asset.id, decoration: BoxDecoration(
child: Stack( border: multiselectEnabled && isSelected
children: [ ? Border.all(
Container( color: Theme.of(context).primaryColorLight,
decoration: BoxDecoration( width: 10,
border: isMultiSelectEnable && selectedAsset.contains(asset) )
? Border.all( : const Border(),
color: Theme.of(context).primaryColorLight, ),
width: 10, child: CachedNetworkImage(
) cacheKey: 'thumbnail-image-${asset.id}',
: const Border(), width: 300,
), height: 300,
child: CachedNetworkImage( memCacheHeight: 200,
cacheKey: 'thumbnail-image-${asset.id}', maxWidthDiskCache: 200,
width: 300, maxHeightDiskCache: 200,
height: 300, fit: BoxFit.cover,
memCacheHeight: 200, imageUrl: thumbnailRequestUrl,
maxWidthDiskCache: 200, httpHeaders: {
maxHeightDiskCache: 200, "Authorization": "Bearer ${box.get(accessTokenKey)}"
fit: BoxFit.cover, },
imageUrl: thumbnailRequestUrl, fadeInDuration: const Duration(milliseconds: 250),
httpHeaders: { progressIndicatorBuilder: (context, url, downloadProgress) {
"Authorization": "Bearer ${box.get(accessTokenKey)}" if (useGrayBoxPlaceholder) {
}, return const DecoratedBox(
fadeInDuration: const Duration(milliseconds: 250), decoration: BoxDecoration(color: Colors.grey),
progressIndicatorBuilder: (context, url, downloadProgress) { );
if (useGrayBoxPlaceholder) { }
return const DecoratedBox( return Transform.scale(
decoration: BoxDecoration(color: Colors.grey), scale: 0.2,
); child: CircularProgressIndicator(
} value: downloadProgress.progress,
return Transform.scale( ),
scale: 0.2, );
child: CircularProgressIndicator( },
value: downloadProgress.progress, errorWidget: (context, url, error) {
), debugPrint("Error getting thumbnail $url = $error");
); CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
},
errorWidget: (context, url, error) { return Icon(
debugPrint("Error getting thumbnail $url = $error"); Icons.image_not_supported_outlined,
CachedNetworkImage.evictFromCache(thumbnailRequestUrl); color: Theme.of(context).primaryColor,
);
return Icon( },
Icons.image_not_supported_outlined, ),
color: Theme.of(context).primaryColor, ),
); if (multiselectEnabled)
}, Padding(
), padding: const EdgeInsets.all(3.0),
), child: Align(
if (isMultiSelectEnable) alignment: Alignment.topLeft,
Padding( child: buildSelectionIcon(asset),
padding: const EdgeInsets.all(3.0), ),
child: Align( ),
alignment: Alignment.topLeft, if (showStorageIndicator)
child: buildSelectionIcon(asset), Positioned(
), right: 10,
), bottom: 5,
if (showStorageIndicator) child: Icon(
Positioned( (deviceId != asset.deviceId)
right: 10, ? Icons.cloud_done_outlined
bottom: 5, : Icons.photo_library_rounded,
child: Icon( color: Colors.white,
(deviceId != asset.deviceId) size: 18,
? Icons.cloud_done_outlined ),
: Icons.photo_library_rounded, ),
color: Colors.white, if (asset.type != AssetTypeEnum.IMAGE)
size: 18, Positioned(
), top: 5,
), right: 5,
if (asset.type != AssetTypeEnum.IMAGE) child: Row(
Positioned( children: [
top: 5, Text(
right: 5, asset.duration.toString().substring(0, 7),
child: Row( style: const TextStyle(
children: [ color: Colors.white,
Text( fontSize: 10,
asset.duration.toString().substring(0, 7), ),
style: const TextStyle( ),
color: Colors.white, const Icon(
fontSize: 10, Icons.play_circle_outline_rounded,
), color: Colors.white,
), ),
const Icon( ],
Icons.play_circle_outline_rounded, ),
color: Colors.white, ),
), ],
], ),
), ),
), );
], }
), }
),
);
}
}

View File

@ -1,107 +0,0 @@
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,
),
)
],
),
);
}
}

View File

@ -1,173 +0,0 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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) {
final date = renderList[pos].date;
return Text(
DateFormat.yMMMd().format(date),
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,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(seconds: 1),
scrollbarTimeToFade: const Duration(seconds: 4),
child: ScrollablePositionedList.builder(
itemBuilder: itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemCount: renderList.length,
),
);
}
}

View File

@ -1,11 +1,15 @@
import 'package:easy_localization/easy_localization.dart'; 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/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
class ControlBottomAppBar extends ConsumerWidget { class ControlBottomAppBar extends ConsumerWidget {
const ControlBottomAppBar({Key? key}) : super(key: key); final Function onShare;
final Function onDelete;
const ControlBottomAppBar(
{Key? key, required this.onShare, required this.onDelete})
: super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return const DeleteDialog(); return DeleteDialog(
onDelete: onDelete,
);
}, },
); );
}, },
@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
iconData: Icons.share, iconData: Icons.share,
label: "control_bottom_app_bar_share".tr(), label: "control_bottom_app_bar_share".tr(),
onPressed: () { onPressed: () {
final homePageState = ref.watch(homePageStateProvider); onShare();
ref.watch(homePageStateProvider.notifier).shareAssets(
homePageState.selectedItems.toList(),
context,
);
ref
.watch(homePageStateProvider.notifier)
.disableMultiSelect();
}, },
), ),
], ],

View File

@ -1,109 +0,0 @@
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).toLocal());
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 SliverToBoxAdapter(
child: 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,
),
)
],
),
),
);
}
}

View File

@ -1,15 +1,14 @@
import 'package:easy_localization/easy_localization.dart'; 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/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
class DeleteDialog extends ConsumerWidget { class DeleteDialog extends ConsumerWidget {
const DeleteDialog({Key? key}) : super(key: key); final Function onDelete;
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final homePageState = ref.watch(homePageStateProvider);
return AlertDialog( return AlertDialog(
// backgroundColor: Colors.grey[200], // backgroundColor: Colors.grey[200],
@ -31,11 +30,7 @@ class DeleteDialog extends ConsumerWidget {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
ref onDelete();
.watch(assetProvider.notifier)
.deleteAssets(homePageState.selectedItems);
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(

View File

@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> assetGroup;
final List<AssetResponseDto> sortedAssetGroup;
final int tilesPerRow;
final bool showStorageIndicator;
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
this.tilesPerRow = 4,
this.showStorageIndicator = true,
}) : super(key: key);
List<AssetResponseDto> imageSortedList = [];
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: tilesPerRow,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return GestureDetector(
onTap: () {},
child: ThumbnailImage(
asset: assetGroup[index],
assetList: sortedAssetGroup,
showStorageIndicator: showStorageIndicator,
),
);
},
childCount: assetGroup.length,
),
);
}
}

View File

@ -2,22 +2,17 @@ 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_render_list_provider.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.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/disable_multi_select_button.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/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/profile_drawer/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget { class HomePage extends HookConsumerWidget {
@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
@override @override
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); var renderList = ref.watch(renderListProvider);
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
ScrollController scrollController = useScrollController(); final selection = useState(<AssetResponseDto>{});
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
List<Widget> imageGridGroup = [];
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider);
List<AssetResponseDto> sortedAssetList = [];
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
useEffect( useEffect(
() { () {
@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
ref.read(assetProvider.notifier).getAllAsset(); ref.read(assetProvider.notifier).getAllAsset();
} }
_buildSelectedItemCountIndicator() { Widget buildBody() {
return DisableMultiSelectButton( void selectionListener(
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect, bool multiselect,
selectedItemCount: homePageState.selectedItems.length, Set<AssetResponseDto> selectedAssets,
); ) {
} multiselectEnabled.state = multiselect;
selection.value = selectedAssets;
Widget _buildBody() {
if (assetGroupByDateTime.isNotEmpty) {
int? lastMonth;
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
try {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
}
}
imageGridGroup.add(
DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
tilesPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
),
);
lastMonth = currentMonth;
} catch (e) {
debugPrint(
"[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}",
);
}
});
} }
_buildSliverAppBar() { void onShareAssets() {
return isMultiSelectEnable ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
? const SliverToBoxAdapter( multiselectEnabled.state = false;
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
);
} }
_buildAssetGrid() { void onDelete() {
if (appSettingService ref.watch(assetProvider.notifier).deleteAssets(selection.value);
.getSetting(AppSettingsEnum.useExperimentalAssetGrid)) { multiselectEnabled.state = false;
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: !multiselectEnabled.state,
top: !isMultiSelectEnable, top: !multiselectEnabled.state,
child: Stack( child: Stack(
children: [ children: [
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
_buildSliverAppBar(), multiselectEnabled.state
? const SliverToBoxAdapter(
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
),
], ],
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0), padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
child: _buildAssetGrid(), child: ImmichAssetGrid(
renderList: renderList,
assetsPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
listener: selectionListener,
selectionActive: multiselectEnabled.state,
),
), ),
if (isMultiSelectEnable) ...[ if (multiselectEnabled.state) ...[
_buildSelectedItemCountIndicator(), ControlBottomAppBar(
const ControlBottomAppBar(), onShare: onShareAssets,
onDelete: onDelete,
),
], ],
], ],
), ),
@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
return Scaffold( return Scaffold(
drawer: const ProfileDrawer(), drawer: const ProfileDrawer(),
body: _buildBody(), body: buildBody(),
); );
} }
} }

View File

@ -1,8 +1,11 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
import 'package:immich_mobile/modules/search/services/search.service.dart'; import 'package:immich_mobile/modules/search/services/search.service.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:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
.format(DateTime.parse(element.createdAt).toLocal()), .format(DateTime.parse(element.createdAt).toLocal()),
); );
}); });
final searchRenderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
return assetGroupsToRenderList(assetGroups, assetsPerRow);
});

View File

@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.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/monthly_title_text.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:openapi/api.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class SearchResultPage extends HookConsumerWidget { class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({Key? key, required this.searchTerm}) const SearchResultPage({Key? key, required this.searchTerm})
@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();
final searchTermController = useTextEditingController(text: ""); final searchTermController = useTextEditingController(text: "");
final isNewSearch = useState(false); final isNewSearch = useState(false);
final currentSearchTerm = useState(searchTerm); final currentSearchTerm = useState(searchTerm);
final List<Widget> imageGridGroup = [];
FocusNode? searchFocusNode; FocusNode? searchFocusNode;
List<AssetResponseDto> sortedAssetList = [];
useEffect( useEffect(
() { () {
searchFocusNode = FocusNode(); searchFocusNode = FocusNode();
@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
_buildSearchResult() { _buildSearchResult() {
var searchResultPageState = ref.watch(searchResultPageProvider); var searchResultPageState = ref.watch(searchResultPageProvider);
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider); var searchResultRenderList = ref.watch(searchRenderListProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
final showStorageIndicator =
settings.getSetting(AppSettingsEnum.storageIndicator);
if (searchResultPageState.isError) { if (searchResultPageState.isError) {
return const Text("Error"); return const Text("Error");
@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget {
} }
if (searchResultPageState.isSuccess) { if (searchResultPageState.isSuccess) {
if (searchResultPageState.searchResult.isNotEmpty) { return ImmichAssetGrid(
int? lastMonth; renderList: searchResultRenderList,
// set sorted List assetsPerRow: assetsPerRow,
for (var group in assetGroupByDateTime.values) { showStorageIndicator: showStorageIndicator,
for (var value in group) { );
sortedAssetList.add(value);
}
}
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
}
}
imageGridGroup.add(
DailyTitleText(
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
);
lastMonth = currentMonth;
});
return DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [...imageGridGroup],
),
);
} else {
return const Text("No assets found");
}
} }
return const SizedBox(); return const SizedBox();

View File

@ -1,11 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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: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 { class ExperimentalSettings extends HookConsumerWidget {
const ExperimentalSettings({ const ExperimentalSettings({
@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return ExpansionTile(
textColor: Theme.of(context).primaryColor, textColor: Theme.of(context).primaryColor,
title: const Text( title: const Text(
@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
fontSize: 13, fontSize: 13,
), ),
).tr(), ).tr(),
children: [ children: const [
SwitchListTile.adaptive( // SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor, // activeColor: Theme.of(context).primaryColor,
title: const Text( // title: const Text(
"experimental_settings_new_asset_list_title", // "experimental_settings_new_asset_list_title",
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
fontWeight: FontWeight.bold, // fontWeight: FontWeight.bold,
), // ),
).tr(), // ).tr(),
subtitle: const Text( // subtitle: const Text(
"experimental_settings_new_asset_list_subtitle", // "experimental_settings_new_asset_list_subtitle",
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
), // ),
).tr(), // ).tr(),
value: useExperimentalAssetGrid.value, // value: useExperimentalAssetGrid.value,
onChanged: changeUseExperimentalAssetGrid, // onChanged: changeUseExperimentalAssetGrid,
), // ),
], ],
); );
} }

View File

@ -43,7 +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(), //const ExperimentalSettings(),
], ],
).toList(), ).toList(),
], ],

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
class TabControllerPage extends ConsumerWidget { class TabControllerPage extends ConsumerWidget {
@ -10,8 +10,7 @@ class TabControllerPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var isMultiSelectEnable = final multiselectEnabled = ref.watch(multiselectProvider);
ref.watch(homePageStateProvider).isMultiSelectEnable;
return AutoTabsRouter( return AutoTabsRouter(
routes: [ routes: [
@ -32,7 +31,7 @@ class TabControllerPage extends ConsumerWidget {
opacity: animation, opacity: animation,
child: child, child: child,
), ),
bottomNavigationBar: isMultiSelectEnable bottomNavigationBar: multiselectEnabled
? null ? null
: BottomNavigationBar( : BottomNavigationBar(
selectedLabelStyle: const TextStyle( selectedLabelStyle: const TextStyle(

View File

@ -0,0 +1,159 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:openapi/api.dart';
void main() {
final List<AssetResponseDto> testAssets = [];
for (int i = 0; i < 150; i++) {
int month = i ~/ 31;
int day = (i % 31).toInt();
DateTime date = DateTime(2022, month, day);
testAssets.add(AssetResponseDto(
type: AssetTypeEnum.IMAGE,
id: '$i',
deviceAssetId: '',
ownerId: '',
deviceId: '',
originalPath: '',
resizePath: '',
createdAt: date.toIso8601String(),
modifiedAt: date.toIso8601String(),
isFavorite: false,
mimeType: 'image/jpeg',
duration: '',
webpPath: '',
encodedVideoPath: '',
));
}
final Map<String, List<AssetResponseDto>> groups = {
'2022-01-05': testAssets.sublist(0, 5).map((e) {
e.createdAt = DateTime(2022, 1, 5).toIso8601String();
return e;
}).toList(),
'2022-01-10': testAssets.sublist(5, 10).map((e) {
e.createdAt = DateTime(2022, 1, 10).toIso8601String();
return e;
}).toList(),
'2022-02-17': testAssets.sublist(10, 15).map((e) {
e.createdAt = DateTime(2022, 2, 17).toIso8601String();
return e;
}).toList(),
'2022-10-15': testAssets.sublist(15, 30).map((e) {
e.createdAt = DateTime(2022, 10, 15).toIso8601String();
return e;
}).toList()
};
group('Asset only list', () {
test('items < itemsPerRow', () {
final assets = testAssets.sublist(0, 2);
final renderList = assetsToRenderList(assets, 3);
expect(renderList.length, 1);
expect(renderList[0].assetRow!.assets.length, 2);
});
test('items = itemsPerRow', () {
final assets = testAssets.sublist(0, 3);
final renderList = assetsToRenderList(assets, 3);
expect(renderList.length, 1);
expect(renderList[0].assetRow!.assets.length, 3);
});
test('items > itemsPerRow', () {
final assets = testAssets.sublist(0, 20);
final renderList = assetsToRenderList(assets, 3);
expect(renderList.length, 7);
expect(renderList[6].assetRow!.assets.length, 2);
});
test('items > itemsPerRow partition 4', () {
final assets = testAssets.sublist(0, 21);
final renderList = assetsToRenderList(assets, 4);
expect(renderList.length, 6);
expect(renderList[5].assetRow!.assets.length, 1);
});
test('items > itemsPerRow check ids', () {
final assets = testAssets.sublist(0, 21);
final renderList = assetsToRenderList(assets, 3);
expect(renderList.length, 7);
expect(renderList[6].assetRow!.assets.length, 3);
expect(renderList[0].assetRow!.assets[0].id, '0');
expect(renderList[1].assetRow!.assets[1].id, '4');
expect(renderList[3].assetRow!.assets[2].id, '11');
expect(renderList[6].assetRow!.assets[2].id, '20');
});
});
group('Test grouped', () {
test('test grouped check months', () {
final renderList = assetGroupsToRenderList(groups, 3);
// Jan
// Day 1
// 5 Assets => 2 Rows
// Day 2
// 5 Assets => 2 Rows
// Feb
// Day 1
// 5 Assets => 2 Rows
// Oct
// Day 1
// 15 Assets => 5 Rows
expect(renderList.length, 18);
expect(renderList[0].type, RenderAssetGridElementType.monthTitle);
expect(renderList[0].date.month, 1);
expect(renderList[7].type, RenderAssetGridElementType.monthTitle);
expect(renderList[7].date.month, 2);
expect(renderList[11].type, RenderAssetGridElementType.monthTitle);
expect(renderList[11].date.month, 10);
});
test('test grouped check types', () {
final renderList = assetGroupsToRenderList(groups, 5);
// Jan
// Day 1
// 5 Assets
// Day 2
// 5 Assets
// Feb
// Day 1
// 5 Assets
// Oct
// Day 1
// 15 Assets => 3 Rows
final types = [
RenderAssetGridElementType.monthTitle,
RenderAssetGridElementType.dayTitle,
RenderAssetGridElementType.assetRow,
RenderAssetGridElementType.dayTitle,
RenderAssetGridElementType.assetRow,
RenderAssetGridElementType.monthTitle,
RenderAssetGridElementType.dayTitle,
RenderAssetGridElementType.assetRow,
RenderAssetGridElementType.monthTitle,
RenderAssetGridElementType.dayTitle,
RenderAssetGridElementType.assetRow,
RenderAssetGridElementType.assetRow,
RenderAssetGridElementType.assetRow
];
expect(renderList.length, types.length);
for (int i = 0; i < renderList.length; i++) {
expect(renderList[i].type, types[i]);
}
});
});
}