feat: manual stack assets (#4198)

This commit is contained in:
shenlong
2023-10-22 02:38:07 +00:00
committed by GitHub
parent 5ead4af2dc
commit cf08ac7538
59 changed files with 2190 additions and 138 deletions

View File

@@ -130,7 +130,9 @@
"control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Share",
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_upload": "Upload",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Share",
@@ -275,6 +277,7 @@
"setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add",
"share_done": "Done",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_create_album": "Create album",
@@ -337,5 +340,8 @@
"trash_page_select_assets_btn": "Select assets",
"trash_page_empty_trash_btn": "Empty trash",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich"
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_unstack": "Un-Stack"
}

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -69,7 +70,8 @@ class AlbumViewerPage extends HookConsumerWidget {
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: albumInfo.assets,
isNewAlbum: false,
canDeselect: false,
query: getRemoteAssetQuery(ref),
),
);

View File

@@ -4,26 +4,27 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:isar/isar.dart';
class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({
Key? key,
required this.existingAssets,
this.isNewAlbum = false,
this.canDeselect = false,
required this.query,
}) : super(key: key);
final Set<Asset> existingAssets;
final bool isNewAlbum;
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
final bool canDeselect;
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentUser = ref.watch(currentUserProvider);
final renderList = ref.watch(remoteAssetsProvider(currentUser?.isarId));
final renderList = ref.watch(renderListQueryProvider(query));
final selected = useState<Set<Asset>>(existingAssets);
final selectionEnabledHook = useState(true);
@@ -39,8 +40,8 @@ class AssetSelectionPage extends HookConsumerWidget {
selected.value = assets;
},
selectionActive: true,
preselectedAssets: isNewAlbum ? selected.value : existingAssets,
canDeselect: isNewAlbum,
preselectedAssets: existingAssets,
canDeselect: canDeselect,
showMultiSelectIndicator: false,
);
}
@@ -65,7 +66,7 @@ class AssetSelectionPage extends HookConsumerWidget {
),
centerTitle: false,
actions: [
if (selected.value.isNotEmpty)
if (selected.value.isNotEmpty || canDeselect)
TextButton(
onPressed: () {
var payload =
@@ -74,7 +75,7 @@ class AssetSelectionPage extends HookConsumerWidget {
.popForced<AssetSelectionPageResult>(payload);
},
child: Text(
"share_add",
canDeselect ? "share_done" : "share_add",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
// ignore: must_be_immutable
class CreateAlbumPage extends HookConsumerWidget {
@@ -31,7 +32,8 @@ class CreateAlbumPage extends HookConsumerWidget {
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets = useState<Set<Asset>>(
initialAssets != null ? Set.from(initialAssets!) : const {},);
initialAssets != null ? Set.from(initialAssets!) : const {},
);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
showSelectUserPage() async {
@@ -59,7 +61,8 @@ class CreateAlbumPage extends HookConsumerWidget {
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: selectedAssets.value,
isNewAlbum: true,
canDeselect: true,
query: getRemoteAssetQuery(ref),
),
);
if (selectedAsset == null) {

View File

@@ -0,0 +1,50 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class AssetStackNotifier extends StateNotifier<List<Asset>> {
final Asset _asset;
final Ref _ref;
AssetStackNotifier(
this._asset,
this._ref,
) : super([]) {
fetchStackChildren();
}
void fetchStackChildren() async {
if (mounted) {
state = await _ref.read(assetStackProvider(_asset).future);
}
}
removeChild(int index) {
if (index < state.length) {
state.removeAt(index);
}
}
}
final assetStackStateProvider = StateNotifierProvider.autoDispose
.family<AssetStackNotifier, List<Asset>, Asset>(
(ref, asset) => AssetStackNotifier(asset, ref),
);
final assetStackProvider =
FutureProvider.autoDispose.family<List<Asset>, Asset>((ref, asset) async {
// Guard [local asset]
if (asset.remoteId == null) {
return [];
}
return await ref
.watch(dbProvider)
.assets
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackParentIdEqualTo(asset.remoteId)
.findAll();
});

View File

@@ -3,6 +3,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:isar/isar.dart';
final renderListProvider =
FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
@@ -13,3 +14,19 @@ final renderListProvider =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
);
});
final renderListQueryProvider = StreamProvider.family<RenderList,
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
(ref, query) async* {
if (query == null) {
return;
}
final settings = ref.watch(appSettingsServiceProvider);
final groupBy = GroupAssetsBy
.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
},
);

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
class AssetStackService {
AssetStackService(this._api);
final ApiService _api;
updateStack(
Asset parentAsset, {
List<Asset>? childrenToAdd,
List<Asset>? childrenToRemove,
}) async {
// Guard [local asset]
if (parentAsset.remoteId == null) {
return;
}
try {
if (childrenToAdd != null) {
final toAdd = childrenToAdd
.where((e) => e.isRemote)
.map((e) => e.remoteId!)
.toList();
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId),
);
}
if (childrenToRemove != null) {
final toRemove = childrenToRemove
.where((e) => e.isRemote)
.map((e) => e.remoteId!)
.toList();
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toRemove, removeParent: true),
);
}
} catch (error) {
debugPrint("Error while updating stack children: ${error.toString()}");
}
}
updateStackParent(Asset oldParent, Asset newParent) async {
// Guard [local asset]
if (oldParent.remoteId == null || newParent.remoteId == null) {
return;
}
try {
await _api.assetApi.updateStackParent(
UpdateStackParentDto(
oldParentId: oldParent.remoteId!,
newParentId: newParent.remoteId!,
),
);
} catch (error) {
debugPrint("Error while updating stack parent: ${error.toString()}");
}
}
}
final assetStackServiceProvider = Provider(
(ref) => AssetStackService(
ref.watch(apiServiceProvider),
),
);

