Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/components/mixins/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import {
mdiMulticast,
} from '@mdi/js'
import { afcIconLogo } from '@/plugins/afcIcons'
import { GuiStateLayoutoption } from '@/store/gui/types'

interface LayoutFixedPanel {
panel: GuiStateLayoutoption
predecessor: string | null
successor: string | null
}

@Component
export default class DashboardMixin extends BaseMixin {
Expand Down Expand Up @@ -84,4 +91,46 @@ export default class DashboardMixin extends BaseMixin {
return mdiInformation
}
}

mergeLayout(
storedPanels: GuiStateLayoutoption[],
oldPanels: GuiStateLayoutoption[],
newPanels: GuiStateLayoutoption[]
): GuiStateLayoutoption[] {
const draggableNames = new Set(oldPanels.map((panel) => panel.name))
const storedByName = new Map(storedPanels.map((panel) => [panel.name, panel]))

const output = newPanels.map((panel) => storedByName.get(panel.name) ?? { name: panel.name, visible: true })
const movableNames = new Set(output.map((panel) => panel.name))

const fixedPanels: LayoutFixedPanel[] = []
let predecessor: string | null = null
storedPanels.forEach((panel) => {
if (draggableNames.has(panel.name)) {
fixedPanels.forEach((entry) => {
if (entry.successor === null) entry.successor = panel.name
})

predecessor = panel.name
return
}

fixedPanels.push({ panel, predecessor, successor: null })
})

fixedPanels.forEach((entry) => {
let index = output.length

if (entry.predecessor !== null && movableNames.has(entry.predecessor)) {
index = output.findIndex((panel) => panel.name === entry.predecessor) + 1
while (index < output.length && !movableNames.has(output[index].name)) index++
} else if (entry.successor !== null && movableNames.has(entry.successor)) {
index = output.findIndex((panel) => panel.name === entry.successor)
}

output.splice(index, 0, entry.panel)
})

return output
}
}
2 changes: 1 addition & 1 deletion src/components/ui/Panel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<slot name="buttons-left" />
<v-toolbar-title class="d-flex align-center">
<slot v-if="hasIconSlot" name="icon" />
<v-icon v-if="icon !== null && !hasIconSlot" left>{{ icon }}</v-icon>
<v-icon v-if="icon !== null && !hasIconSlot" class="panel-header-icon" left>{{ icon }}</v-icon>
<span v-if="title" class="subheading">{{ title }}</span>
</v-toolbar-title>
<slot name="buttons-title" />
Expand Down
231 changes: 138 additions & 93 deletions src/pages/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,79 +1,30 @@
<template>
<div>
<v-row v-if="isMobile">
<v-col>
<status-panel />
<template v-for="component in mobileLayout">
<div class="dashboard" :class="{ 'dashboard--dragging': dragging }">
<v-row>
<v-col v-for="column in columns" :key="`dashboard-column-${column.index}`" :class="column.class">
<status-panel v-if="column.index < 2" />
<draggable
:value="panelsByColumn[column.index]"
:group="dragGroup"
:handle="dragHandle"
:touch-start-threshold="5"
:animation="200"
:force-fallback="true"
class="dashboard-dropzone"
ghost-class="dashboard-panel--placeholder"
drag-class="dashboard-panel--dragged"
fallback-class="dashboard-panel--dragged"
@choose="dragging = true"
@unchoose="dragEnd"
@start="dragStart"
@end="dragEnd"
@input="saveColumn(column.index, $event)">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-mobileLayout-' + component.name"
v-for="component in panelsByColumn[column.index]"
:key="`dashboard-${viewport}-${column.index}-${component.name}`"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
</v-row>
<v-row v-else-if="isTablet">
<v-col class="col-6">
<status-panel />
<template v-for="component in tabletLayout1">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-tabletLayout1-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
<v-col class="col-6">
<template v-for="component in tabletLayout2">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-tabletLayout2-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
</v-row>
<v-row v-else-if="isDesktop">
<v-col class="col-5">
<status-panel />
<template v-for="component in desktopLayout1">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-desktopLayout1-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
<v-col class="col-7">
<template v-for="component in desktopLayout2">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-desktopLayout2-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
</v-row>
<v-row v-else-if="isWidescreen">
<v-col class="col-3">
<status-panel />
<template v-for="component in widescreenLayout1">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-desktopLayout1-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
<v-col class="col-5">
<template v-for="component in widescreenLayout2">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-desktopLayout2-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</v-col>
<v-col class="col-4">
<template v-for="component in widescreenLayout3">
<component
:is="extractPanelName(component.name)"
:key="'dashboard-desktopLayout3-' + component.name"
:panel-id="extractPanelId(component.name)"></component>
</template>
</draggable>
</v-col>
</v-row>
</div>
Expand All @@ -82,6 +33,7 @@
<script lang="ts">
import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import draggable from 'vuedraggable'
import AfcPanel from '@/components/panels/AfcPanel.vue'
import ExtruderControlPanel from '@/components/panels/ExtruderControlPanel.vue'
import DashboardMixin from '@/components/mixins/dashboard'
Expand All @@ -99,10 +51,17 @@ import StatusPanel from '@/components/panels/StatusPanel.vue'
import ToolheadControlPanel from '@/components/panels/ToolheadControlPanel.vue'
import TemperaturePanel from '@/components/panels/TemperaturePanel.vue'
import WebcamPanel from '@/components/panels/WebcamPanel.vue'
import { GuiStateLayoutoption } from '@/store/gui/types'

