Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>tags/v21.0.0beta8
@@ -1,5 +1,5 @@ | |||
<?php | |||
/* | |||
/** | |||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> | |||
* | |||
* @author Julius Härtl <jus@bitgrid.net> |
@@ -190,6 +190,7 @@ class ViewController extends Controller { | |||
// Load the files we need | |||
\OCP\Util::addStyle('files', 'merged'); | |||
\OCP\Util::addScript('files', 'merged-index'); | |||
\OCP\Util::addScript('files', 'dist/templates'); | |||
// mostly for the home storage's free space | |||
// FIXME: Make non static |
@@ -30,7 +30,8 @@ namespace OCA\Files\Event; | |||
use OCP\EventDispatcher\Event; | |||
/** | |||
* This event is triggered when the files app is rendered. It canb e used to add additional scripts to the files app. | |||
* This event is triggered when the files app is rendered. | |||
* It can be used to add additional scripts to the files app. | |||
* | |||
* @since 17.0.0 | |||
*/ |
@@ -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> |
@@ -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) | |||
}) | |||
}) |
@@ -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 } |
@@ -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 } |
@@ -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> |
@@ -2,7 +2,8 @@ const path = require('path'); | |||
module.exports = { | |||
entry: { | |||
'sidebar': path.join(__dirname, 'src', 'sidebar.js'), | |||
sidebar: path.join(__dirname, 'src', 'sidebar.js'), | |||
templates: path.join(__dirname, 'src', 'templates.js'), | |||
'files-app-settings': path.join(__dirname, 'src', 'files-app-settings.js'), | |||
'personal-settings': path.join(__dirname, 'src', 'main-personal-settings.js'), | |||
}, |
@@ -1,5 +1,5 @@ | |||
<?php | |||
/* | |||
/** | |||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> | |||
* | |||
* @author Julius Härtl <jus@bitgrid.net> |
@@ -1,5 +1,5 @@ | |||
<?php | |||
/* | |||
/** | |||
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> | |||
* | |||
* @author Julius Härtl <jus@bitgrid.net> |