From bcc2c34eef4a210657fecfa278c41b7a61903992 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Thu, 25 May 2023 05:52:43 +0200 Subject: [PATCH] feat(mobile): partner sharing (#2541) * feat(mobile): partner sharing * getAllAssets for other users * i18n * fix tests * try to fix web tests * shared with/by confusion * error logging * guard against outdated server version --- mobile/assets/i18n/en-US.json | 9 + mobile/lib/main.dart | 2 + .../providers/shared_album.provider.dart | 5 +- .../suggested_shared_users.provider.dart | 3 +- .../album/ui/sharing_sliver_appbar.dart | 85 -- .../album/views/asset_selection_page.dart | 4 +- ...lect_additional_user_for_sharing_page.dart | 2 +- .../views/select_user_for_sharing_page.dart | 3 +- .../lib/modules/album/views/sharing_page.dart | 104 ++- .../providers/archive_asset_provider.dart | 6 +- .../asset_viewer/ui/description_input.dart | 9 +- .../favorite/providers/favorite_provider.dart | 6 +- mobile/lib/modules/home/ui/delete_dialog.dart | 53 +- mobile/lib/modules/home/views/home_page.dart | 4 +- .../partner/providers/partner.provider.dart | 50 ++ .../partner/services/partner.service.dart | 72 ++ .../lib/modules/partner/ui/partner_list.dart | 30 + .../partner/views/partner_detail_page.dart | 40 + .../modules/partner/views/partner_page.dart | 160 ++++ mobile/lib/routing/router.dart | 5 + mobile/lib/routing/router.gr.dart | 78 ++ mobile/lib/shared/models/album.dart | 4 +- mobile/lib/shared/models/asset.dart | 6 +- mobile/lib/shared/models/etag.dart | 13 + mobile/lib/shared/models/etag.g.dart | 724 ++++++++++++++++++ mobile/lib/shared/models/user.dart | 12 +- mobile/lib/shared/models/user.g.dart | 124 ++- .../lib/shared/providers/asset.provider.dart | 22 +- .../lib/shared/providers/user.provider.dart | 26 + mobile/lib/shared/services/api.service.dart | 2 + mobile/lib/shared/services/asset.service.dart | 32 +- mobile/lib/shared/services/sync.service.dart | 26 +- mobile/lib/shared/services/user.service.dart | 50 +- mobile/lib/shared/ui/confirm_dialog.dart | 54 ++ mobile/lib/shared/ui/user_avatar.dart | 21 + mobile/lib/utils/db.dart | 4 + mobile/lib/utils/openapi_extensions.dart | 2 + mobile/openapi/doc/AssetApi.md | 6 +- mobile/openapi/lib/api/asset_api.dart | 13 +- mobile/openapi/test/asset_api_test.dart | 2 +- mobile/test/sync_service_test.dart | 32 +- .../immich/src/api-v1/asset/asset.service.ts | 5 +- .../src/api-v1/asset/dto/asset-search.dto.ts | 8 +- server/immich-openapi-specs.json | 9 + web/src/api/open-api/api.ts | 22 +- .../side-bar/side-bar.svelte | 2 +- web/src/routes/(user)/archive/+page.svelte | 2 +- web/src/routes/(user)/favorites/+page.svelte | 2 +- 48 files changed, 1729 insertions(+), 226 deletions(-) delete mode 100644 mobile/lib/modules/album/ui/sharing_sliver_appbar.dart create mode 100644 mobile/lib/modules/partner/providers/partner.provider.dart create mode 100644 mobile/lib/modules/partner/services/partner.service.dart create mode 100644 mobile/lib/modules/partner/ui/partner_list.dart create mode 100644 mobile/lib/modules/partner/views/partner_detail_page.dart create mode 100644 mobile/lib/modules/partner/views/partner_page.dart create mode 100644 mobile/lib/shared/models/etag.dart create mode 100644 mobile/lib/shared/models/etag.g.dart create mode 100644 mobile/lib/shared/providers/user.provider.dart create mode 100644 mobile/lib/shared/ui/confirm_dialog.dart create mode 100644 mobile/lib/shared/ui/user_avatar.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 8b204f9b2e..082a3f6de9 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -257,6 +257,15 @@ "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "Create shared album", "sharing_silver_appbar_share_partner": "Share with partner", + "partner_page_title": "Partner", + "partner_page_no_more_users": "No more users to add", + "partner_page_empty_message": "Your photos are not yet shared with any partner.", + "partner_page_shared_to_title": "Shared to", + "partner_page_select_partner": "Select partner", + "partner_page_add_partner": "Add partner", + "partner_page_partner_add_failed": "Failed to add partner", + "partner_page_stop_sharing_title": "Stop sharing your photos?", + "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index dda5bd604a..3c8cedc9de 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/etag.dart'; import 'package:immich_mobile/shared/models/exif_info.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart'; import 'package:immich_mobile/shared/models/store.dart'; @@ -89,6 +90,7 @@ Future loadDb() async { BackupAlbumSchema, DuplicatedAssetSchema, LoggerMessageSchema, + ETagSchema, ], directory: dir.path, maxSizeMiB: 256, diff --git a/mobile/lib/modules/album/providers/shared_album.provider.dart b/mobile/lib/modules/album/providers/shared_album.provider.dart index fd7b396dd4..a6fd8db23e 100644 --- a/mobile/lib/modules/album/providers/shared_album.provider.dart +++ b/mobile/lib/modules/album/providers/shared_album.provider.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.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:isar/isar.dart'; class SharedAlbumNotifier extends StateNotifier> { @@ -73,7 +74,9 @@ final sharedAlbumProvider = }); final sharedAlbumDetailProvider = - StreamProvider.autoDispose.family((ref, albumId) async* { + StreamProvider.family((ref, albumId) async* { + final user = ref.watch(currentUserProvider); + if (user == null) return; final AlbumService sharedAlbumService = ref.watch(albumServiceProvider); await for (final a in sharedAlbumService.watchAlbum(albumId)) { diff --git a/mobile/lib/modules/album/providers/suggested_shared_users.provider.dart b/mobile/lib/modules/album/providers/suggested_shared_users.provider.dart index 552afa9e99..a928ae73c8 100644 --- a/mobile/lib/modules/album/providers/suggested_shared_users.provider.dart +++ b/mobile/lib/modules/album/providers/suggested_shared_users.provider.dart @@ -2,8 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/services/user.service.dart'; -final suggestedSharedUsersProvider = - FutureProvider.autoDispose>((ref) { +final otherUsersProvider = FutureProvider.autoDispose>((ref) { UserService userService = ref.watch(userServiceProvider); return userService.getUsersInDb(); diff --git a/mobile/lib/modules/album/ui/sharing_sliver_appbar.dart b/mobile/lib/modules/album/ui/sharing_sliver_appbar.dart deleted file mode 100644 index 9643f1929e..0000000000 --- a/mobile/lib/modules/album/ui/sharing_sliver_appbar.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SharingSliverAppBar extends StatelessWidget { - const SharingSliverAppBar({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SliverAppBar( - centerTitle: true, - floating: false, - pinned: true, - snap: false, - automaticallyImplyLeading: false, - title: Text( - 'IMMICH', - style: TextStyle( - fontFamily: 'SnowburstOne', - fontWeight: FontWeight.bold, - fontSize: 22, - color: Theme.of(context).primaryColor, - ), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: ElevatedButton.icon( - onPressed: () { - AutoRouter.of(context) - .push(CreateAlbumRoute(isSharedAlbum: true)); - }, - icon: const Icon( - Icons.photo_album_outlined, - size: 20, - ), - label: const Text( - "sharing_silver_appbar_create_shared_album", - maxLines: 1, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, - // color: Theme.of(context).primaryColor, - ), - ).tr(), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: ElevatedButton.icon( - onPressed: null, - icon: const Icon( - Icons.swap_horizontal_circle_outlined, - size: 20, - ), - label: const Text( - "sharing_silver_appbar_share_partner", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, - ), - maxLines: 1, - ).tr(), - ), - ), - ) - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart index 74aac07405..7ea60e2496 100644 --- a/mobile/lib/modules/album/views/asset_selection_page.dart +++ b/mobile/lib/modules/album/views/asset_selection_page.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu 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'; class AssetSelectionPage extends HookConsumerWidget { const AssetSelectionPage({ @@ -21,7 +22,8 @@ class AssetSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final renderList = ref.watch(remoteAssetsProvider); + final currentUser = ref.watch(currentUserProvider); + final renderList = ref.watch(remoteAssetsProvider(currentUser?.isarId)); final selected = useState>(existingAssets); final selectionEnabledHook = useState(true); diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart index 1aadeb3a6e..ec7ddb17e4 100644 --- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart @@ -17,7 +17,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue> suggestedShareUsers = - ref.watch(suggestedSharedUsersProvider); + ref.watch(otherUsersProvider); final sharedUsersList = useState>({}); addNewUsersHandler() { diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart index 4130181c74..eaf9916459 100644 --- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart @@ -20,8 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final sharedUsersList = useState>({}); - AsyncValue> suggestedShareUsers = - ref.watch(suggestedSharedUsersProvider); + final suggestedShareUsers = ref.watch(otherUsersProvider); createSharedAlbum() async { var newAlbum = diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 68a2dda855..b2025ef864 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -5,10 +5,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; -import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart'; +import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; +import 'package:immich_mobile/modules/partner/ui/partner_list.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/album.dart'; -import 'package:immich_mobile/shared/models/store.dart' as store; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; class SharingPage extends HookConsumerWidget { @@ -17,7 +18,8 @@ class SharingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final List sharedAlbums = ref.watch(sharedAlbumProvider); - final userId = store.Store.get(store.StoreKey.currentUser).id; + final userId = ref.watch(currentUserProvider)?.id; + final partner = ref.watch(partnerSharedWithProvider); var isDarkMode = Theme.of(context).brightness == Brightness.dark; useEffect( @@ -63,8 +65,7 @@ class SharingPage extends HookConsumerWidget { final isOwner = album.ownerId == userId; return ListTile( - contentPadding: - const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: ImmichImage( @@ -93,7 +94,8 @@ class SharingPage extends HookConsumerWidget { ) : album.ownerName != null ? Text( - 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]), + 'album_thumbnail_shared_by' + .tr(args: [album.ownerName!]), style: const TextStyle( fontSize: 12.0, ), @@ -110,6 +112,75 @@ class SharingPage extends HookConsumerWidget { ); } + buildTopBottons() { + return Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + bottom: 12.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + AutoRouter.of(context) + .push(CreateAlbumRoute(isSharedAlbum: true)); + }, + icon: const Icon( + Icons.photo_album_outlined, + size: 20, + ), + label: const Text( + "sharing_silver_appbar_create_shared_album", + maxLines: 1, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ).tr(), + ), + ), + const SizedBox(width: 12.0), + Expanded( + child: ElevatedButton.icon( + onPressed: () => + AutoRouter.of(context).push(const PartnerRoute()), + icon: const Icon( + Icons.swap_horizontal_circle_outlined, + size: 20, + ), + label: const Text( + "sharing_silver_appbar_share_partner", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, + ), + maxLines: 1, + ).tr(), + ), + ) + ], + ), + ); + } + + AppBar buildAppBar() { + return AppBar( + centerTitle: true, + automaticallyImplyLeading: false, + title: const Text( + 'IMMICH', + style: TextStyle( + fontFamily: 'SnowburstOne', + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + ); + } + buildEmptyListIndication() { return SliverToBoxAdapter( child: Padding( @@ -123,7 +194,6 @@ class SharingPage extends HookConsumerWidget { width: 0.5, ), ), - // color: Colors.transparent, child: Padding( padding: const EdgeInsets.all(18.0), child: Column( @@ -160,11 +230,27 @@ class SharingPage extends HookConsumerWidget { } return Scaffold( + appBar: buildAppBar(), body: CustomScrollView( slivers: [ - const SharingSliverAppBar(), + SliverToBoxAdapter(child: buildTopBottons()), + if (partner.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4), + sliver: SliverToBoxAdapter( + child: const Text( + "partner_page_title", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + ), + ), + if (partner.isNotEmpty) PartnerList(partner: partner), SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + padding: EdgeInsets.only( + left: 12, + right: 12, + top: partner.isEmpty ? 0 : 16, + ), sliver: SliverToBoxAdapter( child: const Text( "sharing_page_album", diff --git a/mobile/lib/modules/archive/providers/archive_asset_provider.dart b/mobile/lib/modules/archive/providers/archive_asset_provider.dart index 70c8ac89e8..26d0b2eea9 100644 --- a/mobile/lib/modules/archive/providers/archive_asset_provider.dart +++ b/mobile/lib/modules/archive/providers/archive_asset_provider.dart @@ -3,16 +3,18 @@ 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:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:isar/isar.dart'; final archiveProvider = StreamProvider((ref) async* { + final user = ref.watch(currentUserProvider); + if (user == null) return; final query = ref .watch(dbProvider) .assets .filter() - .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) + .ownerIdEqualTo(user.isarId) .isArchivedEqualTo(true) .sortByFileCreatedAt(); final settings = ref.watch(appSettingsServiceProvider); diff --git a/mobile/lib/modules/asset_viewer/ui/description_input.dart b/mobile/lib/modules/asset_viewer/ui/description_input.dart index 9b54633f21..cc459d4732 100644 --- a/mobile/lib/modules/asset_viewer/ui/description_input.dart +++ b/mobile/lib/modules/asset_viewer/ui/description_input.dart @@ -4,9 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:logging/logging.dart'; -import 'package:immich_mobile/shared/models/store.dart' as store; class DescriptionInput extends HookConsumerWidget { DescriptionInput({ @@ -25,9 +25,10 @@ class DescriptionInput extends HookConsumerWidget { final focusNode = useFocusNode(); final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); - final descriptionProvider = ref.watch(assetDescriptionProvider(asset).notifier); + final descriptionProvider = + ref.watch(assetDescriptionProvider(asset).notifier); final description = ref.watch(assetDescriptionProvider(asset)); - final owner = store.Store.get(store.StoreKey.currentUser); + final owner = ref.watch(currentUserProvider); final hasError = useState(false); controller.text = description; @@ -67,7 +68,7 @@ class DescriptionInput extends HookConsumerWidget { } return TextField( - enabled: owner.isarId == asset.ownerId, + enabled: owner?.isarId == asset.ownerId, focusNode: focusNode, onTap: () => isFocus.value = true, onChanged: (value) { diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart index d95742f890..4ddb73ea69 100644 --- a/mobile/lib/modules/favorite/providers/favorite_provider.dart +++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart @@ -3,16 +3,18 @@ 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:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:isar/isar.dart'; final favoriteAssetsProvider = StreamProvider((ref) async* { + final user = ref.watch(currentUserProvider); + if (user == null) return; final query = ref .watch(dbProvider) .assets .filter() - .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) + .ownerIdEqualTo(user.isarId) .isFavoriteEqualTo(true) .sortByFileCreatedAt(); final settings = ref.watch(appSettingsServiceProvider); diff --git a/mobile/lib/modules/home/ui/delete_dialog.dart b/mobile/lib/modules/home/ui/delete_dialog.dart index f1ba868641..7d290cd1a7 100644 --- a/mobile/lib/modules/home/ui/delete_dialog.dart +++ b/mobile/lib/modules/home/ui/delete_dialog.dart @@ -1,47 +1,16 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -class DeleteDialog extends ConsumerWidget { +class DeleteDialog extends ConfirmDialog { final Function onDelete; - const DeleteDialog({Key? key, required this.onDelete}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - - return AlertDialog( - // backgroundColor: Colors.grey[200], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - title: const Text("delete_dialog_title").tr(), - content: const Text("delete_dialog_alert").tr(), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "delete_dialog_cancel", - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - TextButton( - onPressed: () { - onDelete(); - Navigator.of(context).pop(); - }, - child: Text( - "delete_dialog_ok", - style: TextStyle( - color: Colors.red[400], - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ], - ); - } + const DeleteDialog({Key? key, required this.onDelete}) + : super( + key: key, + title: "delete_dialog_title", + content: "delete_dialog_alert", + cancel: "delete_dialog_cancel", + ok: "delete_dialog_ok", + onOk: onDelete, + ); } diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index a7caf511f1..957b15133c 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -20,6 +20,7 @@ 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/providers/server_info.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/services/share.service.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; @@ -38,6 +39,7 @@ class HomePage extends HookConsumerWidget { final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); final sharedAlbums = ref.watch(sharedAlbumProvider); final albumService = ref.watch(albumServiceProvider); + final currentUser = ref.watch(currentUserProvider); final tipOneOpacity = useState(0.0); final refreshCount = useState(0); @@ -300,7 +302,7 @@ class HomePage extends HookConsumerWidget { bottom: false, child: Stack( children: [ - ref.watch(assetsProvider).when( + ref.watch(assetsProvider(currentUser?.isarId)).when( data: (data) => data.isEmpty ? buildLoadingIndicator() : ImmichAssetGrid( diff --git a/mobile/lib/modules/partner/providers/partner.provider.dart b/mobile/lib/modules/partner/providers/partner.provider.dart new file mode 100644 index 0000000000..d484356168 --- /dev/null +++ b/mobile/lib/modules/partner/providers/partner.provider.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +class PartnerSharedWithNotifier extends StateNotifier> { + PartnerSharedWithNotifier(Isar db) : super([]) { + final query = db.users.filter().isPartnerSharedWithEqualTo(true); + query.findAll().then((partners) => state = partners); + query.watch().listen((partners) => state = partners); + } +} + +final partnerSharedWithProvider = + StateNotifierProvider>((ref) { + return PartnerSharedWithNotifier(ref.watch(dbProvider)); +}); + +class PartnerSharedByNotifier extends StateNotifier> { + PartnerSharedByNotifier(Isar db) : super([]) { + final query = db.users.filter().isPartnerSharedByEqualTo(true); + query.findAll().then((partners) => state = partners); + streamSub = query.watch().listen((partners) => state = partners); + } + + late final StreamSubscription> streamSub; + + @override + void dispose() { + streamSub.cancel(); + super.dispose(); + } +} + +final partnerSharedByProvider = + StateNotifierProvider>((ref) { + return PartnerSharedByNotifier(ref.watch(dbProvider)); +}); + +final partnerAvailableProvider = + FutureProvider.autoDispose>((ref) async { + final otherUsers = await ref.watch(otherUsersProvider.future); + final currentPartners = ref.watch(partnerSharedByProvider); + final available = Set.of(otherUsers); + available.removeAll(currentPartners); + return available.toList(); +}); diff --git a/mobile/lib/modules/partner/services/partner.service.dart b/mobile/lib/modules/partner/services/partner.service.dart new file mode 100644 index 0000000000..42fcdb4381 --- /dev/null +++ b/mobile/lib/modules/partner/services/partner.service.dart @@ -0,0 +1,72 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; + +final partnerServiceProvider = Provider( + (ref) => PartnerService( + ref.watch(apiServiceProvider), + ref.watch(dbProvider), + ), +); + +enum PartnerDirection { + sharedWith("shared-with"), + sharedBy("shared-by"); + + const PartnerDirection( + this._value, + ); + + final String _value; +} + +class PartnerService { + final ApiService _apiService; + final Isar _db; + final Logger _log = Logger("PartnerService"); + + PartnerService(this._apiService, this._db); + + Future?> getPartners(PartnerDirection direction) async { + try { + final userDtos = + await _apiService.partnerApi.getPartners(direction._value); + if (userDtos != null) { + return userDtos.map((u) => User.fromDto(u)).toList(); + } + } catch (e) { + _log.warning("failed to get partners for direction $direction:\n$e"); + } + return null; + } + + Future removePartner(User partner) async { + try { + await _apiService.partnerApi.removePartner(partner.id); + partner.isPartnerSharedBy = false; + await _db.writeTxn(() => _db.users.put(partner)); + } catch (e) { + _log.warning("failed to remove partner ${partner.id}:\n$e"); + return false; + } + return true; + } + + Future addPartner(User partner) async { + try { + final dto = await _apiService.partnerApi.createPartner(partner.id); + if (dto != null) { + partner.isPartnerSharedBy = true; + await _db.writeTxn(() => _db.users.put(partner)); + return true; + } + } catch (e) { + _log.warning("failed to add partner ${partner.id}:\n$e"); + } + return false; + } +} diff --git a/mobile/lib/modules/partner/ui/partner_list.dart b/mobile/lib/modules/partner/ui/partner_list.dart new file mode 100644 index 0000000000..92bfcd15fa --- /dev/null +++ b/mobile/lib/modules/partner/ui/partner_list.dart @@ -0,0 +1,30 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/ui/user_avatar.dart'; + +class PartnerList extends HookConsumerWidget { + const PartnerList({Key? key, required this.partner}) : super(key: key); + + final List partner; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SliverList( + delegate: + SliverChildBuilderDelegate(listEntry, childCount: partner.length), + ); + } + + Widget listEntry(BuildContext context, int index) { + final User p = partner[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), + leading: userAvatar(context, p, radius: 30), + title: Text("${p.firstName} ${p.lastName}"), + onTap: () => AutoRouter.of(context).push(PartnerDetailRoute(partner: p)), + ); + } +} diff --git a/mobile/lib/modules/partner/views/partner_detail_page.dart b/mobile/lib/modules/partner/views/partner_detail_page.dart new file mode 100644 index 0000000000..a97e6a1d6c --- /dev/null +++ b/mobile/lib/modules/partner/views/partner_detail_page.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class PartnerDetailPage extends HookConsumerWidget { + const PartnerDetailPage({Key? key, required this.partner}) : super(key: key); + + final User partner; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assets = ref.watch(assetsProvider(partner.isarId)); + + return Scaffold( + appBar: AppBar( + title: Text("${partner.firstName} ${partner.lastName}"), + elevation: 0, + centerTitle: false, + ), + body: assets.when( + data: (renderList) => renderList.isEmpty + ? Padding( + padding: const EdgeInsets.all(16), + child: Text( + "It seems ${partner.firstName} does not have any photos...\n" + "Or your server version does not match the app version."), + ) + : ImmichAssetGrid( + renderList: renderList, + onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), + ), + error: (e, _) => Text("Error loading partners:\n$e"), + loading: () => const Center(child: ImmichLoadingIndicator()), + ), + ); + } +} diff --git a/mobile/lib/modules/partner/views/partner_page.dart b/mobile/lib/modules/partner/views/partner_page.dart new file mode 100644 index 0000000000..789f036c4a --- /dev/null +++ b/mobile/lib/modules/partner/views/partner_page.dart @@ -0,0 +1,160 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; +import 'package:immich_mobile/modules/partner/services/partner.service.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/ui/user_avatar.dart'; + +class PartnerPage extends HookConsumerWidget { + const PartnerPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final List partners = ref.watch(partnerSharedByProvider); + final availableUsers = ref.watch(partnerAvailableProvider); + + addNewUsersHandler() async { + final users = availableUsers.value; + if (users == null || users.isEmpty) { + ImmichToast.show( + context: context, + msg: "partner_page_no_more_users".tr(), + ); + return; + } + + final selectedUser = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("partner_page_select_partner").tr(), + children: [ + for (User u in users) + SimpleDialogOption( + onPressed: () => Navigator.pop(context, u), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: userAvatar(context, u), + ), + Text("${u.firstName} ${u.lastName}"), + ], + ), + ) + ], + ); + }, + ); + if (selectedUser != null) { + final ok = + await ref.read(partnerServiceProvider).addPartner(selectedUser); + if (ok) { + ref.invalidate(partnerSharedByProvider); + } else { + ImmichToast.show( + context: context, + msg: "partner_page_partner_add_failed".tr(), + toastType: ToastType.error, + ); + } + } + } + + onDeleteUser(User u) { + return showDialog( + context: context, + builder: (BuildContext context) { + return ConfirmDialog( + title: "partner_page_stop_sharing_title", + content: + "partner_page_stop_sharing_content".tr(args: [u.firstName]), + onOk: () => ref.read(partnerServiceProvider).removePartner(u), + ); + }, + ); + } + + buildUserList(List users) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 16.0), + child: const Text( + "partner_page_shared_to_title", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + if (users.isNotEmpty) + ListView.builder( + shrinkWrap: true, + itemCount: users.length, + itemBuilder: ((context, index) { + return ListTile( + leading: userAvatar(context, users[index]), + title: Text( + users[index].email, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + trailing: IconButton( + icon: const Icon(Icons.person_remove), + onPressed: () => onDeleteUser(users[index]), + ), + ); + }), + ), + if (users.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: const Text( + "partner_page_empty_message", + style: TextStyle(fontSize: 14), + ).tr(), + ), + ElevatedButton.icon( + onPressed: availableUsers.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + label: const Text("partner_page_add_partner").tr(), + ), + ], + ), + ), + ], + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text("partner_page_title").tr(), + elevation: 0, + centerTitle: false, + actions: [ + IconButton( + onPressed: + availableUsers.whenOrNull(data: (data) => addNewUsersHandler), + icon: const Icon(Icons.person_add), + tooltip: "partner_page_add_partner".tr(), + ) + ], + ), + body: buildUserList(partners), + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 34b8d9132e..cd731dc109 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -6,6 +6,8 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart'; import 'package:immich_mobile/modules/album/views/asset_selection_page.dart'; import 'package:immich_mobile/modules/album/views/create_album_page.dart'; import 'package:immich_mobile/modules/album/views/library_page.dart'; +import 'package:immich_mobile/modules/partner/views/partner_detail_page.dart'; +import 'package:immich_mobile/modules/partner/views/partner_page.dart'; import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/sharing_page.dart'; @@ -35,6 +37,7 @@ import 'package:immich_mobile/routing/duplicate_guard.dart'; import 'package:immich_mobile/routing/gallery_permission_guard.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/album.dart'; +import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; @@ -136,6 +139,8 @@ part 'router.gr.dart'; DuplicateGuard, ], ), + AutoRoute(page: PartnerPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard]) ], ) class AppRouter extends _$AppRouter { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 2a39ea6d7d..d79aac5f5d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -256,6 +256,22 @@ class _$AppRouter extends RootStackRouter { child: const ArchivePage(), ); }, + PartnerRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, + child: const PartnerPage(), + ); + }, + PartnerDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return MaterialPageX( + routeData: routeData, + child: PartnerDetailPage( + key: args.key, + partner: args.partner, + ), + ); + }, HomeRoute.name: (routeData) { return MaterialPageX( routeData: routeData, @@ -523,6 +539,22 @@ class _$AppRouter extends RootStackRouter { duplicateGuard, ], ), + RouteConfig( + PartnerRoute.name, + path: '/partner-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), + RouteConfig( + PartnerDetailRoute.name, + path: '/partner-detail-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), ]; } @@ -1113,6 +1145,52 @@ class ArchiveRoute extends PageRouteInfo { static const String name = 'ArchiveRoute'; } +/// generated route for +/// [PartnerPage] +class PartnerRoute extends PageRouteInfo { + const PartnerRoute() + : super( + PartnerRoute.name, + path: '/partner-page', + ); + + static const String name = 'PartnerRoute'; +} + +/// generated route for +/// [PartnerDetailPage] +class PartnerDetailRoute extends PageRouteInfo { + PartnerDetailRoute({ + Key? key, + required User partner, + }) : super( + PartnerDetailRoute.name, + path: '/partner-detail-page', + args: PartnerDetailRouteArgs( + key: key, + partner: partner, + ), + ); + + static const String name = 'PartnerDetailRoute'; +} + +class PartnerDetailRouteArgs { + const PartnerDetailRouteArgs({ + this.key, + required this.partner, + }); + + final Key? key; + + final User partner; + + @override + String toString() { + return 'PartnerDetailRouteArgs{key: $key, partner: $partner}'; + } +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index 508ab094aa..3223a465b9 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -87,8 +87,8 @@ class Album { remoteId == other.remoteId && localId == other.localId && name == other.name && - createdAt == other.createdAt && - modifiedAt == other.modifiedAt && + createdAt.isAtSameMomentAs(other.createdAt) && + modifiedAt.isAtSameMomentAs(other.modifiedAt) && shared == other.shared && owner.value == other.owner.value && thumbnail.value == other.thumbnail.value && diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index d8fedcd6b2..0bee110f8a 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -179,9 +179,9 @@ class Asset { localId == other.localId && deviceId == other.deviceId && ownerId == other.ownerId && - fileCreatedAt == other.fileCreatedAt && - fileModifiedAt == other.fileModifiedAt && - updatedAt == other.updatedAt && + fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) && + fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) && + updatedAt.isAtSameMomentAs(other.updatedAt) && durationInSeconds == other.durationInSeconds && type == other.type && width == other.width && diff --git a/mobile/lib/shared/models/etag.dart b/mobile/lib/shared/models/etag.dart new file mode 100644 index 0000000000..2f13898992 --- /dev/null +++ b/mobile/lib/shared/models/etag.dart @@ -0,0 +1,13 @@ +import 'package:immich_mobile/utils/hash.dart'; +import 'package:isar/isar.dart'; + +part 'etag.g.dart'; + +@Collection(inheritance: false) +class ETag { + ETag({required this.id, this.value}); + Id get isarId => fastHash(id); + @Index(unique: true, replace: true, type: IndexType.hash) + String id; + String? value; +} diff --git a/mobile/lib/shared/models/etag.g.dart b/mobile/lib/shared/models/etag.g.dart new file mode 100644 index 0000000000..acfec7040e --- /dev/null +++ b/mobile/lib/shared/models/etag.g.dart @@ -0,0 +1,724 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'etag.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetETagCollection on Isar { + IsarCollection get eTags => this.collection(); +} + +const ETagSchema = CollectionSchema( + name: r'ETag', + id: -644290296585643859, + properties: { + r'id': PropertySchema( + id: 0, + name: r'id', + type: IsarType.string, + ), + r'value': PropertySchema( + id: 1, + name: r'value', + type: IsarType.string, + ) + }, + estimateSize: _eTagEstimateSize, + serialize: _eTagSerialize, + deserialize: _eTagDeserialize, + deserializeProp: _eTagDeserializeProp, + idName: r'isarId', + indexes: { + r'id': IndexSchema( + id: -3268401673993471357, + name: r'id', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'id', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _eTagGetId, + getLinks: _eTagGetLinks, + attach: _eTagAttach, + version: '3.0.5', +); + +int _eTagEstimateSize( + ETag object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.id.length * 3; + { + final value = object.value; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _eTagSerialize( + ETag object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.id); + writer.writeString(offsets[1], object.value); +} + +ETag _eTagDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = ETag( + id: reader.readString(offsets[0]), + value: reader.readStringOrNull(offsets[1]), + ); + return object; +} + +P _eTagDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readStringOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _eTagGetId(ETag object) { + return object.isarId; +} + +List> _eTagGetLinks(ETag object) { + return []; +} + +void _eTagAttach(IsarCollection col, Id id, ETag object) {} + +extension ETagByIndex on IsarCollection { + Future getById(String id) { + return getByIndex(r'id', [id]); + } + + ETag? getByIdSync(String id) { + return getByIndexSync(r'id', [id]); + } + + Future deleteById(String id) { + return deleteByIndex(r'id', [id]); + } + + bool deleteByIdSync(String id) { + return deleteByIndexSync(r'id', [id]); + } + + Future> getAllById(List idValues) { + final values = idValues.map((e) => [e]).toList(); + return getAllByIndex(r'id', values); + } + + List getAllByIdSync(List idValues) { + final values = idValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'id', values); + } + + Future deleteAllById(List idValues) { + final values = idValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'id', values); + } + + int deleteAllByIdSync(List idValues) { + final values = idValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'id', values); + } + + Future putById(ETag object) { + return putByIndex(r'id', object); + } + + Id putByIdSync(ETag object, {bool saveLinks = true}) { + return putByIndexSync(r'id', object, saveLinks: saveLinks); + } + + Future> putAllById(List objects) { + return putAllByIndex(r'id', objects); + } + + List putAllByIdSync(List objects, {bool saveLinks = true}) { + return putAllByIndexSync(r'id', objects, saveLinks: saveLinks); + } +} + +extension ETagQueryWhereSort on QueryBuilder { + QueryBuilder anyIsarId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension ETagQueryWhere on QueryBuilder { + QueryBuilder isarIdEqualTo(Id isarId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: isarId, + upper: isarId, + )); + }); + } + + QueryBuilder isarIdNotEqualTo(Id isarId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: isarId, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: isarId, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: isarId, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: isarId, includeUpper: false), + ); + } + }); + } + + QueryBuilder isarIdGreaterThan(Id isarId, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: isarId, includeLower: include), + ); + }); + } + + QueryBuilder isarIdLessThan(Id isarId, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: isarId, includeUpper: include), + ); + }); + } + + QueryBuilder isarIdBetween( + Id lowerIsarId, + Id upperIsarId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerIsarId, + includeLower: includeLower, + upper: upperIsarId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder idEqualTo(String id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'id', + value: [id], + )); + }); + } + + QueryBuilder idNotEqualTo(String id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'id', + lower: [], + upper: [id], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'id', + lower: [id], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'id', + lower: [id], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'id', + lower: [], + upper: [id], + includeUpper: false, + )); + } + }); + } +} + +extension ETagQueryFilter on QueryBuilder { + QueryBuilder idEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idBetween( + 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'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idContains(String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'id', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idMatches(String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'id', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder idIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: '', + )); + }); + } + + QueryBuilder idIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'id', + value: '', + )); + }); + } + + QueryBuilder isarIdEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder isarIdGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder isarIdLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder isarIdBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'isarId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder valueIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'value', + )); + }); + } + + QueryBuilder valueIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'value', + )); + }); + } + + QueryBuilder valueEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueBetween( + 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'value', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueContains(String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueMatches(String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'value', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: '', + )); + }); + } + + QueryBuilder valueIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'value', + value: '', + )); + }); + } +} + +extension ETagQueryObject on QueryBuilder {} + +extension ETagQueryLinks on QueryBuilder {} + +extension ETagQuerySortBy on QueryBuilder { + QueryBuilder sortById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder sortByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder sortByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder sortByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension ETagQuerySortThenBy on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByIsarId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.asc); + }); + } + + QueryBuilder thenByIsarIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.desc); + }); + } + + QueryBuilder thenByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder thenByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension ETagQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctById( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'id', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByValue( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'value', caseSensitive: caseSensitive); + }); + } +} + +extension ETagQueryProperty on QueryBuilder { + QueryBuilder isarIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isarId'); + }); + } + + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder valueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'value'); + }); + } +} diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index 49a47c10a8..d6e3d487cd 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -14,6 +14,8 @@ class User { required this.firstName, required this.lastName, required this.isAdmin, + this.isPartnerSharedBy = false, + this.isPartnerSharedWith = false, }); Id get isarId => fastHash(id); @@ -26,6 +28,8 @@ class User { email = dto.email, firstName = dto.firstName, lastName = dto.lastName, + isPartnerSharedBy = false, + isPartnerSharedWith = false, isAdmin = dto.isAdmin; @Index(unique: true, replace: false, type: IndexType.hash) @@ -34,6 +38,8 @@ class User { String email; String firstName; String lastName; + bool isPartnerSharedBy; + bool isPartnerSharedWith; bool isAdmin; @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); @@ -44,10 +50,12 @@ class User { bool operator ==(other) { if (other is! User) return false; return id == other.id && - updatedAt == other.updatedAt && + updatedAt.isAtSameMomentAs(other.updatedAt) && email == other.email && firstName == other.firstName && lastName == other.lastName && + isPartnerSharedBy == other.isPartnerSharedBy && + isPartnerSharedWith == other.isPartnerSharedWith && isAdmin == other.isAdmin; } @@ -59,5 +67,7 @@ class User { email.hashCode ^ firstName.hashCode ^ lastName.hashCode ^ + isPartnerSharedBy.hashCode ^ + isPartnerSharedWith.hashCode ^ isAdmin.hashCode; } diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 3ba4e0e998..d70fd02df2 100644 --- a/mobile/lib/shared/models/user.g.dart +++ b/mobile/lib/shared/models/user.g.dart @@ -37,13 +37,23 @@ const UserSchema = CollectionSchema( name: r'isAdmin', type: IsarType.bool, ), - r'lastName': PropertySchema( + r'isPartnerSharedBy': PropertySchema( id: 4, + name: r'isPartnerSharedBy', + type: IsarType.bool, + ), + r'isPartnerSharedWith': PropertySchema( + id: 5, + name: r'isPartnerSharedWith', + type: IsarType.bool, + ), + r'lastName': PropertySchema( + id: 6, name: r'lastName', type: IsarType.string, ), r'updatedAt': PropertySchema( - id: 5, + id: 7, name: r'updatedAt', type: IsarType.dateTime, ) @@ -114,8 +124,10 @@ void _userSerialize( writer.writeString(offsets[1], object.firstName); writer.writeString(offsets[2], object.id); writer.writeBool(offsets[3], object.isAdmin); - writer.writeString(offsets[4], object.lastName); - writer.writeDateTime(offsets[5], object.updatedAt); + writer.writeBool(offsets[4], object.isPartnerSharedBy); + writer.writeBool(offsets[5], object.isPartnerSharedWith); + writer.writeString(offsets[6], object.lastName); + writer.writeDateTime(offsets[7], object.updatedAt); } User _userDeserialize( @@ -129,8 +141,10 @@ User _userDeserialize( firstName: reader.readString(offsets[1]), id: reader.readString(offsets[2]), isAdmin: reader.readBool(offsets[3]), - lastName: reader.readString(offsets[4]), - updatedAt: reader.readDateTime(offsets[5]), + isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, + isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, + lastName: reader.readString(offsets[6]), + updatedAt: reader.readDateTime(offsets[7]), ); return object; } @@ -151,8 +165,12 @@ P _userDeserializeProp

( case 3: return (reader.readBool(offset)) as P; case 4: - return (reader.readString(offset)) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 5: + return (reader.readBoolOrNull(offset) ?? false) as P; + case 6: + return (reader.readString(offset)) as P; + case 7: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -741,6 +759,26 @@ extension UserQueryFilter on QueryBuilder { }); } + QueryBuilder isPartnerSharedByEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isPartnerSharedBy', + value: value, + )); + }); + } + + QueryBuilder isPartnerSharedWithEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isPartnerSharedWith', + value: value, + )); + }); + } + QueryBuilder isarIdEqualTo(Id value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( @@ -1140,6 +1178,30 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByIsPartnerSharedBy() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedBy', Sort.asc); + }); + } + + QueryBuilder sortByIsPartnerSharedByDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedBy', Sort.desc); + }); + } + + QueryBuilder sortByIsPartnerSharedWith() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedWith', Sort.asc); + }); + } + + QueryBuilder sortByIsPartnerSharedWithDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedWith', Sort.desc); + }); + } + QueryBuilder sortByLastName() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'lastName', Sort.asc); @@ -1214,6 +1276,30 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByIsPartnerSharedBy() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedBy', Sort.asc); + }); + } + + QueryBuilder thenByIsPartnerSharedByDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedBy', Sort.desc); + }); + } + + QueryBuilder thenByIsPartnerSharedWith() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedWith', Sort.asc); + }); + } + + QueryBuilder thenByIsPartnerSharedWithDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isPartnerSharedWith', Sort.desc); + }); + } + QueryBuilder thenByIsarId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isarId', Sort.asc); @@ -1279,6 +1365,18 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByIsPartnerSharedBy() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isPartnerSharedBy'); + }); + } + + QueryBuilder distinctByIsPartnerSharedWith() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isPartnerSharedWith'); + }); + } + QueryBuilder distinctByLastName( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1324,6 +1422,18 @@ extension UserQueryProperty on QueryBuilder { }); } + QueryBuilder isPartnerSharedByProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isPartnerSharedBy'); + }); + } + + QueryBuilder isPartnerSharedWithProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isPartnerSharedWith'); + }); + } + QueryBuilder lastNameProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'lastName'); diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 4b422ecd7a..c1384f4029 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; 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/services/asset.service.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; @@ -10,6 +11,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; +import 'package:immich_mobile/shared/services/user.service.dart'; import 'package:immich_mobile/utils/db.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -23,6 +25,7 @@ class AssetsState {} class AssetNotifier extends StateNotifier { final AssetService _assetService; final AlbumService _albumService; + final UserService _userService; final SyncService _syncService; final Isar _db; final log = Logger('AssetNotifier'); @@ -32,6 +35,7 @@ class AssetNotifier extends StateNotifier { AssetNotifier( this._assetService, this._albumService, + this._userService, this._syncService, this._db, ) : super(AssetsState()); @@ -51,6 +55,12 @@ class AssetNotifier extends StateNotifier { final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newLocal = await _albumService.refreshDeviceAlbums(); debugPrint("newRemote: $newRemote, newLocal: $newLocal"); + await _userService.refreshUsers(); + final List partners = + await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll(); + for (User u in partners) { + await _assetService.refreshRemoteAssets(u); + } log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); } finally { _getAllAssetInProgress = false; @@ -147,6 +157,7 @@ final assetProvider = StateNotifierProvider((ref) { return AssetNotifier( ref.watch(assetServiceProvider), ref.watch(albumServiceProvider), + ref.watch(userServiceProvider), ref.watch(syncServiceProvider), ref.watch(dbProvider), ); @@ -161,12 +172,14 @@ final assetDetailProvider = } }); -final assetsProvider = StreamProvider.autoDispose((ref) async* { +final assetsProvider = + StreamProvider.family((ref, userId) async* { + if (userId == null) return; final query = ref .watch(dbProvider) .assets .filter() - .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) + .ownerIdEqualTo(userId) .isArchivedEqualTo(false) .sortByFileCreatedAtDesc(); final settings = ref.watch(appSettingsServiceProvider); @@ -179,14 +192,15 @@ final assetsProvider = StreamProvider.autoDispose((ref) async* { }); final remoteAssetsProvider = - StreamProvider.autoDispose((ref) async* { + StreamProvider.family((ref, userId) async* { + if (userId == null) return; final query = ref .watch(dbProvider) .assets .where() .remoteIdIsNotNull() .filter() - .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) + .ownerIdEqualTo(userId) .sortByFileCreatedAt(); final settings = ref.watch(appSettingsServiceProvider); final groupBy = diff --git a/mobile/lib/shared/providers/user.provider.dart b/mobile/lib/shared/providers/user.provider.dart new file mode 100644 index 0000000000..df8ff328dd --- /dev/null +++ b/mobile/lib/shared/providers/user.provider.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/models/user.dart'; + +class CurrentUserProvider extends StateNotifier { + CurrentUserProvider() : super(null) { + state = Store.tryGet(StoreKey.currentUser); + streamSub = + Store.watch(StoreKey.currentUser).listen((user) => state = user); + } + + late final StreamSubscription streamSub; + + @override + void dispose() { + streamSub.cancel(); + super.dispose(); + } +} + +final currentUserProvider = + StateNotifierProvider((ref) { + return CurrentUserProvider(); +}); diff --git a/mobile/lib/shared/services/api.service.dart b/mobile/lib/shared/services/api.service.dart index c957023f96..fc960a52e9 100644 --- a/mobile/lib/shared/services/api.service.dart +++ b/mobile/lib/shared/services/api.service.dart @@ -16,6 +16,7 @@ class ApiService { late AssetApi assetApi; late SearchApi searchApi; late ServerInfoApi serverInfoApi; + late PartnerApi partnerApi; ApiService() { final endpoint = Store.tryGet(StoreKey.serverEndpoint); @@ -37,6 +38,7 @@ class ApiService { assetApi = AssetApi(_apiClient); serverInfoApi = ServerInfoApi(_apiClient); searchApi = SearchApi(_apiClient); + partnerApi = PartnerApi(_apiClient); } Future resolveAndSetEndpoint(String serverUrl) async { diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index edd0df4366..6981cc5251 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -3,8 +3,10 @@ import 'dart:async'; 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/models/etag.dart'; 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/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; @@ -36,37 +38,47 @@ class AssetService { /// Checks the server for updated assets and updates the local database if /// required. Returns `true` if there were any changes. - Future refreshRemoteAssets() async { + Future refreshRemoteAssets([User? user]) async { + user ??= Store.get(StoreKey.currentUser); final Stopwatch sw = Stopwatch()..start(); final int numOwnedRemoteAssets = await _db.assets .where() .remoteIdIsNotNull() .filter() - .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) + .ownerIdEqualTo(user!.isarId) .count(); final bool changes = await _syncService.syncRemoteAssetsToDb( - () async => (await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0)) - ?.map(Asset.remote) - .toList(), + user, + () async => (await _getRemoteAssets( + hasCache: numOwnedRemoteAssets > 0, + user: user!, + )), ); debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); return changes; } /// Returns `null` if the server state did not change, else list of assets - Future?> _getRemoteAssets({ + Future?> _getRemoteAssets({ required bool hasCache, + required User user, }) async { try { - final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null; + final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null; final (List? assets, String? newETag) = - await _apiService.assetApi.getAllAssetsWithETag(eTag: etag); + await _apiService.assetApi + .getAllAssetsWithETag(eTag: etag, userId: user.id); if (assets == null) { return null; + } else if (assets.isNotEmpty && assets.first.ownerId != user.id) { + log.warning("Make sure that server and app versions match!" + " The server returned assets for user ${assets.first.ownerId}" + " while requesting assets of user ${user.id}"); + return null; } else if (newETag != etag) { - Store.put(StoreKey.assetETag, newETag); + _db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag))); } - return assets; + return assets.map(Asset.remote).toList(); } catch (e, stack) { log.severe('Error while getting remote assets', e, stack); return null; diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index b02aa9c15c..fae95a84fc 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -40,7 +40,9 @@ class SyncService { dbUsers, compare: (User a, User b) => a.id.compareTo(b.id), both: (User a, User b) { - if (!a.updatedAt.isAtSameMomentAs(b.updatedAt)) { + if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || + a.isPartnerSharedBy != b.isPartnerSharedBy || + a.isPartnerSharedWith != b.isPartnerSharedWith) { toUpsert.add(a); return true; } @@ -61,9 +63,10 @@ class SyncService { /// Syncs remote assets owned by the logged-in user to the DB /// Returns `true` if there were any changes Future syncRemoteAssetsToDb( + User user, FutureOr?> Function() loadAssets, ) => - _lock.run(() => _syncRemoteAssetsToDb(loadAssets)); + _lock.run(() => _syncRemoteAssetsToDb(user, loadAssets)); /// Syncs remote albums to the database /// returns `true` if there were any changes @@ -149,13 +152,13 @@ class SyncService { /// Syncs remote assets to the databas /// returns `true` if there were any changes Future _syncRemoteAssetsToDb( + User user, FutureOr?> Function() loadAssets, ) async { final List? remote = await loadAssets(); if (remote == null) { return false; } - final User user = Store.get(StoreKey.currentUser); final List inDb = await _db.assets .filter() .ownerIdEqualTo(user.isarId) @@ -349,10 +352,19 @@ class SyncService { ); } else if (album.shared) { final User user = Store.get(StoreKey.currentUser); - // delete assets in DB unless they belong to this user or are part of some other shared album - deleteCandidates.addAll( - await album.assets.filter().not().ownerIdEqualTo(user.isarId).findAll(), - ); + // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner + final userIds = await _db.users + .filter() + .isPartnerSharedWithEqualTo(true) + .isarIdProperty() + .findAll(); + userIds.add(user.isarId); + final orphanedAssets = await album.assets + .filter() + .not() + .anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id)) + .findAll(); + deleteCandidates.addAll(orphanedAssets); } try { final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id)); diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart index 757dde5b8d..89cf25f649 100644 --- a/mobile/lib/shared/services/user.service.dart +++ b/mobile/lib/shared/services/user.service.dart @@ -1,16 +1,19 @@ -import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:http_parser/http_parser.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/modules/partner/services/partner.service.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; +import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/files_helper.dart'; import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final userServiceProvider = Provider( @@ -18,6 +21,7 @@ final userServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(dbProvider), ref.watch(syncServiceProvider), + ref.watch(partnerServiceProvider), ), ); @@ -25,15 +29,22 @@ class UserService { final ApiService _apiService; final Isar _db; final SyncService _syncService; + final PartnerService _partnerService; + final Logger _log = Logger("UserService"); - UserService(this._apiService, this._db, this._syncService); + UserService( + this._apiService, + this._db, + this._syncService, + this._partnerService, + ); Future?> _getAllUsers({required bool isAll}) async { try { final dto = await _apiService.userApi.getAllUsers(isAll); return dto?.map(User.fromDto).toList(); } catch (e) { - debugPrint("Error [getAllUsersInfo] ${e.toString()}"); + _log.warning("Failed get all users:\n$e"); return null; } } @@ -62,16 +73,45 @@ class UserService { ), ); } catch (e) { - debugPrint("Error [uploadProfileImage] ${e.toString()}"); + _log.warning("Failed to upload profile image:\n$e"); return null; } } Future refreshUsers() async { final List? users = await _getAllUsers(isAll: true); - if (users == null) { + final List? sharedBy = + await _partnerService.getPartners(PartnerDirection.sharedBy); + final List? sharedWith = + await _partnerService.getPartners(PartnerDirection.sharedWith); + + if (users == null || sharedBy == null || sharedWith == null) { + _log.warning("Failed to refresh users"); return false; } + + users.sortBy((u) => u.id); + sharedBy.sortBy((u) => u.id); + sharedWith.sortBy((u) => u.id); + + diffSortedListsSync( + users, + sharedBy, + compare: (User a, User b) => a.id.compareTo(b.id), + both: (User a, User b) => a.isPartnerSharedBy = true, + onlyFirst: (_) {}, + onlySecond: (_) {}, + ); + + diffSortedListsSync( + users, + sharedWith, + compare: (User a, User b) => a.id.compareTo(b.id), + both: (User a, User b) => a.isPartnerSharedWith = true, + onlyFirst: (_) {}, + onlySecond: (_) {}, + ); + return _syncService.syncUsersFromServer(users); } } diff --git a/mobile/lib/shared/ui/confirm_dialog.dart b/mobile/lib/shared/ui/confirm_dialog.dart new file mode 100644 index 0000000000..87d77ecd01 --- /dev/null +++ b/mobile/lib/shared/ui/confirm_dialog.dart @@ -0,0 +1,54 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class ConfirmDialog extends ConsumerWidget { + final Function onOk; + final String title; + final String content; + final String cancel; + final String ok; + + const ConfirmDialog({ + Key? key, + required this.onOk, + required this.title, + required this.content, + this.cancel = "delete_dialog_cancel", + this.ok = "backup_controller_page_background_battery_info_ok", + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text(title).tr(), + content: Text(content).tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + cancel, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + TextButton( + onPressed: () { + onOk(); + Navigator.of(context).pop(); + }, + child: Text( + ok, + style: TextStyle( + color: Colors.red[400], + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ], + ); + } +} diff --git a/mobile/lib/shared/ui/user_avatar.dart b/mobile/lib/shared/ui/user_avatar.dart new file mode 100644 index 0000000000..23272870ea --- /dev/null +++ b/mobile/lib/shared/ui/user_avatar.dart @@ -0,0 +1,21 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/models/user.dart'; + +Widget userAvatar(BuildContext context, User u, {double? radius}) { + final url = + "${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${u.id}"; + return CircleAvatar( + radius: radius, + backgroundColor: Theme.of(context).primaryColor.withAlpha(50), + foregroundImage: CachedNetworkImageProvider( + url, + headers: {"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"}, + cacheKey: "user-${u.id}-profile", + ), + // silence errors if user has no profile image, use initials as fallback + onForegroundImageError: (exception, stackTrace) {}, + child: Text((u.firstName[0] + u.lastName[0]).toUpperCase()), + ); +} diff --git a/mobile/lib/utils/db.dart b/mobile/lib/utils/db.dart index 3892e20cb5..354fc08ca9 100644 --- a/mobile/lib/utils/db.dart +++ b/mobile/lib/utils/db.dart @@ -1,7 +1,9 @@ import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/etag.dart'; 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:isar/isar.dart'; Future clearAssetsAndAlbums(Isar db) async { @@ -10,5 +12,7 @@ Future clearAssetsAndAlbums(Isar db) async { await db.assets.clear(); await db.exifInfos.clear(); await db.albums.clear(); + await db.eTags.clear(); + await db.users.clear(); }); } diff --git a/mobile/lib/utils/openapi_extensions.dart b/mobile/lib/utils/openapi_extensions.dart index d45cb94cf3..435410a344 100644 --- a/mobile/lib/utils/openapi_extensions.dart +++ b/mobile/lib/utils/openapi_extensions.dart @@ -14,9 +14,11 @@ extension WithETag on AssetApi { /// ETag of data already cached on the client Future<(List? assets, String? eTag)> getAllAssetsWithETag({ String? eTag, + String? userId, }) async { final response = await getAllAssetsWithHttpInfo( ifNoneMatch: eTag, + userId: userId, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index ed78743548..2e80103317 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -553,7 +553,7 @@ Name | Type | Description | Notes [[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) # **getAllAssets** -> List getAllAssets(isFavorite, isArchived, skip, ifNoneMatch) +> List getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch) @@ -578,13 +578,14 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final isFavorite = true; // bool | final isArchived = true; // bool | final skip = 8.14; // num | final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client try { - final result = api_instance.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch); + final result = api_instance.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch); print(result); } catch (e) { print('Exception when calling AssetApi->getAllAssets: $e\n'); @@ -595,6 +596,7 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- + **userId** | **String**| | [optional] **isFavorite** | **bool**| | [optional] **isArchived** | **bool**| | [optional] **skip** | **num**| | [optional] diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 1bde075f37..dfe61149b4 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -519,6 +519,8 @@ class AssetApi { /// /// Parameters: /// + /// * [String] userId: + /// /// * [bool] isFavorite: /// /// * [bool] isArchived: @@ -527,7 +529,7 @@ class AssetApi { /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future getAllAssetsWithHttpInfo({ bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { + Future getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { // ignore: prefer_const_declarations final path = r'/asset'; @@ -538,6 +540,9 @@ class AssetApi { final headerParams = {}; final formParams = {}; + if (userId != null) { + queryParams.addAll(_queryParams('', 'userId', userId)); + } if (isFavorite != null) { queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); } @@ -570,6 +575,8 @@ class AssetApi { /// /// Parameters: /// + /// * [String] userId: + /// /// * [bool] isFavorite: /// /// * [bool] isArchived: @@ -578,8 +585,8 @@ class AssetApi { /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future?> getAllAssets({ bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { - final response = await getAllAssetsWithHttpInfo( isFavorite: isFavorite, isArchived: isArchived, skip: skip, ifNoneMatch: ifNoneMatch, ); + Future?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { + final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, skip: skip, ifNoneMatch: ifNoneMatch, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 91c3d613c0..cbbd403bb2 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -72,7 +72,7 @@ void main() { // Get all AssetEntity belong to the user // - //Future> getAllAssets({ bool isFavorite, bool isArchived, num skip, String ifNoneMatch }) async + //Future> getAllAssets({ String userId, bool isFavorite, bool isArchived, num skip, String ifNoneMatch }) async test('test getAllAssets', () async { // TODO }); diff --git a/mobile/test/sync_service_test.dart b/mobile/test/sync_service_test.dart index edcc0851b5..177e9439c3 100644 --- a/mobile/test/sync_service_test.dart +++ b/mobile/test/sync_service_test.dart @@ -52,6 +52,14 @@ void main() { group('Test SyncService grouped', () { late final Isar db; + final owner = User( + id: "1", + updatedAt: DateTime.now(), + email: "a@b.c", + firstName: "first", + lastName: "last", + isAdmin: false, + ); setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); await Isar.initializeIsarCore(download: true); @@ -59,17 +67,7 @@ void main() { ImmichLogger(); db.writeTxnSync(() => db.clearSync()); Store.init(db); - await Store.put( - StoreKey.currentUser, - User( - id: "1", - updatedAt: DateTime.now(), - email: "a@b.c", - firstName: "first", - lastName: "last", - isAdmin: false, - ), - ); + await Store.put(StoreKey.currentUser, owner); }); final List initialAssets = [ makeAsset(localId: "1", remoteId: "0-1", deviceId: 0), @@ -92,7 +90,7 @@ void main() { makeAsset(localId: "1", remoteId: "1-1"), ]; expect(db.assets.countSync(), 5); - final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c1, false); expect(db.assets.countSync(), 5); }); @@ -108,7 +106,7 @@ void main() { makeAsset(localId: "1", remoteId: "3-1", deviceId: 3), ]; expect(db.assets.countSync(), 5); - final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c1, true); expect(db.assets.countSync(), 7); }); @@ -124,19 +122,19 @@ void main() { makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2), ]; expect(db.assets.countSync(), 5); - final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c1, true); expect(db.assets.countSync(), 8); - final bool c2 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c2 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c2, false); expect(db.assets.countSync(), 8); remoteAssets.removeAt(4); - final bool c3 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c3, true); expect(db.assets.countSync(), 7); remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2)); remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2)); - final bool c4 = await s.syncRemoteAssetsToDb(() => remoteAssets); + final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets); expect(c4, true); expect(db.assets.countSync(), 9); }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 66fa091913..6ee02d3dea 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -150,7 +150,10 @@ export class AssetService { } public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise { - const assets = await this._assetRepository.getAllByUserId(authUser.id, dto); + if (dto.userId && dto.userId !== authUser.id) { + await this.checkUserAccess(authUser, dto.userId); + } + const assets = await this._assetRepository.getAllByUserId(dto.userId || authUser.id, dto); return assets.map((asset) => mapAsset(asset)); } diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts index ab7812220c..84bededd7b 100644 --- a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts @@ -1,5 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator'; import { toBoolean } from '../../../utils/transform.util'; export class AssetSearchDto { @@ -18,4 +19,9 @@ export class AssetSearchDto { @IsOptional() @IsNumber() skip?: number; + + @IsOptional() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + userId?: string; } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 225a2e9878..48181fe679 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2853,6 +2853,15 @@ "operationId": "getAllAssets", "description": "Get all AssetEntity belong to the user", "parameters": [ + { + "name": "userId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "isFavorite", "required": false, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index cc3e37ee77..e2a4ebe698 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4599,6 +4599,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * Get all AssetEntity belong to the user + * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] * @param {number} [skip] @@ -4606,7 +4607,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -4628,6 +4629,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (userId !== undefined) { + localVarQueryParameter['userId'] = userId; + } + if (isFavorite !== undefined) { localVarQueryParameter['isFavorite'] = isFavorite; } @@ -5551,6 +5556,7 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * Get all AssetEntity belong to the user + * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] * @param {number} [skip] @@ -5558,8 +5564,8 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options); + async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -5837,6 +5843,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }, /** * Get all AssetEntity belong to the user + * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] * @param {number} [skip] @@ -5844,8 +5851,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { - return localVarFp.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); + getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { + return localVarFp.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); }, /** * @@ -6124,6 +6131,7 @@ export class AssetApi extends BaseAPI { /** * Get all AssetEntity belong to the user + * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] * @param {number} [skip] @@ -6132,8 +6140,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + public getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 039110f808..50ce62990e 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -30,7 +30,7 @@ const getFavoriteCount = async () => { try { - const { data: assets } = await api.assetApi.getAllAssets(true, undefined); + const { data: assets } = await api.assetApi.getAllAssets(undefined, true, undefined); return { favorites: assets.length diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte index 139b087bd3..6c4669ef91 100644 --- a/web/src/routes/(user)/archive/+page.svelte +++ b/web/src/routes/(user)/archive/+page.svelte @@ -24,7 +24,7 @@ onMount(async () => { try { - const { data: assets } = await api.assetApi.getAllAssets(undefined, true); + const { data: assets } = await api.assetApi.getAllAssets(undefined, undefined, true); $archivedAsset = assets; } catch { handleError(Error, 'Unable to load archived assets'); diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte index 011c9bb4d7..63b135f096 100644 --- a/web/src/routes/(user)/favorites/+page.svelte +++ b/web/src/routes/(user)/favorites/+page.svelte @@ -20,7 +20,7 @@ onMount(async () => { try { - const { data: assets } = await api.assetApi.getAllAssets(true, undefined); + const { data: assets } = await api.assetApi.getAllAssets(undefined, true, undefined); favorites = assets; } catch { handleError(Error, 'Unable to load favorites');