|
|
@@ -2,16 +2,41 @@ |
|
|
|
<div id="app-dashboard"> |
|
|
|
<h2>{{ greeting.icon }} {{ greeting.text }}</h2> |
|
|
|
|
|
|
|
<div class="panels"> |
|
|
|
<div v-for="panel in panels" :key="panel.id" class="panel"> |
|
|
|
<a :href="panel.url"> |
|
|
|
<h3 :class="panel.iconClass"> |
|
|
|
{{ panel.title }} |
|
|
|
</h3> |
|
|
|
</a> |
|
|
|
<div :ref="panel.id" :data-id="panel.id" /> |
|
|
|
<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> |
|
|
|
<div :ref="panels[panelId].id" :data-id="panels[panelId].id" /> |
|
|
|
</Draggable> |
|
|
|
</Container> |
|
|
|
<a class="add-panels icon-add" @click="showModal">Add more panels</a> |
|
|
|
<Modal v-if="modal" @close="closeModal"> |
|
|
|
<div class="modal__content"> |
|
|
|
<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"> |
|
|
|
<a href="/index.php/apps/settings" class="button">{{ t('dashboard', 'Get more panels from the app store') }}</a> |
|
|
|
</li> |
|
|
|
</transition-group> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</Modal> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
@@ -19,17 +44,46 @@ |
|
|
|
import Vue from 'vue' |
|
|
|
import { loadState } from '@nextcloud/initial-state' |
|
|
|
import { getCurrentUser } from '@nextcloud/auth' |
|
|
|
import { Modal } from '@nextcloud/vue' |
|
|
|
import { Container, Draggable } from 'vue-smooth-dnd' |
|
|
|
import axios from '@nextcloud/axios' |
|
|
|
import { generateUrl } from '@nextcloud/router' |
|
|
|
|
|
|
|
const panels = loadState('dashboard', 'panels') |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'App', |
|
|
|
components: { |
|
|
|
Modal, |
|
|
|
Container, |
|
|
|
Draggable, |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
timer: new Date(), |
|
|
|
callbacks: {}, |
|
|
|
panels, |
|
|
|
name: getCurrentUser()?.displayName, |
|
|
|
layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]), |
|
|
|
modal: false, |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
@@ -50,15 +104,46 @@ export default { |
|
|
|
} |
|
|
|
return { icon: '🦉', text: t('dashboard', 'Have a night owl, {name}', { name: this.name }) } |
|
|
|
}, |
|
|
|
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 |
|
|
|
}) |
|
|
|
}, |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
callbacks() { |
|
|
|
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() { |
|
|
|
for (const app in this.callbacks) { |
|
|
|
const element = this.$refs[app] |
|
|
|
if (this.panels[app].mounted) { |
|
|
|
if (this.panels[app] && this.panels[app].mounted) { |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
if (element) { |
|
|
|
this.callbacks[app](element[0]) |
|
|
|
Vue.set(this.panels[app], 'mounted', true) |
|
|
@@ -67,15 +152,33 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
setInterval(() => { |
|
|
|
this.timer = new Date() |
|
|
|
}, 30000) |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
register(app, callback) { |
|
|
|
Vue.set(this.callbacks, app, callback) |
|
|
|
|
|
|
|
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()) |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
@@ -101,18 +204,30 @@ export default { |
|
|
|
flex-wrap: wrap; |
|
|
|
} |
|
|
|
|
|
|
|
.panel { |
|
|
|
width: 250px; |
|
|
|
margin: 16px; |
|
|
|
.panel, .panels > div { |
|
|
|
width: 280px; |
|
|
|
padding: 16px; |
|
|
|
|
|
|
|
& > a { |
|
|
|
.panel--header h3 { |
|
|
|
cursor: grab; |
|
|
|
&:active { |
|
|
|
cursor: grabbing; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
& > .panel--header { |
|
|
|
position: sticky; |
|
|
|
top: 50px; |
|
|
|
display: block; |
|
|
|
background: linear-gradient(var(--color-main-background-translucent), var(--color-main-background-translucent) 80%, rgba(255, 255, 255, 0)); |
|
|
|
backdrop-filter: blur(4px); |
|
|
|
display: flex; |
|
|
|
a { |
|
|
|
flex-grow: 1; |
|
|
|
} |
|
|
|
|
|
|
|
h3 { |
|
|
|
display: block; |
|
|
|
flex-grow: 1; |
|
|
|
margin: 0; |
|
|
|
font-size: 20px; |
|
|
|
font-weight: bold; |
|
|
@@ -123,4 +238,35 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.add-panels { |
|
|
|
position: fixed; |
|
|
|
bottom: 20px; |
|
|
|
right: 20px; |
|
|
|
padding: 10px; |
|
|
|
padding-left: 35px; |
|
|
|
padding-right: 15px; |
|
|
|
background-position: 10px center; |
|
|
|
border-radius: 100px; |
|
|
|
&:hover { |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
</style> |