mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 20:29:05 +00:00
feat: manual stack assets (#4198)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
47
mobile/lib/modules/home/models/selection_state.dart
Normal file
47
mobile/lib/modules/home/models/selection_state.dart
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()}");
|
||||
}
|
||||
|
||||
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@@ -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
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -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)
|
||||
|
||||
55
mobile/openapi/doc/AssetApi.md
generated
55
mobile/openapi/doc/AssetApi.md
generated
@@ -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)
|
||||
|
||||
|
||||
2
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
2
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
@@ -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)
|
||||
|
||||
|
||||
3
mobile/openapi/doc/AssetResponseDto.md
generated
3
mobile/openapi/doc/AssetResponseDto.md
generated
@@ -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) | |
|
||||
|
||||
16
mobile/openapi/doc/UpdateStackParentDto.md
generated
Normal file
16
mobile/openapi/doc/UpdateStackParentDto.md
generated
Normal 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)
|
||||
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -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';
|
||||
|
||||
39
mobile/openapi/lib/api/asset_api.dart
generated
39
mobile/openapi/lib/api/asset_api.dart
generated
@@ -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:
|
||||
///
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -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':
|
||||
|
||||
40
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
40
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -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;
|
||||
|
||||
28
mobile/openapi/lib/model/asset_response_dto.dart
generated
28
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -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',
|
||||
|
||||
106
mobile/openapi/lib/model/update_stack_parent_dto.dart
generated
Normal file
106
mobile/openapi/lib/model/update_stack_parent_dto.dart
generated
Normal 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',
|
||||
};
|
||||
}
|
||||
|
||||
5
mobile/openapi/test/asset_api_test.dart
generated
5
mobile/openapi/test/asset_api_test.dart
generated
@@ -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
|
||||
|
||||
10
mobile/openapi/test/asset_bulk_update_dto_test.dart
generated
10
mobile/openapi/test/asset_bulk_update_dto_test.dart
generated
@@ -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
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
15
mobile/openapi/test/asset_response_dto_test.dart
generated
15
mobile/openapi/test/asset_response_dto_test.dart
generated
@@ -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
|
||||
|
||||
32
mobile/openapi/test/update_stack_parent_dto_test.dart
generated
Normal file
32
mobile/openapi/test/update_stack_parent_dto_test.dart
generated
Normal 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
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ void main() {
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
stackCount: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ void main() {
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
stackCount: 0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user