mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							1613ae9185
						
					
				
				
					commit
					bcc2c34eef
				
			@@ -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<Isar> loadDb() async {
 | 
			
		||||
      BackupAlbumSchema,
 | 
			
		||||
      DuplicatedAssetSchema,
 | 
			
		||||
      LoggerMessageSchema,
 | 
			
		||||
      ETagSchema,
 | 
			
		||||
    ],
 | 
			
		||||
    directory: dir.path,
 | 
			
		||||
    maxSizeMiB: 256,
 | 
			
		||||
 
 | 
			
		||||
@@ -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<List<Album>> {
 | 
			
		||||
@@ -73,7 +74,9 @@ final sharedAlbumProvider =
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final sharedAlbumDetailProvider =
 | 
			
		||||
    StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
 | 
			
		||||
    StreamProvider.family<Album, int>((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)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<List<User>>((ref) {
 | 
			
		||||
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
 | 
			
		||||
  UserService userService = ref.watch(userServiceProvider);
 | 
			
		||||
 | 
			
		||||
  return userService.getUsersInDb();
 | 
			
		||||
 
 | 
			
		||||
@@ -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(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<Set<Asset>>(existingAssets);
 | 
			
		||||
    final selectionEnabledHook = useState(true);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final AsyncValue<List<User>> suggestedShareUsers =
 | 
			
		||||
        ref.watch(suggestedSharedUsersProvider);
 | 
			
		||||
        ref.watch(otherUsersProvider);
 | 
			
		||||
    final sharedUsersList = useState<Set<User>>({});
 | 
			
		||||
 | 
			
		||||
    addNewUsersHandler() {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final sharedUsersList = useState<Set<User>>({});
 | 
			
		||||
    AsyncValue<List<User>> suggestedShareUsers =
 | 
			
		||||
        ref.watch(suggestedSharedUsersProvider);
 | 
			
		||||
    final suggestedShareUsers = ref.watch(otherUsersProvider);
 | 
			
		||||
 | 
			
		||||
    createSharedAlbum() async {
 | 
			
		||||
      var newAlbum =
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Album> 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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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<RenderList>((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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<RenderList>((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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
        );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								mobile/lib/modules/partner/providers/partner.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								mobile/lib/modules/partner/providers/partner.provider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<List<User>> {
 | 
			
		||||
  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<PartnerSharedWithNotifier, List<User>>((ref) {
 | 
			
		||||
  return PartnerSharedWithNotifier(ref.watch(dbProvider));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
 | 
			
		||||
  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<List<User>> streamSub;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    streamSub.cancel();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final partnerSharedByProvider =
 | 
			
		||||
    StateNotifierProvider<PartnerSharedByNotifier, List<User>>((ref) {
 | 
			
		||||
  return PartnerSharedByNotifier(ref.watch(dbProvider));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final partnerAvailableProvider =
 | 
			
		||||
    FutureProvider.autoDispose<List<User>>((ref) async {
 | 
			
		||||
  final otherUsers = await ref.watch(otherUsersProvider.future);
 | 
			
		||||
  final currentPartners = ref.watch(partnerSharedByProvider);
 | 
			
		||||
  final available = Set<User>.of(otherUsers);
 | 
			
		||||
  available.removeAll(currentPartners);
 | 
			
		||||
  return available.toList();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										72
									
								
								mobile/lib/modules/partner/services/partner.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								mobile/lib/modules/partner/services/partner.service.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<List<User>?> 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<bool> 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<bool> 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								mobile/lib/modules/partner/ui/partner_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mobile/lib/modules/partner/ui/partner_list.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<User> 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)),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								mobile/lib/modules/partner/views/partner_detail_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								mobile/lib/modules/partner/views/partner_detail_page.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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()),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										160
									
								
								mobile/lib/modules/partner/views/partner_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								mobile/lib/modules/partner/views/partner_page.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<User> 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<User>(
 | 
			
		||||
        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<User> 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),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -256,6 +256,22 @@ class _$AppRouter extends RootStackRouter {
 | 
			
		||||
        child: const ArchivePage(),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    PartnerRoute.name: (routeData) {
 | 
			
		||||
      return MaterialPageX<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
        child: const PartnerPage(),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    PartnerDetailRoute.name: (routeData) {
 | 
			
		||||
      final args = routeData.argsAs<PartnerDetailRouteArgs>();
 | 
			
		||||
      return MaterialPageX<dynamic>(
 | 
			
		||||
        routeData: routeData,
 | 
			
		||||
        child: PartnerDetailPage(
 | 
			
		||||
          key: args.key,
 | 
			
		||||
          partner: args.partner,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    HomeRoute.name: (routeData) {
 | 
			
		||||
      return MaterialPageX<dynamic>(
 | 
			
		||||
        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<void> {
 | 
			
		||||
  static const String name = 'ArchiveRoute';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [PartnerPage]
 | 
			
		||||
class PartnerRoute extends PageRouteInfo<void> {
 | 
			
		||||
  const PartnerRoute()
 | 
			
		||||
      : super(
 | 
			
		||||
          PartnerRoute.name,
 | 
			
		||||
          path: '/partner-page',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  static const String name = 'PartnerRoute';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// generated route for
 | 
			
		||||
/// [PartnerDetailPage]
 | 
			
		||||
class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
 | 
			
		||||
  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<void> {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 &&
 | 
			
		||||
 
 | 
			
		||||
@@ -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 &&
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								mobile/lib/shared/models/etag.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								mobile/lib/shared/models/etag.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										724
									
								
								mobile/lib/shared/models/etag.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										724
									
								
								mobile/lib/shared/models/etag.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<ETag> 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<int> offsets,
 | 
			
		||||
  Map<Type, List<int>> 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<int> offsets,
 | 
			
		||||
  Map<Type, List<int>> allOffsets,
 | 
			
		||||
) {
 | 
			
		||||
  writer.writeString(offsets[0], object.id);
 | 
			
		||||
  writer.writeString(offsets[1], object.value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ETag _eTagDeserialize(
 | 
			
		||||
  Id id,
 | 
			
		||||
  IsarReader reader,
 | 
			
		||||
  List<int> offsets,
 | 
			
		||||
  Map<Type, List<int>> allOffsets,
 | 
			
		||||
) {
 | 
			
		||||
  final object = ETag(
 | 
			
		||||
    id: reader.readString(offsets[0]),
 | 
			
		||||
    value: reader.readStringOrNull(offsets[1]),
 | 
			
		||||
  );
 | 
			
		||||
  return object;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
P _eTagDeserializeProp<P>(
 | 
			
		||||
  IsarReader reader,
 | 
			
		||||
  int propertyId,
 | 
			
		||||
  int offset,
 | 
			
		||||
  Map<Type, List<int>> 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<IsarLinkBase<dynamic>> _eTagGetLinks(ETag object) {
 | 
			
		||||
  return [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void _eTagAttach(IsarCollection<dynamic> col, Id id, ETag object) {}
 | 
			
		||||
 | 
			
		||||
extension ETagByIndex on IsarCollection<ETag> {
 | 
			
		||||
  Future<ETag?> getById(String id) {
 | 
			
		||||
    return getByIndex(r'id', [id]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ETag? getByIdSync(String id) {
 | 
			
		||||
    return getByIndexSync(r'id', [id]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> deleteById(String id) {
 | 
			
		||||
    return deleteByIndex(r'id', [id]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool deleteByIdSync(String id) {
 | 
			
		||||
    return deleteByIndexSync(r'id', [id]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<ETag?>> getAllById(List<String> idValues) {
 | 
			
		||||
    final values = idValues.map((e) => [e]).toList();
 | 
			
		||||
    return getAllByIndex(r'id', values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<ETag?> getAllByIdSync(List<String> idValues) {
 | 
			
		||||
    final values = idValues.map((e) => [e]).toList();
 | 
			
		||||
    return getAllByIndexSync(r'id', values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> deleteAllById(List<String> idValues) {
 | 
			
		||||
    final values = idValues.map((e) => [e]).toList();
 | 
			
		||||
    return deleteAllByIndex(r'id', values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int deleteAllByIdSync(List<String> idValues) {
 | 
			
		||||
    final values = idValues.map((e) => [e]).toList();
 | 
			
		||||
    return deleteAllByIndexSync(r'id', values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Id> putById(ETag object) {
 | 
			
		||||
    return putByIndex(r'id', object);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Id putByIdSync(ETag object, {bool saveLinks = true}) {
 | 
			
		||||
    return putByIndexSync(r'id', object, saveLinks: saveLinks);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<Id>> putAllById(List<ETag> objects) {
 | 
			
		||||
    return putAllByIndex(r'id', objects);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<Id> putAllByIdSync(List<ETag> objects, {bool saveLinks = true}) {
 | 
			
		||||
    return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryWhereSort on QueryBuilder<ETag, ETag, QWhere> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhere> anyIsarId() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addWhereClause(const IdWhereClause.any());
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryWhere on QueryBuilder<ETag, ETag, QWhereClause> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdEqualTo(Id isarId) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addWhereClause(IdWhereClause.between(
 | 
			
		||||
        lower: isarId,
 | 
			
		||||
        upper: isarId,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhereClause> 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<ETag, ETag, QAfterWhereClause> isarIdGreaterThan(Id isarId,
 | 
			
		||||
      {bool include = false}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addWhereClause(
 | 
			
		||||
        IdWhereClause.greaterThan(lower: isarId, includeLower: include),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdLessThan(Id isarId,
 | 
			
		||||
      {bool include = false}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addWhereClause(
 | 
			
		||||
        IdWhereClause.lessThan(upper: isarId, includeUpper: include),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhereClause> 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<ETag, ETag, QAfterWhereClause> idEqualTo(String id) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addWhereClause(IndexWhereClause.equalTo(
 | 
			
		||||
        indexName: r'id',
 | 
			
		||||
        value: [id],
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterWhereClause> 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<ETag, ETag, QFilterCondition> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idEqualTo(
 | 
			
		||||
    String value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> idStartsWith(
 | 
			
		||||
    String value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.startsWith(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idEndsWith(
 | 
			
		||||
    String value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.endsWith(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idContains(String value,
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.contains(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idMatches(String pattern,
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.matches(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        wildcard: pattern,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsEmpty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: '',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsNotEmpty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.greaterThan(
 | 
			
		||||
        property: r'id',
 | 
			
		||||
        value: '',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdEqualTo(Id value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'isarId',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdGreaterThan(
 | 
			
		||||
    Id value, {
 | 
			
		||||
    bool include = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.greaterThan(
 | 
			
		||||
        include: include,
 | 
			
		||||
        property: r'isarId',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdLessThan(
 | 
			
		||||
    Id value, {
 | 
			
		||||
    bool include = false,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.lessThan(
 | 
			
		||||
        include: include,
 | 
			
		||||
        property: r'isarId',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> valueIsNull() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(const FilterCondition.isNull(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotNull() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(const FilterCondition.isNotNull(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEqualTo(
 | 
			
		||||
    String? value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> 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<ETag, ETag, QAfterFilterCondition> valueStartsWith(
 | 
			
		||||
    String value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.startsWith(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEndsWith(
 | 
			
		||||
    String value, {
 | 
			
		||||
    bool caseSensitive = true,
 | 
			
		||||
  }) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.endsWith(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueContains(String value,
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.contains(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: value,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueMatches(String pattern,
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.matches(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        wildcard: pattern,
 | 
			
		||||
        caseSensitive: caseSensitive,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsEmpty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: '',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotEmpty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.greaterThan(
 | 
			
		||||
        property: r'value',
 | 
			
		||||
        value: '',
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryObject on QueryBuilder<ETag, ETag, QFilterCondition> {}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryLinks on QueryBuilder<ETag, ETag, QFilterCondition> {}
 | 
			
		||||
 | 
			
		||||
extension ETagQuerySortBy on QueryBuilder<ETag, ETag, QSortBy> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> sortById() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'id', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> sortByIdDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'id', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> sortByValue() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'value', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> sortByValueDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'value', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQuerySortThenBy on QueryBuilder<ETag, ETag, QSortThenBy> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenById() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'id', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenByIdDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'id', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarId() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isarId', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarIdDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isarId', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenByValue() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'value', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QAfterSortBy> thenByValueDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'value', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryWhereDistinct on QueryBuilder<ETag, ETag, QDistinct> {
 | 
			
		||||
  QueryBuilder<ETag, ETag, QDistinct> distinctById(
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, ETag, QDistinct> distinctByValue(
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'value', caseSensitive: caseSensitive);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ETagQueryProperty on QueryBuilder<ETag, ETag, QQueryProperty> {
 | 
			
		||||
  QueryBuilder<ETag, int, QQueryOperations> isarIdProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'isarId');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, String, QQueryOperations> idProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'id');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<ETag, String?, QQueryOperations> valueProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'value');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<Album> albums = IsarLinks<Album>();
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<P>(
 | 
			
		||||
    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<User, User, QFilterCondition> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterFilterCondition> isPartnerSharedByEqualTo(
 | 
			
		||||
      bool value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'isPartnerSharedBy',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterFilterCondition> isPartnerSharedWithEqualTo(
 | 
			
		||||
      bool value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
        property: r'isPartnerSharedWith',
 | 
			
		||||
        value: value,
 | 
			
		||||
      ));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterFilterCondition> isarIdEqualTo(Id value) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addFilterCondition(FilterCondition.equalTo(
 | 
			
		||||
@@ -1140,6 +1178,30 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedBy() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedBy', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedByDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedBy', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedWith() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedWith', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> sortByIsPartnerSharedWithDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedWith', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> sortByLastName() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'lastName', Sort.asc);
 | 
			
		||||
@@ -1214,6 +1276,30 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedBy() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedBy', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedByDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedBy', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedWith() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedWith', Sort.asc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> thenByIsPartnerSharedWithDesc() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isPartnerSharedWith', Sort.desc);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QAfterSortBy> thenByIsarId() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addSortBy(r'isarId', Sort.asc);
 | 
			
		||||
@@ -1279,6 +1365,18 @@ extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QDistinct> distinctByIsPartnerSharedBy() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'isPartnerSharedBy');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QDistinct> distinctByIsPartnerSharedWith() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addDistinctBy(r'isPartnerSharedWith');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, User, QDistinct> distinctByLastName(
 | 
			
		||||
      {bool caseSensitive = true}) {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
@@ -1324,6 +1422,18 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, bool, QQueryOperations> isPartnerSharedByProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'isPartnerSharedBy');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, bool, QQueryOperations> isPartnerSharedWithProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'isPartnerSharedWith');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  QueryBuilder<User, String, QQueryOperations> lastNameProperty() {
 | 
			
		||||
    return QueryBuilder.apply(this, (query) {
 | 
			
		||||
      return query.addPropertyName(r'lastName');
 | 
			
		||||
 
 | 
			
		||||
@@ -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<AssetsState> {
 | 
			
		||||
  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<AssetsState> {
 | 
			
		||||
  AssetNotifier(
 | 
			
		||||
    this._assetService,
 | 
			
		||||
    this._albumService,
 | 
			
		||||
    this._userService,
 | 
			
		||||
    this._syncService,
 | 
			
		||||
    this._db,
 | 
			
		||||
  ) : super(AssetsState());
 | 
			
		||||
@@ -51,6 +55,12 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 | 
			
		||||
      final bool newRemote = await _assetService.refreshRemoteAssets();
 | 
			
		||||
      final bool newLocal = await _albumService.refreshDeviceAlbums();
 | 
			
		||||
      debugPrint("newRemote: $newRemote, newLocal: $newLocal");
 | 
			
		||||
      await _userService.refreshUsers();
 | 
			
		||||
      final List<User> 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<AssetNotifier, AssetsState>((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<RenderList>((ref) async* {
 | 
			
		||||
final assetsProvider =
 | 
			
		||||
    StreamProvider.family<RenderList, int?>((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<RenderList>((ref) async* {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final remoteAssetsProvider =
 | 
			
		||||
    StreamProvider.autoDispose<RenderList>((ref) async* {
 | 
			
		||||
    StreamProvider.family<RenderList, int?>((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 =
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								mobile/lib/shared/providers/user.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								mobile/lib/shared/providers/user.provider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<User?> {
 | 
			
		||||
  CurrentUserProvider() : super(null) {
 | 
			
		||||
    state = Store.tryGet(StoreKey.currentUser);
 | 
			
		||||
    streamSub =
 | 
			
		||||
        Store.watch(StoreKey.currentUser).listen((user) => state = user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  late final StreamSubscription<User?> streamSub;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    streamSub.cancel();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final currentUserProvider =
 | 
			
		||||
    StateNotifierProvider<CurrentUserProvider, User?>((ref) {
 | 
			
		||||
  return CurrentUserProvider();
 | 
			
		||||
});
 | 
			
		||||
@@ -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<String> resolveAndSetEndpoint(String serverUrl) async {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<bool> refreshRemoteAssets() async {
 | 
			
		||||
  Future<bool> 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<List<AssetResponseDto>?> _getRemoteAssets({
 | 
			
		||||
  Future<List<Asset>?> _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<AssetResponseDto>? 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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<bool> syncRemoteAssetsToDb(
 | 
			
		||||
    User user,
 | 
			
		||||
    FutureOr<List<Asset>?> 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<bool> _syncRemoteAssetsToDb(
 | 
			
		||||
    User user,
 | 
			
		||||
    FutureOr<List<Asset>?> Function() loadAssets,
 | 
			
		||||
  ) async {
 | 
			
		||||
    final List<Asset>? remote = await loadAssets();
 | 
			
		||||
    if (remote == null) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    final User user = Store.get(StoreKey.currentUser);
 | 
			
		||||
    final List<Asset> 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));
 | 
			
		||||
 
 | 
			
		||||
@@ -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<List<User>?> _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<bool> refreshUsers() async {
 | 
			
		||||
    final List<User>? users = await _getAllUsers(isAll: true);
 | 
			
		||||
    if (users == null) {
 | 
			
		||||
    final List<User>? sharedBy =
 | 
			
		||||
        await _partnerService.getPartners(PartnerDirection.sharedBy);
 | 
			
		||||
    final List<User>? 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								mobile/lib/shared/ui/confirm_dialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								mobile/lib/shared/ui/confirm_dialog.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								mobile/lib/shared/ui/user_avatar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								mobile/lib/shared/ui/user_avatar.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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()),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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<void> clearAssetsAndAlbums(Isar db) async {
 | 
			
		||||
@@ -10,5 +12,7 @@ Future<void> clearAssetsAndAlbums(Isar db) async {
 | 
			
		||||
    await db.assets.clear();
 | 
			
		||||
    await db.exifInfos.clear();
 | 
			
		||||
    await db.albums.clear();
 | 
			
		||||
    await db.eTags.clear();
 | 
			
		||||
    await db.users.clear();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,11 @@ extension WithETag on AssetApi {
 | 
			
		||||
  ///   ETag of data already cached on the client
 | 
			
		||||
  Future<(List<AssetResponseDto>? 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));
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user