viewer: image editor

pull/175/head
Varun Patil 2022-11-07 04:24:57 -08:00
parent 2147c422e2
commit 3d0905628a
6 changed files with 1636 additions and 111 deletions

973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.0.0",
"camelcase": "^7.0.0",
"filerobot-image-editor": "^4.3.7",
"justified-layout": "^4.1.0",
"moment": "^2.29.4",
"path-posix": "^1.0.0",

View File

@ -0,0 +1,582 @@
<template>
<div ref="editor" class="viewer__image-editor" v-bind="themeDataAttr" />
</template>
<script lang="ts">
import { Component, Prop, Emit, Mixins } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import { basename, dirname, extname, join } from "path";
import { emit } from "@nextcloud/event-bus";
import { showError, showSuccess } from "@nextcloud/dialogs";
import axios from "@nextcloud/axios";
import FilerobotImageEditor from "filerobot-image-editor";
import translations from "./ImageEditorTranslations";
const { TABS, TOOLS } = FilerobotImageEditor as any;
@Component({
components: {},
})
export default class ImageEditor extends Mixins(GlobalMixin) {
@Prop() fileid: number;
@Prop() mime: string;
@Prop() src: string;
private imageEditor: FilerobotImageEditor = null;
get config() {
return {
source: this.src,
defaultSavedImageName: this.defaultSavedImageName,
defaultSavedImageType: this.defaultSavedImageType,
// We use our own translations
useBackendTranslations: false,
// Watch resize
observePluginContainerSize: true,
// Default tab and tool
defaultTabId: TABS.ADJUST,
defaultToolId: TOOLS.CROP,
// Displayed tabs, disabling watermark
tabsIds: Object.values(TABS)
.filter((tab) => tab !== TABS.WATERMARK)
.sort((a: string, b: string) => a.localeCompare(b)),
// onBeforeSave: this.onBeforeSave,
onClose: this.onClose,
// onModify: this.onModify,
onSave: this.onSave,
// Translations
translations,
theme: {
palette: {
"bg-secondary": "var(--color-main-background)",
"bg-primary": "var(--color-background-dark)",
// Accent
"accent-primary": "var(--color-primary)",
// Use by the slider
"border-active-bottom": "var(--color-primary)",
"icons-primary": "var(--color-main-text)",
// Active state
"bg-primary-active": "var(--color-background-dark)",
"bg-primary-hover": "var(--color-background-hover)",
"accent-primary-active": "var(--color-main-text)",
// Used by the save button
"accent-primary-hover": "var(--color-primary)",
warning: "var(--color-error)",
},
typography: {
fontFamily: "var(--font-face)",
},
},
savingPixelRatio: 1,
previewPixelRatio: 1,
};
}
get defaultSavedImageName() {
return basename(this.src, extname(this.src));
}
get defaultSavedImageType() {
return extname(this.src).slice(1) || "jpeg";
}
get hasHighContrastEnabled() {
const themes = globalThis.OCA?.Theming?.enabledThemes || [];
return themes.find((theme) => theme.indexOf("highcontrast") !== -1);
}
get themeDataAttr() {
if (this.hasHighContrastEnabled) {
return {
"data-theme-dark-highcontrast": true,
};
}
return {
"data-theme-dark": true,
};
}
mounted() {
this.imageEditor = new FilerobotImageEditor(
<any>this.$refs.editor,
<any>this.config
);
this.imageEditor.render();
window.addEventListener("keydown", this.handleKeydown, true);
window.addEventListener("DOMNodeInserted", this.handleSfxModal);
}
beforeDestroy() {
if (this.imageEditor) {
this.imageEditor.terminate();
}
window.removeEventListener("keydown", this.handleKeydown, true);
}
onClose(closingReason, haveNotSavedChanges) {
if (haveNotSavedChanges) {
this.onExitWithoutSaving();
return;
}
window.removeEventListener("keydown", this.handleKeydown, true);
this.$emit("close");
}
/**
* User saved the image
*
* @see https://github.com/scaleflex/filerobot-image-editor#onsave
* @param {object} props destructuring object
* @param {string} props.fullName the file name
* @param {HTMLCanvasElement} props.imageCanvas the image canvas
* @param {string} props.mimeType the image mime type
* @param {number} props.quality the image saving quality
*/
async onSave({
fullName,
imageCanvas,
mimeType,
quality,
}: {
fullName: string;
imageCanvas: HTMLCanvasElement;
mimeType: string;
quality: number;
}): Promise<void> {
const { origin, pathname } = new URL(this.src);
const putUrl = origin + join(dirname(pathname), fullName);
// toBlob is not very smart...
mimeType = mimeType.replace("jpg", "jpeg");
// Sanity check, 0 < quality < 1
quality = Math.max(Math.min(quality, 1), 0) || 1;
try {
const blob = await new Promise((resolve: BlobCallback) =>
imageCanvas.toBlob(resolve, mimeType, quality)
);
const response = await axios.put(putUrl, new File([blob], fullName));
showSuccess(this.t("viewer", "Image saved successfully"));
if (putUrl !== this.src) {
emit("files:file:created", {
fileid:
parseInt(response?.headers?.["oc-fileid"]?.split("oc")[0]) || null,
});
} else {
emit("files:file:updated", { fileid: this.fileid });
}
} catch (error) {
showError(this.t("viewer", "Error saving image"));
}
}
/**
* Show warning if unsaved changes
*/
onExitWithoutSaving() {
(<any>OC.dialogs).confirmDestructive(
translations.changesLoseConfirmation +
"\n\n" +
translations.changesLoseConfirmationHint,
this.t("viewer", "Unsaved changes"),
{
type: (<any>OC.dialogs).YES_NO_BUTTONS,
confirm: this.t("viewer", "Drop changes"),
confirmClasses: "error",
cancel: translations.cancel,
},
(decision) => {
if (!decision) {
return;
}
this.onClose("warning-ignored", false);
}
);
}
// Key Handlers, override default Viewer arrow and escape key
handleKeydown(event) {
event.stopImmediatePropagation();
// escape key
if (event.key === "Escape") {
// Since we cannot call the closeMethod and know if there
// are unsaved changes, let's fake a close button trigger.
event.preventDefault();
(
document.querySelector(".FIE_topbar-close-button") as HTMLElement
).click();
}
// ctrl + S = save
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
(
document.querySelector(".FIE_topbar-save-button") as HTMLElement
).click();
}
// ctrl + Z = undo
if (event.ctrlKey && event.key === "z") {
event.preventDefault();
(
document.querySelector(".FIE_topbar-undo-button") as HTMLElement
).click();
}
}
/**
* Watch out for Modal inject in document root
* That way we can adjust the focusTrap
*
* @param {Event} event Dom insertion event
*/
handleSfxModal(event) {
if (
event.target?.classList &&
event.target.classList.contains("SfxModal-Wrapper")
) {
emit("viewer:trapElements:changed", event.target);
}
}
}
</script>
<style lang="scss" scoped>
// Take full screen size ()
.viewer__image-editor {
position: absolute;
z-index: 10100;
top: 0;
left: 0;
width: 100%;
height: 100vh;
}
</style>
<style lang="scss">
// Make sure the editor and its modals are above everything
.SfxModal-Wrapper {
z-index: 10101 !important;
}
.SfxPopper-wrapper {
z-index: 10102 !important;
}
// Default styling
.viewer__image-editor,
.SfxModal-Wrapper,
.SfxPopper-wrapper {
* {
// Fix font size for the entire image editor
font-size: var(--default-font-size) !important;
}
label,
button {
color: var(--color-main-text);
> span {
font-size: var(--default-font-size) !important;
}
}
// Fix button ratio and center content
button {
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 6px 12px;
}
}
// Input styling
.SfxInput-root {
height: auto !important;
padding: 0 !important;
.SfxInput-Base {
margin: 0 !important;
}
}
// Select styling
.SfxSelect-root {
padding: 8px !important;
}
// Global buttons
.SfxButton-root {
min-height: 44px !important;
margin: 0 !important;
border: transparent !important;
&[color="error"] {
color: white !important;
background-color: var(--color-error) !important;
&:hover,
&:focus {
border-color: white !important;
background-color: var(--color-error-hover) !important;
}
}
&[color="primary"] {
color: var(--color-primary-text) !important;
background-color: var(--color-primary-element) !important;
&:hover,
&:focus {
background-color: var(--color-primary-element-hover) !important;
}
}
}
// Menu items
.SfxMenuItem-root {
height: 44px;
padding-left: 8px !important;
// Center the menu entry icon and fix width
> div {
margin-right: 0;
padding: 14px;
// Minus the parent padding-left
padding: calc(14px - 8px);
cursor: pointer;
}
// Disable jpeg saving (jpg is already here)
&[value="jpeg"] {
display: none;
}
}
// Modal
.SfxModal-Container {
min-height: 300px;
padding: 22px;
// Fill height
.SfxModal-root,
.SfxModalTitle-root {
flex: 1 1 100%;
justify-content: center;
color: var(--color-main-text);
}
.SfxModalTitle-Icon {
margin-bottom: 22px !important;
background: none !important;
// Fit EmptyContent styling
svg {
width: 64px;
height: 64px;
opacity: 0.4;
// Override all coloured icons
--color-primary: var(--color-main-text);
--color-error: var(--color-main-text);
}
}
// Hide close icon (use cancel button)
.SfxModalTitle-Close {
display: none !important;
}
// Modal actions buttons display
.SfxModalActions-root {
justify-content: space-evenly !important;
}
}
// Header buttons
.FIE_topbar-center-options > button,
.FIE_topbar-center-options > label {
margin-left: 6px !important;
}
// Tabs
.FIE_tabs {
padding: 6px !important;
overflow: hidden;
overflow-y: auto;
}
.FIE_tab {
width: 80px !important;
height: 80px !important;
padding: 8px;
border-radius: var(--border-radius-large) !important;
svg {
width: 16px;
height: 16px;
}
&-label {
margin-top: 8px !important;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
white-space: nowrap;
display: block !important;
}
&:hover,
&:focus {
background-color: var(--color-background-hover) !important;
}
&[aria-selected="true"] {
color: var(--color-main-text);
background-color: var(--color-background-dark);
box-shadow: 0 0 0 2px var(--color-primary-element);
}
}
// Tools bar
.FIE_tools-bar {
&-wrapper {
max-height: max-content !important;
}
// Matching buttons tools
& > div[class$="-tool-button"],
& > div[class$="-tool"] {
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
height: 44px;
padding: 6px 16px;
border-radius: var(--border-radius-pill);
}
}
// Crop preset select button
.FIE_crop-presets-opener-button {
// override default button width
min-width: 0 !important;
padding: 5px !important;
padding-left: 10px !important;
border: none !important;
background-color: transparent !important;
}
// Force icon-only style
.FIE_topbar-history-buttons button,
.FIE_topbar-close-button,
.FIE_resize-ratio-locker {
border: none !important;
background-color: transparent !important;
&:hover,
&:focus {
background-color: var(--color-background-hover) !important;
}
svg {
width: 16px;
height: 16px;
}
}
// Left top bar buttons
.FIE_topbar-history-buttons button {
&.FIE_topbar-reset-button {
&::before {
content: attr(title);
font-weight: normal;
}
svg {
display: none;
}
}
}
// Save button fixes
.FIE_topbar-save-button {
color: var(--color-primary-text) !important;
border: none !important;
background-color: var(--color-primary-element) !important;
&:hover,
&:focus {
background-color: var(--color-primary-element-hover) !important;
}
}
// Save Modal fixes
.FIE_resize-tool-options {
.FIE_resize-width-option,
.FIE_resize-height-option {
flex: 1 1;
min-width: 0;
}
}
// Resize lock
.FIE_resize-ratio-locker {
margin-right: 8px !important;
// Icon is very thin
svg {
width: 20px;
height: 20px;
path {
stroke-width: 1;
stroke: var(--color-main-text);
fill: var(--color-main-text);
}
}
}
// Close editor button fixes
.FIE_topbar-close-button {
svg path {
// The path viewbox is weird and
// not correct, this fixes it
transform: scale(1.6);
}
}
// Canvas container
.FIE_canvas-container {
background-color: var(--color-main-background) !important;
}
// Loader
.FIE_spinner::after,
.FIE_spinner-label {
display: none !important;
}
.FIE_spinner-wrapper {
background-color: transparent !important;
}
.FIE_spinner::before {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
width: 28px;
height: 28px;
margin: -16px 0 0 -16px;
content: "";
-webkit-transform-origin: center;
-ms-transform-origin: center;
transform-origin: center;
-webkit-animation: rotate 0.8s infinite linear;
animation: rotate 0.8s infinite linear;
border: 2px solid var(--color-loading-light);
border-top-color: var(--color-loading-dark);
border-radius: 100%;
filter: var(--background-invert-if-dark);
}
</style>

