mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
Merge branch 'main' of github.com:immich-app/immich into dev/mobile-cosmetic-improvement
This commit is contained in:
commit
e38166837d
@ -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"
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
});
|
});
|
||||||
|
@ -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))),
|
|
||||||
);
|
|
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
final multiselectProvider = StateProvider((ref) {
|
||||||
|
return false;
|
||||||
|
});
|
@ -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;
|
||||||
|
}
|
72
mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
Normal file
72
mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
Normal 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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
),
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
274
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
Normal file
274
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
}
|
||||||
),
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
],
|
],
|
||||||
|
@ -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(
|
||||||
|
159
mobile/test/asset_grid_data_structure_test.dart
Normal file
159
mobile/test/asset_grid_data_structure_test.dart
Normal 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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user