mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:08 -04:00
Compare commits
32 Commits
b3993dbdad
...
09a2dcca7a
Author | SHA1 | Date | |
---|---|---|---|
|
09a2dcca7a | ||
|
63437529e1 | ||
|
39017922a9 | ||
|
8e90913e0f | ||
|
4c9a7a7ed5 | ||
|
e69bcf3f70 | ||
|
bee7cd9683 | ||
|
4d45e36ea0 | ||
|
701b2be633 | ||
|
8475cfb7a5 | ||
|
c2e4d91b69 | ||
|
0a9d8ac380 | ||
|
ad84a6e8c2 | ||
|
bb50ccb1ca | ||
|
f73deae77c | ||
|
c26baea530 | ||
|
27d390a756 | ||
|
9266197cd2 | ||
|
3417330bdf | ||
|
2dc73c2987 | ||
|
2320e7a0d7 | ||
|
77bfa5d445 | ||
|
31f2b94396 | ||
|
424de03204 | ||
|
30a3f827a2 | ||
|
34ea42c005 | ||
|
27d5b134ac | ||
|
746354b779 | ||
|
9d6a177547 | ||
|
c886fcab74 | ||
|
14a5e982e6 | ||
|
a1df3878a9 |
@ -1,4 +1,15 @@
|
||||
{
|
||||
"collections": "Collections",
|
||||
"on_this_device": "On this device",
|
||||
"add_a_name": "Add a name",
|
||||
"places": "Places",
|
||||
"albums": "Albums",
|
||||
"people": "People",
|
||||
"shared_links": "Shared links",
|
||||
"trash": "Trash",
|
||||
"archived": "Archived",
|
||||
"favorites": "Favorites",
|
||||
"search_albums": "Search albums",
|
||||
"action_common_back": "Back",
|
||||
"action_common_cancel": "Cancel",
|
||||
"action_common_clear": "Clear",
|
||||
|
405
mobile/lib/pages/collections/albums/albums_collection.page.dart
Normal file
405
mobile/lib/pages/collections/albums/albums_collection.page.dart
Normal file
@ -0,0 +1,405 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
enum QuickFilterMode {
|
||||
all,
|
||||
sharedWithMe,
|
||||
myAlbums,
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class AlbumsCollectionPage extends HookConsumerWidget {
|
||||
const AlbumsCollectionPage({super.key, this.showImmichAppbar = false});
|
||||
|
||||
final bool showImmichAppbar;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums =
|
||||
ref.watch(albumProviderV2).where((album) => album.isRemote).toList();
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
|
||||
final isGrid = useState(false);
|
||||
final searchController = useTextEditingController();
|
||||
final debounceTimer = useRef<Timer?>(null);
|
||||
final filterMode = useState(QuickFilterMode.all);
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
toggleViewMode() {
|
||||
isGrid.value = !isGrid.value;
|
||||
}
|
||||
|
||||
onSearch(String value) {
|
||||
debounceTimer.value?.cancel();
|
||||
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||
filterMode.value = QuickFilterMode.all;
|
||||
ref.read(albumProviderV2.notifier).searchAlbums(value);
|
||||
});
|
||||
}
|
||||
|
||||
changeFilter(QuickFilterMode mode) {
|
||||
filterMode.value = mode;
|
||||
ref.read(albumProviderV2.notifier).filterAlbums(mode);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text);
|
||||
});
|
||||
|
||||
return () {
|
||||
searchController.removeListener(() {
|
||||
onSearch(searchController.text);
|
||||
});
|
||||
debounceTimer.value?.cancel();
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: showImmichAppbar
|
||||
? null
|
||||
: Text(
|
||||
"${'albums'.tr()} ${albums.length}",
|
||||
),
|
||||
bottom: showImmichAppbar
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(0),
|
||||
child: ImmichAppBar(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
children: [
|
||||
SearchBar(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
context.colorScheme.surfaceContainer,
|
||||
),
|
||||
autoFocus: false,
|
||||
hintText: "search_albums".tr(),
|
||||
onChanged: onSearch,
|
||||
elevation: const WidgetStatePropertyAll(0.25),
|
||||
controller: searchController,
|
||||
leading: const Icon(Icons.search_rounded),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(10),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
QuickFilterButton(
|
||||
label: 'All',
|
||||
isSelected: filterMode.value == QuickFilterMode.all,
|
||||
onTap: () => changeFilter(QuickFilterMode.all),
|
||||
),
|
||||
QuickFilterButton(
|
||||
label: 'Shared with me',
|
||||
isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
|
||||
onTap: () => changeFilter(QuickFilterMode.sharedWithMe),
|
||||
),
|
||||
QuickFilterButton(
|
||||
label: 'My albums',
|
||||
isSelected: filterMode.value == QuickFilterMode.myAlbums,
|
||||
onTap: () => changeFilter(QuickFilterMode.myAlbums),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SortButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isGrid.value
|
||||
? Icons.view_list_rounded
|
||||
: Icons.grid_view_outlined,
|
||||
size: 24,
|
||||
),
|
||||
onPressed: toggleViewMode,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: isGrid.value
|
||||
? GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return AlbumThumbnailCard(
|
||||
album: sorted[index],
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(albumId: sorted[index].id),
|
||||
),
|
||||
showOwner: true,
|
||||
);
|
||||
},
|
||||
itemCount: sorted.length,
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: LargeLeadingTile(
|
||||
title: Text(
|
||||
sorted[index].name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: sorted[index].ownerId == userId
|
||||
? Text(
|
||||
'${sorted[index].assetCount} items',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: sorted[index].ownerName != null
|
||||
? Text(
|
||||
'${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr(
|
||||
args: [
|
||||
sorted[index].ownerName!,
|
||||
],
|
||||
)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: context
|
||||
.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(albumId: sorted[index].id),
|
||||
),
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(15)),
|
||||
child: ImmichThumbnail(
|
||||
asset: sorted[index].thumbnail.value,
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
// minVerticalPadding: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickFilterButton extends StatelessWidget {
|
||||
const QuickFilterButton({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: isSelected
|
||||
? Icon(
|
||||
Icons.check_rounded,
|
||||
color: context.colorScheme.onPrimary,
|
||||
size: 18,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SortButton extends ConsumerWidget {
|
||||
const SortButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
|
||||
return MenuAnchor(
|
||||
style: MenuStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
consumeOutsideTap: true,
|
||||
menuChildren: AlbumSortMode.values
|
||||
.map(
|
||||
(mode) => MenuItemButton(
|
||||
leadingIcon: albumSortOption == mode
|
||||
? albumSortIsReverse
|
||||
? Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: albumSortOption == mode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: albumSortOption == mode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: const Icon(Icons.abc, color: Colors.transparent),
|
||||
onPressed: () {
|
||||
final selected = albumSortOption == mode;
|
||||
// Switch direction
|
||||
if (selected) {
|
||||
ref
|
||||
.read(albumSortOrderProvider.notifier)
|
||||
.changeSortDirection(!albumSortIsReverse);
|
||||
} else {
|
||||
ref
|
||||
.read(albumSortByOptionsProvider.notifier)
|
||||
.changeSortMode(mode);
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.all(8)),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
albumSortOption == mode
|
||||
? context.colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
mode.label.tr(),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: albumSortOption == mode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
builder: (context, controller, child) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Transform.rotate(
|
||||
angle: 90 * pi / 180,
|
||||
child: Icon(
|
||||
Icons.compare_arrows_rounded,
|
||||
size: 18,
|
||||
color: context.colorScheme.onSurface.withAlpha(225),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.label.tr(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withAlpha(225),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LocalAlbumsCollectionPage extends HookConsumerWidget {
|
||||
const LocalAlbumsCollectionPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref.watch(localAlbumsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('on_this_device'.tr()),
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
itemCount: albums.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: false,
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: ImmichThumbnail(
|
||||
asset: albums[index].thumbnail.value,
|
||||
width: 60,
|
||||
height: 90,
|
||||
),
|
||||
),
|
||||
minVerticalPadding: 1,
|
||||
title: Text(
|
||||
albums[index].name,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text('${albums[index].assetCount} items'),
|
||||
onTap: () => context
|
||||
.pushRoute(AlbumViewerRoute(albumId: albums[index].id)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
310
mobile/lib/pages/collections/collections.page.dart
Normal file
310
mobile/lib/pages/collections/collections.page.dart
Normal file
@ -0,0 +1,310 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/share_partner_button.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CollectionsPage extends ConsumerWidget {
|
||||
const CollectionsPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
|
||||
return Scaffold(
|
||||
appBar: const ImmichAppBar(
|
||||
showUploadButton: false,
|
||||
actions: [CreateNewButton(), SharePartnerButton()],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ActionButton(
|
||||
onPressed: () => context.pushRoute(const FavoritesRoute()),
|
||||
icon: Icons.favorite_outline_rounded,
|
||||
label: 'favorites'.tr(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionButton(
|
||||
onPressed: () => context.pushRoute(const ArchiveRoute()),
|
||||
icon: Icons.archive_outlined,
|
||||
label: 'archived'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
ActionButton(
|
||||
onPressed: () => context.pushRoute(const SharedLinkRoute()),
|
||||
icon: Icons.link_outlined,
|
||||
label: 'shared_links'.tr(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
trashEnabled
|
||||
? ActionButton(
|
||||
onPressed: () => context.pushRoute(const TrashRoute()),
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: 'trash'.tr(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
PeopleCollectionCard(),
|
||||
AlbumsCollectionCard(),
|
||||
AlbumsCollectionCard(isLocal: true),
|
||||
PlacesCollectionCard(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PeopleCollectionCard extends ConsumerWidget {
|
||||
const PeopleCollectionCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final people = ref.watch(getAllPeopleProvider);
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: people.widgetWhen(
|
||||
onData: (people) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: people.take(4).map((person) {
|
||||
return CircleAvatar(
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'people'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumsCollectionCard extends HookConsumerWidget {
|
||||
final bool isLocal;
|
||||
|
||||
const AlbumsCollectionCard({super.key, this.isLocal = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = isLocal
|
||||
? ref.watch(localAlbumsProvider)
|
||||
: ref.watch(remoteAlbumsProvider);
|
||||
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => isLocal
|
||||
? context.pushRoute(
|
||||
const LocalAlbumsCollectionRoute(),
|
||||
)
|
||||
: context.pushRoute(AlbumsCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
isLocal ? 'on_this_device'.tr() : 'albums'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlacesCollectionCard extends StatelessWidget {
|
||||
const PlacesCollectionCard({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const ActionButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||
alignment: Alignment.centerLeft,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(10),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
icon,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateNewButton extends StatelessWidget {
|
||||
const CreateNewButton({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
size: 32,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
104
mobile/lib/pages/collections/people/people_collection.page.dart
Normal file
104
mobile/lib/pages/collections/people/people_collection.page.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PeopleCollectionPage extends HookConsumerWidget {
|
||||
const PeopleCollectionPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final people = ref.watch(getAllPeopleProvider);
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('people'.tr()),
|
||||
),
|
||||
body: people.when(
|
||||
data: (people) {
|
||||
return GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 0.85,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
itemCount: people.length,
|
||||
itemBuilder: (context, index) {
|
||||
final person = people[index];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: person.id,
|
||||
personName: person.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
shape: const CircleBorder(side: BorderSide.none),
|
||||
elevation: 3,
|
||||
child: CircleAvatar(
|
||||
maxRadius: 96 / 2,
|
||||
backgroundImage: NetworkImage(
|
||||
getFaceThumbnailUrl(person.id),
|
||||
headers: headers,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () => showNameEditModel(person.id, person.name),
|
||||
child: person.name.isEmpty
|
||||
? Text(
|
||||
'add_a_name'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
person.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stack) => const Text("error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
125
mobile/lib/pages/collections/places/places_collection.part.dart
Normal file
125
mobile/lib/pages/collections/places/places_collection.part.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PlacesCollectionPage extends HookConsumerWidget {
|
||||
const PlacesCollectionPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getAllPlacesProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('places'.tr()),
|
||||
),
|
||||
body: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
width: context.width,
|
||||
child: MapThumbnail(
|
||||
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
places.when(
|
||||
data: (places) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: places.length,
|
||||
itemBuilder: (context, index) {
|
||||
final place = places[index];
|
||||
|
||||
return PlaceTile(id: place.id, name: place.label);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stask) => const Text('Error getting places'),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceTile extends StatelessWidget {
|
||||
const PlaceTile({super.key, required this.id, required this.name});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbnailUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail';
|
||||
|
||||
void navigateToPlace() {
|
||||
context.pushRoute(
|
||||
SearchInputRoute(
|
||||
prefilter: SearchFilter(
|
||||
people: {},
|
||||
location: SearchLocationFilter(
|
||||
city: name,
|
||||
),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return LargeLeadingTile(
|
||||
onTap: () => navigateToPlace(),
|
||||
title: Text(
|
||||
name,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: CachedNetworkImage(
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: thumbnailUrl,
|
||||
httpHeaders: ApiService.getRequestHeaders(),
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.image_not_supported_outlined),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
47
mobile/lib/pages/common/large_leading_tile.dart
Normal file
47
mobile/lib/pages/common/large_leading_tile.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LargeLeadingTile extends StatelessWidget {
|
||||
const LargeLeadingTile({
|
||||
super.key,
|
||||
required this.leading,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leadingPadding = const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
});
|
||||
|
||||
final Widget leading;
|
||||
final VoidCallback onTap;
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry leadingPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: leadingPadding,
|
||||
child: leading,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
child: title,
|
||||
),
|
||||
subtitle ?? const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -76,24 +76,36 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
selectedIcon: const Icon(Icons.photo_library),
|
||||
label: const Text('tab_controller_nav_photos').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('albums').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.space_dashboard_outlined),
|
||||
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||
label: const Text('collections').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
selectedIcon: const Icon(Icons.search),
|
||||
label: const Text('tab_controller_nav_search').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
selectedIcon: const Icon(Icons.share),
|
||||
label: const Text('tab_controller_nav_sharing').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('tab_controller_nav_library').tr(),
|
||||
),
|
||||
// NavigationRailDestination(
|
||||
// padding: const EdgeInsets.all(4),
|
||||
// icon: const Icon(Icons.share_rounded),
|
||||
// selectedIcon: const Icon(Icons.share),
|
||||
// label: const Text('tab_controller_nav_sharing').tr(),
|
||||
// ),
|
||||
// NavigationRailDestination(
|
||||
// padding: const EdgeInsets.all(4),
|
||||
// icon: const Icon(Icons.photo_album_outlined),
|
||||
// selectedIcon: const Icon(Icons.photo_album),
|
||||
// label: const Text('tab_controller_nav_library').tr(),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -125,27 +137,7 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_search'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.search_rounded,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.search,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_sharing'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.group,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_library'.tr(),
|
||||
label: 'albums'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
@ -156,17 +148,63 @@ class TabControllerPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'collections'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.space_dashboard_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_search'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.search_rounded,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.search,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
// NavigationDestination(
|
||||
// label: 'tab_controller_nav_sharing'.tr(),
|
||||
// icon: const Icon(
|
||||
// Icons.group_outlined,
|
||||
// ),
|
||||
// selectedIcon: Icon(
|
||||
// Icons.group,
|
||||
// color: context.primaryColor,
|
||||
// ),
|
||||
// ),
|
||||
// NavigationDestination(
|
||||
// label: 'tab_controller_nav_library'.tr(),
|
||||
// icon: const Icon(
|
||||
// Icons.photo_album_outlined,
|
||||
// ),
|
||||
// selectedIcon: buildIcon(
|
||||
// Icon(
|
||||
// Icons.photo_album_rounded,
|
||||
// color: context.primaryColor,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
return AutoTabsRouter(
|
||||
routes: const [
|
||||
PhotosRoute(),
|
||||
SearchRoute(),
|
||||
SharingRoute(),
|
||||
LibraryRoute(),
|
||||
routes: [
|
||||
const PhotosRoute(),
|
||||
AlbumsCollectionRoute(showImmichAppbar: true),
|
||||
const CollectionsRoute(),
|
||||
// SharingRoute(),
|
||||
// LibraryRoute(),
|
||||
const SearchRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
|
@ -184,7 +184,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
|
||||
final local = albums.where((a) => a.isLocal).toList();
|
||||
|
||||
Widget? shareTrashButton() {
|
||||
Widget shareTrashButton() {
|
||||
return trashEnabled
|
||||
? InkWell(
|
||||
onTap: () => context.pushRoute(const TrashRoute()),
|
||||
@ -195,12 +195,12 @@ class LibraryPage extends HookConsumerWidget {
|
||||
semanticLabel: 'profile_drawer_trash'.tr(),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
action: shareTrashButton(),
|
||||
actions: [shareTrashButton()],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
|
@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||
Text(
|
||||
name.value,
|
||||
style: context.textTheme.titleLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget {
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dar
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/share_partner_button.dart';
|
||||
import 'package:immich_mobile/widgets/partner/partner_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@ -215,25 +216,13 @@ class SharingPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget sharePartnerButton() {
|
||||
return InkWell(
|
||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Icon(
|
||||
Icons.swap_horizontal_circle_rounded,
|
||||
size: 25,
|
||||
semanticLabel: 'partner_page_title'.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
action: sharePartnerButton(),
|
||||
appBar: const ImmichAppBar(
|
||||
actions: [SharePartnerButton()],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
AlbumNotifier(this._albumService, Isar db) : super([]) {
|
||||
AlbumNotifier(this._albumService, this.db) : super([]) {
|
||||
final query = db.albums
|
||||
.filter()
|
||||
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
|
||||
@ -25,6 +25,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
}
|
||||
|
||||
final AlbumService _albumService;
|
||||
final Isar db;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
Future<void> getAllAlbums() => Future.wait([
|
||||
@ -64,6 +65,16 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void searchAlbums(String value) async {
|
||||
final query = db.albums
|
||||
.filter()
|
||||
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId))
|
||||
.nameContains(value, caseSensitive: false);
|
||||
|
||||
final albums = await query.findAll();
|
||||
state = albums;
|
||||
}
|
||||
}
|
||||
|
||||
final albumProvider =
|
||||
|
178
mobile/lib/providers/album/albumv2.provider.dart
Normal file
178
mobile/lib/providers/album/albumv2.provider.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class AlbumNotifierV2 extends StateNotifier<List<Album>> {
|
||||
AlbumNotifierV2(this._albumService, this.db) : super([]) {
|
||||
final query = db.albums.where();
|
||||
|
||||
query.findAll().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final AlbumService _albumService;
|
||||
final Isar db;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
Future<void> refreshAlbums() async {
|
||||
// Future.wait([
|
||||
// _albumService.refreshDeviceAlbums(),
|
||||
// _albumService.refreshAllRemoteAlbums(),
|
||||
// ]);
|
||||
await _albumService.refreshDeviceAlbums();
|
||||
await _albumService.refreshRemoteAlbums(isShared: false);
|
||||
await _albumService.refreshRemoteAlbums(isShared: true);
|
||||
}
|
||||
|
||||
Future<void> getDeviceAlbums() {
|
||||
return _albumService.refreshDeviceAlbums();
|
||||
}
|
||||
|
||||
Future<bool> deleteAlbum(Album album) {
|
||||
return _albumService.deleteAlbum(album);
|
||||
}
|
||||
|
||||
Future<Album?> createAlbum(
|
||||
String albumTitle,
|
||||
Set<Asset> assets,
|
||||
) {
|
||||
return _albumService.createAlbum(albumTitle, assets, []);
|
||||
}
|
||||
|
||||
Future<Album?> getAlbumByName(String albumName, {bool remoteOnly = false}) {
|
||||
return _albumService.getAlbumByName(albumName, remoteOnly);
|
||||
}
|
||||
|
||||
/// Create an album on the server with the same name as the selected album for backup
|
||||
/// First this will check if the album already exists on the server with name
|
||||
/// If it does not exist, it will create the album on the server
|
||||
Future<void> createSyncAlbum(
|
||||
String albumName,
|
||||
) async {
|
||||
final album = await getAlbumByName(albumName, remoteOnly: true);
|
||||
if (album != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createAlbum(albumName, {});
|
||||
}
|
||||
|
||||
void searchAlbums(String value) async {
|
||||
final query = db.albums
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.nameContains(value, caseSensitive: false);
|
||||
|
||||
final albums = await query.findAll();
|
||||
state = albums;
|
||||
}
|
||||
|
||||
void filterAlbums(QuickFilterMode mode) async {
|
||||
switch (mode) {
|
||||
case QuickFilterMode.all:
|
||||
state = await db.albums.filter().remoteIdIsNotNull().findAll();
|
||||
return;
|
||||
case QuickFilterMode.sharedWithMe:
|
||||
state = await db.albums
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.owner(
|
||||
(q) =>
|
||||
q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
)
|
||||
.findAll();
|
||||
return;
|
||||
case QuickFilterMode.myAlbums:
|
||||
state = await db.albums
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.owner(
|
||||
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
|
||||
)
|
||||
.findAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final albumProviderV2 =
|
||||
StateNotifierProvider.autoDispose<AlbumNotifierV2, List<Album>>((ref) {
|
||||
return AlbumNotifierV2(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class RemoteAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||
RemoteAlbumsNotifier(this.db) : super([]) {
|
||||
final query = db.albums.filter().remoteIdIsNotNull();
|
||||
|
||||
query.findAll().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final Isar db;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
|
||||
LocalAlbumsNotifier(this.db) : super([]) {
|
||||
final query = db.albums.filter().not().remoteIdIsNotNull();
|
||||
|
||||
query.findAll().then((value) {
|
||||
if (mounted) {
|
||||
state = value;
|
||||
}
|
||||
});
|
||||
|
||||
_streamSub = query.watch().listen((data) => state = data);
|
||||
}
|
||||
|
||||
final Isar db;
|
||||
late final StreamSubscription<List<Album>> _streamSub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final localAlbumsProvider =
|
||||
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
|
||||
return LocalAlbumsNotifier(ref.watch(dbProvider));
|
||||
});
|
||||
|
||||
final remoteAlbumsProvider =
|
||||
StateNotifierProvider.autoDispose<RemoteAlbumsNotifier, List<Album>>((ref) {
|
||||
return RemoteAlbumsNotifier(ref.watch(dbProvider));
|
||||
});
|
@ -63,6 +63,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
case TabEnum.library:
|
||||
_ref.read(albumProvider.notifier).getAllAlbums();
|
||||
case TabEnum.collections:
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
enum TabEnum {
|
||||
home,
|
||||
search,
|
||||
sharing,
|
||||
library,
|
||||
}
|
||||
enum TabEnum { home, search, sharing, library, collections }
|
||||
|
||||
/// Provides the currently active tab
|
||||
final tabProvider = StateProvider<TabEnum>(
|
||||
|
@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/collections/albums/local_albums_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/collections/people/people_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/collections/places/places_collection.part.dart';
|
||||
import 'package:immich_mobile/pages/collections/collections.page.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||
@ -113,6 +118,14 @@ class AppRouter extends RootStackRouter {
|
||||
page: LibraryRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: CollectionsRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: AlbumsCollectionRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
],
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
),
|
||||
@ -135,7 +148,11 @@ class AppRouter extends RootStackRouter {
|
||||
),
|
||||
AutoRoute(page: EditImageRoute.page),
|
||||
AutoRoute(page: CropImageRoute.page),
|
||||
AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
CustomRoute(
|
||||
page: FavoritesRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(
|
||||
page: AllMotionPhotosRoute.page,
|
||||
@ -181,8 +198,16 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
CustomRoute(
|
||||
page: ArchiveRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: PartnerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
AutoRoute(
|
||||
page: PartnerDetailRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
@ -198,10 +223,15 @@ class AppRouter extends RootStackRouter {
|
||||
page: AlbumOptionsRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(
|
||||
CustomRoute(
|
||||
page: TrashRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: SharedLinkRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
AutoRoute(
|
||||
page: SharedLinkEditRoute.page,
|
||||
@ -230,6 +260,26 @@ class AppRouter extends RootStackRouter {
|
||||
page: HeaderSettingsRoute.page,
|
||||
guards: [_duplicateGuard],
|
||||
),
|
||||
CustomRoute(
|
||||
page: PeopleCollectionRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: AlbumsCollectionRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: LocalAlbumsCollectionRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: PlacesCollectionRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -319,6 +319,53 @@ class AlbumViewerRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumsCollectionPage]
|
||||
class AlbumsCollectionRoute extends PageRouteInfo<AlbumsCollectionRouteArgs> {
|
||||
AlbumsCollectionRoute({
|
||||
Key? key,
|
||||
bool showImmichAppbar = false,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumsCollectionRoute.name,
|
||||
args: AlbumsCollectionRouteArgs(
|
||||
key: key,
|
||||
showImmichAppbar: showImmichAppbar,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumsCollectionRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<AlbumsCollectionRouteArgs>(
|
||||
orElse: () => const AlbumsCollectionRouteArgs());
|
||||
return AlbumsCollectionPage(
|
||||
key: args.key,
|
||||
showImmichAppbar: args.showImmichAppbar,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class AlbumsCollectionRouteArgs {
|
||||
const AlbumsCollectionRouteArgs({
|
||||
this.key,
|
||||
this.showImmichAppbar = false,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final bool showImmichAppbar;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumsCollectionRouteArgs{key: $key, showImmichAppbar: $showImmichAppbar}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AllMotionPhotosPage]
|
||||
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
||||
@ -555,6 +602,25 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [CollectionsPage]
|
||||
class CollectionsRoute extends PageRouteInfo<void> {
|
||||
const CollectionsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
CollectionsRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'CollectionsRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const CollectionsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [CreateAlbumPage]
|
||||
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
||||
@ -857,6 +923,25 @@ class LibraryRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LocalAlbumsCollectionPage]
|
||||
class LocalAlbumsCollectionRoute extends PageRouteInfo<void> {
|
||||
const LocalAlbumsCollectionRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
LocalAlbumsCollectionRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LocalAlbumsCollectionRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const LocalAlbumsCollectionPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LoginPage]
|
||||
class LoginRoute extends PageRouteInfo<void> {
|
||||
@ -1059,6 +1144,25 @@ class PartnerRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PeopleCollectionPage]
|
||||
class PeopleCollectionRoute extends PageRouteInfo<void> {
|
||||
const PeopleCollectionRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PeopleCollectionRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PeopleCollectionRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const PeopleCollectionPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PermissionOnboardingPage]
|
||||
class PermissionOnboardingRoute extends PageRouteInfo<void> {
|
||||
@ -1149,6 +1253,25 @@ class PhotosRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PlacesCollectionPage]
|
||||
class PlacesCollectionRoute extends PageRouteInfo<void> {
|
||||
const PlacesCollectionRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PlacesCollectionRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PlacesCollectionRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const PlacesCollectionPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecentlyAddedPage]
|
||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
|
||||
@ -50,6 +51,10 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
}
|
||||
|
||||
if (route.name == 'CollectionsRoute') {
|
||||
ref.read(albumProviderV2.notifier).refreshAlbums();
|
||||
}
|
||||
|
||||
if (route.name == 'HomeRoute') {
|
||||
ref.invalidate(memoryFutureProvider);
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
|
@ -175,6 +175,49 @@ class AlbumService {
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// V2
|
||||
Future<bool> refreshAllRemoteAlbums() async {
|
||||
if (!_remoteCompleter.isCompleted) {
|
||||
// guard against concurrent calls
|
||||
return _remoteCompleter.future;
|
||||
}
|
||||
_remoteCompleter = Completer();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
bool changes = false;
|
||||
try {
|
||||
final albumList = await Future.wait([
|
||||
// _apiService.albumsApi.getAllAlbums(shared: true),
|
||||
// _apiService.albumsApi.getAllAlbums(shared: false),
|
||||
]);
|
||||
|
||||
// for (int i = 0; i < albumList.length; i++) {
|
||||
// final albums = albumList[i];
|
||||
// final isShared = i == 1;
|
||||
// if (albums != null) {
|
||||
// final hasChange = await _syncService.syncRemoteAlbumsToDb(
|
||||
// albums,
|
||||
// isShared: isShared,
|
||||
// loadDetails: (dto) async => dto.assetCount == dto.assets.length
|
||||
// ? dto
|
||||
// : (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
|
||||
// );
|
||||
|
||||
// if (hasChange) {
|
||||
// changes = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} catch (e) {
|
||||
debugPrint("Error refreshing all albums: $e");
|
||||
return false;
|
||||
} finally {
|
||||
_remoteCompleter.complete(changes);
|
||||
}
|
||||
|
||||
debugPrint("refreshAllRemoteAlbums took ${sw.elapsedMilliseconds}ms");
|
||||
return changes;
|
||||
}
|
||||
|
||||
Future<Album?> createAlbum(
|
||||
String albumName,
|
||||
Iterable<Asset> assets, [
|
||||
|
@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
/// Whether or not to show the owner of the album (or "Owned")
|
||||
/// in the subtitle of the album
|
||||
final bool showOwner;
|
||||
final bool showTitle;
|
||||
|
||||
const AlbumThumbnailCard({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
this.showOwner = false,
|
||||
this.showTitle = true,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
: 'album_thumbnail_card_items'
|
||||
.tr(args: ['${album.assetCount}']),
|
||||
),
|
||||
if (owner != null) const TextSpan(text: ' · '),
|
||||
if (owner != null) const TextSpan(text: ' • '),
|
||||
if (owner != null) TextSpan(text: owner),
|
||||
],
|
||||
),
|
||||
@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
: buildAlbumThumbnail(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: cardSize,
|
||||
child: Text(
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
if (showTitle) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: cardSize,
|
||||
child: Text(
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildAlbumTextRow(),
|
||||
buildAlbumTextRow(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
final Widget? action;
|
||||
final List<Widget>? actions;
|
||||
final bool showUploadButton;
|
||||
|
||||
const ImmichAppBar({super.key, this.action});
|
||||
const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (action != null)
|
||||
Padding(padding: const EdgeInsets.only(right: 20), child: action!),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildBackupIndicator(),
|
||||
),
|
||||
if (actions != null)
|
||||
...actions!.map(
|
||||
(action) => Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: action,
|
||||
),
|
||||
),
|
||||
if (showUploadButton)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildBackupIndicator(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildProfileIndicator(),
|
||||
|
21
mobile/lib/widgets/common/share_partner_button.dart
Normal file
21
mobile/lib/widgets/common/share_partner_button.dart
Normal file
@ -0,0 +1,21 @@
|
||||
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 SharePartnerButton extends StatelessWidget {
|
||||
const SharePartnerButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Icon(
|
||||
Icons.swap_horizontal_circle_rounded,
|
||||
size: 25,
|
||||
semanticLabel: 'partner_page_title'.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
||||
});
|
||||
|
||||
final double size;
|
||||
final bool showTitle = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -5,6 +5,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
@ -36,6 +37,7 @@ let instance: StorageCore | null;
|
||||
export class StorageCore {
|
||||
private constructor(
|
||||
private assetRepository: IAssetRepository,
|
||||
private configRepository: IConfigRepository,
|
||||
private cryptoRepository: ICryptoRepository,
|
||||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
@ -46,6 +48,7 @@ export class StorageCore {
|
||||
|
||||
static create(
|
||||
assetRepository: IAssetRepository,
|
||||
configRepository: IConfigRepository,
|
||||
cryptoRepository: ICryptoRepository,
|
||||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
@ -56,6 +59,7 @@ export class StorageCore {
|
||||
if (!instance) {
|
||||
instance = new StorageCore(
|
||||
assetRepository,
|
||||
configRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
@ -245,7 +249,11 @@ export class StorageCore {
|
||||
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
||||
return false;
|
||||
}
|
||||
const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger };
|
||||
const repos = {
|
||||
configRepo: this.configRepository,
|
||||
metadataRepo: this.systemMetadataRepository,
|
||||
logger: this.logger,
|
||||
};
|
||||
const config = await getConfig(repos, { withCache: true });
|
||||
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
||||
const { checksum } = assetInfo;
|
||||
|
@ -3,6 +3,7 @@ import { VectorExtension } from 'src/interfaces/database.interface';
|
||||
export const IConfigRepository = 'IConfigRepository';
|
||||
|
||||
export interface EnvData {
|
||||
configFile?: string;
|
||||
database: {
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
|
@ -6,6 +6,7 @@ import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||
export class ConfigRepository implements IConfigRepository {
|
||||
getEnv(): EnvData {
|
||||
return {
|
||||
configFile: process.env.IMMICH_CONFIG_FILE,
|
||||
database: {
|
||||
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
||||
vectorExtension: getVectorExtension(),
|
||||
|
@ -4,6 +4,7 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -19,6 +20,7 @@ import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -45,6 +47,7 @@ describe(AssetService.name, () => {
|
||||
let sut: AssetService;
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
@ -66,6 +69,7 @@ describe(AssetService.name, () => {
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
@ -77,6 +81,7 @@ describe(AssetService.name, () => {
|
||||
sut = new AssetService(
|
||||
accessMock,
|
||||
assetMock,
|
||||
configMock,
|
||||
jobMock,
|
||||
systemMock,
|
||||
userMock,
|
||||
|
@ -22,6 +22,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, Permission } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IAssetDeleteJob,
|
||||
@ -46,6 +47,7 @@ export class AssetService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@ -54,7 +56,7 @@ export class AssetService extends BaseService {
|
||||
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(AssetService.name);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -20,6 +21,7 @@ import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -57,6 +59,7 @@ const oauthUserWithDefaultQuota = {
|
||||
|
||||
describe('AuthService', () => {
|
||||
let sut: AuthService;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
@ -89,6 +92,7 @@ describe('AuthService', () => {
|
||||
}),
|
||||
} as any);
|
||||
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
@ -98,7 +102,17 @@ describe('AuthService', () => {
|
||||
shareMock = newSharedLinkRepositoryMock();
|
||||
keyMock = newKeyRepositoryMock();
|
||||
|
||||
sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
|
||||
sut = new AuthService(
|
||||
configMock,
|
||||
cryptoMock,
|
||||
eventMock,
|
||||
systemMock,
|
||||
loggerMock,
|
||||
userMock,
|
||||
sessionMock,
|
||||
shareMock,
|
||||
keyMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
@ -31,6 +31,7 @@ import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -72,6 +73,7 @@ export type ValidateRequest = {
|
||||
@Injectable()
|
||||
export class AuthService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@ -81,7 +83,7 @@ export class AuthService extends BaseService {
|
||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(AuthService.name);
|
||||
|
||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||
|
@ -1,32 +1,30 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
|
||||
export class BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
private get repos() {
|
||||
return {
|
||||
configRepo: this.configRepository,
|
||||
metadataRepo: this.systemMetadataRepository,
|
||||
logger: this.logger,
|
||||
};
|
||||
}
|
||||
|
||||
getConfig(options: { withCache: boolean }) {
|
||||
return getConfig(
|
||||
{
|
||||
metadataRepo: this.systemMetadataRepository,
|
||||
logger: this.logger,
|
||||
},
|
||||
options,
|
||||
);
|
||||
return getConfig(this.repos, options);
|
||||
}
|
||||
|
||||
updateConfig(newConfig: SystemConfig) {
|
||||
return updateConfig(
|
||||
{
|
||||
metadataRepo: this.systemMetadataRepository,
|
||||
logger: this.logger,
|
||||
},
|
||||
newConfig,
|
||||
);
|
||||
return updateConfig(this.repos, newConfig);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
@ -13,18 +15,20 @@ import { Mocked, describe, it } from 'vitest';
|
||||
describe(CliService.name, () => {
|
||||
let sut: CliService;
|
||||
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new CliService(cryptoMock, systemMock, userMock, loggerMock);
|
||||
sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock);
|
||||
});
|
||||
|
||||
describe('resetAdminPassword', () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
@ -10,12 +11,13 @@ import { BaseService } from 'src/services/base.service';
|
||||
@Injectable()
|
||||
export class CliService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(CliService.name);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -8,6 +9,7 @@ import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -20,6 +22,7 @@ vitest.useFakeTimers();
|
||||
describe(SearchService.name, () => {
|
||||
let sut: DuplicateService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
@ -28,13 +31,14 @@ describe(SearchService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
searchMock = newSearchRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
|
||||
sut = new DuplicateService(systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
|
||||
sut = new DuplicateService(configMock, systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import {
|
||||
IBaseJob,
|
||||
@ -24,6 +25,7 @@ import { usePagination } from 'src/utils/pagination';
|
||||
@Injectable()
|
||||
export class DuplicateService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@ -31,7 +33,7 @@ export class DuplicateService extends BaseService {
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(DuplicateService.name);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults } from 'src/config';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IJobRepository,
|
||||
@ -18,6 +19,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -37,6 +39,7 @@ const makeMockHandlers = (status: JobStatus) => {
|
||||
describe(JobService.name, () => {
|
||||
let sut: JobService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
@ -46,13 +49,14 @@ describe(JobService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
metricMock = newMetricRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new JobService(assetMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
|
||||
sut = new JobService(assetMock, configMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -5,6 +5,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import { AssetType, ManualJobName } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
ConcurrentQueueName,
|
||||
@ -49,6 +50,7 @@ export class JobService extends BaseService {
|
||||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@ -56,7 +58,7 @@ export class JobService extends BaseService {
|
||||
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(JobService.name);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import {
|
||||
@ -26,6 +27,7 @@ import { libraryStub } from 'test/fixtures/library.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
@ -43,15 +45,17 @@ describe(LibraryService.name, () => {
|
||||
let sut: LibraryService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let libraryMock: Mocked<ILibraryRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
@ -63,12 +67,13 @@ describe(LibraryService.name, () => {
|
||||
|
||||
sut = new LibraryService(
|
||||
assetMock,
|
||||
systemMock,
|
||||
configMock,
|
||||
cryptoMock,
|
||||
databaseMock,
|
||||
jobMock,
|
||||
libraryMock,
|
||||
storageMock,
|
||||
databaseMock,
|
||||
systemMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ArgOf } from 'src/interfaces/event.interface';
|
||||
@ -48,15 +49,16 @@ export class LibraryService extends BaseService {
|
||||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(LibraryService.name);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
VideoCodec,
|
||||
} from 'src/enum';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -26,6 +27,7 @@ import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -39,6 +41,7 @@ import { Mocked } from 'vitest';
|
||||
describe(MediaService.name, () => {
|
||||
let sut: MediaService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let mediaMock: Mocked<IMediaRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
@ -50,6 +53,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
mediaMock = newMediaRepositoryMock();
|
||||
@ -61,6 +65,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
sut = new MediaService(
|
||||
assetMock,
|
||||
configMock,
|
||||
personMock,
|
||||
jobMock,
|
||||
mediaMock,
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import {
|
||||
IBaseJob,
|
||||
@ -55,6 +56,7 @@ export class MediaService extends BaseService {
|
||||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@ -64,10 +66,11 @@ export class MediaService extends BaseService {
|
||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(MediaService.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
configRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
|
@ -6,6 +6,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { AssetType, SourceType } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
@ -29,6 +30,7 @@ import { personStub } from 'test/fixtures/person.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
@ -46,10 +48,14 @@ import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(MetadataService.name, () => {
|
||||
let sut: MetadataService;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoRepository: Mocked<ICryptoRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let mapMock: Mocked<IMapRepository>;
|
||||
let metadataMock: Mocked<IMetadataRepository>;
|
||||
@ -57,16 +63,15 @@ describe(MetadataService.name, () => {
|
||||
let mediaMock: Mocked<IMediaRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let tagMock: Mocked<ITagRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let tagMock: Mocked<ITagRepository>;
|
||||
let sut: MetadataService;
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoRepository = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
mapMock = newMapRepositoryMock();
|
||||
@ -85,9 +90,10 @@ describe(MetadataService.name, () => {
|
||||
sut = new MetadataService(
|
||||
albumMock,
|
||||
assetMock,
|
||||
eventMock,
|
||||
configMock,
|
||||
cryptoRepository,
|
||||
databaseMock,
|
||||
eventMock,
|
||||
jobMock,
|
||||
mapMock,
|
||||
mediaMock,
|
||||
|
@ -15,6 +15,7 @@ import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetType, SourceType } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||
@ -103,9 +104,10 @@ export class MetadataService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@ -118,10 +120,11 @@ export class MetadataService extends BaseService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(MetadataService.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
configRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
|
@ -6,6 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -18,6 +19,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -66,6 +68,7 @@ const configs = {
|
||||
describe(NotificationService.name, () => {
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
@ -77,6 +80,7 @@ describe(NotificationService.name, () => {
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
@ -85,6 +89,7 @@ describe(NotificationService.name, () => {
|
||||
userMock = newUserRepositoryMock();
|
||||
|
||||
sut = new NotificationService(
|
||||
configMock,
|
||||
eventMock,
|
||||
systemMock,
|
||||
notificationMock,
|
||||
|
@ -5,6 +5,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IEmailJob,
|
||||
@ -28,6 +29,7 @@ import { getPreferences } from 'src/utils/preferences';
|
||||
@Injectable()
|
||||
export class NotificationService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
||||
@ -37,7 +39,7 @@ export class NotificationService extends BaseService {
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(NotificationService.name);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -23,6 +24,7 @@ import { personStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -67,6 +69,7 @@ const detectFaceMock: DetectedFaces = {
|
||||
describe(PersonService.name, () => {
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
||||
@ -82,6 +85,7 @@ describe(PersonService.name, () => {
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
machineLearningMock = newMachineLearningRepositoryMock();
|
||||
@ -92,9 +96,11 @@ describe(PersonService.name, () => {
|
||||
searchMock = newSearchRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new PersonService(
|
||||
accessMock,
|
||||
assetMock,
|
||||
configMock,
|
||||
machineLearningMock,
|
||||
moveMock,
|
||||
mediaMock,
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
} from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import {
|
||||
IBaseJob,
|
||||
@ -70,6 +71,7 @@ export class PersonService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@ -81,10 +83,11 @@ export class PersonService extends BaseService {
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(PersonService.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
configRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
repository,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SearchSuggestionType } from 'src/dtos/search.dto';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
@ -12,6 +13,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||
@ -25,6 +27,7 @@ vitest.useFakeTimers();
|
||||
describe(SearchService.name, () => {
|
||||
let sut: SearchService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let machineMock: Mocked<IMachineLearningRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
@ -34,6 +37,7 @@ describe(SearchService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
machineMock = newMachineLearningRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
@ -41,7 +45,16 @@ describe(SearchService.name, () => {
|
||||
partnerMock = newPartnerRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new SearchService(systemMock, machineMock, personMock, searchMock, assetMock, partnerMock, loggerMock);
|
||||
sut = new SearchService(
|
||||
configMock,
|
||||
systemMock,
|
||||
machineMock,
|
||||
personMock,
|
||||
searchMock,
|
||||
assetMock,
|
||||
partnerMock,
|
||||
loggerMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
@ -30,6 +31,7 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||
@Injectable()
|
||||
export class SearchService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||
@ -38,7 +40,7 @@ export class SearchService extends BaseService {
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(SearchService.name);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
@ -6,6 +7,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
@ -16,6 +18,7 @@ import { Mocked } from 'vitest';
|
||||
|
||||
describe(ServerService.name, () => {
|
||||
let sut: ServerService;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
@ -24,6 +27,7 @@ describe(ServerService.name, () => {
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newConfigRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
serverInfoMock = newServerInfoRepositoryMock();
|
||||
@ -31,7 +35,7 @@ describe(ServerService.name, () => {
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
|
||||
sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
|
||||
sut = new ServerService(configMock, userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
UsageByUserDto,
|
||||
} from 'src/dtos/server.dto';
|
||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
@ -23,13 +24,13 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { asHumanReadable } from 'src/utils/bytes';
|
||||
import { isUsingConfigFile } from 'src/utils/config';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class ServerService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@ -37,7 +38,7 @@ export class ServerService extends BaseService {
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(ServerService.name);
|
||||
}
|
||||
|
||||
@ -91,6 +92,7 @@ export class ServerService extends BaseService {
|
||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
||||
await this.getConfig({ withCache: false });
|
||||
const { configFile } = this.configRepository.getEnv();
|
||||
|
||||
return {
|
||||
smartSearch: isSmartSearchEnabled(machineLearning),
|
||||
@ -105,7 +107,7 @@ export class ServerService extends BaseService {
|
||||
oauth: oauth.enabled,
|
||||
oauthAutoLaunch: oauth.autoLaunch,
|
||||
passwordLogin: passwordLogin.enabled,
|
||||
configFile: isUsingConfigFile(),
|
||||
configFile: !!configFile,
|
||||
email: notifications.smtp.enabled,
|
||||
};
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
@ -13,6 +14,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
||||
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
|
||||
describe(SharedLinkService.name, () => {
|
||||
let sut: SharedLinkService;
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let shareMock: Mocked<ISharedLinkRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
@ -29,12 +32,13 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
shareMock = newSharedLinkRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
logMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new SharedLinkService(accessMock, cryptoMock, logMock, shareMock, systemMock);
|
||||
sut = new SharedLinkService(accessMock, configMock, cryptoMock, logMock, shareMock, systemMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -15,6 +15,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { Permission, SharedLinkType } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
@ -27,12 +28,13 @@ import { OpenGraphTags } from 'src/utils/misc';
|
||||
export class SharedLinkService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(SharedLinkService.name);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -11,6 +12,7 @@ import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
|
||||
describe(SmartInfoService.name, () => {
|
||||
let sut: SmartInfoService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
@ -31,13 +34,24 @@ describe(SmartInfoService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
searchMock = newSearchRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
machineMock = newMachineLearningRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
|
||||
|
||||
sut = new SmartInfoService(
|
||||
assetMock,
|
||||
configMock,
|
||||
databaseMock,
|
||||
jobMock,
|
||||
machineMock,
|
||||
searchMock,
|
||||
systemMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ArgOf } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
@ -26,6 +27,7 @@ import { usePagination } from 'src/utils/pagination';
|
||||
export class SmartInfoService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||
@ -33,7 +35,7 @@ export class SmartInfoService extends BaseService {
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(SmartInfoService.name);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
@ -19,6 +20,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -33,6 +35,7 @@ describe(StorageTemplateService.name, () => {
|
||||
let sut: StorageTemplateService;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
@ -49,6 +52,7 @@ describe(StorageTemplateService.name, () => {
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
@ -63,6 +67,7 @@ describe(StorageTemplateService.name, () => {
|
||||
sut = new StorageTemplateService(
|
||||
albumMock,
|
||||
assetMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
moveMock,
|
||||
personMock,
|
||||
|
@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ArgOf } from 'src/interfaces/event.interface';
|
||||
@ -63,6 +64,7 @@ export class StorageTemplateService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
@ -72,10 +74,11 @@ export class StorageTemplateService extends BaseService {
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(StorageTemplateService.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
configRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
|
@ -12,11 +12,13 @@ import {
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
@ -187,16 +189,18 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
|
||||
describe(SystemConfigService.name, () => {
|
||||
let sut: SystemConfigService;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.IMMICH_CONFIG_FILE;
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new SystemConfigService(systemMock, eventMock, loggerMock);
|
||||
|
||||
sut = new SystemConfigService(configMock, eventMock, systemMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@ -231,8 +235,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should load the config from a json file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||
@ -241,7 +244,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should log errors with the config file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
|
||||
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
||||
|
||||
@ -256,7 +259,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should load the config from a yaml file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
const partialConfig = `
|
||||
ffmpeg:
|
||||
crf: 30
|
||||
@ -275,7 +278,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should accept an empty configuration file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||
@ -284,7 +287,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should allow underscores in the machine learning url', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
@ -300,7 +303,7 @@ describe(SystemConfigService.name, () => {
|
||||
|
||||
for (const { should, externalDomain, result } of externalDomainTests) {
|
||||
it(`should normalize an external domain ${should}`, async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { server: { externalDomain } };
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
@ -310,7 +313,7 @@ describe(SystemConfigService.name, () => {
|
||||
}
|
||||
|
||||
it('should warn for unknown options in yaml', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
const partialConfig = `
|
||||
unknownOption: true
|
||||
`;
|
||||
@ -331,7 +334,7 @@ describe(SystemConfigService.name, () => {
|
||||
|
||||
for (const test of tests) {
|
||||
it(`should ${test.should}`, async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
|
||||
if (test.warn) {
|
||||
@ -390,7 +393,7 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw an error if a config file is in use', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
|
@ -15,21 +15,23 @@ import {
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||
import { LogLevel } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { clearConfigCache, isUsingConfigFile } from 'src/utils/config';
|
||||
import { clearConfigCache } from 'src/utils/config';
|
||||
import { toPlainObject } from 'src/utils/object';
|
||||
|
||||
@Injectable()
|
||||
export class SystemConfigService extends BaseService {
|
||||
constructor(
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(SystemConfigService.name);
|
||||
}
|
||||
|
||||
@ -67,7 +69,8 @@ export class SystemConfigService extends BaseService {
|
||||
}
|
||||
|
||||
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||
if (isUsingConfigFile()) {
|
||||
const { configFile } = this.configRepository.getEnv();
|
||||
if (configFile) {
|
||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { CacheControl, UserMetadataKey } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -14,6 +15,7 @@ import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@ -34,6 +36,7 @@ describe(UserService.name, () => {
|
||||
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
@ -41,14 +44,24 @@ describe(UserService.name, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
configMock = newConfigRepositoryMock();
|
||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
|
||||
sut = new UserService(
|
||||
albumMock,
|
||||
configMock,
|
||||
cryptoRepositoryMock,
|
||||
jobMock,
|
||||
storageMock,
|
||||
systemMock,
|
||||
userMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
||||
userMock.get.mockImplementation((userId) =>
|
||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
|
||||
|
@ -12,6 +12,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@ -26,6 +27,7 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
|
||||
export class UserService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@ -33,7 +35,7 @@ export class UserService extends BaseService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(UserService.name);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
@ -9,6 +10,7 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
@ -30,6 +32,7 @@ const mockRelease = (version: string) => ({
|
||||
|
||||
describe(VersionService.name, () => {
|
||||
let sut: VersionService;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
@ -39,6 +42,7 @@ describe(VersionService.name, () => {
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newConfigRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
@ -47,7 +51,16 @@ describe(VersionService.name, () => {
|
||||
versionMock = newVersionHistoryRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new VersionService(databaseMock, eventMock, jobMock, serverMock, systemMock, versionMock, loggerMock);
|
||||
sut = new VersionService(
|
||||
configMock,
|
||||
databaseMock,
|
||||
eventMock,
|
||||
jobMock,
|
||||
serverMock,
|
||||
systemMock,
|
||||
versionMock,
|
||||
loggerMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -6,6 +6,7 @@ import { OnEvent } from 'src/decorators';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
@ -27,6 +28,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
||||
@Injectable()
|
||||
export class VersionService extends BaseService {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@ -35,7 +37,7 @@ export class VersionService extends BaseService {
|
||||
@Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository,
|
||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||
) {
|
||||
super(systemMetadataRepository, logger);
|
||||
super(configRepository, systemMetadataRepository, logger);
|
||||
this.logger.setContext(VersionService.name);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import * as _ from 'lodash';
|
||||
import { SystemConfig, defaults } from 'src/config';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
@ -15,6 +16,7 @@ import { DeepPartial } from 'typeorm';
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
type RepoDeps = {
|
||||
configRepo: IConfigRepository;
|
||||
metadataRepo: ISystemMetadataRepository;
|
||||
logger: ILoggerRepository;
|
||||
};
|
||||
@ -28,10 +30,6 @@ export const clearConfigCache = () => {
|
||||
lastUpdated = null;
|
||||
};
|
||||
|
||||
export const isUsingConfigFile = () => {
|
||||
return !!process.env.IMMICH_CONFIG_FILE;
|
||||
};
|
||||
|
||||
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
|
||||
if (!withCache || !config) {
|
||||
const timestamp = lastUpdated;
|
||||
@ -80,11 +78,12 @@ const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string
|
||||
};
|
||||
|
||||
const buildConfig = async (repos: RepoDeps) => {
|
||||
const { metadataRepo, logger } = repos;
|
||||
const { configRepo, metadataRepo, logger } = repos;
|
||||
const { configFile } = configRepo.getEnv();
|
||||
|
||||
// load partial
|
||||
const partial = isUsingConfigFile()
|
||||
? await loadFromFile(repos, process.env.IMMICH_CONFIG_FILE as string)
|
||||
const partial = configFile
|
||||
? await loadFromFile(repos, configFile)
|
||||
: await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
|
||||
|
||||
// merge with defaults
|
||||
@ -106,7 +105,7 @@ const buildConfig = async (repos: RepoDeps) => {
|
||||
// validate full config
|
||||
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
||||
if (errors.length > 0) {
|
||||
if (isUsingConfigFile()) {
|
||||
if (configFile) {
|
||||
throw new Error(`Invalid value(s) in file: ${errors}`);
|
||||
} else {
|
||||
logger.error('Validation error', errors);
|
||||
|
Loading…
x
Reference in New Issue
Block a user