View File

@ -0,0 +1,111 @@
import { translate as t } from "@nextcloud/l10n";
/**
* Translations file from library source
* We also use that to edit the end strings of
* some buttons, like resetOperations
*
* @see https://raw.githubusercontent.com/scaleflex/filerobot-image-editor/v4/packages/react-filerobot-image-editor/src/context/defaultTranslations.js
*/
export default {
name: t("viewer", "Name"),
save: t("viewer", "Save"),
saveAs: t("viewer", "Save as"),
back: t("viewer", "Back"),
loading: t("viewer", "Loading …"),
// resetOperations: 'Reset/delete all operations',
resetOperations: t("viewer", "Reset"),
changesLoseConfirmation: t("viewer", "All changes will be lost."),
changesLoseConfirmationHint: t(
"viewer",
"Are you sure you want to continue?"
),
cancel: t("viewer", "Cancel"),
continue: t("viewer", "Continue"),
undoTitle: t("viewer", "Undo"),
redoTitle: t("viewer", "Redo"),
showImageTitle: t("viewer", "Show original image"),
zoomInTitle: t("viewer", "Zoom in"),
zoomOutTitle: t("viewer", "Zoom out"),
toggleZoomMenuTitle: t("viewer", "Toggle zoom menu"),
adjustTab: t("viewer", "Adjust"),
finetuneTab: t("viewer", "Fine-tune"),
filtersTab: t("viewer", "Filters"),
watermarkTab: t("viewer", "Watermark"),
annotateTab: t("viewer", "Draw"),
resize: t("viewer", "Resize"),
resizeTab: t("viewer", "Resize"),
invalidImageError: t("viewer", "Invalid image."),
uploadImageError: t("viewer", "Error while uploading the image."),
areNotImages: t("viewer", "are not images"),
isNotImage: t("viewer", "is not an image"),
toBeUploaded: t("viewer", "to be uploaded"),
cropTool: t("viewer", "Crop"),
original: t("viewer", "Original"),
custom: t("viewer", "Custom"),
square: t("viewer", "Square"),
landscape: t("viewer", "Landscape"),
portrait: t("viewer", "Portrait"),
ellipse: t("viewer", "Ellipse"),
classicTv: t("viewer", "Classic TV"),
cinemascope: t("viewer", "CinemaScope"),
arrowTool: t("viewer", "Arrow"),
blurTool: t("viewer", "Blur"),
brightnessTool: t("viewer", "Brightness"),
contrastTool: t("viewer", "Contrast"),
ellipseTool: t("viewer", "Ellipse"),
unFlipX: t("viewer", "Un-flip X"),
flipX: t("viewer", "Flip X"),
unFlipY: t("viewer", "Un-flip Y"),
flipY: t("viewer", "Flip Y"),
hsvTool: t("viewer", "HSV"),
hue: t("viewer", "Hue"),
saturation: t("viewer", "Saturation"),
value: t("viewer", "Value"),
imageTool: t("viewer", "Image"),
importing: t("viewer", "Importing …"),
addImage: t("viewer", "+ Add image"),
lineTool: t("viewer", "Line"),
penTool: t("viewer", "Pen"),
polygonTool: t("viewer", "Polygon"),
sides: t("viewer", "Sides"),
rectangleTool: t("viewer", "Rectangle"),
cornerRadius: t("viewer", "Corner Radius"),
resizeWidthTitle: t("viewer", "Width in pixels"),
resizeHeightTitle: t("viewer", "Height in pixels"),
toggleRatioLockTitle: t("viewer", "Toggle ratio lock"),
reset: t("viewer", "Reset"),
resetSize: t("viewer", "Reset to original image size"),
rotateTool: t("viewer", "Rotate"),
textTool: t("viewer", "Text"),
textSpacings: t("viewer", "Text spacing"),
textAlignment: t("viewer", "Text alignment"),
fontFamily: t("viewer", "Font family"),
size: t("viewer", "Size"),
letterSpacing: t("viewer", "Letter spacing"),
lineHeight: t("viewer", "Line height"),
warmthTool: t("viewer", "Warmth"),
addWatermark: t("viewer", "+ Add watermark"),
addWatermarkTitle: t("viewer", "Choose watermark type"),
uploadWatermark: t("viewer", "Upload watermark"),
addWatermarkAsText: t("viewer", "Add as text"),
padding: t("viewer", "Padding"),
shadow: t("viewer", "Shadow"),
horizontal: t("viewer", "Horizontal"),
vertical: t("viewer", "Vertical"),
blur: t("viewer", "Blur"),
opacity: t("viewer", "Opacity"),
position: t("viewer", "Position"),
stroke: t("viewer", "Stroke"),
saveAsModalLabel: t("viewer", "Save image as"),
extension: t("viewer", "Extension"),
nameIsRequired: t("viewer", "Name is required."),
quality: t("viewer", "Quality"),
imageDimensionsHoverTitle: t("viewer", "Saved image size (width x height)"),
cropSizeLowerThanResizedWarning: t(
"viewer",
"Note that the selected crop area is lower than the applied resize which might cause quality decrease"
),
actualSize: t("viewer", "Actual size (100%)"),
fitSize: t("viewer", "Fit size"),
};