View File

@@ -8,11 +8,13 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
@@ -44,6 +46,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final int totalAssets;
final int initialIndex;
final int heroOffset;
final bool showStack;
GalleryViewerPage({
super.key,
@@ -51,6 +54,7 @@ class GalleryViewerPage extends HookConsumerWidget {
required this.loadAsset,
required this.totalAssets,
this.heroOffset = 0,
this.showStack = false,
}) : controller = PageController(initialPage: initialIndex);
final PageController controller;
@@ -77,8 +81,17 @@ class GalleryViewerPage extends HookConsumerWidget {
final isFromTrash = isTrashEnabled &&
navStack.length > 2 &&
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
final stackIndex = useState(-1);
final stack = showStack && currentAsset.stackCount > 0
? ref.watch(assetStackStateProvider(currentAsset))
: <Asset>[];
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
Asset asset() => currentAsset;
Asset asset() => stackIndex.value == -1
? currentAsset
: stackElements.elementAt(stackIndex.value);
bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
useEffect(
() {
@@ -165,19 +178,28 @@ class GalleryViewerPage extends HookConsumerWidget {
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ExifBottomSheet(asset: currentAsset),
child: ExifBottomSheet(asset: asset()),
);
},
);
}
void removeAssetFromStack() {
if (stackIndex.value > 0 && showStack) {
ref
.read(assetStackStateProvider(currentAsset).notifier)
.removeChild(stackIndex.value - 1);
stackIndex.value = stackIndex.value - 1;
}
}
void handleDelete(Asset deleteAsset) async {
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{deleteAsset},
force: force,
);
if (isDeleted) {
if (isDeleted && isParent) {
if (totalAssets == 1) {
// Handle only one asset
AutoRouter.of(context).pop();
@@ -195,14 +217,17 @@ class GalleryViewerPage extends HookConsumerWidget {
// Asset is trashed
if (isTrashEnabled && !isFromTrash) {
final isDeleted = await onDelete(false);
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && isDeleted && deleteAsset.isRemote) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
if (isDeleted) {
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && deleteAsset.isRemote && isParent) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
}
removeAssetFromStack();
}
return;
}
@@ -211,7 +236,14 @@ class GalleryViewerPage extends HookConsumerWidget {
showDialog(
context: context,
builder: (BuildContext _) {
return DeleteDialog(onDelete: () => onDelete(true));
return DeleteDialog(
onDelete: () async {
final isDeleted = await onDelete(true);
if (isDeleted) {
removeAssetFromStack();
}
},
);
},
);
}
@@ -268,7 +300,11 @@ class GalleryViewerPage extends HookConsumerWidget {
ref
.watch(assetProvider.notifier)
.toggleArchive([asset], !asset.isArchived);
AutoRouter.of(context).pop();
if (isParent) {
AutoRouter.of(context).pop();
return;
}
removeAssetFromStack();
}
handleUpload(Asset asset) {
@@ -385,7 +421,186 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
buildBottomBar() {
Widget buildStackedChildren() {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: stackElements.length,
itemBuilder: (context, index) {
final assetId = stackElements.elementAt(index).remoteId;
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
onTap: () => stackIndex.value = index,
child: Container(
width: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: index == stackIndex.value
? Border.all(
color: Colors.white,
width: 2,
)
: null,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl:
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
httpHeaders: {
"Authorization":
"Bearer ${Store.get(StoreKey.accessToken)}",
},
errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined),
),
),
),
),
);
},
);
}
void showStackActionItems() {
showModalBottomSheet<void>(
context: context,
enableDrag: false,
builder: (BuildContext ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!isParent)
ListTile(
leading: const Icon(
Icons.bookmark_border_outlined,
size: 24,
),
onTap: () async {
await ref
.read(assetStackServiceProvider)
.updateStackParent(
currentAsset,
stackElements.elementAt(stackIndex.value),
);
Navigator.pop(ctx);
AutoRouter.of(context).pop();
},
title: const Text(
"viewer_stack_use_as_main_asset",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
ListTile(
leading: const Icon(
Icons.copy_all_outlined,
size: 24,
),
onTap: () async {
if (isParent) {
await ref
.read(assetStackServiceProvider)
.updateStackParent(
currentAsset,
stackElements
.elementAt(1), // Next asset as parent
);
// Remove itself from stack
await ref.read(assetStackServiceProvider).updateStack(
stackElements.elementAt(1),
childrenToRemove: [currentAsset],
);
Navigator.pop(ctx);
AutoRouter.of(context).pop();
} else {
await ref.read(assetStackServiceProvider).updateStack(
currentAsset,
childrenToRemove: [
stackElements.elementAt(stackIndex.value),
],
);
removeAssetFromStack();
Navigator.pop(ctx);
}
},
title: const Text(
"viewer_remove_from_stack",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
ListTile(
leading: const Icon(
Icons.filter_none_outlined,
size: 18,
),
onTap: () async {
await ref.read(assetStackServiceProvider).updateStack(
currentAsset,
childrenToRemove: stack,
);
Navigator.pop(ctx);
AutoRouter.of(context).pop();
},
title: const Text(
"viewer_unstack",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
],
),
),
);
},
);
}
Widget buildBottomBar() {
// !!!! itemsList and actionlist should always be in sync
final itemsList = [
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
asset().isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
),
if (stack.isNotEmpty)
BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined),
label: 'control_bottom_app_bar_stack'.tr(),
tooltip: 'control_bottom_app_bar_stack'.tr(),
),
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
];
List<Function(int)> actionslist = [
(_) => shareAsset(),
(_) => handleArchive(asset()),
if (stack.isNotEmpty) (_) => showStackActionItems(),
(_) => handleDelete(asset()),
];
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
@@ -393,6 +608,17 @@ class GalleryViewerPage extends HookConsumerWidget {
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: Column(
children: [
if (stack.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 10,
bottom: 30,
),
child: SizedBox(
height: 40,
child: buildStackedChildren(),
),
),
Visibility(
visible: !asset().isImage && !isPlayingMotionVideo.value,
child: Container(
@@ -421,44 +647,10 @@ class GalleryViewerPage extends HookConsumerWidget {
selectedLabelStyle: const TextStyle(color: Colors.black),
showSelectedLabels: false,
showUnselectedLabels: false,
items: [
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid
? Icons.share_rounded
: Icons.ios_share_rounded,
),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
asset().isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
),
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
],
items: itemsList,
onTap: (index) {
switch (index) {
case 0:
shareAsset();
break;
case 1:
handleArchive(asset());
break;
case 2:
handleDelete(asset());
break;
if (index < actionslist.length) {
actionslist[index].call(index);
}
},
),
@@ -504,6 +696,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final next = currentIndex.value < value ? value + 1 : value - 1;
precacheNextImage(next);
currentIndex.value = value;
stackIndex.value = -1;
HapticFeedback.selectionClick();
},
loadingBuilder: (context, event, index) {
@@ -544,10 +737,11 @@ class GalleryViewerPage extends HookConsumerWidget {
: webPThumbnail;
},
builder: (context, index) {
final asset = loadAsset(index);
final ImageProvider provider = finalImageProvider(asset);
final a =
index == currentIndex.value ? asset() : loadAsset(index);
final ImageProvider provider = finalImageProvider(a);
if (asset.isImage && !isPlayingMotionVideo.value) {
if (a.isImage && !isPlayingMotionVideo.value) {
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
@@ -558,13 +752,13 @@ class GalleryViewerPage extends HookConsumerWidget {
},
imageProvider: provider,
heroAttributes: PhotoViewHeroAttributes(
tag: asset.id + heroOffset,
tag: a.id + heroOffset,
),
filterQuality: FilterQuality.high,
tightMode: true,
minScale: PhotoViewComputedScale.contained,
errorBuilder: (context, error, stackTrace) => ImmichImage(
asset,
a,
fit: BoxFit.contain,
),
);
@@ -575,7 +769,7 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(
tag: asset.id + heroOffset,
tag: a.id + heroOffset,
),
filterQuality: FilterQuality.high,
maxScale: 1.0,
@@ -584,7 +778,7 @@ class GalleryViewerPage extends HookConsumerWidget {
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: asset,
asset: a,
isMotionVideo: isPlayingMotionVideo.value,
placeholder: Image(
image: provider,

View File

@@ -0,0 +1,47 @@
import 'package:immich_mobile/shared/models/asset.dart';
class SelectionAssetState {
final bool hasRemote;
final bool hasLocal;
final bool hasMerged;
const SelectionAssetState({
this.hasRemote = false,
this.hasLocal = false,
this.hasMerged = false,
});
SelectionAssetState copyWith({
bool? hasRemote,
bool? hasLocal,
bool? hasMerged,
}) {
return SelectionAssetState(
hasRemote: hasRemote ?? this.hasRemote,
hasLocal: hasLocal ?? this.hasLocal,
hasMerged: hasMerged ?? this.hasMerged,
);
}
SelectionAssetState.fromSelection(Set<Asset> selection)
: hasLocal = selection.any((e) => e.storage == AssetState.local),
hasMerged = selection.any((e) => e.storage == AssetState.merged),
hasRemote = selection.any((e) => e.storage == AssetState.remote);
@override
String toString() =>
'SelectionAssetState(hasRemote: $hasRemote, hasMerged: $hasMerged, hasMerged: $hasMerged)';
@override
bool operator ==(covariant SelectionAssetState other) {
if (identical(this, other)) return true;
return other.hasRemote == hasRemote &&
other.hasLocal == hasLocal &&
other.hasMerged == hasMerged;
}
@override
int get hashCode =>
hasRemote.hashCode ^ hasLocal.hashCode ^ hasMerged.hashCode;
}

View File

@@ -32,6 +32,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
final Widget? topWidget;
final bool shrinkWrap;
final bool showDragScroll;
final bool showStack;
const ImmichAssetGrid({
super.key,
@@ -51,6 +52,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.topWidget,
this.shrinkWrap = false,
this.showDragScroll = true,
this.showStack = false,
});
@override
@@ -114,6 +116,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
heroOffset: heroOffset(),
shrinkWrap: shrinkWrap,
showDragScroll: showDragScroll,
showStack: showStack,
),
);
}

View File

@@ -37,6 +37,7 @@ class ImmichAssetGridView extends StatefulWidget {
final int heroOffset;
final bool shrinkWrap;
final bool showDragScroll;
final bool showStack;
const ImmichAssetGridView({
super.key,
@@ -56,6 +57,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.heroOffset = 0,
this.shrinkWrap = false,
this.showDragScroll = true,
this.showStack = false,
});
@override
@@ -71,7 +73,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
bool _scrolling = false;
final Set<Asset> _selectedAssets =
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
Set<Asset> _getSelectedAssets() {
return Set.from(_selectedAssets);
@@ -90,7 +92,13 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
void _deselectAssets(List<Asset> assets) {
setState(() {
_selectedAssets.removeAll(assets);
_selectedAssets.removeAll(
assets.where(
(a) =>
widget.canDeselect ||
!(widget.preselectedAssets?.contains(a) ?? false),
),
);
_callSelectionListener(_selectedAssets.isNotEmpty);
});
}
@@ -129,6 +137,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
heroOffset: widget.heroOffset,
showStack: widget.showStack,
);
}
@@ -377,10 +386,6 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
setState(() {
_selectedAssets.clear();
});
} else if (widget.preselectedAssets != null) {
setState(() {
_selectedAssets.addAll(widget.preselectedAssets!);
});
}
}

View File

@@ -12,6 +12,7 @@ class ThumbnailImage extends StatelessWidget {
final Asset Function(int index) loadAsset;
final int totalAssets;
final bool showStorageIndicator;
final bool showStack;
final bool useGrayBoxPlaceholder;
final bool isSelected;
final bool multiselectEnabled;
@@ -26,6 +27,7 @@ class ThumbnailImage extends StatelessWidget {
required this.loadAsset,
required this.totalAssets,
this.showStorageIndicator = true,
this.showStack = false,
this.useGrayBoxPlaceholder = false,
this.isSelected = false,
this.multiselectEnabled = false,
@@ -93,6 +95,35 @@ class ThumbnailImage extends StatelessWidget {
);
}
Widget buildStackIcon() {
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
if (asset.stackCount > 1)
Text(
"${asset.stackCount}",
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
if (asset.stackCount > 1)
const SizedBox(
width: 3,
),
const Icon(
Icons.burst_mode_rounded,
color: Colors.white,
size: 18,
),
],
),
);
}
Widget buildImage() {
final image = SizedBox(
width: 300,
@@ -113,9 +144,9 @@ class ThumbnailImage extends StatelessWidget {
decoration: BoxDecoration(
border: Border.all(
width: 0,
color: assetContainerColor,
color: onDeselect == null ? Colors.grey : assetContainerColor,
),
color: assetContainerColor,
color: onDeselect == null ? Colors.grey : assetContainerColor,
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
@@ -144,6 +175,7 @@ class ThumbnailImage extends StatelessWidget {
loadAsset: loadAsset,
totalAssets: totalAssets,
heroOffset: heroOffset,
showStack: showStack,
),
);
}
@@ -196,6 +228,7 @@ class ThumbnailImage extends StatelessWidget {
),
),
if (!asset.isImage) buildVideoIcon(),
if (asset.isImage && asset.stackCount > 0) buildStackIcon(),
],
),
);

