1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

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_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"
"experimental_settings_subtitle": "Use at your own risk!"
}

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: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/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 DateTime date;
final List<AssetResponseDto>? relatedAssetList;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
});
}
final renderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
List<RenderAssetGridElement> elements = [];
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;
return assetGroupsToRenderList(assetGroups, assetsPerRow);
});

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

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:flutter/material.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';
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
Widget build(BuildContext context, WidgetRef ref) {
@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
showDialog(
context: context,
builder: (BuildContext context) {
return const DeleteDialog();
return DeleteDialog(
onDelete: onDelete,
);
},
);
},
@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
iconData: Icons.share,
label: "control_bottom_app_bar_share".tr(),
onPressed: () {
final homePageState = ref.watch(homePageStateProvider);
ref.watch(homePageStateProvider.notifier).shareAssets(
homePageState.selectedItems.toList(),
context,
);
ref
.watch(homePageStateProvider.notifier)
.disableMultiSelect();
onShare();
},
),
],

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:flutter/material.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 {
const DeleteDialog({Key? key}) : super(key: key);
final Function onDelete;
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final homePageState = ref.watch(homePageStateProvider);
return AlertDialog(
// backgroundColor: Colors.grey[200],
@ -31,11 +30,7 @@ class DeleteDialog extends ConsumerWidget {
),
TextButton(
onPressed: () {
ref
.watch(assetProvider.notifier)
.deleteAssets(homePageState.selectedItems);
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
onDelete();
Navigator.of(context).pop();
},
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: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/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/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/monthly_title_text.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/services/app_settings.service.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/websocket.provider.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
var renderList = ref.watch(renderListProvider);
ScrollController scrollController = useScrollController();
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);
}
}
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selection = useState(<AssetResponseDto>{});
useEffect(
() {
@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
ref.read(assetProvider.notifier).getAllAsset();
}
_buildSelectedItemCountIndicator() {
return DisableMultiSelectButton(
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
selectedItemCount: homePageState.selectedItems.length,
);
}
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()}",
);
}
});
Widget buildBody() {
void selectionListener(
bool multiselect,
Set<AssetResponseDto> selectedAssets,
) {
multiselectEnabled.state = multiselect;
selection.value = selectedAssets;
}
_buildSliverAppBar() {
return isMultiSelectEnable
? const SliverToBoxAdapter(
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
);
void onShareAssets() {
ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
multiselectEnabled.state = false;
}
_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,
],
),
);
}
void onDelete() {
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
multiselectEnabled.state = false;
}
return SafeArea(
bottom: !isMultiSelectEnable,
top: !isMultiSelectEnable,
bottom: !multiselectEnabled.state,
top: !multiselectEnabled.state,
child: Stack(
children: [
CustomScrollView(
slivers: [
_buildSliverAppBar(),
multiselectEnabled.state
? const SliverToBoxAdapter(
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
),
],
),
Padding(
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) ...[
_buildSelectedItemCountIndicator(),
const ControlBottomAppBar(),
if (multiselectEnabled.state) ...[
ControlBottomAppBar(
onShare: onShareAssets,
onDelete: onDelete,
),
],
],
),
@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
return Scaffold(
drawer: const ProfileDrawer(),
body: _buildBody(),
body: buildBody(),
);
}
}

View File

@ -1,8 +1,11 @@
import 'package:collection/collection.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/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:openapi/api.dart';
@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
.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_spinkit/flutter_spinkit.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/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/home/ui/asset_grid/immich_asset_grid.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/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 {
const SearchResultPage({Key? key, required this.searchTerm})
@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();
final searchTermController = useTextEditingController(text: "");
final isNewSearch = useState(false);
final currentSearchTerm = useState(searchTerm);
final List<Widget> imageGridGroup = [];
FocusNode? searchFocusNode;
List<AssetResponseDto> sortedAssetList = [];
useEffect(
() {
searchFocusNode = FocusNode();
@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
_buildSearchResult() {
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) {
return const Text("Error");
@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget {
}
if (searchResultPageState.isSuccess) {
if (searchResultPageState.searchResult.isNotEmpty) {
int? lastMonth;
// set sorted List
for (var group in assetGroupByDateTime.values) {
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 ImmichAssetGrid(
renderList: searchResultRenderList,
assetsPerRow: assetsPerRow,
showStorageIndicator: showStorageIndicator,
);
}
return const SizedBox();

View File

@ -1,11 +1,6 @@
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({
@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
@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(
@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
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,
),
children: const [
// 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,
// ),
],
);
}

View File

@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget {
const ThemeSetting(),
const AssetListSettings(),
if (Platform.isAndroid) const NotificationSetting(),
const ExperimentalSettings(),
//const ExperimentalSettings(),
],
).toList(),
],

View File

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