Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/components/SourceView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div
id="editor-container"
data-text-el="editor-container"
class="text-editor source-viewer">
<Component
:is="readerComponent"
:content="content"
:file-id="fileid"
:read-only="true"
:show-menu-bar="false" />
<NcButton
v-if="isEmbedded"
class="toggle-interactive"
@click="$emit('edit')">
{{ t('text', 'Edit') }}
<template #icon>
<PencilOutlineIcon />
</template>
</NcButton>
</div>
</template>

<script>
import axios from '@nextcloud/axios'
import { getClient, getRootPath } from '@nextcloud/files/dav'
import { t } from '@nextcloud/l10n'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import Vue from 'vue'
import PencilOutlineIcon from 'vue-material-design-icons/PencilOutline.vue'
import MarkdownContentEditor from './Editor/MarkdownContentEditor.vue'
import PlainTextReader from './PlainTextReader.vue'
export default {
name: 'SourceView',
components: {
NcButton: Vue.extend(NcButton),
PencilOutlineIcon: Vue.extend(PencilOutlineIcon),
PlainTextReader: Vue.extend(PlainTextReader),
MarkdownContentEditor: Vue.extend(MarkdownContentEditor),
},
inject: ['isEmbedded'],
props: {
filename: {
type: String,
default: null,
},
fileid: {
type: Number,
default: null,
},
mime: {
type: String,
default: null,
},
source: {
type: String,
default: undefined,
},
},
data() {
return {
content: '',
}
},
computed: {
isEncrypted() {
return this.$attrs.e2EeIsEncrypted || false
},
isMarkdown() {
return (
this.mime === 'text/markdown' || this.mime === 'text/x-web-markdown'
)
},
/** @return {boolean} */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The annotation already existed before the refactoring, but the return value of readerComponent seems to be rather a Vue component and not a boolean a boolean, right? Maybe a good opportunity to fix (or remove?) the annotation?

readerComponent() {
return this.isMarkdown ? MarkdownContentEditor : PlainTextReader
},
},
watch: {
source() {
this.loadFileContent()
},
},
mounted() {
this.loadFileContent()
},
methods: {
async loadFileContent() {
if (this.isEncrypted) {
this.content = await this.fetchDecryptedContent()
this.contentLoaded = true
} else {
const response = await axios.get(this.source)
this.content = response.data
this.contentLoaded = true
}
this.$emit('loaded', true)
},
async fetchDecryptedContent() {
const client = getClient()
const response = await client.getFileContents(
`${getRootPath()}${this.filename}`,
{ details: true },
)
const blob = new Blob([response.data], {
type: response.headers['content-type'],
})
const reader = new FileReader()
reader.readAsText(blob)
return new Promise((resolve) => {
reader.onload = () => {
resolve(reader.result)
}
})
},
t,
},
}
</script>
<style lang="scss" scoped>
.source-viewer {
display: block;
.text-editor__content-wrapper {
margin-top: var(--header-height);
}
.toggle-interactive {
position: sticky;
bottom: 0;
right: 0;
z-index: 1;
margin-left: auto;
margin-right: 0;
}
}
</style>
114 changes: 16 additions & 98 deletions src/components/ViewerComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,27 @@
:class="{ 'text-editor--embedding': isEmbedded }"
:mime="mime"
:show-outline-outside="showOutlineOutside" />
<div
<SourceView
v-else
id="editor-container"
data-text-el="editor-container"
class="text-editor source-viewer">
<Component
:is="readerComponent"
:content="content"
:file-id="fileid"
:read-only="true"
:show-menu-bar="false" />
<NcButton v-if="isEmbedded" class="toggle-interactive" @click="toggleEdit">
{{ t('text', 'Edit') }}
<template #icon>
<PencilOutlineIcon />
</template>
</NcButton>
</div>
:fileid="fileid"
:filename="filename"
:mime="mime"
:source="source"
v-bind="$attrs"
@loaded="onLoaded">
@edit="toggleEdit">
</SourceView>
</template>

<script>
import axios from '@nextcloud/axios'
import { getClient, getRootPath } from '@nextcloud/files/dav'
import { t } from '@nextcloud/l10n'
import { getSharingToken } from '@nextcloud/sharing/public'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import Vue from 'vue'
import PencilOutlineIcon from 'vue-material-design-icons/PencilOutline.vue'
import MarkdownContentEditor from './Editor/MarkdownContentEditor.vue'
import PlainTextReader from './PlainTextReader.vue'

import getEditorInstance from './Editor.singleton.js'
import SourceView from './SourceView.vue'

export default {
name: 'ViewerComponent',
components: {
NcButton: Vue.extend(NcButton),
PencilOutlineIcon: Vue.extend(PencilOutlineIcon),
PlainTextReader: Vue.extend(PlainTextReader),
MarkdownContentEditor: Vue.extend(MarkdownContentEditor),
SourceView,
Editor: getEditorInstance,
},
provide() {
Expand Down Expand Up @@ -105,7 +86,6 @@ export default {
},
data() {
return {
content: '',
hasToggledInteractiveEmbedding: false,
}
},
Expand All @@ -121,66 +101,21 @@ export default {
&& !this.hasToggledInteractiveEmbedding
)
},

isEncrypted() {
return this.$attrs.e2EeIsEncrypted || false
},

isMarkdown() {
return (
this.mime === 'text/markdown' || this.mime === 'text/x-web-markdown'
)
},

/** @return {boolean} */
readerComponent() {
return this.isMarkdown ? MarkdownContentEditor : PlainTextReader
},
},

watch: {
source() {
this.loadFileContent()
},
},

mounted() {
this.loadFileContent()
if (!this.useSourceView) {
this.onLoaded()
}
},

methods: {
async loadFileContent() {
if (this.useSourceView) {
if (this.isEncrypted) {
this.content = await this.fetchDecryptedContent()
this.contentLoaded = true
} else {
const response = await axios.get(this.source)
this.content = response.data
this.contentLoaded = true
}
}
async onLoaded() {
this.$emit('update:loaded', true)
},
toggleEdit() {
this.hasToggledInteractiveEmbedding = true
},
async fetchDecryptedContent() {
const client = getClient()
const response = await client.getFileContents(
`${getRootPath()}${this.filename}`,
{ details: true },
)
const blob = new Blob([response.data], {
type: response.headers['content-type'],
})
const reader = new FileReader()
reader.readAsText(blob)
return new Promise((resolve) => {
reader.onload = () => {
resolve(reader.result)
}
})
this.onLoaded()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems new that toggleEdit() now calls this.onLoaded() which emits update.loaded. Was this change in behaviour intended?

},
t,
},
Expand All @@ -197,23 +132,6 @@ export default {
position: relative;
background-color: var(--color-main-background);

&.source-viewer {
display: block;

.text-editor__content-wrapper {
margin-top: var(--header-height);
}

.toggle-interactive {
position: sticky;
bottom: 0;
right: 0;
z-index: 1;
margin-left: auto;
margin-right: 0;
}
}

&.text-editor--embedding {
min-height: 400px;
}
Expand Down
Loading