interface DashboardColumn {
index: number
class: string
}

@Component({
components: {
AfcPanel,
draggable,
ExtruderControlPanel,
KlippyStatePanel,
LedEffectsPanel,
Expand All @@ -121,36 +80,48 @@ import WebcamPanel from '@/components/panels/WebcamPanel.vue'
},
})
export default class PageDashboard extends Mixins(DashboardMixin) {
get mobileLayout() {
return this.$store.getters['gui/getPanels']('mobile', 0, true)
}
dragHandle = '.panel-header-icon'

get tabletLayout1() {
return this.$store.getters['gui/getPanels']('tablet', 1, true)
}
dragging = false
resizeObserver: ResizeObserver | null = null

get tabletLayout2() {
return this.$store.getters['gui/getPanels']('tablet', 2, true)
}
get columns(): DashboardColumn[] {
switch (this.viewport) {
case 'mobile':
return [{ index: 0, class: '' }]

get desktopLayout1() {
return this.$store.getters['gui/getPanels']('desktop', 1, true)
}
case 'tablet':
return [
{ index: 1, class: 'col-6' },
{ index: 2, class: 'col-6' },
]

get desktopLayout2() {
return this.$store.getters['gui/getPanels']('desktop', 2, true)
}
case 'desktop':
return [
{ index: 1, class: 'col-5' },
{ index: 2, class: 'col-7' },
]

get widescreenLayout1() {
return this.$store.getters['gui/getPanels']('widescreen', 1, true)
default:
return [
{ index: 1, class: 'col-3' },
{ index: 2, class: 'col-5' },
{ index: 3, class: 'col-4' },
]
}
}

get widescreenLayout2() {
return this.$store.getters['gui/getPanels']('widescreen', 2, true)
get panelsByColumn(): Record<number, GuiStateLayoutoption[]> {
const output: Record<number, GuiStateLayoutoption[]> = {}
this.columns.forEach((column) => {
output[column.index] = this.$store.getters['gui/getPanels'](this.viewport, column.index, true)
})

return output
}

get widescreenLayout3() {
return this.$store.getters['gui/getPanels']('widescreen', 3, true)
get dragGroup() {
return `dashboard-${this.viewport}`
}

extractPanelName(name: string) {
Expand All @@ -160,5 +131,79 @@ export default class PageDashboard extends Mixins(DashboardMixin) {
extractPanelId(name: string) {
return name.split('_')[1] ?? null
}

dragStart(event: { item: HTMLElement }) {
this.resizeObserver = new ResizeObserver(() => {
const clone = document.querySelector<HTMLElement>('.dashboard-panel--dragged')
if (clone === null) return

clone.style.width = `${event.item.offsetWidth}px`
clone.style.height = `${event.item.offsetHeight}px`
})
this.resizeObserver.observe(event.item)
}

dragEnd() {
this.dragging = false
this.resizeObserver?.disconnect()
this.resizeObserver = null
}

saveColumn(column: number, panels: GuiStateLayoutoption[]) {
const layoutName = column ? `${this.viewport}Layout${column}` : `${this.viewport}Layout`
const storedPanels = this.$store.getters['gui/getStoredPanels'](this.viewport, column)

this.$store.dispatch('gui/saveSetting', {
name: `dashboard.${layoutName}`,
value: this.mergeLayout(storedPanels, this.panelsByColumn[column] ?? [], panels),
})
}
}
</script>

<style scoped>
.dashboard-dropzone {
min-height: 64px;
border-radius: 8px;
outline: 2px dotted transparent;
outline-offset: 6px;
transition:
outline-color 150ms ease-in-out,
background-color 150ms ease-in-out;
}

.dashboard--dragging .dashboard-dropzone {
outline-color: var(--v-primary-base);
background-color: rgba(125, 125, 125, 0.06);
}
</style>

<style>
.dashboard-dropzone .panel-header-icon {
cursor: grab;
user-select: none;
-webkit-touch-callout: none;
}

.dashboard--dragging {
cursor: grabbing;
}

.dashboard--dragging .panel > * {
pointer-events: none;
}

.dashboard-panel--placeholder {
opacity: 0.5;
}

.dashboard-panel--dragged {
background-color: rgba(125, 125, 125, 0.15) !important;
box-shadow: none !important;
pointer-events: none;
}

.dashboard-panel--dragged > *:not(.panel-toolbar) {
display: none !important;
}
</style>
8 changes: 8 additions & 0 deletions src/store/gui/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ export const getters: GetterTree<GuiState, RootState> = {
return panels.filter((element) => allPossiblePanels.includes(element.name))
},

// return the unfiltered layout, to save it back without losing hidden or unavailable panels
getStoredPanels: (state) => (viewport: string, column: number) => {
const layoutName = (column ? `${viewport}Layout${column}` : `${viewport}Layout`) as keyof GuiStateDashboard
const panels = state.dashboard[layoutName] as GuiStateLayoutoption[]

return panels?.filter((element) => element !== null) ?? []
},

getAllPanelsFromViewport: (state) => (viewport: string) => {
let panels: GuiStateLayoutoption[] = []

Expand Down
Loading
Loading