diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-01-14 13:31:15 +0100 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2021-01-28 12:00:18 +0100 |
commit | 78e114ed72a01e3c37fe0e955ffe6ef649782575 (patch) | |
tree | c5e01ff4546b780dbb8a597e642b877fa01ab48d /apps/files/src | |
parent | 497440477492b6e7df8ca1eb6c79eb7100a2fe24 (diff) | |
download | nextcloud-server-78e114ed72a01e3c37fe0e955ffe6ef649782575.tar.gz nextcloud-server-78e114ed72a01e3c37fe0e955ffe6ef649782575.zip |
Add template picker
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/TemplatePreview.vue | 203 | ||||
-rw-r--r-- | apps/files/src/templates.js | 92 | ||||
-rw-r--r-- | apps/files/src/utils/davUtils.js | 42 | ||||
-rw-r--r-- | apps/files/src/utils/fileUtils.js | 53 | ||||
-rw-r--r-- | apps/files/src/views/TemplatePicker.vue | 268 |
5 files changed, 658 insertions, 0 deletions
diff --git a/apps/files/src/components/TemplatePreview.vue b/apps/files/src/components/TemplatePreview.vue new file mode 100644 index 00000000000..538e1bcff7b --- /dev/null +++ b/apps/files/src/components/TemplatePreview.vue @@ -0,0 +1,203 @@ +<!-- + - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <li class="template-picker__item"> + <input :id="id" + :checked="checked" + type="radio" + class="radio" + name="template-picker" + @change="onCheck"> + + <label :for="id" class="template-picker__label"> + <div class="template-picker__preview"> + <img class="template-picker__image" + :class="failedPreview ? 'template-picker__image--failed' : ''" + :src="realPreviewUrl" + alt="" + draggable="false" + @error="onFailure"> + </div> + + <span class="template-picker__title"> + {{ basename }} + </span> + </label> + </li> +</template> + +<script> +import { generateUrl } from '@nextcloud/router' +import { encodeFilePath } from '../utils/fileUtils' +import { getToken, isPublic } from '../utils/davUtils' + +// preview width generation +const previewWidth = 256 + +export default { + name: 'TemplatePreview', + inheritAttrs: false, + + props: { + basename: { + type: String, + required: true, + }, + checked: { + type: Boolean, + default: false, + }, + fileid: { + type: [String, Number], + required: true, + }, + filename: { + type: String, + required: true, + }, + previewUrl: { + type: String, + default: null, + }, + hasPreview: { + type: Boolean, + default: true, + }, + mime: { + type: String, + required: true, + }, + ratio: { + type: Number, + default: null, + }, + }, + + data() { + return { + failedPreview: false, + } + }, + + computed: { + id() { + return `template-picker-${this.fileid}` + }, + + realPreviewUrl() { + // If original preview failed, fallback to mime icon + if (this.failedPreview && this.mimeIcon) { + return generateUrl(this.mimeIcon) + } + + if (this.previewUrl) { + return this.previewUrl + } + // TODO: find a nicer standard way of doing this? + if (isPublic()) { + return generateUrl(`/apps/files_sharing/publicpreview/${getToken()}?fileId=${this.fileid}&file=${encodeFilePath(this.filename)}&x=${previewWidth}&y=${previewWidth}&a=1`) + } + return generateUrl(`/core/preview?fileId=${this.fileid}&x=${previewWidth}&y=${previewWidth}&a=1`) + }, + + mimeIcon() { + return OC.MimeType.getIconUrl(this.mime) + }, + }, + + methods: { + onCheck() { + this.$emit('check', this.fileid) + }, + onFailure() { + this.failedPreview = true + }, + }, +} +</script> + +<style lang="scss" scoped> + +.template-picker { + &__item { + display: flex; + } + + &__label { + display: flex; + // Align in the middle of the grid + align-items: center; + flex: 1 1; + flex-direction: column; + margin: var(--margin); + + &, * { + cursor: pointer; + user-select: none; + } + + &::before { + display: none !important; + } + } + + &__preview { + display: flex; + overflow: hidden; + // Stretch so all entries are the same width + flex: 1 1; + width: var(--width); + min-height: var(--width); + max-height: var(--height); + padding: var(--margin); + border: var(--border) solid var(--color-border); + border-radius: var(--border-radius-large); + + input:checked + label > & { + border-color: var(--color-primary); + } + } + + &__image { + max-width: 100%; + background-color: var(--color-main-background); + + &--failed { + width: calc(var(--margin) * 8); + // Center mime icon + margin: auto; + background-color: transparent !important; + } + } + + &__title { + overflow: hidden; + // also count preview border + max-width: calc(var(--width) + 2*2px); + padding: var(--margin); + white-space: nowrap; + text-overflow: ellipsis; + } +} + +</style> diff --git a/apps/files/src/templates.js b/apps/files/src/templates.js new file mode 100644 index 00000000000..3cffc30085c --- /dev/null +++ b/apps/files/src/templates.js @@ -0,0 +1,92 @@ +/** + * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import { getLoggerBuilder } from '@nextcloud/logger' +import { loadState } from '@nextcloud/initial-state' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import Vue from 'vue' + +import TemplatePickerView from './views/TemplatePicker' + +// Set up logger +const logger = getLoggerBuilder() + .setApp('files') + .detectUser() + .build() + +// Add translates functions +Vue.mixin({ + methods: { + t, + n, + }, +}) + +// Create document root +const TemplatePickerRoot = document.createElement('div') +TemplatePickerRoot.id = 'template-picker' +document.body.appendChild(TemplatePickerRoot) + +// Retrieve and init templates +const templates = loadState('files', 'templates', []) +logger.debug('Templates providers', templates) + +// Init vue app +const View = Vue.extend(TemplatePickerView) +const TemplatePicker = new View({ + name: 'TemplatePicker', + propsData: { + logger, + }, +}) +TemplatePicker.$mount('#template-picker') + +// Init template engine after load +window.addEventListener('DOMContentLoaded', function() { + // Init template files menu + templates.forEach((provider, index) => { + + const newTemplatePlugin = { + attach(menu) { + const fileList = menu.fileList + + // only attach to main file list, public view is not supported yet + if (fileList.id !== 'files' && fileList.id !== 'files.public') { + return + } + + // register the new menu entry + menu.addMenuEntry({ + id: `template-new-${provider.app}-${index}`, + displayName: provider.label, + templateName: provider.label + provider.extension, + iconClass: provider.iconClass || 'icon-file', + fileType: 'file', + actionHandler(name) { + TemplatePicker.open(name, provider) + }, + }) + }, + } + OC.Plugins.register('OCA.Files.NewFileMenu', newTemplatePlugin) + }) +}) diff --git a/apps/files/src/utils/davUtils.js b/apps/files/src/utils/davUtils.js new file mode 100644 index 00000000000..f64801f08dd --- /dev/null +++ b/apps/files/src/utils/davUtils.js @@ -0,0 +1,42 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import { generateRemoteUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' + +const getRootPath = function() { + if (getCurrentUser()) { + return generateRemoteUrl(`dav/files/${getCurrentUser().uid}`) + } else { + return generateRemoteUrl('webdav').replace('/remote.php', '/public.php') + } +} + +const isPublic = function() { + return !getCurrentUser() +} + +const getToken = function() { + return document.getElementById('sharingToken') && document.getElementById('sharingToken').value +} + +export { getRootPath, getToken, isPublic } diff --git a/apps/files/src/utils/fileUtils.js b/apps/files/src/utils/fileUtils.js new file mode 100644 index 00000000000..97d1c333566 --- /dev/null +++ b/apps/files/src/utils/fileUtils.js @@ -0,0 +1,53 @@ +/** + * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * Get an url encoded path + * + * @param {String} path the full path + * @returns {string} url encoded file path + */ +const encodeFilePath = function(path) { + const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') + let relativePath = '' + pathSections.forEach((section) => { + if (section !== '') { + relativePath += '/' + encodeURIComponent(section) + } + }) + return relativePath +} + +/** + * Extract dir and name from file path + * + * @param {String} path the full path + * @returns {String[]} [dirPath, fileName] + */ +const extractFilePaths = function(path) { + const pathSections = path.split('/') + const fileName = pathSections[pathSections.length - 1] + const dirPath = pathSections.slice(0, pathSections.length - 1).join('/') + return [dirPath, fileName] +} + +export { encodeFilePath, extractFilePaths } diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue new file mode 100644 index 00000000000..84c7c38aba4 --- /dev/null +++ b/apps/files/src/views/TemplatePicker.vue @@ -0,0 +1,268 @@ +<!-- + - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <Modal v-if="opened" + :clear-view-delay="-1" + class="templates-picker" + size="large" + @close="close"> + <form class="templates-picker__form" + :style="style" + @submit.prevent.stop="onSubmit"> + <h3>{{ t('files', 'Pick a template') }}</h3> + + <!-- Templates list --> + <ul class="templates-picker__list"> + <TemplatePreview + v-bind="emptyTemplate" + :checked="checked === emptyTemplate.fileid" + @check="onCheck" /> + + <TemplatePreview + v-for="template in provider.templates" + :key="template.fileid" + v-bind="template" + :checked="checked === template.fileid" + :ratio="provider.ratio" + @check="onCheck" /> + </ul> + + <!-- Cancel and submit --> + <div class="templates-picker__buttons"> + <button @click="close"> + {{ t('files', 'Cancel') }} + </button> + <input type="submit" + class="primary" + :value="t('files', 'Create')" + :aria-label="t('files', 'Create a new file with the ')"> + </div> + </form> + + <EmptyContent class="templates-picker__loading" v-if="loading" icon="icon-loading"> + {{ t('files', 'Creating file') }} + </EmptyContent> + </Modal> +</template> + +<script> +import { generateOcsUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' + +import axios from '@nextcloud/axios' +import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' +import Modal from '@nextcloud/vue/dist/Components/Modal' + +import TemplatePreview from '../components/TemplatePreview' + +const border = 2 +const margin = 8 +const width = margin * 20 + +export default { + name: 'TemplatePicker', + + components: { + EmptyContent, + Modal, + TemplatePreview, + }, + + props: { + logger: { + type: Object, + required: true, + }, + }, + + data() { + return { + // Check empty template by default + checked: -1, + loading: false, + name: null, + opened: false, + provider: null, + } + }, + + computed: { + emptyTemplate() { + return { + basename: t('files', 'Blank'), + fileid: -1, + filename: this.t('files', 'Blank'), + hasPreview: false, + mime: this.provider?.mimetypes[0] || this.provider?.mimetypes, + } + }, + + selectedTemplate() { + return this.provider.templates.find(template => template.fileid === this.checked) + }, + + /** + * Style css vars bin,d + * @returns {Object} + */ + style() { + return { + '--margin': margin + 'px', + '--width': width + 'px', + '--border': border + 'px', + '--fullwidth': width + 2 * margin + 2 * border + 'px', + '--height': this.ratio ? width * this.ratio + 'px' : null, + } + }, + }, + + methods: { + /** + * Open the picker + * @param {string} name the file name to create + * @param {object} provider the template provider picked + */ + open(name, provider) { + this.checked = this.emptyTemplate.fileid + this.name = name + this.opened = true + this.provider = provider + }, + + /** + * Close the picker and reset variables + */ + close() { + this.checked = this.emptyTemplate.fileid + this.loading = false + this.name = null + this.opened = false + this.provider = null + }, + + /** + * Manages the radio template picker change + * @param {number} fileid the selected template file id + */ + onCheck(fileid) { + this.checked = fileid + }, + + async onSubmit() { + this.loading = true + const currentDirectory = this.getCurrentDirectory() + const fileList = OCA?.Files?.App?.currentFileList + + try { + const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates', 2) + 'create', { + filePath: `${currentDirectory}/${this.name}`, + templatePath: this.selectedTemplate?.filename, + templateType: this.selectedTemplate?.templateType, + }) + + const fileInfo = response.data.ocs.data + this.logger.debug('Created new file', fileInfo) + + // Run default action + const fileAction = OCA.Files.fileActions.getDefaultFileAction(fileInfo.mime, 'file', OC.PERMISSION_ALL) + fileAction.action(fileInfo.basename, { + $file: null, + dir: currentDirectory, + fileList, + fileActions: fileList?.fileActions, + }) + + // Reload files list + fileList?.reload?.() || window.location.reload() + + this.close() + } catch (error) { + this.logger.error('Error while creating the new file from template', error) + showError(this.t('files', 'Unable to create new file from template')) + } finally { + this.loading = false + } + }, + + /** + * Return the current directory, fallback to root + * @returns {string} + */ + getCurrentDirectory() { + const currentDirInfo = OCA?.Files?.App?.currentFileList?.dirInfo + || { path: '/', name: '' } + + // Make sure we don't have double slashes + return `${currentDirInfo.path}/${currentDirInfo.name}`.replace(/\/\//gi, '/') + }, + }, +} +</script> + +<style lang="scss" scoped> +.templates-picker { + &__form { + padding: calc(var(--margin) * 2); + // Will be handled by the buttons + padding-bottom: 0; + } + + &__list { + display: grid; + grid-gap: calc(var(--margin) * 2); + grid-auto-columns: 1fr; + // We want maximum 5 columns. Putting 6 as we don't count the grid gap. So it will always be lower than 6 + max-width: calc(var(--fullwidth) * 6); + grid-template-columns: repeat(auto-fit, minmax(var(--fullwidth), 1fr)); + // Make sure all rows are the same height + grid-auto-rows: 1fr; + } + &__buttons { + display: flex; + justify-content: space-between; + padding: calc(var(--margin) * 2) var(--margin); + position: sticky; + // Make sure the templates list doesn't weirdly peak under when scrolled. Happens on some rare occasions + bottom: -1px; + background-color: var(--color-main-background); + } + + // Make sure we're relative for the loading emptycontent on top + /deep/ .modal-container { + position: relative; + overflow-y: auto !important; + } + + &__loading { + position: absolute; + top: 0; + left: 0; + justify-content: center; + width: 100%; + height: 100%; + margin: 0; + background-color: var(--color-main-background-translucent); + } +} + +</style> |