diff --git a/CHANGELOG.md b/CHANGELOG.md
index bac68c8b5..bb4f16926 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- **Feature**: RAW files are now hidden (stacked) when another file with the same basename exists ([#537](https://github.com/pulsejet/memories/issues/537), [#152](https://github.com/pulsejet/memories/issues/152), [#419](https://github.com/pulsejet/memories/issues/419))
+- **Feature**: Bulk rotating of images. You can now rotate images losslessly by editing the rotation EXIF metadata. ([#856](https://github.com/pulsejet/memories/issues/856))
- **Feature**: Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898))
+- **Feature**: Swipe to refresh on timeline ([#547](https://github.com/pulsejet/memories/issues/547))
- **Bugfix**: Allow switching video to direct on Safari ([#650](https://github.com/pulsejet/memories/issues/650))
- Many other [bug fixes](https://github.com/pulsejet/memories/milestone/18?closed=1)
- Android app is now open source ([see](https://github.com/pulsejet/memories/tree/master/android))
diff --git a/l10n/zh_CN.js b/l10n/zh_CN.js
index e0e1d80cd..c0ddcf78e 100644
--- a/l10n/zh_CN.js
+++ b/l10n/zh_CN.js
@@ -61,12 +61,20 @@ OC.L10N.register(
"Timeline Path" : "时间线路径",
"Square grid mode" : "方形网格模式",
"Show past photos on top of timeline" : "在时间线顶部显示过去的照片",
+ "Stack RAW files with same name" : "堆叠同名 RAW 文件",
+ "Photo Viewer" : "照片浏览器",
"Autoplay Live Photos" : "自动播放实时照片",
+ "Show full file path in sidebar" : "在侧边栏显示完整的文件路径",
+ "High resolution image loading behavior" : "高清图像加载偏好",
+ "Load high resolution image on zoom" : "在缩放时加载高清图像",
+ "Always load high resolution image (not recommended)" : "总是加载高清图像(不推荐)",
+ "Never load high resolution image" : "不加载高清图像",
"Account" : "账户",
"Logged in as {user}" : "以 {user} 登录",
"Sign out" : "登出",
"Device Folders" : "设备文件夹",
"Local folders to include in the timeline view" : "要包含在时间线视图中的本地文件夹",
+ "Run initial device setup" : "运行设备初始化设定",
"Folders Path" : "文件夹路径",
"Show hidden folders" : "显示隐藏文件夹",
"Sort folders oldest-first" : "将文件夹从最旧开始排序",
@@ -81,6 +89,9 @@ OC.L10N.register(
"Failed to update setting" : "更新设置失败",
"Albums support is enabled through the Photos app." : "相册支持通过“照片”应用启用。",
"Albums are disabled because the Photos app is not available." : "相册已禁用,因为“照片”应用不可用。",
+ "Recognize is installed and enabled for face recognition." : "Recognize 人脸识别已安装并启用。",
+ "Recognize is installed but not enabled for face recognition." : "Recognize 人脸识别已安装但并未启用。",
+ "Recognize is not installed. Face recognition and object tagging may be unavailable." : "Recognize 未安装。人脸识别和物品标记可能无法使用。",
"Face Recognition is installed and enabled" : "人脸识别已安装并启用",
"Preview generator is installed and enabled. Additional configuration may still be required." : "预览生成器已安装并启用。可能还需要额外的配置。",
"Preview generator is not installed and configured. This may make Memories very slow." : "预览生成器未安装和配置。这可能会使“记忆”非常缓慢。",
@@ -93,6 +104,11 @@ OC.L10N.register(
"If you are using Imaginary for preview generation, you can ignore this section." : "如果您正在使用Imaginary生成预览,则可以忽略此部分。",
"To enable RAW support, install the Camera RAW Previews app." : "要启用RAW支持,请安装Camera RAW Previews应用。",
"Documentation." : "文档",
+ "PHP-Imagick is available [{version}]." : "PHP-Imagick 可用 [{version}]。",
+ "PHP-Imagick is not available." : "PHP-Imagick 不可用。",
+ "Image editing will not work correctly." : "图像编辑无法正常使用。",
+ "Thumbnail generation may not work for some formats (HEIC, TIFF)." : "缩略图生成可能不适用于某些格式(HEIC,TIFF)。",
+ "Thumbnails for videos will be generated with this binary." : "视频缩略图将使用此二进制文件生成。",
"The following MIME types are configured for preview generation." : "为生成预览配置了以下MIME类型。",
"Max preview size (trade-off between quality and storage requirements)." : "最大预览大小(质量和存储需求之间的权衡)",
"Max memory for preview generation (MB)" : "生成预览时的最大内存(MB)",
diff --git a/l10n/zh_CN.json b/l10n/zh_CN.json
index 116c6e616..2a5de3a37 100644
--- a/l10n/zh_CN.json
+++ b/l10n/zh_CN.json
@@ -59,12 +59,20 @@
"Timeline Path" : "时间线路径",
"Square grid mode" : "方形网格模式",
"Show past photos on top of timeline" : "在时间线顶部显示过去的照片",
+ "Stack RAW files with same name" : "堆叠同名 RAW 文件",
+ "Photo Viewer" : "照片浏览器",
"Autoplay Live Photos" : "自动播放实时照片",
+ "Show full file path in sidebar" : "在侧边栏显示完整的文件路径",
+ "High resolution image loading behavior" : "高清图像加载偏好",
+ "Load high resolution image on zoom" : "在缩放时加载高清图像",
+ "Always load high resolution image (not recommended)" : "总是加载高清图像(不推荐)",
+ "Never load high resolution image" : "不加载高清图像",
"Account" : "账户",
"Logged in as {user}" : "以 {user} 登录",
"Sign out" : "登出",
"Device Folders" : "设备文件夹",
"Local folders to include in the timeline view" : "要包含在时间线视图中的本地文件夹",
+ "Run initial device setup" : "运行设备初始化设定",
"Folders Path" : "文件夹路径",
"Show hidden folders" : "显示隐藏文件夹",
"Sort folders oldest-first" : "将文件夹从最旧开始排序",
@@ -79,6 +87,9 @@
"Failed to update setting" : "更新设置失败",
"Albums support is enabled through the Photos app." : "相册支持通过“照片”应用启用。",
"Albums are disabled because the Photos app is not available." : "相册已禁用,因为“照片”应用不可用。",
+ "Recognize is installed and enabled for face recognition." : "Recognize 人脸识别已安装并启用。",
+ "Recognize is installed but not enabled for face recognition." : "Recognize 人脸识别已安装但并未启用。",
+ "Recognize is not installed. Face recognition and object tagging may be unavailable." : "Recognize 未安装。人脸识别和物品标记可能无法使用。",
"Face Recognition is installed and enabled" : "人脸识别已安装并启用",
"Preview generator is installed and enabled. Additional configuration may still be required." : "预览生成器已安装并启用。可能还需要额外的配置。",
"Preview generator is not installed and configured. This may make Memories very slow." : "预览生成器未安装和配置。这可能会使“记忆”非常缓慢。",
@@ -91,6 +102,11 @@
"If you are using Imaginary for preview generation, you can ignore this section." : "如果您正在使用Imaginary生成预览,则可以忽略此部分。",
"To enable RAW support, install the Camera RAW Previews app." : "要启用RAW支持,请安装Camera RAW Previews应用。",
"Documentation." : "文档",
+ "PHP-Imagick is available [{version}]." : "PHP-Imagick 可用 [{version}]。",
+ "PHP-Imagick is not available." : "PHP-Imagick 不可用。",
+ "Image editing will not work correctly." : "图像编辑无法正常使用。",
+ "Thumbnail generation may not work for some formats (HEIC, TIFF)." : "缩略图生成可能不适用于某些格式(HEIC,TIFF)。",
+ "Thumbnails for videos will be generated with this binary." : "视频缩略图将使用此二进制文件生成。",
"The following MIME types are configured for preview generation." : "为生成预览配置了以下MIME类型。",
"Max preview size (trade-off between quality and storage requirements)." : "最大预览大小(质量和存储需求之间的权衡)",
"Max memory for preview generation (MB)" : "生成预览时的最大内存(MB)",
diff --git a/lib/Controller/ImageController.php b/lib/Controller/ImageController.php
index 77a28d73e..5e2c7a05e 100644
--- a/lib/Controller/ImageController.php
+++ b/lib/Controller/ImageController.php
@@ -259,7 +259,12 @@ public function setExif(int $id, array $raw): Http\Response
// Set the exif data
Exif::setFileExif($file, $raw);
- return new JSONResponse([], Http::STATUS_OK);
+ // If rotation changed then update the previews
+ if ($raw['Orientation'] ?? false) {
+ $this->deletePreviews($file);
+ }
+
+ return $this->info($id, true);
});
}
@@ -452,4 +457,22 @@ private function getTags(int $fileId): array
// Get the tag names
return array_map(static fn ($t) => $t->getName(), $visible);
}
+
+ /**
+ * Invalidate previews for a file.
+ */
+ private function deletePreviews(\OCP\Files\File $file): void
+ {
+ try {
+ $previewRoot = new \OC\Preview\Storage\Root(
+ \OC::$server->get(IRootFolder::class),
+ \OC::$server->get(\OC\SystemConfig::class),
+ );
+
+ $fileId = (string) $file->getId();
+ $previewRoot->getFolder($fileId)->delete();
+ } catch (\Exception $e) {
+ return;
+ }
+ }
}
diff --git a/lib/Exif.php b/lib/Exif.php
index 1d085e660..30fc93969 100644
--- a/lib/Exif.php
+++ b/lib/Exif.php
@@ -334,7 +334,7 @@ public static function setExif(string $path, array $data): void
$data['SourceFile'] = $path;
$raw = json_encode([$data], JSON_UNESCAPED_UNICODE);
$cmd = array_merge(self::getExiftool(), [
- '-overwrite_original',
+ '-overwrite_original', '-n',
'-api', 'LargeFileSupport=1',
'-json=-', $path,
]);
diff --git a/lib/Util.php b/lib/Util.php
index 606f1f365..bbe37dbfe 100644
--- a/lib/Util.php
+++ b/lib/Util.php
@@ -370,11 +370,8 @@ public static function explode_exact(string $delimiter, string $string, int $cou
public static function callerIsNative(): bool
{
// Should not use IRequest here since this method is called during registration
- if (\array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER)) {
- return 'gallery.memories' === $_SERVER['HTTP_X_REQUESTED_WITH'];
- }
-
- return str_contains($_SERVER['HTTP_USER_AGENT'] ?? '', 'MemoriesNative');
+ return 'gallery.memories' === ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')
+ || str_contains($_SERVER['HTTP_USER_AGENT'] ?? '', 'MemoriesNative');
}
/**
diff --git a/src/components/ScrollerManager.vue b/src/components/ScrollerManager.vue
index 4ccf4d047..540cadba7 100644
--- a/src/components/ScrollerManager.vue
+++ b/src/components/ScrollerManager.vue
@@ -95,6 +95,7 @@ export default defineComponent({
emits: {
interactend: () => true,
+ scroll: (event: { current: number; previous: number }) => true,
},
data: () => ({
@@ -223,11 +224,13 @@ export default defineComponent({
const scroll = this.recycler?.$el?.scrollTop || 0;
// Emit scroll event
- utils.bus.emit('memories.recycler.scroll', {
+ const event = {
current: scroll,
previous: this.lastKnownRecyclerScroll,
dynTopMatterVisible: scroll < this.dynTopMatterHeight,
- });
+ };
+ utils.bus.emit('memories.recycler.scroll', event);
+ this.$emit('scroll', event);
this.lastKnownRecyclerScroll = scroll;
// Get cursor px position
diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue
index 3b543334c..9067addea 100644
--- a/src/components/SelectionManager.vue
+++ b/src/components/SelectionManager.vue
@@ -62,6 +62,7 @@ import MoveIcon from 'vue-material-design-icons/ImageMove.vue';
import AlbumsIcon from 'vue-material-design-icons/ImageAlbum.vue';
import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue';
import FolderMoveIcon from 'vue-material-design-icons/FolderMove.vue';
+import RotateLeftIcon from 'vue-material-design-icons/RotateLeft.vue';
import type { IDay, IHeadRow, IPhoto, IRow } from '@typings';
@@ -231,6 +232,11 @@ export default defineComponent({
icon: EditFileIcon,
callback: this.editMetadataSelection.bind(this),
},
+ {
+ name: t('memories', 'Rotate / Flip'),
+ icon: RotateLeftIcon,
+ callback: () => this.editMetadataSelection(this.selection, [5]),
+ },
{
name: t('memories', 'View in folder'),
icon: OpenInNewIcon,
diff --git a/src/components/SwipeRefresh.vue b/src/components/SwipeRefresh.vue
new file mode 100644
index 000000000..8b23f6789
--- /dev/null
+++ b/src/components/SwipeRefresh.vue
@@ -0,0 +1,228 @@
+
+