mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02: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:
parent
1613ae9185
commit
bcc2c34eef
@ -257,6 +257,15 @@
|
|||||||
"sharing_page_empty_list": "EMPTY LIST",
|
"sharing_page_empty_list": "EMPTY LIST",
|
||||||
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
||||||
"sharing_silver_appbar_share_partner": "Share with partner",
|
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||||
|
"partner_page_title": "Partner",
|
||||||
|
"partner_page_no_more_users": "No more users to add",
|
||||||
|
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||||
|
"partner_page_shared_to_title": "Shared to",
|
||||||
|
"partner_page_select_partner": "Select partner",
|
||||||
|
"partner_page_add_partner": "Add partner",
|
||||||
|
"partner_page_partner_add_failed": "Failed to add partner",
|
||||||
|
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
||||||
|
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
||||||
"tab_controller_nav_library": "Library",
|
"tab_controller_nav_library": "Library",
|
||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
"tab_controller_nav_search": "Search",
|
"tab_controller_nav_search": "Search",
|
||||||
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
@ -89,6 +90,7 @@ Future<Isar> loadDb() async {
|
|||||||
BackupAlbumSchema,
|
BackupAlbumSchema,
|
||||||
DuplicatedAssetSchema,
|
DuplicatedAssetSchema,
|
||||||
LoggerMessageSchema,
|
LoggerMessageSchema,
|
||||||
|
ETagSchema,
|
||||||
],
|
],
|
||||||
directory: dir.path,
|
directory: dir.path,
|
||||||
maxSizeMiB: 256,
|
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/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
@ -73,7 +74,9 @@ final sharedAlbumProvider =
|
|||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider =
|
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);
|
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
await for (final a in sharedAlbumService.watchAlbum(albumId)) {
|
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/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
|
||||||
final suggestedSharedUsersProvider =
|
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
|
||||||
FutureProvider.autoDispose<List<User>>((ref) {
|
|
||||||
UserService userService = ref.watch(userServiceProvider);
|
UserService userService = ref.watch(userServiceProvider);
|
||||||
|
|
||||||
return userService.getUsersInDb();
|
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/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
|
|
||||||
class AssetSelectionPage extends HookConsumerWidget {
|
class AssetSelectionPage extends HookConsumerWidget {
|
||||||
const AssetSelectionPage({
|
const AssetSelectionPage({
|
||||||
@ -21,7 +22,8 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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 selected = useState<Set<Asset>>(existingAssets);
|
||||||
final selectionEnabledHook = useState(true);
|
final selectionEnabledHook = useState(true);
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final AsyncValue<List<User>> suggestedShareUsers =
|
final AsyncValue<List<User>> suggestedShareUsers =
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
ref.watch(otherUsersProvider);
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<User>>({});
|
||||||
|
|
||||||
addNewUsersHandler() {
|
addNewUsersHandler() {
|
||||||
|
@ -20,8 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<User>>({});
|
||||||
AsyncValue<List<User>> suggestedShareUsers =
|
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
|
||||||
|
|
||||||
createSharedAlbum() async {
|
createSharedAlbum() async {
|
||||||
var newAlbum =
|
var newAlbum =
|
||||||
|
@ -5,10 +5,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.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/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/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.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';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
class SharingPage extends HookConsumerWidget {
|
||||||
@ -17,7 +18,8 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
|
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;
|
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -63,8 +65,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
final isOwner = album.ownerId == userId;
|
final isOwner = album.ownerId == userId;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: ImmichImage(
|
child: ImmichImage(
|
||||||
@ -93,7 +94,8 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: album.ownerName != null
|
: album.ownerName != null
|
||||||
? Text(
|
? Text(
|
||||||
'album_thumbnail_shared_by'.tr(args: [album.ownerName!]),
|
'album_thumbnail_shared_by'
|
||||||
|
.tr(args: [album.ownerName!]),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12.0,
|
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() {
|
buildEmptyListIndication() {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -123,7 +194,6 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// color: Colors.transparent,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(18.0),
|
padding: const EdgeInsets.all(18.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -160,11 +230,27 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: buildAppBar(),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
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(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: partner.isEmpty ? 0 : 16,
|
||||||
|
),
|
||||||
sliver: SliverToBoxAdapter(
|
sliver: SliverToBoxAdapter(
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"sharing_page_album",
|
"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/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
final archiveProvider = StreamProvider<RenderList>((ref) async* {
|
final archiveProvider = StreamProvider<RenderList>((ref) async* {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) return;
|
||||||
final query = ref
|
final query = ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(user.isarId)
|
||||||
.isArchivedEqualTo(true)
|
.isArchivedEqualTo(true)
|
||||||
.sortByFileCreatedAt();
|
.sortByFileCreatedAt();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.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/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart' as store;
|
|
||||||
|
|
||||||
class DescriptionInput extends HookConsumerWidget {
|
class DescriptionInput extends HookConsumerWidget {
|
||||||
DescriptionInput({
|
DescriptionInput({
|
||||||
@ -25,9 +25,10 @@ class DescriptionInput extends HookConsumerWidget {
|
|||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
final isFocus = useState(false);
|
final isFocus = useState(false);
|
||||||
final isTextEmpty = useState(controller.text.isEmpty);
|
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 description = ref.watch(assetDescriptionProvider(asset));
|
||||||
final owner = store.Store.get(store.StoreKey.currentUser);
|
final owner = ref.watch(currentUserProvider);
|
||||||
final hasError = useState(false);
|
final hasError = useState(false);
|
||||||
|
|
||||||
controller.text = description;
|
controller.text = description;
|
||||||
@ -67,7 +68,7 @@ class DescriptionInput extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
enabled: owner.isarId == asset.ownerId,
|
enabled: owner?.isarId == asset.ownerId,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
onTap: () => isFocus.value = true,
|
onTap: () => isFocus.value = true,
|
||||||
onChanged: (value) {
|
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/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
|
final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) return;
|
||||||
final query = ref
|
final query = ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(user.isarId)
|
||||||
.isFavoriteEqualTo(true)
|
.isFavoriteEqualTo(true)
|
||||||
.sortByFileCreatedAt();
|
.sortByFileCreatedAt();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
@ -1,47 +1,16 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||||
|
|
||||||
class DeleteDialog extends ConsumerWidget {
|
class DeleteDialog extends ConfirmDialog {
|
||||||
final Function onDelete;
|
final Function onDelete;
|
||||||
|
|
||||||
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
|
const DeleteDialog({Key? key, required this.onDelete})
|
||||||
|
: super(
|
||||||
@override
|
key: key,
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
title: "delete_dialog_title",
|
||||||
|
content: "delete_dialog_alert",
|
||||||
return AlertDialog(
|
cancel: "delete_dialog_cancel",
|
||||||
// backgroundColor: Colors.grey[200],
|
ok: "delete_dialog_ok",
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
onOk: onDelete,
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.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 albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
|
|
||||||
final tipOneOpacity = useState(0.0);
|
final tipOneOpacity = useState(0.0);
|
||||||
final refreshCount = useState(0);
|
final refreshCount = useState(0);
|
||||||
@ -300,7 +302,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ref.watch(assetsProvider).when(
|
ref.watch(assetsProvider(currentUser?.isarId)).when(
|
||||||
data: (data) => data.isEmpty
|
data: (data) => data.isEmpty
|
||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: 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/asset_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/create_album_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/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_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/select_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/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/routing/gallery_permission_guard.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.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/models/logger_message.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
@ -136,6 +139,8 @@ part 'router.gr.dart';
|
|||||||
DuplicateGuard,
|
DuplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
AutoRoute(page: PartnerPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard])
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
@ -256,6 +256,22 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
child: const ArchivePage(),
|
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) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
@ -523,6 +539,22 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
duplicateGuard,
|
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';
|
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
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
@ -87,8 +87,8 @@ class Album {
|
|||||||
remoteId == other.remoteId &&
|
remoteId == other.remoteId &&
|
||||||
localId == other.localId &&
|
localId == other.localId &&
|
||||||
name == other.name &&
|
name == other.name &&
|
||||||
createdAt == other.createdAt &&
|
createdAt.isAtSameMomentAs(other.createdAt) &&
|
||||||
modifiedAt == other.modifiedAt &&
|
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
||||||
shared == other.shared &&
|
shared == other.shared &&
|
||||||
owner.value == other.owner.value &&
|
owner.value == other.owner.value &&
|
||||||
thumbnail.value == other.thumbnail.value &&
|
thumbnail.value == other.thumbnail.value &&
|
||||||
|
@ -179,9 +179,9 @@ class Asset {
|
|||||||
localId == other.localId &&
|
localId == other.localId &&
|
||||||
deviceId == other.deviceId &&
|
deviceId == other.deviceId &&
|
||||||
ownerId == other.ownerId &&
|
ownerId == other.ownerId &&
|
||||||
fileCreatedAt == other.fileCreatedAt &&
|
fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) &&
|
||||||
fileModifiedAt == other.fileModifiedAt &&
|
fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) &&
|
||||||
updatedAt == other.updatedAt &&
|
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
||||||
durationInSeconds == other.durationInSeconds &&
|
durationInSeconds == other.durationInSeconds &&
|
||||||
type == other.type &&
|
type == other.type &&
|
||||||
width == other.width &&
|
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;
|
||||||
|
}
|
BIN
mobile/lib/shared/models/etag.g.dart
Normal file
BIN
mobile/lib/shared/models/etag.g.dart
Normal file
Binary file not shown.
@ -14,6 +14,8 @@ class User {
|
|||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
|
this.isPartnerSharedBy = false,
|
||||||
|
this.isPartnerSharedWith = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Id get isarId => fastHash(id);
|
Id get isarId => fastHash(id);
|
||||||
@ -26,6 +28,8 @@ class User {
|
|||||||
email = dto.email,
|
email = dto.email,
|
||||||
firstName = dto.firstName,
|
firstName = dto.firstName,
|
||||||
lastName = dto.lastName,
|
lastName = dto.lastName,
|
||||||
|
isPartnerSharedBy = false,
|
||||||
|
isPartnerSharedWith = false,
|
||||||
isAdmin = dto.isAdmin;
|
isAdmin = dto.isAdmin;
|
||||||
|
|
||||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||||
@ -34,6 +38,8 @@ class User {
|
|||||||
String email;
|
String email;
|
||||||
String firstName;
|
String firstName;
|
||||||
String lastName;
|
String lastName;
|
||||||
|
bool isPartnerSharedBy;
|
||||||
|
bool isPartnerSharedWith;
|
||||||
bool isAdmin;
|
bool isAdmin;
|
||||||
@Backlink(to: 'owner')
|
@Backlink(to: 'owner')
|
||||||
final IsarLinks<Album> albums = IsarLinks<Album>();
|
final IsarLinks<Album> albums = IsarLinks<Album>();
|
||||||
@ -44,10 +50,12 @@ class User {
|
|||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! User) return false;
|
if (other is! User) return false;
|
||||||
return id == other.id &&
|
return id == other.id &&
|
||||||
updatedAt == other.updatedAt &&
|
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
||||||
email == other.email &&
|
email == other.email &&
|
||||||
firstName == other.firstName &&
|
firstName == other.firstName &&
|
||||||
lastName == other.lastName &&
|
lastName == other.lastName &&
|
||||||
|
isPartnerSharedBy == other.isPartnerSharedBy &&
|
||||||
|
isPartnerSharedWith == other.isPartnerSharedWith &&
|
||||||
isAdmin == other.isAdmin;
|
isAdmin == other.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,5 +67,7 @@ class User {
|
|||||||
email.hashCode ^
|
email.hashCode ^
|
||||||
firstName.hashCode ^
|
firstName.hashCode ^
|
||||||
lastName.hashCode ^
|
lastName.hashCode ^
|
||||||
|
isPartnerSharedBy.hashCode ^
|
||||||
|
isPartnerSharedWith.hashCode ^
|
||||||
isAdmin.hashCode;
|
isAdmin.hashCode;
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -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/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/asset.service.dart';
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/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/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/sync.service.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:immich_mobile/utils/db.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -23,6 +25,7 @@ class AssetsState {}
|
|||||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final UserService _userService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final log = Logger('AssetNotifier');
|
final log = Logger('AssetNotifier');
|
||||||
@ -32,6 +35,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
AssetNotifier(
|
AssetNotifier(
|
||||||
this._assetService,
|
this._assetService,
|
||||||
this._albumService,
|
this._albumService,
|
||||||
|
this._userService,
|
||||||
this._syncService,
|
this._syncService,
|
||||||
this._db,
|
this._db,
|
||||||
) : super(AssetsState());
|
) : super(AssetsState());
|
||||||
@ -51,6 +55,12 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
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");
|
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
} finally {
|
} finally {
|
||||||
_getAllAssetInProgress = false;
|
_getAllAssetInProgress = false;
|
||||||
@ -147,6 +157,7 @@ final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
|||||||
return AssetNotifier(
|
return AssetNotifier(
|
||||||
ref.watch(assetServiceProvider),
|
ref.watch(assetServiceProvider),
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
|
ref.watch(userServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
ref.watch(dbProvider),
|
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
|
final query = ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(userId)
|
||||||
.isArchivedEqualTo(false)
|
.isArchivedEqualTo(false)
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
@ -179,14 +192,15 @@ final assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final remoteAssetsProvider =
|
final remoteAssetsProvider =
|
||||||
StreamProvider.autoDispose<RenderList>((ref) async* {
|
StreamProvider.family<RenderList, int?>((ref, userId) async* {
|
||||||
|
if (userId == null) return;
|
||||||
final query = ref
|
final query = ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.where()
|
.where()
|
||||||
.remoteIdIsNotNull()
|
.remoteIdIsNotNull()
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(userId)
|
||||||
.sortByFileCreatedAt();
|
.sortByFileCreatedAt();
|
||||||
final settings = ref.watch(appSettingsServiceProvider);
|
final settings = ref.watch(appSettingsServiceProvider);
|
||||||
final groupBy =
|
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 AssetApi assetApi;
|
||||||
late SearchApi searchApi;
|
late SearchApi searchApi;
|
||||||
late ServerInfoApi serverInfoApi;
|
late ServerInfoApi serverInfoApi;
|
||||||
|
late PartnerApi partnerApi;
|
||||||
|
|
||||||
ApiService() {
|
ApiService() {
|
||||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||||
@ -37,6 +38,7 @@ class ApiService {
|
|||||||
assetApi = AssetApi(_apiClient);
|
assetApi = AssetApi(_apiClient);
|
||||||
serverInfoApi = ServerInfoApi(_apiClient);
|
serverInfoApi = ServerInfoApi(_apiClient);
|
||||||
searchApi = SearchApi(_apiClient);
|
searchApi = SearchApi(_apiClient);
|
||||||
|
partnerApi = PartnerApi(_apiClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||||
|
@ -3,8 +3,10 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.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/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.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/api.service.dart';
|
||||||
@ -36,37 +38,47 @@ class AssetService {
|
|||||||
|
|
||||||
/// Checks the server for updated assets and updates the local database if
|
/// Checks the server for updated assets and updates the local database if
|
||||||
/// required. Returns `true` if there were any changes.
|
/// 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 Stopwatch sw = Stopwatch()..start();
|
||||||
final int numOwnedRemoteAssets = await _db.assets
|
final int numOwnedRemoteAssets = await _db.assets
|
||||||
.where()
|
.where()
|
||||||
.remoteIdIsNotNull()
|
.remoteIdIsNotNull()
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
.ownerIdEqualTo(user!.isarId)
|
||||||
.count();
|
.count();
|
||||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||||
() async => (await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0))
|
user,
|
||||||
?.map(Asset.remote)
|
() async => (await _getRemoteAssets(
|
||||||
.toList(),
|
hasCache: numOwnedRemoteAssets > 0,
|
||||||
|
user: user!,
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `null` if the server state did not change, else list of assets
|
/// 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 bool hasCache,
|
||||||
|
required User user,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
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) =
|
final (List<AssetResponseDto>? assets, String? newETag) =
|
||||||
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
|
await _apiService.assetApi
|
||||||
|
.getAllAssetsWithETag(eTag: etag, userId: user.id);
|
||||||
if (assets == null) {
|
if (assets == null) {
|
||||||
return 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) {
|
} 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) {
|
} catch (e, stack) {
|
||||||
log.severe('Error while getting remote assets', e, stack);
|
log.severe('Error while getting remote assets', e, stack);
|
||||||
return null;
|
return null;
|
||||||
|
@ -40,7 +40,9 @@ class SyncService {
|
|||||||
dbUsers,
|
dbUsers,
|
||||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||||
both: (User a, User b) {
|
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);
|
toUpsert.add(a);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -61,9 +63,10 @@ class SyncService {
|
|||||||
/// Syncs remote assets owned by the logged-in user to the DB
|
/// Syncs remote assets owned by the logged-in user to the DB
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
Future<bool> syncRemoteAssetsToDb(
|
Future<bool> syncRemoteAssetsToDb(
|
||||||
|
User user,
|
||||||
FutureOr<List<Asset>?> Function() loadAssets,
|
FutureOr<List<Asset>?> Function() loadAssets,
|
||||||
) =>
|
) =>
|
||||||
_lock.run(() => _syncRemoteAssetsToDb(loadAssets));
|
_lock.run(() => _syncRemoteAssetsToDb(user, loadAssets));
|
||||||
|
|
||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
@ -149,13 +152,13 @@ class SyncService {
|
|||||||
/// Syncs remote assets to the databas
|
/// Syncs remote assets to the databas
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> _syncRemoteAssetsToDb(
|
Future<bool> _syncRemoteAssetsToDb(
|
||||||
|
User user,
|
||||||
FutureOr<List<Asset>?> Function() loadAssets,
|
FutureOr<List<Asset>?> Function() loadAssets,
|
||||||
) async {
|
) async {
|
||||||
final List<Asset>? remote = await loadAssets();
|
final List<Asset>? remote = await loadAssets();
|
||||||
if (remote == null) {
|
if (remote == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final User user = Store.get(StoreKey.currentUser);
|
|
||||||
final List<Asset> inDb = await _db.assets
|
final List<Asset> inDb = await _db.assets
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(user.isarId)
|
.ownerIdEqualTo(user.isarId)
|
||||||
@ -349,10 +352,19 @@ class SyncService {
|
|||||||
);
|
);
|
||||||
} else if (album.shared) {
|
} else if (album.shared) {
|
||||||
final User user = Store.get(StoreKey.currentUser);
|
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
|
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
|
||||||
deleteCandidates.addAll(
|
final userIds = await _db.users
|
||||||
await album.assets.filter().not().ownerIdEqualTo(user.isarId).findAll(),
|
.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 {
|
try {
|
||||||
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:image_picker/image_picker.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/store.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.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/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/sync.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:immich_mobile/utils/files_helper.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final userServiceProvider = Provider(
|
final userServiceProvider = Provider(
|
||||||
@ -18,6 +21,7 @@ final userServiceProvider = Provider(
|
|||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
|
ref.watch(partnerServiceProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -25,15 +29,22 @@ class UserService {
|
|||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final SyncService _syncService;
|
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 {
|
Future<List<User>?> _getAllUsers({required bool isAll}) async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
final dto = await _apiService.userApi.getAllUsers(isAll);
|
||||||
return dto?.map(User.fromDto).toList();
|
return dto?.map(User.fromDto).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAllUsersInfo] ${e.toString()}");
|
_log.warning("Failed get all users:\n$e");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,16 +73,45 @@ class UserService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [uploadProfileImage] ${e.toString()}");
|
_log.warning("Failed to upload profile image:\n$e");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> refreshUsers() async {
|
Future<bool> refreshUsers() async {
|
||||||
final List<User>? users = await _getAllUsers(isAll: true);
|
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;
|
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);
|
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/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.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/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
Future<void> clearAssetsAndAlbums(Isar db) async {
|
Future<void> clearAssetsAndAlbums(Isar db) async {
|
||||||
@ -10,5 +12,7 @@ Future<void> clearAssetsAndAlbums(Isar db) async {
|
|||||||
await db.assets.clear();
|
await db.assets.clear();
|
||||||
await db.exifInfos.clear();
|
await db.exifInfos.clear();
|
||||||
await db.albums.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
|
/// ETag of data already cached on the client
|
||||||
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
|
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
|
||||||
String? eTag,
|
String? eTag,
|
||||||
|
String? userId,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await getAllAssetsWithHttpInfo(
|
final response = await getAllAssetsWithHttpInfo(
|
||||||
ifNoneMatch: eTag,
|
ifNoneMatch: eTag,
|
||||||
|
userId: userId,
|
||||||
);
|
);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
@ -52,6 +52,14 @@ void main() {
|
|||||||
|
|
||||||
group('Test SyncService grouped', () {
|
group('Test SyncService grouped', () {
|
||||||
late final Isar db;
|
late final Isar db;
|
||||||
|
final owner = User(
|
||||||
|
id: "1",
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
email: "a@b.c",
|
||||||
|
firstName: "first",
|
||||||
|
lastName: "last",
|
||||||
|
isAdmin: false,
|
||||||
|
);
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Isar.initializeIsarCore(download: true);
|
await Isar.initializeIsarCore(download: true);
|
||||||
@ -59,17 +67,7 @@ void main() {
|
|||||||
ImmichLogger();
|
ImmichLogger();
|
||||||
db.writeTxnSync(() => db.clearSync());
|
db.writeTxnSync(() => db.clearSync());
|
||||||
Store.init(db);
|
Store.init(db);
|
||||||
await Store.put(
|
await Store.put(StoreKey.currentUser, owner);
|
||||||
StoreKey.currentUser,
|
|
||||||
User(
|
|
||||||
id: "1",
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
email: "a@b.c",
|
|
||||||
firstName: "first",
|
|
||||||
lastName: "last",
|
|
||||||
isAdmin: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
final List<Asset> initialAssets = [
|
final List<Asset> initialAssets = [
|
||||||
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
makeAsset(localId: "1", remoteId: "0-1", deviceId: 0),
|
||||||
@ -92,7 +90,7 @@ void main() {
|
|||||||
makeAsset(localId: "1", remoteId: "1-1"),
|
makeAsset(localId: "1", remoteId: "1-1"),
|
||||||
];
|
];
|
||||||
expect(db.assets.countSync(), 5);
|
expect(db.assets.countSync(), 5);
|
||||||
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c1, false);
|
expect(c1, false);
|
||||||
expect(db.assets.countSync(), 5);
|
expect(db.assets.countSync(), 5);
|
||||||
});
|
});
|
||||||
@ -108,7 +106,7 @@ void main() {
|
|||||||
makeAsset(localId: "1", remoteId: "3-1", deviceId: 3),
|
makeAsset(localId: "1", remoteId: "3-1", deviceId: 3),
|
||||||
];
|
];
|
||||||
expect(db.assets.countSync(), 5);
|
expect(db.assets.countSync(), 5);
|
||||||
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c1, true);
|
expect(c1, true);
|
||||||
expect(db.assets.countSync(), 7);
|
expect(db.assets.countSync(), 7);
|
||||||
});
|
});
|
||||||
@ -124,19 +122,19 @@ void main() {
|
|||||||
makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2),
|
makeAsset(localId: "1", remoteId: "2-1d", deviceId: 2),
|
||||||
];
|
];
|
||||||
expect(db.assets.countSync(), 5);
|
expect(db.assets.countSync(), 5);
|
||||||
final bool c1 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c1, true);
|
expect(c1, true);
|
||||||
expect(db.assets.countSync(), 8);
|
expect(db.assets.countSync(), 8);
|
||||||
final bool c2 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c2 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c2, false);
|
expect(c2, false);
|
||||||
expect(db.assets.countSync(), 8);
|
expect(db.assets.countSync(), 8);
|
||||||
remoteAssets.removeAt(4);
|
remoteAssets.removeAt(4);
|
||||||
final bool c3 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c3, true);
|
expect(c3, true);
|
||||||
expect(db.assets.countSync(), 7);
|
expect(db.assets.countSync(), 7);
|
||||||
remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2));
|
remoteAssets.add(makeAsset(localId: "1", remoteId: "2-1e", deviceId: 2));
|
||||||
remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2));
|
remoteAssets.add(makeAsset(localId: "2", remoteId: "2-2", deviceId: 2));
|
||||||
final bool c4 = await s.syncRemoteAssetsToDb(() => remoteAssets);
|
final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
|
||||||
expect(c4, true);
|
expect(c4, true);
|
||||||
expect(db.assets.countSync(), 9);
|
expect(db.assets.countSync(), 9);
|
||||||
});
|
});
|
||||||
|
@ -150,7 +150,10 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||||
const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
|
if (dto.userId && dto.userId !== authUser.id) {
|
||||||
|
await this.checkUserAccess(authUser, dto.userId);
|
||||||
|
}
|
||||||
|
const assets = await this._assetRepository.getAllByUserId(dto.userId || authUser.id, dto);
|
||||||
|
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||||
import { toBoolean } from '../../../utils/transform.util';
|
import { toBoolean } from '../../../utils/transform.util';
|
||||||
|
|
||||||
export class AssetSearchDto {
|
export class AssetSearchDto {
|
||||||
@ -18,4 +19,9 @@ export class AssetSearchDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID('4')
|
||||||
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
userId?: string;
|
||||||
}
|
}
|
||||||
|
@ -2853,6 +2853,15 @@
|
|||||||
"operationId": "getAllAssets",
|
"operationId": "getAllAssets",
|
||||||
"description": "Get all AssetEntity belong to the user",
|
"description": "Get all AssetEntity belong to the user",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isFavorite",
|
"name": "isFavorite",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
22
web/src/api/open-api/api.ts
generated
22
web/src/api/open-api/api.ts
generated
@ -4599,6 +4599,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {string} [userId]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
@ -4606,7 +4607,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
const localVarPath = `/asset`;
|
const localVarPath = `/asset`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -4628,6 +4629,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (userId !== undefined) {
|
||||||
|
localVarQueryParameter['userId'] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
if (isFavorite !== undefined) {
|
if (isFavorite !== undefined) {
|
||||||
localVarQueryParameter['isFavorite'] = isFavorite;
|
localVarQueryParameter['isFavorite'] = isFavorite;
|
||||||
}
|
}
|
||||||
@ -5551,6 +5556,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {string} [userId]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
@ -5558,8 +5564,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -5837,6 +5843,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {string} [userId]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
@ -5844,8 +5851,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
|
getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath));
|
return localVarFp.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -6124,6 +6131,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
|
* @param {string} [userId]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {number} [skip]
|
* @param {number} [skip]
|
||||||
@ -6132,8 +6140,8 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) {
|
public getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
const getFavoriteCount = async () => {
|
const getFavoriteCount = async () => {
|
||||||
try {
|
try {
|
||||||
const { data: assets } = await api.assetApi.getAllAssets(true, undefined);
|
const { data: assets } = await api.assetApi.getAllAssets(undefined, true, undefined);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favorites: assets.length
|
favorites: assets.length
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const { data: assets } = await api.assetApi.getAllAssets(undefined, true);
|
const { data: assets } = await api.assetApi.getAllAssets(undefined, undefined, true);
|
||||||
$archivedAsset = assets;
|
$archivedAsset = assets;
|
||||||
} catch {
|
} catch {
|
||||||
handleError(Error, 'Unable to load archived assets');
|
handleError(Error, 'Unable to load archived assets');
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const { data: assets } = await api.assetApi.getAllAssets(true, undefined);
|
const { data: assets } = await api.assetApi.getAllAssets(undefined, true, undefined);
|
||||||
favorites = assets;
|
favorites = assets;
|
||||||
} catch {
|
} catch {
|
||||||
handleError(Error, 'Unable to load favorites');
|
handleError(Error, 'Unable to load favorites');
|
||||||
|
Loading…
Reference in New Issue
Block a user