Skip to content

Commit db630eb

Browse files
authored
Merge branch 'main' into timestams-with-seconds
2 parents 77d78b5 + d15f67d commit db630eb

File tree

10 files changed

+156
-37
lines changed

10 files changed

+156
-37
lines changed

docs/docs/guides/database-queries.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Run `docker exec -it immich_postgres psql --dbname=<DB_DATABASE_NAME> --username
1212

1313
## Assets
1414

15+
### Name
16+
1517
:::note
1618
The `"originalFileName"` column is the name of the file at time of upload, including the extension.
1719
:::
@@ -27,6 +29,8 @@ SELECT * FROM "asset" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-
2729
SELECT * FROM "asset" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
2830
```
2931

32+
### ID
33+
3034
```sql title="Find by ID"
3135
SELECT * FROM "asset" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
3236
```
@@ -35,6 +39,8 @@ SELECT * FROM "asset" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
3539
SELECT * FROM "asset" WHERE "id"::text LIKE '%ab431d3a%';
3640
```
3741

42+
### Checksum
43+
3844
:::note
3945
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
4046
:::
@@ -51,6 +57,8 @@ SELECT T1."checksum", array_agg(T2."id") ids FROM "asset" T1
5157
WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum";
5258
```
5359

60+
### Metadata
61+
5462
```sql title="Live photos"
5563
SELECT * FROM "asset" WHERE "livePhotoVideoId" IS NOT NULL;
5664
```
@@ -77,27 +85,37 @@ SELECT * FROM "asset"
7785
ORDER BY "asset_exif"."fileSizeInByte" ASC;
7886
```
7987

80-
```sql title="Without thumbnails"
81-
SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL;
82-
```
88+
### Type
8389

8490
```sql title="By type"
8591
SELECT * FROM "asset" WHERE "asset"."type" = 'VIDEO';
8692
SELECT * FROM "asset" WHERE "asset"."type" = 'IMAGE';
8793
```
8894

8995
```sql title="Count by type"
90-
SELECT "asset"."type", COUNT(1) FROM "asset" GROUP BY "asset"."type";
96+
SELECT "asset"."type", COUNT(*) FROM "asset" GROUP BY "asset"."type";
9197
```
9298

9399
```sql title="Count by type (per user)"
94-
SELECT "user"."email", "asset"."type", COUNT(1) FROM "asset"
100+
SELECT "user"."email", "asset"."type", COUNT(*) FROM "asset"
95101
JOIN "user" ON "asset"."ownerId" = "user"."id"
96102
GROUP BY "asset"."type", "user"."email" ORDER BY "user"."email";
97103
```
98104

99-
```sql title="Failed file movements"
100-
SELECT * FROM "move_history";
105+
## Tags
106+
107+
```sql title="Count by tag"
108+
SELECT "t"."value" AS "tag_name", COUNT(*) AS "number_assets" FROM "tag" "t"
109+
JOIN "tag_asset" "ta" ON "t"."id" = "ta"."tagsId" JOIN "asset" "a" ON "ta"."assetsId" = "a"."id"
110+
WHERE "a"."visibility" != 'hidden'
111+
GROUP BY "t"."value" ORDER BY "number_assets" DESC;
112+
```
113+
114+
```sql title="Count by tag (per user)"
115+
SELECT "t"."value" AS "tag_name", "u"."email" as "user_email", COUNT(*) AS "number_assets" FROM "tag" "t"
116+
JOIN "tag_asset" "ta" ON "t"."id" = "ta"."tagsId" JOIN "asset" "a" ON "ta"."assetsId" = "a"."id" JOIN "user" "u" ON "a"."ownerId" = "u"."id"
117+
WHERE "a"."visibility" != 'hidden'
118+
GROUP BY "t"."value", "u"."email" ORDER BY "number_assets" DESC;
101119
```
102120

103121
## Users
@@ -110,18 +128,30 @@ SELECT * FROM "user";
110128
SELECT "user".* FROM "user" JOIN "asset" ON "user"."id" = "asset"."ownerId" WHERE "asset"."id" = 'fa310b01-2f26-4b7a-9042-d578226e021f';
111129
```
112130

113-
## System Config
131+
## Persons
132+
133+
```sql title="Delete person and unset it for the faces it was associated with"
134+
DELETE FROM "person" WHERE "name" = 'PersonNameHere';
135+
```
136+
137+
## System
138+
139+
### Config
114140

115141
```sql title="Custom settings"
116142
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
117143
```
118144

119145
(Only used when not using the [config file](/docs/install/config-file))
120146

121-
## Persons
147+
### File properties
122148

123-
```sql title="Delete person and unset it for the faces it was associated with"
124-
DELETE FROM "person" WHERE "name" = 'PersonNameHere';
149+
```sql title="Without thumbnails"
150+
SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL;
151+
```
152+
153+
```sql title="Failed file movements"
154+
SELECT * FROM "move_history";
125155
```
126156

