From f36d64b6c4fd3c798433d47c25dd80c2e7c2853a Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Mon, 10 Aug 2026 12:48:54 +0100 Subject: [PATCH] =?UTF-8?q?feat(viewer):=20add=20visual=20image=20rotation?= =?UTF-8?q?=20(90=C2=B0=20steps)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add rotate left/right buttons to the image viewer toolbar. Rotation is visual-only (CSS transform), no file modification. Buttons appear inline in the header for images (jpeg/png/webp). Rotation resets when navigating to next/previous image. Assisted-by: ClaudeCode:claude-sonnet-4-6 Assisted-by: ClaudeCode:claude-opus-5 --- src/components/Images.vue | 10 ++++++++++ src/views/Viewer.vue | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/components/Images.vue b/src/components/Images.vue index dd07e1441..1b4a161b2 100644 --- a/src/components/Images.vue +++ b/src/components/Images.vue @@ -104,6 +104,10 @@ export default { type: Boolean, default: false, }, + rotation: { + type: Number, + default: 0, + }, }, data() { return { @@ -134,10 +138,12 @@ export default { return this.basename }, imgStyle() { + const transform = this.rotation !== 0 ? `rotate(${this.rotation}deg)` : undefined if (this.zoomRatio === 1) { return { height: this.zoomHeight + 'px', width: this.zoomWidth + 'px', + transform, } } return { @@ -145,6 +151,7 @@ export default { marginLeft: Math.round(this.shiftX * 2) + 'px', height: this.zoomHeight + 'px', width: this.zoomWidth + 'px', + transform, } }, livePhoto() { @@ -200,6 +207,9 @@ export default { }, }, watch: { + rotation() { + this.resetZoom() + }, active(val, old) { // the item was hidden before and is now the current view if (val === true && old === false) { diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 2dd198cd8..6c7c16f13 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -41,7 +41,7 @@ :enable-swipe="canSwipe && !editing" :has-next="hasNext" :has-previous="hasPrevious" - :inline-actions="canEdit ? 1 : 0" + :inline-actions="(canEdit ? 1 : 0) + (canRotate ? 2 : 0)" :spread-navigation="true" :style="{ width: isSidebarShown ? `${sidebarPosition}px` : null }" :name="currentFile.basename" @@ -61,6 +61,24 @@ {{ t('viewer', 'Edit') }} + + + {{ t('viewer', 'Rotate counterclockwise') }} + + + + {{ t('viewer', 'Rotate clockwise') }} + @@ -148,6 +166,7 @@ :is-full-screen="isFullscreen" :is-sidebar-shown="isSidebarShown" :loaded.sync="currentFile.loaded" + :rotation="rotation" class="viewer__file viewer__file--active" @update:editing="toggleEditor" @error="currentFailed" /> @@ -207,6 +226,8 @@ import Fullscreen from 'vue-material-design-icons/Fullscreen.vue' import FullscreenExit from 'vue-material-design-icons/FullscreenExit.vue' import Pencil from 'vue-material-design-icons/PencilOutline.vue' import DockRight from 'vue-material-design-icons/DockRight.vue' +import RotateLeft from 'vue-material-design-icons/RotateLeft.vue' +import RotateRight from 'vue-material-design-icons/RotateRight.vue' import '@nextcloud/dialogs/style.css' @@ -229,6 +250,8 @@ export default defineComponent({ NcActionLink, NcModal, Pencil, + RotateLeft, + RotateRight, }, mixins: [isFullscreen, isMobile], @@ -251,6 +274,7 @@ export default defineComponent({ comparisonFile: null, nextFile: {}, fileList: [], + rotation: 0, sortingConfig: null, // States @@ -333,6 +357,10 @@ export default defineComponent({ return ['image/jpeg', 'image/png', 'image/webp'].includes(this.currentFile?.mime) }, + canRotate() { + return this.isImage && !this.comparisonFile + }, + sidebarOpenFilePath() { try { const relativePath = this.currentFile?.davPath?.split(defaultRootPath)[1] @@ -1062,7 +1090,16 @@ export default defineComponent({ /** * Open previous available file */ + rotateLeft() { + this.rotation = ((this.rotation - 90) + 360) % 360 + }, + + rotateRight() { + this.rotation = (this.rotation + 90) % 360 + }, + previous() { + this.rotation = 0 this.currentIndex-- if (this.currentIndex < 0) { this.currentIndex = this.fileList.length - 1 @@ -1078,6 +1115,7 @@ export default defineComponent({ * Open next available file */ next() { + this.rotation = 0 this.currentIndex++ if (this.currentIndex > this.fileList.length - 1) { this.currentIndex = 0