Skip to content

Commit

Permalink
Add image editor
Browse files Browse the repository at this point in the history
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Aug 9, 2022
1 parent 7c66829 commit 4069126
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 8 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"wait-on": "wait-on -i 500 -t 300000"
},
"dependencies": {
"@filerobot/image-editor": "^1.0.123-alpha.1",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.10.0",
"@nextcloud/dialogs": "^3.1.4",
Expand All @@ -51,6 +52,7 @@
"@skjnldsv/vue-plyr": "^7.2.0",
"camelcase": "^7.0.0",
"debounce": "^1.2.1",
"filerobot-image-editor": "^4.3.1",
"fontsource-roboto": "^4.0.0",
"nextcloud-server": "^0.15.10",
"path-parse": "^1.0.7",
Expand Down
155 changes: 155 additions & 0 deletions src/components/ImageEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<template>
<div ref="editor" class="viewer__image-editor" />
</template>
<script>
import FilerobotImageEditor from 'filerobot-image-editor'
import { basename, dirname, extname, join } from 'path'
import client from '../services/DavClient.js'
import logger from '../services/logger.js'
const { TABS, TOOLS } = FilerobotImageEditor
export default {
props: {
filename: {
type: String,
required: true,
},
mime: {
type: String,
required: true,
},
src: {
type: String,
required: true,
},
},
data() {
return {
imageEditor: null,
}
},
computed: {
config() {
return {
source: this.src,
defaultSavedImageName: this.defaultSavedImageName,
defaultSavedImageType: this.defaultSavedImageType,
// We use our own translations
useBackendTranslations: false,
// Disable default tool
defaultTabId: TABS.RESIZE,
defaultToolId: TOOLS.RESIZE,
// onBeforeSave: this.onBeforeSave,
onClose: this.onClose,
// onModify: this.onModify,
onSave: this.onSave,
}
},
defaultSavedImageName() {
return basename(this.src, extname(this.src))
},
defaultSavedImageType() {
return extname(this.src).slice(1) || 'jpeg'
},
},
mounted() {
this.imageEditor = new FilerobotImageEditor(
this.$refs.editor,
this.config
)
this.imageEditor.render()
},
beforeDestroy() {
if (this.imageEditor) {
this.imageEditor.terminate()
}
},
methods: {
onClose(closingReason, haveNotSavedChanges) {
this.$emit('close')
},
async onSave(imageData) {
const filename = join(dirname(this.filename), imageData.fullName)
logger.debug('Saving image...', { src: this.src, filename, imageData })
try {
await client.putFileContents(filename, imageData.imageBase64)
logger.info('Edited image saved!')
} catch (error) {
logger.error('Error saving image', { error })
}
},
},
}
</script>

<style lang="scss">
.viewer__image-editor {
width: 100%;
height: calc(100% + var(--header-height));
top: 0;
position: absolute;
z-index: 10100;
}
</style>
<style lang="scss">
// Make sure the editor and its modals ae above everything
.SfxModal-Wrapper {
z-index: 10101 !important;
}
.SfxPopper-wrapper {
z-index: 10102 !important;
}
// Input styling
.SfxInput-root {
height: auto !important;
padding: 0 !important;
}
.SfxInput-root .SfxInput-Base {
margin: 0 !important;
}
// Select styling
.SfxSelect-root {
padding: 8px !important;
}
// Global buttons
.SfxButton-root {
margin: 0 !important;
}
// Menu items
.SfxMenuItem-root {
height: 44px;
}
// Canvas container
.FIE_canvas-container {
background-color: white !important;
}
// Header buttons
.FIE_topbar-center-options > button,
.FIE_topbar-center-options > label {
margin-left: 6px !important;
}
// Crop preset select button
.FIE_crop-presets-opener-button {
background-color: transparent !important;
border: none !important;
padding: 5px !important;
padding-left: 10px !important;
}
</style>
24 changes: 23 additions & 1 deletion src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
-->

<template>
<img :class="{
<ImageEditor v-if="editing"
:mime="mime"
:src="davPath"
:filename="filename"
@close="onClose" />

<img v-else
:class="{
dragging,
loaded,
zoomed: zoomRatio !== 1
Expand All @@ -43,18 +50,28 @@
import axios from '@nextcloud/axios'
import Vue from 'vue'
import AsyncComputed from 'vue-async-computed'
import ImageEditor from './ImageEditor.vue'

Vue.use(AsyncComputed)

export default {
name: 'Images',

components: {
ImageEditor,
},

props: {
// file etag, used for cache reset
etag: {
type: String,
required: true,
},

editing: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand All @@ -64,6 +81,7 @@ export default {
zoomRatio: 1,
}
},

computed: {
zoomHeight() {
return Math.round(this.height * this.zoomRatio)
Expand Down Expand Up @@ -207,6 +225,10 @@ export default {
this.zoomRatio = 1.3
}
},

onClose() {
this.$emit('update:editing', false)
},
},
}
</script>
Expand Down
42 changes: 35 additions & 7 deletions src/views/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
:clear-view-delay="-1 /* disable fade-out because of accessibility reasons */"
:dark="true"
:enable-slideshow="hasPrevious || hasNext"
:enable-swipe="canSwipe"
:enable-swipe="canSwipe && !editing"
:has-next="hasNext && (canLoop ? true : !isEndOfList)"
:has-previous="hasPrevious && (canLoop ? true : !isStartOfList)"
:spread-navigation="true"
Expand All @@ -50,6 +50,14 @@
@click="showSidebar">
{{ t('viewer', 'Open sidebar') }}
</ActionButton>
<ActionButton v-if="canEdit"
:close-after-click="true"
@click="onEdit">
<template #icon>
<ImageEdit :size="24" />
</template>
{{ t('viewer', 'Edit') }}
</ActionButton>
<ActionLink v-if="canDownload"
:download="currentFile.basename"
:close-after-click="true"
Expand All @@ -70,7 +78,7 @@
<div class="viewer__content" @click.self.exact="close">
<!-- PREVIOUS -->
<component :is="previousFile.modal"
v-if="previousFile && !previousFile.failed"
v-if="previousFile && !previousFile.failed && !editing"
:key="previousFile.fileid"
ref="previous-content"
v-bind="previousFile"
Expand All @@ -86,21 +94,22 @@
v-if="!currentFile.failed"
:key="currentFile.fileid"
ref="content"
v-bind="currentFile"
:active="true"
:can-swipe.sync="canSwipe"
v-bind="currentFile"
:editing.sync="editing"
:file-list="fileList"
:is-full-screen="isFullscreen"
:loaded.sync="currentFile.loaded"
:is-sidebar-shown="isSidebarShown"
:loaded.sync="currentFile.loaded"
class="viewer__file viewer__file--active"
@error="currentFailed" />
<Error v-else
:name="currentFile.basename" />

<!-- NEXT -->
<component :is="nextFile.modal"
v-if="nextFile && !nextFile.failed"
v-if="nextFile && !nextFile.failed && !editing"
:key="nextFile.fileid"
ref="next-content"
v-bind="nextFile"
Expand Down Expand Up @@ -140,16 +149,18 @@ import Mime from '../mixins/Mime.js'
import logger from '../services/logger.js'

import Download from 'vue-material-design-icons/Download.vue'
import ImageEdit from 'vue-material-design-icons/ImageEdit.vue'

export default {
name: 'Viewer',

components: {
ActionButton,
ActionLink,
Modal,
Error,
Download,
Error,
ImageEdit,
Modal,
},

mixins: [isFullscreen],
Expand All @@ -176,6 +187,7 @@ export default {
// States
isLoaded: false,
initiated: false,
editing: false,

// cancellable requests
cancelRequestFile: () => {},
Expand Down Expand Up @@ -248,6 +260,18 @@ export default {
canDownload() {
return canDownload()
},

/**
* Is the current user allowed to edit the file ?
* https://github.com/nextcloud/server/blob/7718c9776c5903474b8f3cf958cdd18a53b2449e/apps/dav/lib/Connector/Sabre/Node.php#L357-L387
*
* @return {boolean}
*/
canEdit() {
return canDownload()
&& this.currentFile?.permissions?.includes('W')
&& ['image/jpeg', 'image/png', 'image/webp'].includes(this.currentFile?.mime)
},
},

watch: {
Expand Down Expand Up @@ -784,6 +808,10 @@ export default {
showError(error)
}
},

onEdit() {
this.editing = true
},
},
}
</script>
Expand Down

0 comments on commit 4069126

Please sign in to comment.