2020-06-08 16:39:26 +03:00
|
|
|
<template>
|
|
|
|
<div id="app-dashboard">
|
2020-07-10 15:16:45 +03:00
|
|
|
<h2>{{ greeting.icon }} {{ greeting.text }}</h2>
|
2020-06-08 16:39:26 +03:00
|
|
|
|
2020-06-15 09:18:50 +03:00
|
|
|
<Container class="panels"
|
|
|
|
orientation="horizontal"
|
|
|
|
drag-handle-selector=".panel--header"
|
|
|
|
@drop="onDrop">
|
|
|
|
<Draggable v-for="panelId in layout" :key="panels[panelId].id" class="panel">
|
|
|
|
<div class="panel--header">
|
|
|
|
<a :href="panels[panelId].url">
|
|
|
|
<h3 :class="panels[panelId].iconClass">
|
|
|
|
{{ panels[panelId].title }}
|
|
|
|
</h3>
|
|
|
|
</a>
|
|
|
|
</div>
|
2020-07-10 18:35:18 +03:00
|
|
|
<div class="panel--content">
|
|
|
|
<div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
|
|
|
|
</div>
|
2020-06-15 09:18:50 +03:00
|
|
|
</Draggable>
|
|
|
|
</Container>
|
2020-07-10 18:04:57 +03:00
|
|
|
<a class="edit-panels icon-add" @click="showModal">{{ t('dashboard', 'Edit panels') }}</a>
|
2020-06-15 09:18:50 +03:00
|
|
|
<Modal v-if="modal" @close="closeModal">
|
|
|
|
<div class="modal__content">
|
2020-07-10 18:35:18 +03:00
|
|
|
<h3>{{ t('dashboard', 'Edit panels') }}</h3>
|
2020-06-15 09:18:50 +03:00
|
|
|
<transition-group name="flip-list" tag="ol">
|
|
|
|
<li v-for="panel in sortedPanels" :key="panel.id">
|
|
|
|
<input :id="'panel-checkbox-' + panel.id"
|
|
|
|
type="checkbox"
|
|
|
|
class="checkbox"
|
|
|
|
:checked="isActive(panel)"
|
|
|
|
@input="updateCheckbox(panel, $event.target.checked)">
|
|
|
|
<label :for="'panel-checkbox-' + panel.id">
|
|
|
|
{{ panel.title }}
|
|
|
|
</label>
|
|
|
|
</li>
|
|
|
|
<li key="appstore">
|
2020-07-10 18:35:18 +03:00
|
|
|
<a href="generateUrl('/apps/settings')" class="button">{{ t('dashboard', 'Get more panels from the app store') }}</a>
|
2020-06-15 09:18:50 +03:00
|
|
|
</li>
|
|
|
|
</transition-group>
|
2020-06-08 16:39:26 +03:00
|
|
|
</div>
|
2020-06-15 09:18:50 +03:00
|
|
|
</Modal>
|
2020-06-08 16:39:26 +03:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import Vue from 'vue'
|
|
|
|
import { loadState } from '@nextcloud/initial-state'
|
|
|
|
import { getCurrentUser } from '@nextcloud/auth'
|
2020-06-15 09:18:50 +03:00
|
|
|
import { Modal } from '@nextcloud/vue'
|
|
|
|
import { Container, Draggable } from 'vue-smooth-dnd'
|
|
|
|
import axios from '@nextcloud/axios'
|
|
|
|
import { generateUrl } from '@nextcloud/router'
|
2020-06-08 16:39:26 +03:00
|
|
|
|
|
|
|
const panels = loadState('dashboard', 'panels')
|
|
|
|
|
2020-06-15 09:18:50 +03:00
|
|
|
const applyDrag = (arr, dragResult) => {
|
|
|
|
const { removedIndex, addedIndex, payload } = dragResult
|
|
|
|
if (removedIndex === null && addedIndex === null) return arr
|
|
|
|
|
|
|
|
const result = [...arr]
|
|
|
|
let itemToAdd = payload
|
|
|
|
|
|
|
|
if (removedIndex !== null) {
|
|
|
|
itemToAdd = result.splice(removedIndex, 1)[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addedIndex !== null) {
|
|
|
|
result.splice(addedIndex, 0, itemToAdd)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-06-08 16:39:26 +03:00
|
|
|
export default {
|
|
|
|
name: 'App',
|
2020-06-15 09:18:50 +03:00
|
|
|
components: {
|
|
|
|
Modal,
|
|
|
|
Container,
|
|
|
|
Draggable,
|
|
|
|
},
|
2020-06-08 16:39:26 +03:00
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
timer: new Date(),
|
|
|
|
callbacks: {},
|
|
|
|
panels,
|
|
|
|
name: getCurrentUser()?.displayName,
|
2020-06-15 09:18:50 +03:00
|
|
|
layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]),
|
|
|
|
modal: false,
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
greeting() {
|
|
|
|
const time = this.timer.getHours()
|
|
|
|
|
|
|
|
if (time > 18) {
|
2020-07-10 15:16:45 +03:00
|
|
|
return { icon: '🌙', text: t('dashboard', 'Good evening, {name}', { name: this.name }) }
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
if (time > 12) {
|
2020-07-10 15:16:45 +03:00
|
|
|
return { icon: '☀', text: t('dashboard', 'Good afternoon, {name}', { name: this.name }) }
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
if (time === 12) {
|
2020-07-10 15:16:45 +03:00
|
|
|
return { icon: '🍽', text: t('dashboard', 'Time for lunch, {name}', { name: this.name }) }
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
2020-06-11 05:20:57 +03:00
|
|
|
if (time > 5) {
|
2020-07-10 15:16:45 +03:00
|
|
|
return { icon: '🌄', text: t('dashboard', 'Good morning, {name}', { name: this.name }) }
|
2020-06-11 05:20:57 +03:00
|
|
|
}
|
2020-07-10 15:16:45 +03:00
|
|
|
return { icon: '🦉', text: t('dashboard', 'Have a night owl, {name}', { name: this.name }) }
|
2020-06-08 16:39:26 +03:00
|
|
|
},
|
2020-06-15 09:18:50 +03:00
|
|
|
isActive() {
|
|
|
|
return (panel) => this.layout.indexOf(panel.id) > -1
|
|
|
|
},
|
|
|
|
sortedPanels() {
|
|
|
|
return Object.values(this.panels).sort((a, b) => {
|
|
|
|
const indexA = this.layout.indexOf(a.id)
|
|
|
|
const indexB = this.layout.indexOf(b.id)
|
|
|
|
if (indexA === -1 || indexB === -1) {
|
|
|
|
return indexB - indexA || a.id - b.id
|
|
|
|
}
|
|
|
|
return indexA - indexB || a.id - b.id
|
|
|
|
})
|
|
|
|
},
|
2020-06-08 16:39:26 +03:00
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
callbacks() {
|
2020-06-15 09:18:50 +03:00
|
|
|
this.rerenderPanels()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
setInterval(() => {
|
|
|
|
this.timer = new Date()
|
|
|
|
}, 30000)
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
/**
|
|
|
|
* Method to register panels that will be called by the integrating apps
|
|
|
|
*
|
|
|
|
* @param {string} app The unique app id for the widget
|
|
|
|
* @param {function} callback The callback function to register a panel which gets the DOM element passed as parameter
|
|
|
|
*/
|
|
|
|
register(app, callback) {
|
|
|
|
Vue.set(this.callbacks, app, callback)
|
|
|
|
},
|
|
|
|
rerenderPanels() {
|
2020-06-08 16:39:26 +03:00
|
|
|
for (const app in this.callbacks) {
|
|
|
|
const element = this.$refs[app]
|
2020-06-15 09:18:50 +03:00
|
|
|
if (this.panels[app] && this.panels[app].mounted) {
|
2020-07-10 11:02:44 +03:00
|
|
|
continue
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
if (element) {
|
|
|
|
this.callbacks[app](element[0])
|
|
|
|
Vue.set(this.panels[app], 'mounted', true)
|
|
|
|
} else {
|
|
|
|
console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-06-15 09:18:50 +03:00
|
|
|
|
|
|
|
saveLayout() {
|
|
|
|
axios.post(generateUrl('/apps/dashboard/layout'), {
|
|
|
|
layout: this.layout.join(','),
|
|
|
|
})
|
|
|
|
},
|
|
|
|
onDrop(dropResult) {
|
|
|
|
this.layout = applyDrag(this.layout, dropResult)
|
|
|
|
this.saveLayout()
|
|
|
|
},
|
|
|
|
showModal() {
|
|
|
|
this.modal = true
|
|
|
|
},
|
|
|
|
closeModal() {
|
|
|
|
this.modal = false
|
|
|
|
},
|
|
|
|
updateCheckbox(panel, currentValue) {
|
|
|
|
const index = this.layout.indexOf(panel.id)
|
|
|
|
if (!currentValue && index > -1) {
|
|
|
|
this.layout.splice(index, 1)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
this.layout.push(panel.id)
|
|
|
|
}
|
|
|
|
Vue.set(this.panels[panel.id], 'mounted', false)
|
|
|
|
this.saveLayout()
|
|
|
|
this.$nextTick(() => this.rerenderPanels())
|
2020-06-08 16:39:26 +03:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
#app-dashboard {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
h2 {
|
|
|
|
text-align: center;
|
2020-06-11 05:21:24 +03:00
|
|
|
font-size: 32px;
|
|
|
|
line-height: 130%;
|
|
|
|
padding: 80px 16px 32px;
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
.panels {
|
|
|
|
width: 100%;
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
2020-06-11 05:21:24 +03:00
|
|
|
flex-direction: row;
|
2020-07-10 11:02:44 +03:00
|
|
|
align-items: flex-start;
|
2020-06-11 05:21:24 +03:00
|
|
|
flex-wrap: wrap;
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
|
2020-06-15 09:18:50 +03:00
|
|
|
.panel, .panels > div {
|
2020-07-10 18:04:57 +03:00
|
|
|
width: 320px;
|
|
|
|
max-width: 100%;
|
|
|
|
margin: 16px;
|
2020-07-10 18:35:18 +03:00
|
|
|
background-color: var(--color-main-background-translucent);
|
2020-07-10 18:04:57 +03:00
|
|
|
border-radius: var(--border-radius-large);
|
|
|
|
border: 2px solid var(--color-border);
|
2020-06-08 16:39:26 +03:00
|
|
|
|
2020-06-15 09:18:50 +03:00
|
|
|
& > .panel--header {
|
2020-06-08 16:39:26 +03:00
|
|
|
position: sticky;
|
2020-07-10 18:35:18 +03:00
|
|
|
display: flex;
|
2020-06-08 16:39:26 +03:00
|
|
|
top: 50px;
|
2020-07-10 18:35:18 +03:00
|
|
|
padding: 16px;
|
2020-07-10 18:59:17 +03:00
|
|
|
// TO DO: use variables here
|
|
|
|
background: linear-gradient(170deg, rgba(0, 130,201, 0.2) 0%, rgba(255,255,255,.1) 50%, rgba(255,255,255,0) 100%);
|
|
|
|
border-top-left-radius: calc(var(--border-radius-large) - 2px);
|
|
|
|
border-top-right-radius: calc(var(--border-radius-large) - 2px);
|
2020-06-11 05:21:24 +03:00
|
|
|
backdrop-filter: blur(4px);
|
2020-07-10 18:35:18 +03:00
|
|
|
cursor: grab;
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
cursor: grabbing;
|
|
|
|
}
|
|
|
|
|
2020-06-15 09:18:50 +03:00
|
|
|
a {
|
|
|
|
flex-grow: 1;
|
|
|
|
}
|
2020-06-08 16:39:26 +03:00
|
|
|
|
2020-06-11 05:21:24 +03:00
|
|
|
h3 {
|
2020-06-15 09:18:50 +03:00
|
|
|
display: block;
|
|
|
|
flex-grow: 1;
|
2020-06-11 05:21:24 +03:00
|
|
|
margin: 0;
|
|
|
|
font-size: 20px;
|
|
|
|
font-weight: bold;
|
|
|
|
background-size: 32px;
|
|
|
|
background-position: 10px 10px;
|
|
|
|
padding: 16px 8px 16px 52px;
|
2020-07-10 18:35:18 +03:00
|
|
|
cursor: grab;
|
2020-06-11 05:21:24 +03:00
|
|
|
}
|
|
|
|
}
|
2020-07-10 18:35:18 +03:00
|
|
|
|
|
|
|
& > .panel--content {
|
|
|
|
margin: 0 16px 16px 16px;
|
|
|
|
}
|
2020-06-08 16:39:26 +03:00
|
|
|
}
|
|
|
|
|
2020-07-10 18:04:57 +03:00
|
|
|
.edit-panels {
|
2020-06-15 09:18:50 +03:00
|
|
|
position: fixed;
|
|
|
|
bottom: 20px;
|
|
|
|
right: 20px;
|
|
|
|
padding: 10px;
|
|
|
|
padding-left: 35px;
|
|
|
|
padding-right: 15px;
|
|
|
|
background-position: 10px center;
|
2020-07-10 18:35:18 +03:00
|
|
|
opacity: .7;
|
|
|
|
background-color: var(--color-main-background);
|
2020-07-10 18:04:57 +03:00
|
|
|
border-radius: var(--border-radius-pill);
|
2020-06-15 09:18:50 +03:00
|
|
|
&:hover {
|
2020-07-10 18:35:18 +03:00
|
|
|
opacity: 1;
|
2020-06-15 09:18:50 +03:00
|
|
|
background-color: var(--color-background-hover);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.modal__content {
|
|
|
|
width: 30vw;
|
|
|
|
margin: 20px;
|
|
|
|
ol {
|
|
|
|
list-style-type: none;
|
|
|
|
}
|
|
|
|
li label {
|
|
|
|
padding: 10px;
|
|
|
|
display: block;
|
|
|
|
list-style-type: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.flip-list-move {
|
|
|
|
transition: transform 1s;
|
|
|
|
}
|
|
|
|
|
2020-06-08 16:39:26 +03:00
|
|
|
</style>
|