mirror of
https://github.com/immich-app/immich.git
synced 2026-01-22 09:28:56 -08:00
Compare commits
2 Commits
feat/small
...
fix/bring-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89af17f279 | ||
|
|
3304c8efd8 |
@@ -30,3 +30,8 @@ class MultiSelectToggleEvent extends Event {
|
||||
final bool isEnabled;
|
||||
const MultiSelectToggleEvent(this.isEnabled);
|
||||
}
|
||||
|
||||
// Map Events
|
||||
class MapMarkerReloadEvent extends Event {
|
||||
const MapMarkerReloadEvent();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:immich_mobile/domain/models/map.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
|
||||
@@ -11,7 +12,8 @@ class MapFactory {
|
||||
|
||||
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
|
||||
|
||||
MapService remote(String ownerId) => MapService(_mapRepository.remote(ownerId));
|
||||
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
|
||||
MapService(_mapRepository.remote(ownerIds, options));
|
||||
}
|
||||
|
||||
class MapService {
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int count);
|
||||
|
||||
@@ -82,8 +81,8 @@ class TimelineFactory {
|
||||
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
||||
|
||||
TimelineService map(String userId, LatLngBounds bounds) =>
|
||||
TimelineService(_timelineRepository.map(userId, bounds, groupBy));
|
||||
TimelineService map(List<String> userIds, TimelineMapOptions options) =>
|
||||
TimelineService(_timelineRepository.map(userIds, options, groupBy));
|
||||
}
|
||||
|
||||
class TimelineService {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/map.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class DriftMapRepository extends DriftDatabaseRepository {
|
||||
@@ -12,9 +13,27 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
||||
|
||||
const DriftMapRepository(super._db) : _db = _db;
|
||||
|
||||
MapQuery remote(String ownerId) => _mapQueryBuilder(
|
||||
assetFilter: (row) =>
|
||||
row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId),
|
||||
MapQuery remote(List<String> ownerIds, TimelineMapOptions options) => _mapQueryBuilder(
|
||||
assetFilter: (row) {
|
||||
Expression<bool> condition =
|
||||
row.deletedAt.isNull() &
|
||||
row.ownerId.isIn(ownerIds) &
|
||||
_db.remoteAssetEntity.visibility.isIn([
|
||||
AssetVisibility.timeline.index,
|
||||
if (options.includeArchived) AssetVisibility.archive.index,
|
||||
]);
|
||||
|
||||
if (options.onlyFavorites) {
|
||||
condition = condition & _db.remoteAssetEntity.isFavorite.equals(true);
|
||||
}
|
||||
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate);
|
||||
}
|
||||
|
||||
return condition;
|
||||
},
|
||||
);
|
||||
|
||||
MapQuery _mapQueryBuilder({Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter}) {
|
||||
|
||||
@@ -15,6 +15,22 @@ import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
class TimelineMapOptions {
|
||||
final LatLngBounds bounds;
|
||||
final bool onlyFavorites;
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
final int relativeDays;
|
||||
|
||||
const TimelineMapOptions({
|
||||
required this.bounds,
|
||||
this.onlyFavorites = false,
|
||||
this.includeArchived = false,
|
||||
this.withPartners = false,
|
||||
this.relativeDays = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
|
||||
@@ -467,15 +483,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||
}
|
||||
|
||||
TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count),
|
||||
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count),
|
||||
origin: TimelineOrigin.map,
|
||||
);
|
||||
|
||||
Stream<List<Bucket>> _watchMapBucket(
|
||||
String userId,
|
||||
LatLngBounds bounds, {
|
||||
List<String> userId,
|
||||
TimelineMapOptions options, {
|
||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||
}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
@@ -496,14 +512,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.remoteAssetEntity.ownerId.isIn(userId) &
|
||||
_db.remoteExifEntity.inBounds(options.bounds) &
|
||||
_db.remoteAssetEntity.visibility.isIn([
|
||||
AssetVisibility.timeline.index,
|
||||
if (options.includeArchived) AssetVisibility.archive.index,
|
||||
]) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull(),
|
||||
)
|
||||
..groupBy([dateExp])
|
||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||
|
||||
if (options.onlyFavorites) {
|
||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||
}
|
||||
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||
}
|
||||
|
||||
return query.map((row) {
|
||||
final timeline = row.read(dateExp)!.truncateDate(groupBy);
|
||||
final assetCount = row.read(assetCountExp)!;
|
||||
@@ -512,8 +540,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<List<BaseAsset>> _getMapBucketAssets(
|
||||
String userId,
|
||||
LatLngBounds bounds, {
|
||||
List<String> userId,
|
||||
TimelineMapOptions options, {
|
||||
required int offset,
|
||||
required int count,
|
||||
}) {
|
||||
@@ -526,13 +554,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteExifEntity.inBounds(bounds) &
|
||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||
_db.remoteAssetEntity.ownerId.isIn(userId) &
|
||||
_db.remoteExifEntity.inBounds(options.bounds) &
|
||||
_db.remoteAssetEntity.visibility.isIn([
|
||||
AssetVisibility.timeline.index,
|
||||
if (options.includeArchived) AssetVisibility.archive.index,
|
||||
]) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull(),
|
||||
)
|
||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||
..limit(count, offset: offset);
|
||||
|
||||
if (options.onlyFavorites) {
|
||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||
}
|
||||
|
||||
if (options.relativeDays != 0) {
|
||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||
}
|
||||
|
||||
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -10,6 +11,16 @@ class DriftMapPage extends StatelessWidget {
|
||||
|
||||
const DriftMapPage({super.key, this.initialLocation});
|
||||
|
||||
void onSettingsPressed(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
elevation: 0.0,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => const DriftMapSettingsSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -18,8 +29,8 @@ class DriftMapPage extends StatelessWidget {
|
||||
children: [
|
||||
DriftMap(initialLocation: initialLocation),
|
||||
Positioned(
|
||||
left: 16,
|
||||
top: 60,
|
||||
left: 20,
|
||||
top: 70,
|
||||
child: IconButton.filled(
|
||||
color: Colors.white,
|
||||
onPressed: () => context.pop(),
|
||||
@@ -32,6 +43,21 @@ class DriftMapPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 20,
|
||||
top: 70,
|
||||
child: IconButton.filled(
|
||||
color: Colors.white,
|
||||
onPressed: () => onSettingsPressed(context),
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
style: IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(8),
|
||||
backgroundColor: Colors.indigo,
|
||||
shadowColor: Colors.black26,
|
||||
elevation: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ class MapBottomSheet extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.9,
|
||||
maxChildSize: 0.75,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
actions: [],
|
||||
@@ -38,8 +38,13 @@ class _ScopedMapTimeline extends StatelessWidget {
|
||||
throw Exception('User must be logged in to access archive');
|
||||
}
|
||||
|
||||
final bounds = ref.watch(mapStateProvider).bounds;
|
||||
final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds);
|
||||
final users = ref.watch(mapStateProvider).withPartners
|
||||
? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
|
||||
: [user.id];
|
||||
|
||||
final timelineService = ref
|
||||
.watch(timelineFactoryProvider)
|
||||
.map(users, ref.watch(mapStateProvider).toOptions());
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class MapState {
|
||||
final ThemeMode themeMode;
|
||||
final LatLngBounds bounds;
|
||||
final bool onlyFavorites;
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
final int relativeDays;
|
||||
|
||||
const MapState({required this.bounds});
|
||||
const MapState({
|
||||
this.themeMode = ThemeMode.system,
|
||||
required this.bounds,
|
||||
this.onlyFavorites = false,
|
||||
this.includeArchived = false,
|
||||
this.withPartners = false,
|
||||
this.relativeDays = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant MapState other) {
|
||||
@@ -15,9 +34,31 @@ class MapState {
|
||||
@override
|
||||
int get hashCode => bounds.hashCode;
|
||||
|
||||
MapState copyWith({LatLngBounds? bounds}) {
|
||||
return MapState(bounds: bounds ?? this.bounds);
|
||||
MapState copyWith({
|
||||
LatLngBounds? bounds,
|
||||
ThemeMode? themeMode,
|
||||
bool? onlyFavorites,
|
||||
bool? includeArchived,
|
||||
bool? withPartners,
|
||||
int? relativeDays,
|
||||
}) {
|
||||
return MapState(
|
||||
bounds: bounds ?? this.bounds,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
onlyFavorites: onlyFavorites ?? this.onlyFavorites,
|
||||
includeArchived: includeArchived ?? this.includeArchived,
|
||||
withPartners: withPartners ?? this.withPartners,
|
||||
relativeDays: relativeDays ?? this.relativeDays,
|
||||
);
|
||||
}
|
||||
|
||||
TimelineMapOptions toOptions() => TimelineMapOptions(
|
||||
bounds: bounds,
|
||||
onlyFavorites: onlyFavorites,
|
||||
includeArchived: includeArchived,
|
||||
withPartners: withPartners,
|
||||
relativeDays: relativeDays,
|
||||
);
|
||||
}
|
||||
|
||||
class MapStateNotifier extends Notifier<MapState> {
|
||||
@@ -31,11 +72,50 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
return true;
|
||||
}
|
||||
|
||||
void switchTheme(ThemeMode mode) {
|
||||
// TODO: Remove this line when map theme provider is removed
|
||||
// Until then, keep both in sync as MapThemeOverride uses map state provider
|
||||
// ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index);
|
||||
ref.read(mapStateNotifierProvider.notifier).switchTheme(mode);
|
||||
state = state.copyWith(themeMode: mode);
|
||||
}
|
||||
|
||||
void switchFavoriteOnly(bool isFavoriteOnly) {
|
||||
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly);
|
||||
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
void switchIncludeArchived(bool isIncludeArchived) {
|
||||
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived);
|
||||
state = state.copyWith(includeArchived: isIncludeArchived);
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
void switchWithPartners(bool isWithPartners) {
|
||||
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners);
|
||||
state = state.copyWith(withPartners: isWithPartners);
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
void setRelativeTime(int relativeDays) {
|
||||
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays);
|
||||
state = state.copyWith(relativeDays: relativeDays);
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
MapState build() => MapState(
|
||||
// TODO: set default bounds
|
||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||
);
|
||||
MapState build() {
|
||||
final appSettingsService = ref.read(appSettingsServiceProvider);
|
||||
return MapState(
|
||||
themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)],
|
||||
onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly),
|
||||
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
||||
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
||||
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
|
||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This provider watches the markers from the map service and serves the markers.
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
@@ -51,11 +53,19 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
final _reloadMutex = AsyncMutex();
|
||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||
StreamSubscription? _eventSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_eventSubscription = EventStream.shared.listen<MapMarkerReloadEvent>(_onEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncer.dispose();
|
||||
bottomSheetOffset.dispose();
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -63,6 +73,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
mapController = controller;
|
||||
}
|
||||
|
||||
void _onEvent(_) => _debouncer.run(() => setBounds(forceReload: true));
|
||||
|
||||
Future<void> onMapReady() async {
|
||||
final controller = mapController;
|
||||
if (controller == null) {
|
||||
@@ -98,7 +110,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
);
|
||||
}
|
||||
|
||||
_debouncer.run(setBounds);
|
||||
_debouncer.run(() => setBounds(forceReload: true));
|
||||
controller.addListener(onMapMoved);
|
||||
}
|
||||
|
||||
@@ -110,7 +122,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
_debouncer.run(setBounds);
|
||||
}
|
||||
|
||||
Future<void> setBounds() async {
|
||||
Future<void> setBounds({bool forceReload = false}) async {
|
||||
final controller = mapController;
|
||||
if (controller == null || !mounted) {
|
||||
return;
|
||||
@@ -127,7 +139,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
final bounds = await controller.getVisibleRegion();
|
||||
unawaited(
|
||||
_reloadMutex.run(() async {
|
||||
if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) {
|
||||
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
||||
final markers = await ref.read(mapMarkerProvider(bounds).future);
|
||||
await reloadMarkers(markers);
|
||||
}
|
||||
@@ -203,7 +215,7 @@ class _Map extends StatelessWidget {
|
||||
onMapCreated: onMapCreated,
|
||||
onStyleLoadedCallback: onMapReady,
|
||||
attributionButtonPosition: AttributionButtonPosition.topRight,
|
||||
attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72),
|
||||
attributionButtonMargins: const Point(8, kToolbarHeight),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -244,7 +256,7 @@ class _DynamicMyLocationButton extends StatelessWidget {
|
||||
valueListenable: bottomSheetOffset,
|
||||
builder: (context, offset, child) {
|
||||
return Positioned(
|
||||
right: 16,
|
||||
right: 20,
|
||||
bottom: context.height * (offset - 0.02) + context.padding.bottom,
|
||||
child: AnimatedOpacity(
|
||||
opacity: offset < 0.8 ? 1 : 0,
|
||||
|
||||
59
mobile/lib/presentation/widgets/map/map_settings_sheet.dart
Normal file
59
mobile/lib/presentation/widgets/map/map_settings_sheet.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart';
|
||||
|
||||
class DriftMapSettingsSheet extends HookConsumerWidget {
|
||||
const DriftMapSettingsSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapState = ref.watch(mapStateProvider);
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
builder: (ctx, scrollController) => SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Card(
|
||||
elevation: 0.0,
|
||||
shadowColor: Colors.transparent,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
MapThemePicker(
|
||||
themeMode: mapState.themeMode,
|
||||
onThemeChange: (mode) => ref.read(mapStateProvider.notifier).switchTheme(mode),
|
||||
),
|
||||
const Divider(height: 30, thickness: 2),
|
||||
MapSettingsListTile(
|
||||
title: "map_settings_only_show_favorites",
|
||||
selected: mapState.onlyFavorites,
|
||||
onChanged: (favoriteOnly) => ref.read(mapStateProvider.notifier).switchFavoriteOnly(favoriteOnly),
|
||||
),
|
||||
MapSettingsListTile(
|
||||
title: "map_settings_include_show_archived",
|
||||
selected: mapState.includeArchived,
|
||||
onChanged: (includeArchive) =>
|
||||
ref.read(mapStateProvider.notifier).switchIncludeArchived(includeArchive),
|
||||
),
|
||||
MapSettingsListTile(
|
||||
title: "map_settings_include_show_partners",
|
||||
selected: mapState.withPartners,
|
||||
onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners),
|
||||
),
|
||||
MapTimeDropDown(
|
||||
relativeTime: mapState.relativeDays,
|
||||
onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/domain/services/map.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
final mapRepositoryProvider = Provider<DriftMapRepository>((ref) => DriftMapRepository(ref.watch(driftProvider)));
|
||||
@@ -13,7 +15,11 @@ final mapServiceProvider = Provider<MapService>(
|
||||
throw Exception('User must be logged in to access map');
|
||||
}
|
||||
|
||||
final mapService = ref.watch(mapFactoryProvider).remote(user.id);
|
||||
final users = ref.watch(mapStateProvider).withPartners
|
||||
? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
|
||||
: [user.id];
|
||||
|
||||
final mapService = ref.watch(mapFactoryProvider).remote(users, ref.watch(mapStateProvider).toOptions());
|
||||
return mapService;
|
||||
},
|
||||
// Empty dependencies to inform the framework that this provider
|
||||
|
||||
@@ -38,11 +38,6 @@
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
</p>
|
||||
|
||||
## Warnung
|
||||
|
||||
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
|
||||
- ⚠️ Gehe von möglichen Fehlern und von Änderungen mit Breaking-Changes aus.
|
||||
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
|
||||
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
|
||||
|
||||
> [!NOTE]
|
||||
@@ -62,7 +57,7 @@
|
||||
|
||||
## Demo
|
||||
|
||||
Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben.
|
||||
Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Smartphone-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben.
|
||||
|
||||
### Login Daten
|
||||
|
||||
@@ -93,7 +88,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App
|
||||
| LivePhoto/MotionPhoto Sicherung und Wiedergabe | Ja | Ja |
|
||||
| Unterstützung für 360-Grad-Bilder | Nein | Ja |
|
||||
| Benutzerdefinierte Speicherstruktur | Ja | Ja |
|
||||
| Öffentliches Teilen | Nein | Ja |
|
||||
| Öffentliches Teilen | Ja | Ja |
|
||||
| Archiv und Favoriten | Ja | Ja |
|
||||
| Globale Karte | Ja | Ja |
|
||||
| Partnerfreigabe (Teilen) | Ja | Ja |
|
||||
@@ -103,7 +98,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. Für die Handy-App
|
||||
| Schreibgeschützte Gallerie | Ja | Ja |
|
||||
| Gestapelte Bilder | Ja | Ja |
|
||||
| Tags | Nein | Ja |
|
||||
| Ordner-Ansicht | Nein | Ja |
|
||||
| Ordner-Ansicht | Ja | Ja |
|
||||
|
||||
|
||||
## Übersetzungen
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from 'src/enum';
|
||||
import { ConcurrentQueueName, FullsizeImageOptions, ImageOptions } from 'src/types';
|
||||
|
||||
export type SystemConfig = {
|
||||
export interface SystemConfig {
|
||||
backup: {
|
||||
database: {
|
||||
enabled: boolean;
|
||||
@@ -187,7 +187,7 @@ export type SystemConfig = {
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type MachineLearningConfig = SystemConfig['machineLearning'];
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from TagsList', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any);
|
||||
mockReadTags({ TagsList: ['Parent'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -398,7 +398,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchy from TagsList', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child'] } } as any);
|
||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||
@@ -419,7 +419,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a string', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any);
|
||||
mockReadTags({ Keywords: 'Parent' });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -430,7 +430,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a list', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any);
|
||||
mockReadTags({ Keywords: ['Parent'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -441,10 +441,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a list with a number', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent', '2024'] } } as any);
|
||||
mockReadTags({ Keywords: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -456,7 +453,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchal tags from Keywords', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child'] } } as any);
|
||||
mockReadTags({ Keywords: 'Parent/Child' });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -476,10 +473,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore Keywords when TagsList is present', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'Child'] }),
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'Child'] } } as any);
|
||||
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -499,10 +493,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'TagA'] }),
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'TagA'] } } as any);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||
@@ -524,10 +515,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent', '2024'] } } as any);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
@@ -539,7 +527,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Mom|Dad'] }) });
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Mom|Dad'] } } as any);
|
||||
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
|
||||
@@ -554,10 +542,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }),
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'Parent2/Child2'] } } as any);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { JobStatus } from 'src/enum';
|
||||
import { TagService } from 'src/services/tag.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(TagService.name, () => {
|
||||
@@ -192,10 +191,7 @@ describe(TagService.name, () => {
|
||||
it('should upsert records', async () => {
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
tags: [factory.tag({ value: 'tag-1' }), factory.tag({ value: 'tag-2' })],
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ tags: [{ value: 'tag-1' }, { value: 'tag-2' }] } as any);
|
||||
mocks.tag.upsertAssetIds.mockResolvedValue([
|
||||
{ tagId: 'tag-1', assetId: 'asset-1' },
|
||||
{ tagId: 'tag-1', assetId: 'asset-2' },
|
||||
@@ -246,10 +242,7 @@ describe(TagService.name, () => {
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.tag.addAssetIds.mockResolvedValue();
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
tags: [factory.tag({ value: 'tag-1' })],
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValue({ tags: [{ value: 'tag-1' }] } as any);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2']));
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -23,39 +23,34 @@ import {
|
||||
VideoCodec,
|
||||
} from 'src/enum';
|
||||
|
||||
export type DeepPartial<T> =
|
||||
T extends Record<string, unknown>
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T extends Array<infer R>
|
||||
? DeepPartial<R>[]
|
||||
: T;
|
||||
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
||||
|
||||
export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
|
||||
|
||||
export type FullsizeImageOptions = {
|
||||
export interface FullsizeImageOptions {
|
||||
format: ImageFormat;
|
||||
quality: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type ImageOptions = {
|
||||
export interface ImageOptions {
|
||||
format: ImageFormat;
|
||||
quality: number;
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type RawImageInfo = {
|
||||
export interface RawImageInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
channels: 1 | 2 | 3 | 4;
|
||||
};
|
||||
}
|
||||
|
||||
type DecodeImageOptions = {
|
||||
interface DecodeImageOptions {
|
||||
colorspace: string;
|
||||
processInvalidImages: boolean;
|
||||
raw?: RawImageInfo;
|
||||
edits?: AssetEditActionItem[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DecodeToBufferOptions extends DecodeImageOptions {
|
||||
size?: number;
|
||||
@@ -509,7 +504,7 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string,
|
||||
[SystemMetadataKey.MemoriesState]: MemoriesState;
|
||||
}
|
||||
|
||||
export type UserPreferences = {
|
||||
export interface UserPreferences {
|
||||
albums: {
|
||||
defaultAssetOrder: AssetOrder;
|
||||
};
|
||||
@@ -552,7 +547,7 @@ export type UserPreferences = {
|
||||
cast: {
|
||||
gCastEnabled: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
|
||||
key: T;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Activity,
|
||||
ApiKey,
|
||||
AssetFace,
|
||||
AssetFile,
|
||||
AuthApiKey,
|
||||
AuthSharedLink,
|
||||
@@ -10,16 +9,12 @@ import {
|
||||
Library,
|
||||
Memory,
|
||||
Partner,
|
||||
Person,
|
||||
Session,
|
||||
Stack,
|
||||
Tag,
|
||||
User,
|
||||
UserAdmin,
|
||||
} from 'src/database';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEditAction, AssetEditActionItem, MirrorAxis } from 'src/dtos/editing.dto';
|
||||
import { QueueStatisticsDto } from 'src/dtos/queue.dto';
|
||||
import {
|
||||
AssetFileType,
|
||||
@@ -28,11 +23,10 @@ import {
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
Permission,
|
||||
SourceType,
|
||||
UserMetadataKey,
|
||||
UserStatus,
|
||||
} from 'src/enum';
|
||||
import { DeepPartial, OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { v4, v7 } from 'uuid';
|
||||
|
||||
export const newUuid = () => v4();
|
||||
@@ -166,18 +160,11 @@ const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
|
||||
...dto,
|
||||
});
|
||||
|
||||
const stackFactory = ({ owner, assets, ...stack }: DeepPartial<Stack> = {}): Stack => {
|
||||
const ownerId = newUuid();
|
||||
|
||||
return {
|
||||
id: newUuid(),
|
||||
primaryAssetId: assets?.[0].id ?? newUuid(),
|
||||
ownerId,
|
||||
owner: userFactory(owner ?? { id: ownerId }),
|
||||
assets: assets?.map((asset) => assetFactory(asset)) ?? [],
|
||||
...stack,
|
||||
};
|
||||
};
|
||||
const stackFactory = () => ({
|
||||
id: newUuid(),
|
||||
ownerId: newUuid(),
|
||||
primaryAssetId: newUuid(),
|
||||
});
|
||||
|
||||
const userFactory = (user: Partial<User> = {}) => ({
|
||||
id: newUuid(),
|
||||
@@ -236,43 +223,39 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const assetFactory = (
|
||||
asset: Omit<DeepPartial<MapAsset>, 'exifInfo' | 'owner' | 'stack' | 'tags' | 'faces' | 'files' | 'edits'> = {},
|
||||
) => {
|
||||
return {
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUuidV7(),
|
||||
status: AssetStatus.Active,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `/data/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.Image,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
isEdited: false,
|
||||
...asset,
|
||||
};
|
||||
};
|
||||
const assetFactory = (asset: Partial<MapAsset> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUuidV7(),
|
||||
status: AssetStatus.Active,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `/data/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.Image,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
isEdited: false,
|
||||
...asset,
|
||||
});
|
||||
|
||||
const activityFactory = (activity: Partial<Activity> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
@@ -408,102 +391,6 @@ const assetFileFactory = (file: Partial<AssetFile> = {}): AssetFile => ({
|
||||
...file,
|
||||
});
|
||||
|
||||
const exifFactory = (exif: Partial<Exif> = {}) => ({
|
||||
assetId: newUuid(),
|
||||
autoStackId: null,
|
||||
bitsPerSample: null,
|
||||
city: 'Austin',
|
||||
colorspace: null,
|
||||
country: 'United States of America',
|
||||
dateTimeOriginal: newDate(),
|
||||
description: '',
|
||||
exifImageHeight: 420,
|
||||
exifImageWidth: 42,
|
||||
exposureTime: null,
|
||||
fileSizeInByte: 69,
|
||||
fNumber: 1.7,
|
||||
focalLength: 4.38,
|
||||
fps: null,
|
||||
iso: 947,
|
||||
latitude: 30.267_334_570_570_195,
|
||||
longitude: -97.789_833_534_282_07,
|
||||
lensModel: null,
|
||||
livePhotoCID: null,
|
||||
make: 'Google',
|
||||
model: 'Pixel 7',
|
||||
modifyDate: newDate(),
|
||||
orientation: '1',
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: 4,
|
||||
state: 'Texas',
|
||||
tags: ['parent/child'],
|
||||
timeZone: 'UTC-6',
|
||||
...exif,
|
||||
});
|
||||
|
||||
const tagFactory = (tag: Partial<Tag>): Tag => ({
|
||||
id: newUuid(),
|
||||
color: null,
|
||||
createdAt: newDate(),
|
||||
parentId: null,
|
||||
updatedAt: newDate(),
|
||||
value: `tag-${newUuid()}`,
|
||||
...tag,
|
||||
});
|
||||
|
||||
const faceFactory = ({ person, ...face }: DeepPartial<AssetFace> = {}): AssetFace => ({
|
||||
assetId: newUuid(),
|
||||
boundingBoxX1: 1,
|
||||
boundingBoxX2: 2,
|
||||
boundingBoxY1: 1,
|
||||
boundingBoxY2: 2,
|
||||
deletedAt: null,
|
||||
id: newUuid(),
|
||||
imageHeight: 420,
|
||||
imageWidth: 42,
|
||||
isVisible: true,
|
||||
personId: null,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
person: person === null ? null : personFactory(person),
|
||||
...face,
|
||||
});
|
||||
|
||||
const assetEditFactory = (edit?: Partial<AssetEditActionItem>): AssetEditActionItem => {
|
||||
switch (edit?.action) {
|
||||
case AssetEditAction.Crop: {
|
||||
return { action: AssetEditAction.Crop, parameters: { height: 42, width: 42, x: 0, y: 10 }, ...edit };
|
||||
}
|
||||
case AssetEditAction.Mirror: {
|
||||
return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal }, ...edit };
|
||||
}
|
||||
case AssetEditAction.Rotate: {
|
||||
return { action: AssetEditAction.Rotate, parameters: { angle: 90 }, ...edit };
|
||||
}
|
||||
default: {
|
||||
return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const personFactory = (person?: Partial<Person>): Person => ({
|
||||
birthDate: newDate(),
|
||||
color: null,
|
||||
createdAt: newDate(),
|
||||
faceAssetId: null,
|
||||
id: newUuid(),
|
||||
isFavorite: false,
|
||||
isHidden: false,
|
||||
name: 'person',
|
||||
ownerId: newUuid(),
|
||||
thumbnailPath: '/path/to/person/thumbnail.jpg',
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
...person,
|
||||
});
|
||||
|
||||
export const factory = {
|
||||
activity: activityFactory,
|
||||
apiKey: apiKeyFactory,
|
||||
@@ -525,11 +412,6 @@ export const factory = {
|
||||
jobAssets: {
|
||||
sidecarWrite: assetSidecarWriteFactory,
|
||||
},
|
||||
exif: exifFactory,
|
||||
face: faceFactory,
|
||||
person: personFactory,
|
||||
assetEdit: assetEditFactory,
|
||||
tag: tagFactory,
|
||||
uuid: newUuid,
|
||||
date: newDate,
|
||||
responses: {
|
||||
|
||||
Reference in New Issue
Block a user