Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>pull/25090/head
parent
4974404774
commit
78e114ed72
@ -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> |
||||
Loading…
Reference in new issue