View File

@@ -4,9 +4,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/models/selection_state.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart';
@@ -19,11 +19,12 @@ class ControlBottomAppBar extends ConsumerWidget {
final Function(Album album) onAddToAlbum;
final void Function() onCreateNewAlbum;
final void Function() onUpload;
final void Function() onStack;
final List<Album> albums;
final List<Album> sharedAlbums;
final bool enabled;
final AssetState selectionAssetState;
final SelectionAssetState selectionAssetState;
const ControlBottomAppBar({
Key? key,
@@ -36,19 +37,24 @@ class ControlBottomAppBar extends ConsumerWidget {
required this.onAddToAlbum,
required this.onCreateNewAlbum,
required this.onUpload,
this.selectionAssetState = AssetState.remote,
required this.onStack,
this.selectionAssetState = const SelectionAssetState(),
this.enabled = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var hasRemote = selectionAssetState == AssetState.remote;
var hasRemote =
selectionAssetState.hasRemote || selectionAssetState.hasMerged;
var hasLocal = selectionAssetState.hasLocal;
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
Widget renderActionButtons() {
return Row(
return Wrap(
spacing: 10,
runSpacing: 15,
children: [
ControlBoxButton(
iconData: Platform.isAndroid
@@ -92,7 +98,7 @@ class ControlBottomAppBar extends ConsumerWidget {
if (!hasRemote)
ControlBoxButton(
iconData: Icons.backup_outlined,
label: "Upload",
label: "control_bottom_app_bar_upload".tr(),
onPressed: enabled
? () => showDialog(
context: context,
@@ -104,6 +110,12 @@ class ControlBottomAppBar extends ConsumerWidget {
)
: null,
),
if (!hasLocal)
ControlBoxButton(
iconData: Icons.filter_none_rounded,
label: "control_bottom_app_bar_stack".tr(),
onPressed: enabled ? onStack : null,
),
],
);
}
@@ -111,7 +123,7 @@ class ControlBottomAppBar extends ConsumerWidget {
return DraggableScrollableSheet(
initialChildSize: hasRemote ? 0.30 : 0.18,
minChildSize: 0.18,
maxChildSize: hasRemote ? 0.57 : 0.18,
maxChildSize: hasRemote ? 0.60 : 0.18,
snap: true,
builder: (
BuildContext context,

View File

@@ -7,11 +7,15 @@ 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/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/models/selection_state.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';
@@ -36,7 +40,7 @@ class HomePage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selectionEnabledHook = useState(false);
final selectionAssetState = useState(AssetState.remote);
final selectionAssetState = useState(const SelectionAssetState());
final selection = useState(<Asset>{});
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
@@ -83,9 +87,8 @@ class HomePage extends HookConsumerWidget {
) {
selectionEnabledHook.value = multiselect;
selection.value = selectedAssets;
selectionAssetState.value = selectedAssets.any((e) => e.isRemote)
? AssetState.remote
: AssetState.local;
selectionAssetState.value =
SelectionAssetState.fromSelection(selectedAssets);
}
void onShareAssets() {
@@ -246,6 +249,55 @@ class HomePage extends HookConsumerWidget {
}
}
void onStack() async {
try {
processing.value = true;
if (!selectionEnabledHook.value) {
return;
}
final selectedAsset = selection.value.elementAt(0);
if (selection.value.length == 1) {
final stackChildren =
(await ref.read(assetStackProvider(selectedAsset).future))
.toSet();
AssetSelectionPageResult? returnPayload =
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: stackChildren,
canDeselect: true,
query: getAssetStackSelectionQuery(ref, selectedAsset),
),
);
if (returnPayload != null) {
Set<Asset> selectedAssets = returnPayload.selectedAssets;
// Do not add itself as its stack child
selectedAssets.remove(selectedAsset);
final removedChildren = stackChildren.difference(selectedAssets);
final addedChildren = selectedAssets.difference(stackChildren);
await ref.read(assetStackServiceProvider).updateStack(
selectedAsset,
childrenToAdd: addedChildren.toList(),
childrenToRemove: removedChildren.toList(),
);
}
} else {
// Merge assets
selection.value.remove(selectedAsset);
final selectedAssets = selection.value;
await ref.read(assetStackServiceProvider).updateStack(
selectedAsset,
childrenToAdd: selectedAssets.toList(),
);
}
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
Future<void> refreshAssets() async {
final fullRefresh = refreshCount.value > 0;
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
@@ -322,6 +374,7 @@ class HomePage extends HookConsumerWidget {
currentUser.memoryEnabled!)
? const MemoryLane()
: const SizedBox(),
showStack: true,
),
error: (error, _) => Center(child: Text(error.toString())),
loading: buildLoadingIndicator,
@@ -339,6 +392,7 @@ class HomePage extends HookConsumerWidget {
onUpload: onUpload,
enabled: !processing.value,
selectionAssetState: selectionAssetState.value,
onStack: onStack,
),
if (processing.value) const Center(child: ImmichLoadingIndicator()),
],

View File

@@ -252,6 +252,7 @@ class TrashPage extends HookConsumerWidget {
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
showMultiSelectIndicator: false,
showStack: true,
topWidget: Padding(
padding: const EdgeInsets.only(
top: 24,

View File

@@ -51,6 +51,7 @@ import 'package:immich_mobile/shared/views/app_log_detail_page.dart';
import 'package:immich_mobile/shared/views/app_log_page.dart';
import 'package:immich_mobile/shared/views/splash_screen.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
import 'package:isar/isar.dart';
import 'package:photo_manager/photo_manager.dart';
part 'router.gr.dart';

View File

@@ -71,6 +71,7 @@ class _$AppRouter extends RootStackRouter {
loadAsset: args.loadAsset,
totalAssets: args.totalAssets,
heroOffset: args.heroOffset,
showStack: args.showStack,
),
);
},
@@ -153,7 +154,8 @@ class _$AppRouter extends RootStackRouter {
child: AssetSelectionPage(
key: args.key,
existingAssets: args.existingAssets,
isNewAlbum: args.isNewAlbum,
canDeselect: args.canDeselect,
query: args.query,
),
transitionsBuilder: TransitionsBuilders.slideBottom,
opaque: true,
@@ -711,6 +713,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
required Asset Function(int) loadAsset,
required int totalAssets,
int heroOffset = 0,
bool showStack = false,
}) : super(
GalleryViewerRoute.name,
path: '/gallery-viewer-page',
@@ -720,6 +723,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
loadAsset: loadAsset,
totalAssets: totalAssets,
heroOffset: heroOffset,
showStack: showStack,
),
);
@@ -733,6 +737,7 @@ class GalleryViewerRouteArgs {
required this.loadAsset,
required this.totalAssets,
this.heroOffset = 0,
this.showStack = false,
});
final Key? key;
@@ -745,9 +750,11 @@ class GalleryViewerRouteArgs {
final int heroOffset;
final bool showStack;
@override
String toString() {
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset}';
return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack}';
}
}
@@ -961,14 +968,16 @@ class AssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
AssetSelectionRoute({
Key? key,
required Set<Asset> existingAssets,
bool isNewAlbum = false,
bool canDeselect = false,
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
}) : super(
AssetSelectionRoute.name,
path: '/asset-selection-page',
args: AssetSelectionRouteArgs(
key: key,
existingAssets: existingAssets,
isNewAlbum: isNewAlbum,
canDeselect: canDeselect,
query: query,
),
);
@@ -979,18 +988,21 @@ class AssetSelectionRouteArgs {
const AssetSelectionRouteArgs({
this.key,
required this.existingAssets,
this.isNewAlbum = false,
this.canDeselect = false,
required this.query,
});
final Key? key;
final Set<Asset> existingAssets;
final bool isNewAlbum;
final bool canDeselect;
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
@override
String toString() {
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, isNewAlbum: $isNewAlbum}';
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
}
}

View File

@@ -31,7 +31,9 @@ class Asset {
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null,
isFavorite = remote.isFavorite,
isArchived = remote.isArchived,
isTrashed = remote.isTrashed;
isTrashed = remote.isTrashed,
stackParentId = remote.stackParentId,
stackCount = remote.stackCount;
Asset.local(AssetEntity local, List<int> hash)
: localId = local.id,
@@ -47,6 +49,7 @@ class Asset {
isFavorite = local.isFavorite,
isArchived = false,
isTrashed = false,
stackCount = 0,
fileCreatedAt = local.createDateTime {
if (fileCreatedAt.year == 1970) {
fileCreatedAt = fileModifiedAt;
@@ -77,6 +80,8 @@ class Asset {
required this.isFavorite,
required this.isArchived,
required this.isTrashed,
this.stackParentId,
required this.stackCount,
});
@ignore
@@ -146,6 +151,10 @@ class Asset {
@ignore
ExifInfo? exifInfo;
String? stackParentId;
int stackCount;
/// `true` if this [Asset] is present on the device
@ignore
bool get isLocal => localId != null;
@@ -200,7 +209,9 @@ class Asset {
isFavorite == other.isFavorite &&
isLocal == other.isLocal &&
isArchived == other.isArchived &&
isTrashed == other.isTrashed;
isTrashed == other.isTrashed &&
stackCount == other.stackCount &&
stackParentId == other.stackParentId;
}
@override
@@ -223,7 +234,9 @@ class Asset {
isFavorite.hashCode ^
isLocal.hashCode ^
isArchived.hashCode ^
isTrashed.hashCode;
isTrashed.hashCode ^
stackCount.hashCode ^
stackParentId.hashCode;
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
@@ -236,9 +249,11 @@ class Asset {
width == null && a.width != null ||
height == null && a.height != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
stackParentId == null && a.stackParentId != null ||
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed;
isTrashed != a.isTrashed ||
stackCount != a.stackCount;
}
/// Returns a new [Asset] with values from this and merged & updated with [a]
@@ -267,6 +282,8 @@ class Asset {
id: id,
remoteId: remoteId,
livePhotoVideoId: livePhotoVideoId,
stackParentId: stackParentId,
stackCount: stackCount,
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
@@ -281,6 +298,8 @@ class Asset {
width: a.width,
height: a.height,
livePhotoVideoId: a.livePhotoVideoId,
stackParentId: a.stackParentId,
stackCount: a.stackCount,
// isFavorite + isArchived are not set by device-only assets
isFavorite: a.isFavorite,
isArchived: a.isArchived,
@@ -318,6 +337,8 @@ class Asset {
bool? isArchived,
bool? isTrashed,
ExifInfo? exifInfo,
String? stackParentId,
int? stackCount,
}) =>
Asset(
id: id ?? this.id,
@@ -338,6 +359,8 @@ class Asset {
isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed,
exifInfo: exifInfo ?? this.exifInfo,
stackParentId: stackParentId ?? this.stackParentId,
stackCount: stackCount ?? this.stackCount,
);
Future<void> put(Isar db) async {
@@ -379,6 +402,8 @@ class Asset {
"checksum": "$checksum",
"ownerId": $ownerId,
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
"stackCount": "$stackCount",
"stackParentId": "${stackParentId ?? "N/A"}",
"fileCreatedAt": "$fileCreatedAt",
"fileModifiedAt": "$fileModifiedAt",
"updatedAt": "$updatedAt",

View File

@@ -82,19 +82,29 @@ const AssetSchema = CollectionSchema(
name: r'remoteId',
type: IsarType.string,
),
r'type': PropertySchema(
r'stackCount': PropertySchema(
id: 13,
name: r'stackCount',
type: IsarType.long,
),
r'stackParentId': PropertySchema(
id: 14,
name: r'stackParentId',
type: IsarType.string,
),
r'type': PropertySchema(
id: 15,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 14,
id: 16,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 15,
id: 17,
name: r'width',
type: IsarType.int,
)
@@ -184,6 +194,12 @@ int _assetEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.stackParentId;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
@@ -206,9 +222,11 @@ void _assetSerialize(
writer.writeString(offsets[10], object.localId);
writer.writeLong(offsets[11], object.ownerId);
writer.writeString(offsets[12], object.remoteId);
writer.writeByte(offsets[13], object.type.index);
writer.writeDateTime(offsets[14], object.updatedAt);
writer.writeInt(offsets[15], object.width);
writer.writeLong(offsets[13], object.stackCount);
writer.writeString(offsets[14], object.stackParentId);
writer.writeByte(offsets[15], object.type.index);
writer.writeDateTime(offsets[16], object.updatedAt);
writer.writeInt(offsets[17], object.width);
}
Asset _assetDeserialize(
@@ -232,10 +250,12 @@ Asset _assetDeserialize(
localId: reader.readStringOrNull(offsets[10]),
ownerId: reader.readLong(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[13])] ??
stackCount: reader.readLong(offsets[13]),
stackParentId: reader.readStringOrNull(offsets[14]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[15])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[14]),
width: reader.readIntOrNull(offsets[15]),
updatedAt: reader.readDateTime(offsets[16]),
width: reader.readIntOrNull(offsets[17]),
);
return object;
}
@@ -274,11 +294,15 @@ P _assetDeserializeProp<P>(
case 12:
return (reader.readStringOrNull(offset)) as P;
case 13:
return (reader.readLong(offset)) as P;
case 14:
return (reader.readStringOrNull(offset)) as P;
case 15:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 14:
case 16:
return (reader.readDateTime(offset)) as P;
case 15:
case 17:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -1801,6 +1825,205 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountEqualTo(
int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'stackCount',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountGreaterThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'stackCount',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountLessThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'stackCount',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackCountBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'stackCount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'stackParentId',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'stackParentId',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'stackParentId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'stackParentId',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'stackParentId',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'stackParentId',
value: '',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> stackParentIdIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'stackParentId',
value: '',
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> typeEqualTo(
AssetType value) {
return QueryBuilder.apply(this, (query) {
@@ -2137,6 +2360,30 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackCount', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackCount', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackParentId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackParentId', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByStackParentIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackParentId', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
@@ -2343,6 +2590,30 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackCount', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackCount', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackParentId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackParentId', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByStackParentIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'stackParentId', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
@@ -2465,6 +2736,20 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByStackCount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'stackCount');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByStackParentId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'stackParentId',
caseSensitive: caseSensitive);
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'type');
@@ -2569,6 +2854,18 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
});
}
QueryBuilder<Asset, int, QQueryOperations> stackCountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'stackCount');
});
}
QueryBuilder<Asset, String?, QQueryOperations> stackParentIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'stackParentId');
});
}
QueryBuilder<Asset, AssetType, QQueryOperations> typeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'type');

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/services/asset.service.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';
@@ -217,6 +218,7 @@ final assetsProvider =
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.stackParentIdIsNull()
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
@@ -227,10 +229,12 @@ final assetsProvider =
}
});
final remoteAssetsProvider =
StreamProvider.family<RenderList, int?>((ref, userId) async* {
if (userId == null) return;
final query = ref
QueryBuilder<Asset, Asset, QAfterSortBy>? getRemoteAssetQuery(WidgetRef ref) {
final userId = ref.watch(currentUserProvider)?.isarId;
if (userId == null) {
return null;
}
return ref
.watch(dbProvider)
.assets
.where()
@@ -238,12 +242,34 @@ final remoteAssetsProvider =
.filter()
.ownerIdEqualTo(userId)
.isTrashedEqualTo(false)
.stackParentIdIsNull()
.sortByFileCreatedAtDesc();
final settings = ref.watch(appSettingsServiceProvider);
final groupBy =
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
yield await RenderList.fromQuery(query, groupBy);
await for (final _ in query.watchLazy()) {
yield await RenderList.fromQuery(query, groupBy);
}
QueryBuilder<Asset, Asset, QAfterSortBy>? getAssetStackSelectionQuery(
WidgetRef ref,
Asset parentAsset,
) {
final userId = ref.watch(currentUserProvider)?.isarId;
if (userId == null || !parentAsset.isRemote) {
return null;
}
});
return ref
.watch(dbProvider)
.assets
.where()
.remoteIdIsNotNull()
.filter()
.isArchivedEqualTo(false)
.ownerIdEqualTo(userId)
.not()
.remoteIdEqualTo(parentAsset.remoteId)
// Show existing stack children in selection page
.group(
(q) => q
.stackParentIdIsNull()
.or()
.stackParentIdEqualTo(parentAsset.remoteId),
)
.sortByFileCreatedAtDesc();
}

View File

@@ -133,6 +133,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_asset_delete', _handleOnAssetDelete);
socket.on('on_asset_trash', _handleServerUpdates);
socket.on('on_asset_restore', _handleServerUpdates);
socket.on('on_asset_update', _handleServerUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}

View File

@@ -149,6 +149,7 @@ doc/TranscodePolicy.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md
doc/UpdateStackParentDto.md
doc/UpdateTagDto.md
doc/UpdateUserDto.md
doc/UsageByUserDto.md
@@ -314,6 +315,7 @@ lib/model/transcode_policy.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart
lib/model/update_stack_parent_dto.dart
lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart
lib/model/usage_by_user_dto.dart
@@ -468,6 +470,7 @@ test/transcode_policy_test.dart
test/update_album_dto_test.dart
test/update_asset_dto_test.dart
test/update_library_dto_test.dart
test/update_stack_parent_dto_test.dart
test/update_tag_dto_test.dart
test/update_user_dto_test.dart
test/usage_by_user_dto_test.dart

View File

@@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |
*AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |
*AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix |
*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes |
@@ -330,6 +331,7 @@ Class | Method | HTTP request | Description
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
- [UpdateStackParentDto](doc//UpdateStackParentDto.md)
- [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md)

View File

@@ -38,6 +38,7 @@ Method | HTTP request | Description
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |
[**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset |
[**updateStackParent**](AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent |
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
@@ -1696,6 +1697,60 @@ void (empty response body)
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **updateStackParent**
> updateStackParent(updateStackParentDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final updateStackParentDto = UpdateStackParentDto(); // UpdateStackParentDto |
try {
api_instance.updateStackParent(updateStackParentDto);
} catch (e) {
print('Exception when calling AssetApi->updateStackParent: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**updateStackParentDto** | [**UpdateStackParentDto**](UpdateStackParentDto.md)| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **uploadFile**
> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, duration, isArchived, isExternal, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData)

View File

@@ -11,6 +11,8 @@ Name | Type | Description | Notes
**ids** | **List<String>** | | [default to const []]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**removeParent** | **bool** | | [optional]
**stackParentId** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -33,6 +33,9 @@ Name | Type | Description | Notes
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]
**stackCount** | **int** | |
**stackParentId** | **String** | | [optional]
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
**thumbhash** | **String** | |
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |

View File

@@ -0,0 +1,16 @@
# openapi.model.UpdateStackParentDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**newParentId** | **String** | |
**oldParentId** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -176,6 +176,7 @@ part 'model/transcode_policy.dart';
part 'model/update_album_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart';
part 'model/update_stack_parent_dto.dart';
part 'model/update_tag_dto.dart';
part 'model/update_user_dto.dart';
part 'model/usage_by_user_dto.dart';

View File

@@ -1654,6 +1654,45 @@ class AssetApi {
}
}
/// Performs an HTTP 'PUT /asset/stack/parent' operation and returns the [Response].
/// Parameters:
///
/// * [UpdateStackParentDto] updateStackParentDto (required):
Future<Response> updateStackParentWithHttpInfo(UpdateStackParentDto updateStackParentDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/stack/parent';
// ignore: prefer_final_locals
Object? postBody = updateStackParentDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [UpdateStackParentDto] updateStackParentDto (required):
Future<void> updateStackParent(UpdateStackParentDto updateStackParentDto,) async {
final response = await updateStackParentWithHttpInfo(updateStackParentDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
/// Parameters:
///

View File

@@ -443,6 +443,8 @@ class ApiClient {
return UpdateAssetDto.fromJson(value);
case 'UpdateLibraryDto':
return UpdateLibraryDto.fromJson(value);
case 'UpdateStackParentDto':
return UpdateStackParentDto.fromJson(value);
case 'UpdateTagDto':
return UpdateTagDto.fromJson(value);
case 'UpdateUserDto':

View File

@@ -16,6 +16,8 @@ class AssetBulkUpdateDto {
this.ids = const [],
this.isArchived,
this.isFavorite,
this.removeParent,
this.stackParentId,
});
List<String> ids;
@@ -36,21 +38,41 @@ class AssetBulkUpdateDto {
///
bool? isFavorite;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? removeParent;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? stackParentId;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.ids == ids &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite;
other.isFavorite == isFavorite &&
other.removeParent == removeParent &&
other.stackParentId == stackParentId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode);
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(removeParent == null ? 0 : removeParent!.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode);
@override
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite]';
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -65,6 +87,16 @@ class AssetBulkUpdateDto {
} else {
// json[r'isFavorite'] = null;
}
if (this.removeParent != null) {
json[r'removeParent'] = this.removeParent;
} else {
// json[r'removeParent'] = null;
}
if (this.stackParentId != null) {
json[r'stackParentId'] = this.stackParentId;
} else {
// json[r'stackParentId'] = null;
}
return json;
}
@@ -81,6 +113,8 @@ class AssetBulkUpdateDto {
: const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
removeParent: mapValueOfType<bool>(json, r'removeParent'),
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
);
}
return null;

View File

@@ -38,6 +38,9 @@ class AssetResponseDto {
this.people = const [],
required this.resized,
this.smartInfo,
this.stack = const [],
required this.stackCount,
this.stackParentId,
this.tags = const [],
required this.thumbhash,
required this.type,
@@ -113,6 +116,12 @@ class AssetResponseDto {
///
SmartInfoResponseDto? smartInfo;
List<AssetResponseDto> stack;
int stackCount;
String? stackParentId;
List<TagResponseDto> tags;
String? thumbhash;
@@ -148,6 +157,9 @@ class AssetResponseDto {
other.people == people &&
other.resized == resized &&
other.smartInfo == smartInfo &&
other.stack == stack &&
other.stackCount == stackCount &&
other.stackParentId == stackParentId &&
other.tags == tags &&
other.thumbhash == thumbhash &&
other.type == type &&
@@ -181,13 +193,16 @@ class AssetResponseDto {
(people.hashCode) +
(resized.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(stack.hashCode) +
(stackCount.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode) +
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -231,6 +246,13 @@ class AssetResponseDto {
json[r'smartInfo'] = this.smartInfo;
} else {
// json[r'smartInfo'] = null;
}
json[r'stack'] = this.stack;
json[r'stackCount'] = this.stackCount;
if (this.stackParentId != null) {
json[r'stackParentId'] = this.stackParentId;
} else {
// json[r'stackParentId'] = null;
}
json[r'tags'] = this.tags;
if (this.thumbhash != null) {
@@ -276,6 +298,9 @@ class AssetResponseDto {
people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetResponseDto.listFromJson(json[r'stack']),
stackCount: mapValueOfType<int>(json, r'stackCount')!,
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -347,6 +372,7 @@ class AssetResponseDto {
'originalPath',
'ownerId',
'resized',
'stackCount',
'thumbhash',
'type',
'updatedAt',

View File

@@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class UpdateStackParentDto {
/// Returns a new [UpdateStackParentDto] instance.
UpdateStackParentDto({
required this.newParentId,
required this.oldParentId,
});
String newParentId;
String oldParentId;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateStackParentDto &&
other.newParentId == newParentId &&
other.oldParentId == oldParentId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(newParentId.hashCode) +
(oldParentId.hashCode);
@override
String toString() => 'UpdateStackParentDto[newParentId=$newParentId, oldParentId=$oldParentId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'newParentId'] = this.newParentId;
json[r'oldParentId'] = this.oldParentId;
return json;
}
/// Returns a new [UpdateStackParentDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UpdateStackParentDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UpdateStackParentDto(
newParentId: mapValueOfType<String>(json, r'newParentId')!,
oldParentId: mapValueOfType<String>(json, r'oldParentId')!,
);
}
return null;
}
static List<UpdateStackParentDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UpdateStackParentDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UpdateStackParentDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UpdateStackParentDto> mapFromJson(dynamic json) {
final map = <String, UpdateStackParentDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UpdateStackParentDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UpdateStackParentDto-objects as value to a dart map
static Map<String, List<UpdateStackParentDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UpdateStackParentDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UpdateStackParentDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'newParentId',
'oldParentId',
};
}

View File

@@ -174,6 +174,11 @@ void main() {
// TODO
});
//Future updateStackParent(UpdateStackParentDto updateStackParentDto) async
test('test updateStackParent', () async {
// TODO
});
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, String duration, bool isArchived, bool isExternal, bool isOffline, bool isReadOnly, bool isVisible, String libraryId, MultipartFile livePhotoData, MultipartFile sidecarData }) async
test('test uploadFile', () async {
// TODO

View File

@@ -31,6 +31,16 @@ void main() {
// TODO
});
// bool removeParent
test('to test the property `removeParent`', () async {
// TODO
});
// String stackParentId
test('to test the property `stackParentId`', () async {
// TODO
});
});

View File

@@ -142,6 +142,21 @@ void main() {
// TODO
});
// List<AssetResponseDto> stack (default value: const [])
test('to test the property `stack`', () async {
// TODO
});
// int stackCount
test('to test the property `stackCount`', () async {
// TODO
});
// String stackParentId
test('to test the property `stackParentId`', () async {
// TODO
});
// List<TagResponseDto> tags (default value: const [])
test('to test the property `tags`', () async {
// TODO

View File

@@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for UpdateStackParentDto
void main() {
// final instance = UpdateStackParentDto();
group('test UpdateStackParentDto', () {
// String newParentId
test('to test the property `newParentId`', () async {
// TODO
});
// String oldParentId
test('to test the property `oldParentId`', () async {
// TODO
});
});
}

View File

@@ -25,6 +25,7 @@ void main() {
isFavorite: false,
isArchived: false,
isTrashed: false,
stackCount: 0,
),
);
}

View File

@@ -35,6 +35,7 @@ void main() {
isFavorite: false,
isArchived: false,
isTrashed: false,
stackCount: 0,
);
}