Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0de9943e0 | |||
| 64b92cb24c | |||
| 19f2f888ee | |||
| d12b1c907d | |||
| 947c053c15 | |||
| 79592701dd | |||
| 39697cd973 | |||
| 10e518db42 | |||
| 72fa31f9e9 |
@@ -248,6 +248,7 @@
|
|||||||
"download_waiting_to_retry": "Waiting to retry",
|
"download_waiting_to_retry": "Waiting to retry",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
|
"edit_date_time_dialog_search_timezone": "Search timezone...",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
"edit_location_dialog_title": "Location",
|
"edit_location_dialog_title": "Location",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart';
|
||||||
import 'package:timezone/timezone.dart' as tz;
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
import 'package:timezone/timezone.dart';
|
import 'package:timezone/timezone.dart';
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ Future<String?> showDateTimePicker({
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
||||||
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
return "${location.name} (${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DateTimePicker extends HookWidget {
|
class _DateTimePicker extends HookWidget {
|
||||||
@@ -73,7 +74,6 @@ class _DateTimePicker extends HookWidget {
|
|||||||
// returns a list of location<name> along with it's offset in duration
|
// returns a list of location<name> along with it's offset in duration
|
||||||
List<_TimeZoneOffset> getAllTimeZones() {
|
List<_TimeZoneOffset> getAllTimeZones() {
|
||||||
return tz.timeZoneDatabase.locations.values
|
return tz.timeZoneDatabase.locations.values
|
||||||
.where((l) => !l.currentTimeZone.abbreviation.contains("0"))
|
|
||||||
.map(_TimeZoneOffset.fromLocation)
|
.map(_TimeZoneOffset.fromLocation)
|
||||||
.sorted()
|
.sorted()
|
||||||
.toList();
|
.toList();
|
||||||
@@ -133,10 +133,8 @@ class _DateTimePicker extends HookWidget {
|
|||||||
context.pop(dtWithOffset);
|
context.pop(dtWithOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return AlertDialog(
|
||||||
builder: (context, constraint) => AlertDialog(
|
contentPadding: const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
@@ -194,9 +192,7 @@ class _DateTimePicker extends HookWidget {
|
|||||||
onTap: pickDate,
|
onTap: pickDate,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
DropdownMenu(
|
DropdownSearchMenu(
|
||||||
width: 275,
|
|
||||||
menuHeight: 300,
|
|
||||||
trailingIcon: Icon(
|
trailingIcon: Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
@@ -204,13 +200,12 @@ class _DateTimePicker extends HookWidget {
|
|||||||
hintText: "edit_date_time_dialog_timezone".tr(),
|
hintText: "edit_date_time_dialog_timezone".tr(),
|
||||||
label: const Text('edit_date_time_dialog_timezone').tr(),
|
label: const Text('edit_date_time_dialog_timezone').tr(),
|
||||||
textStyle: context.textTheme.bodyMedium,
|
textStyle: context.textTheme.bodyMedium,
|
||||||
onSelected: (value) => tzOffset.value = value!,
|
onSelected: (value) => tzOffset.value = value,
|
||||||
initialSelection: tzOffset.value,
|
initialSelection: tzOffset.value,
|
||||||
dropdownMenuEntries: menuEntries,
|
dropdownMenuEntries: menuEntries,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class DropdownSearchMenu<T> extends HookWidget {
|
||||||
|
const DropdownSearchMenu({
|
||||||
|
super.key,
|
||||||
|
required this.dropdownMenuEntries,
|
||||||
|
this.initialSelection,
|
||||||
|
this.onSelected,
|
||||||
|
this.trailingIcon,
|
||||||
|
this.hintText,
|
||||||
|
this.label,
|
||||||
|
this.textStyle,
|
||||||
|
this.menuConstraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<DropdownMenuEntry<T>> dropdownMenuEntries;
|
||||||
|
final T? initialSelection;
|
||||||
|
final ValueChanged<T>? onSelected;
|
||||||
|
final Widget? trailingIcon;
|
||||||
|
final String? hintText;
|
||||||
|
final Widget? label;
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
final BoxConstraints? menuConstraints;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedItem = useState<DropdownMenuEntry<T>?>(
|
||||||
|
dropdownMenuEntries
|
||||||
|
.firstWhereOrNull((item) => item.value == initialSelection),
|
||||||
|
);
|
||||||
|
final showTimeZoneDropdown = useState<bool>(false);
|
||||||
|
|
||||||
|
final effectiveConstraints = menuConstraints ??
|
||||||
|
const BoxConstraints(
|
||||||
|
minWidth: 280,
|
||||||
|
maxWidth: 280,
|
||||||
|
minHeight: 0,
|
||||||
|
maxHeight: 280,
|
||||||
|
);
|
||||||
|
|
||||||
|
final inputDecoration = InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: trailingIcon,
|
||||||
|
label: label,
|
||||||
|
hintText: hintText,
|
||||||
|
).applyDefaults(context.themeData.inputDecorationTheme);
|
||||||
|
|
||||||
|
if (!showTimeZoneDropdown.value) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: effectiveConstraints,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => showTimeZoneDropdown.value = true,
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: inputDecoration,
|
||||||
|
child: selectedItem.value != null
|
||||||
|
? Text(
|
||||||
|
selectedItem.value!.label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textStyle,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: effectiveConstraints,
|
||||||
|
child: Autocomplete<DropdownMenuEntry<T>>(
|
||||||
|
displayStringForOption: (option) => option.label,
|
||||||
|
optionsBuilder: (textEditingValue) {
|
||||||
|
return dropdownMenuEntries.where(
|
||||||
|
(item) => item.label
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.contains(textEditingValue.text.toLowerCase().trim()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (option) {
|
||||||
|
selectedItem.value = option;
|
||||||
|
showTimeZoneDropdown.value = false;
|
||||||
|
onSelected?.call(option.value);
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (context, textEditingController, focusNode, _) {
|
||||||
|
return TextField(
|
||||||
|
autofocus: true,
|
||||||
|
focusNode: focusNode,
|
||||||
|
controller: textEditingController,
|
||||||
|
decoration: inputDecoration.copyWith(
|
||||||
|
hintText: "edit_date_time_dialog_search_timezone".tr(),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
expands: false,
|
||||||
|
onTapOutside: (event) {
|
||||||
|
showTimeZoneDropdown.value = false;
|
||||||
|
focusNode.unfocus();
|
||||||
|
},
|
||||||
|
onSubmitted: (_) {
|
||||||
|
showTimeZoneDropdown.value = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
optionsViewBuilder: (context, onSelected, options) {
|
||||||
|
// This widget is a copy of the default implementation.
|
||||||
|
// We have only changed the `constraints` parameter.
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: effectiveConstraints,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: options.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final option = options.elementAt(index);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => onSelected(option),
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final bool highlight =
|
||||||
|
AutocompleteHighlightedOption.of(context) ==
|
||||||
|
index;
|
||||||
|
if (highlight) {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback(
|
||||||
|
(Duration timeStamp) {
|
||||||
|
Scrollable.ensureVisible(
|
||||||
|
context,
|
||||||
|
alignment: 0.5,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
debugLabel: 'AutocompleteOptions.ensureVisible',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
color: highlight
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.12)
|
||||||
|
: null,
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
option.label,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-11
@@ -40,12 +40,12 @@
|
|||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## ข้อจำกัดความรับผิดชอบ
|
## ข้อควรระวัง
|
||||||
|
|
||||||
- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**ที่มีการเปลี่ยนแปลงบ่อยมาก**
|
- ⚠️ โพรเจกต์นี้กำลังอยู่ระหว่างการพัฒนา**มีการเปลี่ยนแปลงบ่อยมาก**
|
||||||
- ⚠️ คาดว่าจะมีข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย
|
- ⚠️ อาจจะเกิดข้อผิดพลาดและการเปลี่ยนแปลงที่ส่งผลเสีย
|
||||||
- ⚠️ **ห้ามใช้แอปนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ**
|
- ⚠️ **ห้ามใช้ระบบนี้เป็นวิธีการเดียวในการจัดเก็บภาพถ่ายและวิดีโอของคุณ**
|
||||||
- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ!
|
- ⚠️ ปฏิบัติตามแผนการสำรองข้อมูลแบบ [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) สำหรับภาพถ่ายและวิดีโอที่สำคัญของคุณอยู่เสมอ
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> คุณสามารถหาคู่มือหลัก รวมถึงคู่มือการติดตั้ง ได้ที่ https://immich.app/
|
> คุณสามารถหาคู่มือหลัก รวมถึงคู่มือการติดตั้ง ได้ที่ https://immich.app/
|
||||||
@@ -79,15 +79,15 @@
|
|||||||
| :----------------------------------------- | ------ | ------ |
|
| :----------------------------------------- | ------ | ------ |
|
||||||
| อัปโหลดและดูวิดีโอและภาพถ่าย | ใช่ | ใช่ |
|
| อัปโหลดและดูวิดีโอและภาพถ่าย | ใช่ | ใช่ |
|
||||||
| การสำรองข้อมูลอัตโนมัติเมื่อเปิดแอป | ใช่ | N/A |
|
| การสำรองข้อมูลอัตโนมัติเมื่อเปิดแอป | ใช่ | N/A |
|
||||||
| ป้องกันการซ้ำซ้อนของไฟล์ | ใช่ | ใช่ |
|
| ป้องกันการซ้ำของไฟล์ | ใช่ | ใช่ |
|
||||||
| เลือกอัลบั้มสำหรับสำรองข้อมูล | ใช่ | N/A |
|
| เลือกอัลบั้มสำหรับสำรองข้อมูล | ใช่ | N/A |
|
||||||
| ดาวน์โหลดภาพถ่ายและวิดีโอไปยังอุปกรณ์ | ใช่ | ใช่ |
|
| ดาวน์โหลดภาพถ่ายและวิดีโอไปยังอุปกรณ์ | ใช่ | ใช่ |
|
||||||
| รองรับผู้ใช้หลายคน | ใช่ | ใช่ |
|
| รองรับผู้ใช้หลายคน | ใช่ | ใช่ |
|
||||||
| อัลบั้มและอัลบั้มแชร์ | ใช่ | ใช่ |
|
| อัลบั้มและอัลบั้มแชร์ | ใช่ | ใช่ |
|
||||||
| แถบเลื่อนแบบลากได้ | ใช่ | ใช่ |
|
| แถบเลื่อนแบบลากได้ | ใช่ | ใช่ |
|
||||||
| รองรับรูปแบบไฟล์ RAW | ใช่ | ใช่ |
|
| รองรับรูปแบบไฟล์ RAW | ใช่ | ใช่ |
|
||||||
| ดูข้อมูลเมตา (EXIF, แผนที่) | ใช่ | ใช่ |
|
| ดูข้อมูลเมตาดาต้า (EXIF, แผนที่) | ใช่ | ใช่ |
|
||||||
| ค้นหาจากข้อมูลเมตา วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ |
|
| ค้นหาจากข้อมูลเมตาดาต้า วัตถุ ใบหน้า และ CLIP | ใช่ | ใช่ |
|
||||||
| ฟังก์ชันการจัดการผู้ดูแลระบบ | ไม่ใช่ | ใช่ |
|
| ฟังก์ชันการจัดการผู้ดูแลระบบ | ไม่ใช่ | ใช่ |
|
||||||
| การสำรองข้อมูลพื้นหลัง | ใช่ | N/A |
|
| การสำรองข้อมูลพื้นหลัง | ใช่ | N/A |
|
||||||
| การเลื่อนแบบเสมือน | ใช่ | ใช่ |
|
| การเลื่อนแบบเสมือน | ใช่ | ใช่ |
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
| การจัดเก็บและรายการโปรด | ใช่ | ใช่ |
|
| การจัดเก็บและรายการโปรด | ใช่ | ใช่ |
|
||||||
| แผนที่ทั่วโลก | ใช่ | ใช่ |
|
| แผนที่ทั่วโลก | ใช่ | ใช่ |
|
||||||
| การแชร์กับคู่หู | ใช่ | ใช่ |
|
| การแชร์กับคู่หู | ใช่ | ใช่ |
|
||||||
| การจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ |
|
| ระบบจดจำใบหน้าและการจัดกลุ่ม | ใช่ | ใช่ |
|
||||||
| ความทรงจำ (x ปีที่แล้ว) | ใช่ | ใช่ |
|
| ความทรงจำ (x ปีที่แล้ว) | ใช่ | ใช่ |
|
||||||
| รองรับแบบออฟไลน์ | ใช่ | ไม่ใช่ |
|
| รองรับแบบออฟไลน์ | ใช่ | ไม่ใช่ |
|
||||||
| แกลเลอรีแบบอ่านอย่างเดียว | ใช่ | ใช่ |
|
| แกลเลอรีแบบอ่านอย่างเดียว | ใช่ | ใช่ |
|
||||||
@@ -108,13 +108,13 @@
|
|||||||
|
|
||||||
## การแปลภาษา
|
## การแปลภาษา
|
||||||
|
|
||||||
อ่านเพิ่มเติมเกี่ยวกับการแปลภาษา [ที่นี่](https://immich.app/docs/developer/translations)
|
อ่านเพิ่มเติมเกี่ยวกับการแปล [ที่นี่](https://immich.app/docs/developer/translations)
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/immich/">
|
<a href="https://hosted.weblate.org/engage/immich/">
|
||||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="สถานะการแปล" />
|
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="สถานะการแปล" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## กิจกรรมของคลังเก็บข้อมูล
|
## กิจกรรมของ Repository
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ if (immichApp) {
|
|||||||
let apiProcess: ChildProcess | undefined;
|
let apiProcess: ChildProcess | undefined;
|
||||||
|
|
||||||
const onError = (name: string, error: Error) => {
|
const onError = (name: string, error: Error) => {
|
||||||
console.error(`${name} worker error: ${error}`);
|
console.error(`${name} worker error: ${error}, stack: ${error.stack}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExit = (name: string, exitCode: number | null) => {
|
const onExit = (name: string, exitCode: number | null) => {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
||||||
.select((eb) => eb.fn.count('assets.id').as('assetCount'))
|
.select((eb) => eb.fn.count('assets.id').as('assetCount'))
|
||||||
.where('albums.id', 'in', ids)
|
.where('albums.id', 'in', ids)
|
||||||
|
.where('assets.deletedAt', 'is', null)
|
||||||
.groupBy('albums.id')
|
.groupBy('albums.id')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,28 @@ describe('getEnv', () => {
|
|||||||
|
|
||||||
expect(() => getEnv()).toThrowError('Invalid ssl option: invalid');
|
expect(() => getEnv()).toThrowError('Invalid ssl option: invalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle socket: URLs', () => {
|
||||||
|
process.env.DB_URL = 'socket:/run/postgresql?db=database1';
|
||||||
|
|
||||||
|
const { database } = getEnv();
|
||||||
|
|
||||||
|
expect(database.config.kysely).toMatchObject({
|
||||||
|
host: '/run/postgresql',
|
||||||
|
database: 'database1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle sockets in postgres: URLs', () => {
|
||||||
|
process.env.DB_URL = 'postgres:///database2?host=/path/to/socket';
|
||||||
|
|
||||||
|
const { database } = getEnv();
|
||||||
|
|
||||||
|
expect(database.config.kysely).toMatchObject({
|
||||||
|
host: '/path/to/socket',
|
||||||
|
database: 'database2',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('redis', () => {
|
describe('redis', () => {
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
await sql`truncate ${sql.table('smart_search')}`.execute(trx);
|
await sql`truncate ${sql.table('smart_search')}`.execute(trx);
|
||||||
await trx.schema
|
await trx.schema
|
||||||
.alterTable('smart_search')
|
.alterTable('smart_search')
|
||||||
.alterColumn('embedding', (col) => col.setDataType(sql.lit(`vector(${dimSize})`)))
|
.alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`)))
|
||||||
.execute();
|
.execute();
|
||||||
await sql`reindex index clip_index`.execute(trx);
|
await sql`reindex index clip_index`.execute(trx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -416,6 +416,34 @@ describe(AssetService.name, () => {
|
|||||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||||
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
|
await sut.updateAll(authStub.admin, {
|
||||||
|
ids: ['asset-1'],
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
isArchived: undefined,
|
||||||
|
isFavorite: undefined,
|
||||||
|
duplicateId: undefined,
|
||||||
|
rating: undefined,
|
||||||
|
});
|
||||||
|
expect(assetMock.updateAll).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update Assets table if isArchived field is provided', async () => {
|
||||||
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
|
await sut.updateAll(authStub.admin, {
|
||||||
|
ids: ['asset-1'],
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
isArchived: undefined,
|
||||||
|
isFavorite: false,
|
||||||
|
duplicateId: undefined,
|
||||||
|
rating: undefined,
|
||||||
|
});
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteAll', () => {
|
describe('deleteAll', () => {
|
||||||
|
|||||||
@@ -142,8 +142,15 @@ export class AssetService extends BaseService {
|
|||||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.isArchived != undefined ||
|
||||||
|
options.isFavorite != undefined ||
|
||||||
|
options.duplicateId != undefined ||
|
||||||
|
options.rating != undefined
|
||||||
|
) {
|
||||||
await this.assetRepository.updateAll(ids, options);
|
await this.assetRepository.updateAll(ids, options);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
|
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
|
||||||
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
|
|||||||
@@ -337,12 +337,31 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], {
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], {
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
fileCreatedAt: assetStub.trashedOffline.fileModifiedAt,
|
|
||||||
fileModifiedAt: assetStub.trashedOffline.fileModifiedAt,
|
fileModifiedAt: assetStub.trashedOffline.fileModifiedAt,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
originalFileName: 'path.jpg',
|
originalFileName: 'path.jpg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => {
|
||||||
|
const mockAssetJob: ILibraryAssetJob = {
|
||||||
|
id: assetStub.external.id,
|
||||||
|
importPaths: ['/'],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
assetMock.getById.mockResolvedValue(assetStub.trashedOffline);
|
||||||
|
storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
|
||||||
|
|
||||||
|
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(
|
||||||
|
[assetStub.trashedOffline.id],
|
||||||
|
expect.not.objectContaining({
|
||||||
|
fileCreatedAt: expect.anything(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update file when mtime has changed', async () => {
|
it('should update file when mtime has changed', async () => {
|
||||||
@@ -360,7 +379,6 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
||||||
fileModifiedAt: newMTime,
|
fileModifiedAt: newMTime,
|
||||||
fileCreatedAt: newMTime,
|
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
originalFileName: 'photo.jpg',
|
originalFileName: 'photo.jpg',
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
|||||||
@@ -511,7 +511,6 @@ export class LibraryService extends BaseService {
|
|||||||
await this.assetRepository.updateAll([asset.id], {
|
await this.assetRepository.updateAll([asset.id], {
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
fileCreatedAt: mtime,
|
|
||||||
fileModifiedAt: mtime,
|
fileModifiedAt: mtime,
|
||||||
originalFileName: parse(asset.originalPath).base,
|
originalFileName: parse(asset.originalPath).base,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ async function bootstrap() {
|
|||||||
app.use(app.get(ApiService).ssr(excludePaths));
|
app.use(app.get(ApiService).ssr(excludePaths));
|
||||||
|
|
||||||
const server = await (host ? app.listen(port, host) : app.listen(port));
|
const server = await (host ? app.listen(port, host) : app.listen(port));
|
||||||
server.requestTimeout = 30 * 60 * 1000;
|
server.requestTimeout = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${environment}] `);
|
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${environment}] `);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user