127157
## Postgres internal

mobile/lib/domain/models/timeline.model.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,13 @@ class TimeBucket extends Bucket {
4545
class TimelineReloadEvent extends Event {
4646
const TimelineReloadEvent();
4747
}
48+
49+
class ScrollToTopEvent extends Event {
50+
const ScrollToTopEvent();
51+
}
52+
53+
class ScrollToDateEvent extends Event {
54+
final DateTime date;
55+
56+
const ScrollToDateEvent(this.date);
57+
}

mobile/lib/infrastructure/repositories/log.repository.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ class IsarLogRepository extends IsarDatabaseRepository {
1919

2020
Future<bool> insert(LogMessage log) async {
2121
final logEntity = LoggerMessage.fromDto(log);
22-
await transaction(() async {
23-
await _db.loggerMessages.put(logEntity);
24-
});
22+
23+
try {
24+
await transaction(() => _db.loggerMessages.put(logEntity));
25+
} catch (e) {
26+
return false;
27+
}
28+
2529
return true;
2630
}
2731

mobile/lib/pages/common/tab_shell.page.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import 'package:auto_route/auto_route.dart';
22
import 'package:easy_localization/easy_localization.dart';
33
import 'package:flutter/material.dart';
44
import 'package:hooks_riverpod/hooks_riverpod.dart';
5+
import 'package:immich_mobile/domain/models/timeline.model.dart';
6+
import 'package:immich_mobile/domain/utils/event_stream.dart';
57
import 'package:immich_mobile/extensions/build_context_extensions.dart';
68
import 'package:immich_mobile/providers/app_settings.provider.dart';
7-
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
89
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
910
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
1011
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -155,7 +156,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
155156
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
156157
// On Photos page menu tapped
157158
if (router.activeIndex == 0 && index == 0) {
158-
scrollToTopNotifierProvider.scrollToTop();
159+
EventStream.shared.emit(const ScrollToTopEvent());
159160
}
160161

161162
// On Search page tapped

mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:hooks_riverpod/hooks_riverpod.dart';
44
import 'package:immich_mobile/constants/enums.dart';
55
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
6+
import 'package:immich_mobile/domain/models/timeline.model.dart';
67
import 'package:immich_mobile/domain/utils/event_stream.dart';
78
import 'package:immich_mobile/extensions/build_context_extensions.dart';
89
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
@@ -15,6 +16,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse
1516
import 'package:immich_mobile/providers/routes.provider.dart';
1617
import 'package:immich_mobile/providers/user.provider.dart';
1718
import 'package:immich_mobile/providers/websocket.provider.dart';
19+
import 'package:immich_mobile/routing/router.dart';
1820

1921
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
2022
const ViewerTopAppBar({super.key});
@@ -30,6 +32,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
3032
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
3133
final isInLockedView = ref.watch(inLockedViewProvider);
3234

35+
final previousRouteName = ref.watch(previousRouteNameProvider);
36+
final showViewInTimelineButton = previousRouteName != TabShellRoute.name && previousRouteName != null;
37+
3338
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
3439
int opacity = ref.watch(
3540
assetViewerProvider.select((state) => state.backgroundOpacity),
@@ -50,6 +55,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
5055
const CastActionButton(
5156
menuItem: true,
5257
),
58+
if (showViewInTimelineButton)
59+
IconButton(
60+
onPressed: () async {
61+
await context.maybePop();
62+
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
63+
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
64+
},
65+
icon: const Icon(Icons.image_search),
66+
tooltip: 'view_in_timeline',
67+
),
5368
if (asset.hasRemote && isOwner && !asset.isFavorite)
5469
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
5570
if (asset.hasRemote && isOwner && asset.isFavorite)

mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import 'package:auto_route/auto_route.dart';
44
import 'package:easy_localization/easy_localization.dart';
55
import 'package:flutter/material.dart';
66
import 'package:immich_mobile/domain/models/memory.model.dart';
7-
8-
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
7+
import 'package:immich_mobile/domain/models/timeline.model.dart';
8+
import 'package:immich_mobile/domain/utils/event_stream.dart';
9+
import 'package:immich_mobile/routing/router.dart';
910

1011
class DriftMemoryBottomInfo extends StatelessWidget {
1112
final DriftMemory memory;
@@ -44,18 +45,22 @@ class DriftMemoryBottomInfo extends StatelessWidget {
4445
),
4546
],
4647
),
47-
MaterialButton(
48-
minWidth: 0,
49-
onPressed: () {
50-
context.maybePop();
51-
scrollToDateNotifierProvider.scrollToDate(fileCreatedDate);
52-
},
53-
shape: const CircleBorder(),
54-
color: Colors.white.withValues(alpha: 0.2),
55-
elevation: 0,
56-
child: const Icon(
57-
Icons.open_in_new,
58-
color: Colors.white,
48+
Tooltip(
49+
message: 'view_in_timeline'.tr(),
50+
child: MaterialButton(
51+
minWidth: 0,
52+
onPressed: () {
53+
context.maybePop();
54+
context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
55+
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
56+
},
57+
shape: const CircleBorder(),
58+
color: Colors.white.withValues(alpha: 0.2),
59+
elevation: 0,
60+
child: const Icon(
61+
Icons.open_in_new,
62+
color: Colors.white,
63+
),
5964
),
6065
),
6166
]),

mobile/lib/presentation/widgets/timeline/timeline.widget.dart

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,73 @@ class _SliverTimeline extends ConsumerStatefulWidget {
9797

9898
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
9999
final _scrollController = ScrollController();
100-
StreamSubscription? _reloadSubscription;
100+
StreamSubscription? _eventSubscription;
101101

102102
@override
103103
void initState() {
104104
super.initState();
105-
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
105+
_eventSubscription = EventStream.shared.listen(_onEvent);
106+
}
107+
108+
void _onEvent(Event event) {
109+
switch (event) {
110+
case ScrollToTopEvent():
111+
_scrollController.animateTo(
112+
0,
113+
duration: const Duration(milliseconds: 250),
114+
curve: Curves.easeInOut,
115+
);
116+
case ScrollToDateEvent scrollToDateEvent:
117+
_scrollToDate(scrollToDateEvent.date);
118+
case TimelineReloadEvent():
119+
setState(() {});
120+
default:
121+
break;
122+
}
106123
}
107124

108125
@override
109126
void dispose() {
110127
_scrollController.dispose();
111-
_reloadSubscription?.cancel();
128+
_eventSubscription?.cancel();
112129
super.dispose();
113130
}
114131

132+
void _scrollToDate(DateTime date) {
133+
final asyncSegments = ref.read(timelineSegmentProvider);
134+
asyncSegments.whenData((segments) {
135+
// Find the segment that contains assets from the target date
136+
final targetSegment = segments.firstWhereOrNull((segment) {
137+
if (segment.bucket is TimeBucket) {
138+
final segmentDate = (segment.bucket as TimeBucket).date;
139+
// Check if the segment date matches the target date (year, month, day)
140+
return segmentDate.year == date.year && segmentDate.month == date.month && segmentDate.day == date.day;
141+
}
142+
return false;
143+
});
144+
145+
// If exact date not found, try to find the closest month
146+
final fallbackSegment = targetSegment ??
147+
segments.firstWhereOrNull((segment) {
148+
if (segment.bucket is TimeBucket) {
149+
final segmentDate = (segment.bucket as TimeBucket).date;
150+
return segmentDate.year == date.year && segmentDate.month == date.month;
151+
}
152+
return false;
153+
});
154+
155+
if (fallbackSegment != null) {
156+
// Scroll to the segment with a small offset to show the header
157+
final targetOffset = fallbackSegment.startOffset - 50;
158+
_scrollController.animateTo(
159+
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
160+
duration: const Duration(milliseconds: 500),
161+
curve: Curves.easeInOut,
162+
);
163+
}
164+
});
165+
}
166+
115167
@override
116168
Widget build(BuildContext _) {
117169
final asyncSegments = ref.watch(timelineSegmentProvider);

mobile/lib/providers/routes.provider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
22

33
final inLockedViewProvider = StateProvider<bool>((ref) => false);
44
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
5+
final previousRouteNameProvider = StateProvider<String?>((ref) => null);

mobile/lib/routing/app_navigation_observer.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class AppNavigationObserver extends AutoRouterObserver {
2626
void didPush(Route route, Route? previousRoute) {
2727
_handleLockedViewState(route, previousRoute);
2828
_handleDriftLockedFolderState(route, previousRoute);
29-
Future(
30-
() => ref.read(currentRouteNameProvider.notifier).state = route.settings.name,
31-
);
29+
Future(() {
30+
ref.read(currentRouteNameProvider.notifier).state = route.settings.name;
31+
ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name;
32+
});
3233
}
3334

3435
_handleLockedViewState(Route route, Route? previousRoute) {

web/src/lib/modals/AlbumPickerModal.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
2929
onMount(async () => {
3030
albums = await getAllAlbums({ shared: shared || undefined });
31-
recentAlbums = albums.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1)).slice(0, 3);
31+
recentAlbums = albums.sort((a, b) => (new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1)).slice(0, 3);
3232
loading = false;
3333
});
3434

0 commit comments

Comments
 (0)