View File

@ -5,7 +5,15 @@
:class="{ fullyOpened }"
:style="{ width: outerWidth }"
>
<div class="inner" ref="inner">
<ImageEditor
v-if="editorOpen"
:mime="currentPhoto.mimetype"
:src="currentDownloadLink"
:fileid="currentPhoto.fileid"
@close="editorOpen = false"
/>
<div class="inner" ref="inner" v-show="!editorOpen">
<div class="top-bar" v-if="photoswipe" :class="{ showControls }">
<NcActions
:inline="numInlineActions"
@ -49,6 +57,17 @@
<InfoIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Edit')"
v-if="canEdit"
@click="openEditor"
:close-after-click="true"
>
{{ t("memories", "Edit") }}
<template #icon>
<TuneIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Download')"
@click="downloadCurrent"
@ -77,8 +96,8 @@
<script lang="ts">
import { Component, Emit, Mixins } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import { IDay, IPhoto, IRow, IRowType } from "../types";
import { NcActions, NcActionButton } from "@nextcloud/vue";
@ -86,6 +105,8 @@ import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import { generateUrl } from "@nextcloud/router";
import { showError } from "@nextcloud/dialogs";
import ImageEditor from "./ImageEditor.vue";
import * as dav from "../services/DavRequests";
import * as utils from "../services/Utils";
import { getPreviewUrl } from "../services/FileUtils";
@ -104,11 +125,13 @@ import StarOutlineIcon from "vue-material-design-icons/StarOutline.vue";
import DownloadIcon from "vue-material-design-icons/Download.vue";
import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
import OpenInNewIcon from "vue-material-design-icons/OpenInNew.vue";
import TuneIcon from "vue-material-design-icons/Tune.vue";
@Component({
components: {
NcActions,
NcActionButton,
ImageEditor,
ShareIcon,
DeleteIcon,
StarIcon,
@ -116,6 +139,7 @@ import OpenInNewIcon from "vue-material-design-icons/OpenInNew.vue";
DownloadIcon,
InfoIcon,
OpenInNewIcon,
TuneIcon,
},
})
export default class Viewer extends Mixins(GlobalMixin) {
@ -125,6 +149,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
public isOpen = false;
private originalTitle = null;
public editorOpen = false;
private show = false;
private showControls = false;
@ -142,6 +167,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
private globalCount = 0;
private globalAnchor = -1;
private currIndex = -1;
mounted() {
subscribe("files:sidebar:opened", this.handleAppSidebarOpen);
@ -155,10 +181,14 @@ export default class Viewer extends Mixins(GlobalMixin) {
/** Number of buttons to show inline */
get numInlineActions() {
let base = 3;
if (this.canShare) base++;
if (this.canEdit) base++;
if (window.innerWidth < 768) {
return 3;
return Math.min(base, 3);
} else {
return 4;
return Math.min(base, 5);
}
}
@ -176,17 +206,24 @@ export default class Viewer extends Mixins(GlobalMixin) {
}
/** Get the currently open photo */
private getCurrentPhoto() {
get currentPhoto() {
if (!this.list.length || !this.photoswipe) {
return null;
}
const idx = this.photoswipe.currIndex - this.globalAnchor;
const idx = this.currIndex - this.globalAnchor;
if (idx < 0 || idx >= this.list.length) {
return null;
}
return this.list[idx];
}
/** Get download link for current photo */
get currentDownloadLink() {
return this.currentPhoto
? window.location.origin + getDownloadLink(this.currentPhoto)
: null;
}
/** Create the base photoswipe object */
private async createBase(args: PhotoSwipeOptions) {
this.show = true;
@ -298,6 +335,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
// Update vue route for deep linking
this.photoswipe.on("slideActivate", (e) => {
this.currIndex = this.photoswipe.currIndex;
this.setRouteHash(e.slide?.data?.photo);
this.updateTitle(e.slide?.data?.photo);
});
@ -312,6 +350,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
// Create video element
content.videoElement = document.createElement("video") as any;
content.videoElement.setAttribute("preload", "none");
content.videoElement.classList.add("video-js");
// Get DAV URL for video
@ -332,7 +371,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
fluid: true,
autoplay: content.data.playvideo,
controls: true,
preload: "metadata",
preload: "none",
muted: true,
html5: {
vhs: {
@ -557,6 +596,16 @@ export default class Viewer extends Mixins(GlobalMixin) {
}
}
get canEdit() {
return this.currentPhoto?.mimetype === "image/jpeg";
}
private openEditor() {
// Only for JPEG for now
if (!this.canEdit) return;
this.editorOpen = true;
}
/** Does the browser support native share API */
get canShare() {
return "share" in navigator;
@ -573,7 +622,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
if (!img?.src) return;
// Shre image data using navigator api
const photo = this.getCurrentPhoto();
const photo = this.currentPhoto;
if (!photo) return;
// No videos yet
@ -626,14 +675,14 @@ export default class Viewer extends Mixins(GlobalMixin) {
/** Is the current photo a favorite */
private isFavorite() {
const p = this.getCurrentPhoto();
const p = this.currentPhoto;
if (!p) return false;
return Boolean(p.flag & this.c.FLAG_IS_FAVORITE);
}
/** Favorite the current photo */
private async favoriteCurrent() {
const photo = this.getCurrentPhoto();
const photo = this.currentPhoto;
const val = !this.isFavorite();
try {
this.updateLoading(1);
@ -655,14 +704,14 @@ export default class Viewer extends Mixins(GlobalMixin) {
/** Download the current photo */
private async downloadCurrent() {
const photo = this.getCurrentPhoto();
const photo = this.currentPhoto;
if (!photo) return;
dav.downloadFilesByPhotos([photo]);
}
/** Open the sidebar */
private async openSidebar(photo?: IPhoto) {
const fInfo = await dav.getFiles([photo || this.getCurrentPhoto()]);
const fInfo = await dav.getFiles([photo || this.currentPhoto]);
globalThis.OCA?.Files?.Sidebar?.setFullScreenMode?.(true);
globalThis.OCA.Files.Sidebar.open(fInfo[0].filename);
}
@ -720,8 +769,7 @@ export default class Viewer extends Mixins(GlobalMixin) {
* Open the files app with the current file.
*/
private async viewInFolder() {
const photo = this.getCurrentPhoto();
if (photo) dav.viewInFolder(photo);
if (this.currentPhoto) dav.viewInFolder(this.currentPhoto);
}
}
</script>

View File

@ -98,9 +98,9 @@ export function getDownloadLink(photo: IPhoto) {
route.params.name
);
if (fInfos.length) {
return `remote.php/dav/${fInfos[0].originalFilename}`;
return `/remote.php/dav/${fInfos[0].originalFilename}`;
}
}
return `remote.php/dav/${photo.filename}`; // normal route
return `/remote.php/dav/${photo.filename}`; // normal route
}