mirror of
https://github.com/immich-app/immich.git
synced 2026-07-25 05:50:40 +03:00
Compare commits
4 Commits
fix/min-fa
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4186d171e | ||
|
|
7b023d9a71 | ||
|
|
9abe72aced | ||
|
|
829b4e748e |
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:20ec607c68642c64c73269ce245aa0f060913f4129d15d9687850aa158e6269c AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:5c34b355088846dddc8afb7442c20b9433dccdc8d66192dc52c616adeaa106a3 AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:6771159cd4fa5d9bba1258caf0b82e6b73458c694d178ad97c5e925c2d0e1a91 AS builder-openvino
|
||||
|
||||
FROM builder-cpu AS builder-cuda
|
||||
|
||||
@@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:e2d3af735aff6eeee600b1933bedd99da6645fedf572cc12ef4cc1331f2ceebe AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:b18992999dbe963a45a8a4da40ac2b1975be1a776d939d098c647482bcad5cba AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:f82c96458eedc847b233e582eb31336f4954b39cae020b6dcf5b3ed0e5cbcd74 AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:6771159cd4fa5d9bba1258caf0b82e6b73458c694d178ad97c5e925c2d0e1a91 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
725
machine-learning/uv.lock
generated
725
machine-learning/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,50 +17,67 @@ struct ImmichWidgetView: View {
|
||||
var entry: ImageEntry
|
||||
|
||||
var body: some View {
|
||||
if entry.image == nil {
|
||||
Image("LaunchImage")
|
||||
.tintedWidgetImageModifier()
|
||||
.overlay(alignment: .bottom) {
|
||||
if let error = entry.metadata.error?.errorDescription {
|
||||
Text(error)
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
.alignmentGuide(.bottom) { dimensions in
|
||||
// Place the text below the bottom of the image
|
||||
dimensions[.top] - 8
|
||||
}
|
||||
}
|
||||
}
|
||||
if let image = entry.image {
|
||||
ImmichWidgetContentView(image: image, subtitle: entry.metadata.subtitle, deepLink: entry.metadata.deepLink)
|
||||
} else {
|
||||
ZStack(alignment: .leading) {
|
||||
Color.clear.overlay(
|
||||
Image(uiImage: entry.image!)
|
||||
.resizable()
|
||||
.tintedWidgetImageModifier()
|
||||
.scaledToFill()
|
||||
|
||||
)
|
||||
VStack {
|
||||
Spacer()
|
||||
if let subtitle = entry.metadata.subtitle {
|
||||
Text(subtitle)
|
||||
.foregroundColor(.white)
|
||||
.padding(8)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.cornerRadius(8)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.widgetURL(entry.metadata.deepLink)
|
||||
ImmichWidgetLoadingView(message: entry.metadata.error?.errorDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ImmichWidgetLoadingView: View {
|
||||
let message: String?
|
||||
|
||||
var body: some View {
|
||||
let messageText = Text(message ?? "")
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
// This is used as a nicer way to center the image, rather than using offsets
|
||||
messageText.hidden()
|
||||
|
||||
Image("LaunchImage")
|
||||
.tintedWidgetImageModifier()
|
||||
|
||||
messageText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ImmichWidgetContentView: View {
|
||||
let image: UIImage
|
||||
let subtitle: String?
|
||||
let deepLink: URL?
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Color.clear.overlay(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.tintedWidgetImageModifier()
|
||||
.scaledToFill()
|
||||
)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.foregroundColor(.white)
|
||||
.padding(6)
|
||||
.background(ContainerRelativeShape().fill(Color.black.opacity(0.6)))
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.widgetURL(deepLink)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(
|
||||
"Medium",
|
||||
as: .systemMedium,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
@@ -69,10 +86,93 @@ struct ImmichWidgetView: View {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(named: "ImmichLogo"),
|
||||
image: UIImage(named: "LaunchImage"),
|
||||
metadata: EntryMetadata(
|
||||
subtitle: "1 year ago"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#Preview(
|
||||
"Medium No Data",
|
||||
as: .systemMedium,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
},
|
||||
timeline: {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: nil
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#Preview(
|
||||
"Medium No Data Error",
|
||||
as: .systemMedium,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
},
|
||||
timeline: {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: WidgetError.fetchFailed)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#Preview(
|
||||
"Small",
|
||||
as: .systemSmall,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
},
|
||||
timeline: {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(named: "LaunchImage"),
|
||||
metadata: EntryMetadata(
|
||||
subtitle: "Yesterday"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#Preview(
|
||||
"Small No Data Error",
|
||||
as: .systemSmall,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
},
|
||||
timeline: {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: WidgetError.fetchFailed)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
#Preview(
|
||||
"Large",
|
||||
as: .systemLarge,
|
||||
widget: {
|
||||
ImmichRandomWidget()
|
||||
},
|
||||
timeline: {
|
||||
let date = Date()
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(named: "LaunchImage"),
|
||||
metadata: EntryMetadata(
|
||||
subtitle: "2000 seconds ago"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ extension WidgetError: LocalizedError {
|
||||
return "Login to Immich"
|
||||
|
||||
case .fetchFailed:
|
||||
return "Unable to connect to your Immich instance"
|
||||
return "Unable to connect to Immich"
|
||||
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
@@ -50,12 +50,10 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
||||
faces.isVisible.equals(true) &
|
||||
faces.deletedAt.isNull(),
|
||||
)
|
||||
..groupBy([
|
||||
people.id,
|
||||
], having: faces.assetId.count(distinct: true).isBiggerOrEqualValue(minFaces) | people.name.equals('').not())
|
||||
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(minFaces) | people.name.equals('').not())
|
||||
..orderBy([
|
||||
OrderingTerm(expression: people.name.equals('').not(), mode: OrderingMode.desc),
|
||||
OrderingTerm(expression: faces.assetId.count(distinct: true), mode: OrderingMode.desc),
|
||||
OrderingTerm(expression: faces.id.count(), mode: OrderingMode.desc),
|
||||
]);
|
||||
|
||||
return query.map((row) {
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools."aqua:flutter/flutter"]]
|
||||
version = "3.44.6"
|
||||
version = "3.44.8"
|
||||
backend = "aqua:flutter/flutter"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.6-stable.zip"
|
||||
checksum = "blake3:79c780876c64f5a66c015845f544bdb20275f7f3bf819e6c831f23df3e96b8db"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.8-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.6-stable.zip"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.8-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.6-stable.zip"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.8-stable.zip"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tools]
|
||||
"aqua:flutter/flutter" = "3.44.6"
|
||||
"aqua:flutter/flutter" = "3.44.8"
|
||||
java = "21.0.2"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
@@ -8,6 +9,7 @@ class ImmichColumnButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final FutureOr<void> Function()? onLongPress;
|
||||
final bool disabled;
|
||||
final bool? loading;
|
||||
|
||||
@@ -16,6 +18,7 @@ class ImmichColumnButton extends StatefulWidget {
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.disabled = false,
|
||||
this.loading,
|
||||
});
|
||||
@@ -25,26 +28,44 @@ class ImmichColumnButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ImmichColumnButtonState extends State<ImmichColumnButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
bool _running = false;
|
||||
bool get _isLoading => widget.loading ?? _running;
|
||||
bool get _isDisabled => widget.disabled || _isLoading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
Future<void> _runAction(FutureOr<void> Function() action) async {
|
||||
setState(() => _running = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
await action();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
setState(() => _running = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? _onPressed() {
|
||||
if (_isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onPressed);
|
||||
}
|
||||
|
||||
Future<void>? _onLongPress() {
|
||||
if (_isDisabled || widget.onLongPress == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onLongPress!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final foreground = context.colorOverride ?? Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
return TextButton(
|
||||
onPressed: widget.disabled || _isLoading ? null : _onPressed,
|
||||
onPressed: _onPressed,
|
||||
onLongPress: _onLongPress,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
padding: const .symmetric(horizontal: ImmichSpacing.sm, vertical: ImmichSpacing.md),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_ui/src/internal.dart';
|
||||
class ImmichIconButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final FutureOr<void> Function()? onLongPress;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
@@ -16,6 +17,7 @@ class ImmichIconButton extends StatefulWidget {
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.color = .primary,
|
||||
this.variant = .filled,
|
||||
this.disabled = false,
|
||||
@@ -27,20 +29,37 @@ class ImmichIconButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ImmichIconButtonState extends State<ImmichIconButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
bool _running = false;
|
||||
bool get _isLoading => widget.loading ?? _running;
|
||||
bool get _isDisabled => widget.disabled || _isLoading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
Future<void> _runAction(FutureOr<void> Function() action) async {
|
||||
setState(() => _running = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
await action();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
setState(() => _running = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? _onPressed() {
|
||||
if (_isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onPressed);
|
||||
}
|
||||
|
||||
Future<void>? _onLongPress() {
|
||||
if (_isDisabled || widget.onLongPress == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onLongPress!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -73,7 +92,8 @@ class _ImmichIconButtonState extends State<ImmichIconButton> {
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md),
|
||||
)
|
||||
: Icon(widget.icon),
|
||||
onPressed: widget.disabled || _isLoading ? null : _onPressed,
|
||||
onPressed: _onPressed,
|
||||
onLongPress: _onLongPress,
|
||||
style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class ImmichTextButton extends StatefulWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final FutureOr<void> Function()? onLongPress;
|
||||
final ImmichVariant variant;
|
||||
final bool expanded;
|
||||
final bool disabled;
|
||||
@@ -17,6 +18,7 @@ class ImmichTextButton extends StatefulWidget {
|
||||
required this.labelText,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.variant = .filled,
|
||||
this.expanded = true,
|
||||
|
||||
@@ -29,20 +31,37 @@ class ImmichTextButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ImmichTextButtonState extends State<ImmichTextButton> {
|
||||
bool _loading = false;
|
||||
bool get _isLoading => widget.loading ?? _loading;
|
||||
bool _running = false;
|
||||
bool get _isLoading => widget.loading ?? _running;
|
||||
bool get _isDisabled => widget.disabled || _isLoading;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
setState(() => _loading = true);
|
||||
Future<void> _runAction(FutureOr<void> Function() action) async {
|
||||
setState(() => _running = true);
|
||||
try {
|
||||
await widget.onPressed();
|
||||
await action();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
setState(() => _running = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? _onPressed() {
|
||||
if (_isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onPressed);
|
||||
}
|
||||
|
||||
Future<void>? _onLongPress() {
|
||||
if (_isDisabled || widget.onLongPress == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runAction(widget.onLongPress!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget? icon = _isLoading
|
||||
@@ -59,11 +78,22 @@ class _ImmichTextButtonState extends State<ImmichTextButton> {
|
||||
style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold),
|
||||
);
|
||||
final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md));
|
||||
final onPressed = widget.disabled || _isLoading ? null : _onPressed;
|
||||
|
||||
final button = switch (widget.variant) {
|
||||
ImmichVariant.filled => ElevatedButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
ImmichVariant.ghost => TextButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
|
||||
ImmichVariant.filled => ElevatedButton.icon(
|
||||
style: style,
|
||||
onPressed: _onPressed,
|
||||
onLongPress: _onLongPress,
|
||||
icon: icon,
|
||||
label: label,
|
||||
),
|
||||
ImmichVariant.ghost => TextButton.icon(
|
||||
style: style,
|
||||
onPressed: _onPressed,
|
||||
onLongPress: _onLongPress,
|
||||
icon: icon,
|
||||
label: label,
|
||||
),
|
||||
};
|
||||
|
||||
if (widget.expanded) {
|
||||
|
||||
@@ -2014,4 +2014,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: "3.44.6"
|
||||
flutter: "3.44.8"
|
||||
|
||||
@@ -6,7 +6,7 @@ version: 3.0.3+3056
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
flutter: 3.44.6
|
||||
flutter: 3.44.8
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.1
|
||||
|
||||
@@ -74,53 +74,4 @@ void main() {
|
||||
expect(people, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('getAllPeople', () {
|
||||
test('counts distinct assets, not face records, against minFaces', () async {
|
||||
// Regression check: a person can have multiple face records on the same asset
|
||||
// (e.g., metadata import + ML detection), which must not inflate the count used
|
||||
// to compare against minFaces. An unnamed person with 2 distinct photos but 3
|
||||
// face records (2 of them on the same photo) must not pass a minFaces of 3.
|
||||
final user = await ctx.newUser();
|
||||
final asset1 = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final asset2 = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
|
||||
final person = await ctx.newPerson(ownerId: user.id, name: '');
|
||||
await ctx.newFace(assetId: asset1.id, personId: person.id);
|
||||
await ctx.newFace(assetId: asset1.id, personId: person.id);
|
||||
await ctx.newFace(assetId: asset2.id, personId: person.id);
|
||||
|
||||
final people = await sut.getAllPeople(minFaces: 3);
|
||||
|
||||
expect(people, isEmpty);
|
||||
});
|
||||
|
||||
test('returns unnamed people who meet minFaces based on distinct assets', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset1 = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final asset2 = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final asset3 = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
|
||||
final person = await ctx.newPerson(ownerId: user.id, name: '');
|
||||
await ctx.newFace(assetId: asset1.id, personId: person.id);
|
||||
await ctx.newFace(assetId: asset2.id, personId: person.id);
|
||||
await ctx.newFace(assetId: asset3.id, personId: person.id);
|
||||
|
||||
final people = await sut.getAllPeople(minFaces: 3);
|
||||
|
||||
expect(people.map((p) => p.id), [person.id]);
|
||||
});
|
||||
|
||||
test('always returns named people regardless of minFaces', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
|
||||
final person = await ctx.newPerson(ownerId: user.id, name: 'Jane');
|
||||
await ctx.newFace(assetId: asset.id, personId: person.id);
|
||||
|
||||
final people = await sut.getAllPeople(minFaces: 3);
|
||||
|
||||
expect(people.map((p) => p.id), [person.id]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ group by
|
||||
having
|
||||
(
|
||||
"person"."name" != $3
|
||||
or count(distinct ("asset_face"."assetId")) >= COALESCE(
|
||||
or count("asset_face"."assetId") >= COALESCE(
|
||||
(
|
||||
SELECT
|
||||
value -> 'people' ->> 'minimumFaces'
|
||||
@@ -59,7 +59,7 @@ order by
|
||||
"person"."isHidden" asc,
|
||||
"person"."isFavorite" desc,
|
||||
NULLIF(person.name, '') is null asc,
|
||||
count(distinct ("asset_face"."assetId")) desc,
|
||||
count("asset_face"."assetId") desc,
|
||||
NULLIF(person.name, '') asc nulls last,
|
||||
"person"."createdAt"
|
||||
limit
|
||||
|
||||
@@ -168,7 +168,7 @@ export class PersonRepository {
|
||||
eb.or([
|
||||
eb('person.name', '!=', ''),
|
||||
eb(
|
||||
(innerEb) => innerEb.fn.count(innerEb.fn('distinct', ['asset_face.assetId'])),
|
||||
(innerEb) => innerEb.fn.count('asset_face.assetId'),
|
||||
'>=',
|
||||
sql<number>`COALESCE(
|
||||
(SELECT value -> 'people' ->> 'minimumFaces'
|
||||
@@ -201,7 +201,7 @@ export class PersonRepository {
|
||||
.$if(!options?.closestFaceAssetId, (qb) =>
|
||||
qb
|
||||
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
|
||||
.orderBy((eb) => eb.fn.count(eb.fn('distinct', ['asset_face.assetId'])), 'desc')
|
||||
.orderBy((eb) => eb.fn.count('asset_face.assetId'), 'desc')
|
||||
.orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast())
|
||||
.orderBy('person.createdAt'),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user