feat: customize sidebar navi (#1336)
This commit is contained in:
parent
e6505fe55a
commit
a3316eb23b
@ -80,6 +80,7 @@ import SettingsDashboardTab from '@/components/settings/SettingsDashboardTab.vue
|
||||
import SettingsGCodeViewerTab from '@/components/settings/SettingsGCodeViewerTab.vue'
|
||||
import SettingsEditorTab from '@/components/settings/SettingsEditorTab.vue'
|
||||
import SettingsTimelapseTab from '@/components/settings/SettingsTimelapseTab.vue'
|
||||
import SettingsNavigationTab from '@/components/settings/SettingsNavigationTab.vue'
|
||||
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import {
|
||||
@ -98,6 +99,7 @@ import {
|
||||
mdiVideo3d,
|
||||
mdiWebcam,
|
||||
mdiDipSwitch,
|
||||
mdiMenu,
|
||||
} from '@mdi/js'
|
||||
import SettingsMiscellaneousTab from '@/components/settings/SettingsMiscellaneousTab.vue'
|
||||
@Component({
|
||||
@ -116,6 +118,7 @@ import SettingsMiscellaneousTab from '@/components/settings/SettingsMiscellaneou
|
||||
SettingsEditorTab,
|
||||
SettingsTimelapseTab,
|
||||
SettingsMiscellaneousTab,
|
||||
SettingsNavigationTab,
|
||||
},
|
||||
})
|
||||
export default class TheSettingsMenu extends Mixins(BaseMixin) {
|
||||
@ -194,6 +197,11 @@ export default class TheSettingsMenu extends Mixins(BaseMixin) {
|
||||
name: 'miscellaneous',
|
||||
title: this.$t('Settings.MiscellaneousTab.Miscellaneous'),
|
||||
},
|
||||
{
|
||||
icon: mdiMenu,
|
||||
name: 'navigation',
|
||||
title: this.$t('Settings.NavigationTab.Navigation'),
|
||||
},
|
||||
]
|
||||
|
||||
if (this.moonrakerComponents.includes('timelapse')) {
|
||||
|
@ -1,46 +1,3 @@
|
||||
<style>
|
||||
.nav-logo {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.small-list-item {
|
||||
height: var(--sidebar-menu-item-height);
|
||||
}
|
||||
|
||||
.no-text-decoration {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.no-background:before {
|
||||
background-color: rgba(255, 255, 255, 0) !important;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.active-nav-item {
|
||||
border-right: 4px solid var(--v-primary-base);
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.menu-item-title {
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.nav-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
:key="navigationStyle"
|
||||
@ -59,78 +16,27 @@
|
||||
v-if="isMobile"
|
||||
router
|
||||
to="/"
|
||||
:class="
|
||||
'sidebar-logo no-text-decoration no-background no-border ' +
|
||||
(navigationStyle === 'iconsOnly' ? 'pa-0 justify-center' : '')
|
||||
"
|
||||
:class="mobileLogoClass"
|
||||
:style="'height: ' + topbarHeight + 'px'"
|
||||
:ripple="false">
|
||||
<template v-if="sidebarLogo">
|
||||
<img :src="sidebarLogo" :style="logoCssVars" class="nav-logo" alt="Logo" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<mainsail-logo
|
||||
:color="logoColor"
|
||||
:style="logoCssVars"
|
||||
class="nav-logo"
|
||||
:ripple="false"></mainsail-logo>
|
||||
<mainsail-logo :color="logoColor" :style="logoCssVars" class="nav-logo" :ripple="false" />
|
||||
</template>
|
||||
<template v-if="navigationStyle !== 'iconsOnly'">
|
||||
<span class="text-h6 font-weight-regular text-truncate">{{ printerName }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<template v-if="countPrinters">
|
||||
<v-tooltip right :open-delay="500" :disabled="navigationStyle !== 'iconsOnly'">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-list-item
|
||||
router
|
||||
to="/allPrinters"
|
||||
class="small-list-item mt-1"
|
||||
v-bind="attrs"
|
||||
v-on="on">
|
||||
<v-list-item-icon class="my-3 mr-3 menu-item-icon">
|
||||
<v-icon>{{ mdiViewDashboardOutline }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title tile class="menu-item-title">
|
||||
{{ $t('App.Printers') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>{{ $t('App.Printers') }}</span>
|
||||
</v-tooltip>
|
||||
<v-divider class="my-1"></v-divider>
|
||||
</template>
|
||||
<div v-for="(category, index) in naviPoints" :key="index">
|
||||
<v-tooltip right :open-delay="500" :disabled="navigationStyle !== 'iconsOnly'">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-list-item
|
||||
router
|
||||
:to="category.path"
|
||||
class="small-list-item"
|
||||
v-bind="attrs"
|
||||
v-on="on">
|
||||
<v-list-item-icon class="my-3 mr-3 menu-item-icon">
|
||||
<v-icon>{{ category.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title tile class="menu-item-title">
|
||||
{{ $t(`Router.${category.title}`) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>{{ $t(`Router.${category.title}`) }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<sidebar-item v-for="(category, index) in visibleNaviPoints" :key="index" :item="category" />
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</overlay-scrollbars>
|
||||
<template #append>
|
||||
<v-list-item class="small-list-item mb-2">
|
||||
<v-list-item-icon class="menu-item-icon">
|
||||
<about-dialog></about-dialog>
|
||||
<about-dialog />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@ -139,32 +45,27 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component'
|
||||
import routes, { AppRoute } from '@/routes'
|
||||
import { Mixins } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { PrinterStateKlipperConfig } from '@/store/printer/types'
|
||||
import TheSelectPrinterDialog from '@/components/TheSelectPrinterDialog.vue'
|
||||
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
|
||||
import { navigationWidth, topbarHeight } from '@/store/variables'
|
||||
import MainsailLogo from '@/components/ui/MainsailLogo.vue'
|
||||
import { mdiViewDashboardOutline } from '@mdi/js'
|
||||
import SidebarItem from '@/components/ui/SidebarItem.vue'
|
||||
import NavigationMixin from '@/components/mixins/navigation'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SidebarItem,
|
||||
TheSelectPrinterDialog,
|
||||
AboutDialog,
|
||||
MainsailLogo,
|
||||
},
|
||||
})
|
||||
export default class TheSidebar extends Mixins(BaseMixin) {
|
||||
export default class TheSidebar extends Mixins(NavigationMixin, BaseMixin) {
|
||||
navigationWidth = navigationWidth
|
||||
topbarHeight = topbarHeight
|
||||
|
||||
/**
|
||||
* Icons
|
||||
*/
|
||||
mdiViewDashboardOutline = mdiViewDashboardOutline
|
||||
|
||||
get naviDrawer(): boolean {
|
||||
return this.$store.state.naviDrawer
|
||||
}
|
||||
@ -181,45 +82,12 @@ export default class TheSidebar extends Mixins(BaseMixin) {
|
||||
return this.$store.getters['files/getSidebarBackground']
|
||||
}
|
||||
|
||||
get naviPoints(): AppRoute[] {
|
||||
return routes.filter((element) => {
|
||||
return element.showInNavi && this.showInNavi(element)
|
||||
})
|
||||
}
|
||||
|
||||
get klippy_state(): string {
|
||||
return this.$store.state.server.klippy_state
|
||||
}
|
||||
|
||||
get boolNaviWebcam(): boolean {
|
||||
return this.$store.state.gui.uiSettings.boolWebcamNavi
|
||||
}
|
||||
|
||||
get moonrakerComponents(): string[] {
|
||||
return this.$store.state.server.components
|
||||
}
|
||||
|
||||
get registeredDirectories(): string[] {
|
||||
return this.$store.state.server.registered_directories
|
||||
}
|
||||
|
||||
get klipperConfigfileSettings(): PrinterStateKlipperConfig[] {
|
||||
return this.$store.state.printer.configfile?.settings ?? {}
|
||||
}
|
||||
|
||||
get currentPage(): string {
|
||||
return this.$route.fullPath
|
||||
}
|
||||
|
||||
get countPrinters() {
|
||||
return this.$store.getters['farm/countPrinters']
|
||||
}
|
||||
|
||||
get boolNaviTemp(): boolean {
|
||||
if (!this.isMobile && this.$vuetify.breakpoint.mdAndDown) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return !this.isMobile && this.$vuetify.breakpoint.mdAndDown
|
||||
}
|
||||
|
||||
get sidebarCssVars(): any {
|
||||
@ -255,16 +123,15 @@ export default class TheSidebar extends Mixins(BaseMixin) {
|
||||
return {}
|
||||
}
|
||||
|
||||
showInNavi(route: AppRoute): boolean {
|
||||
if (['shutdown', 'error', 'disconnected'].includes(this.klippy_state) && !route.alwaysShow) return false
|
||||
else if (route.title === 'Webcam' && !this.boolNaviWebcam) return false
|
||||
else if (route.moonrakerComponent && !this.moonrakerComponents.includes(route.moonrakerComponent)) return false
|
||||
else if (route.registeredDirectory && !this.registeredDirectories.includes(route.registeredDirectory))
|
||||
return false
|
||||
else if (route.klipperComponent && !(route.klipperComponent in this.klipperConfigfileSettings)) return false
|
||||
else if (route.klipperIsConnected && !this.klippyIsConnected) return false
|
||||
get mobileLogoClass() {
|
||||
const output = ['sidebar-logo', 'no-text-decoration', 'no-background', 'no-border']
|
||||
|
||||
return true
|
||||
if (this.navigationStyle === 'iconsOnly') {
|
||||
output.push('pa-0')
|
||||
output.push('justify-center')
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@ -272,3 +139,30 @@ export default class TheSidebar extends Mixins(BaseMixin) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-text-decoration {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.no-background:before {
|
||||
background-color: rgba(255, 255, 255, 0) !important;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.nav-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
172
src/components/mixins/navigation.ts
Normal file
172
src/components/mixins/navigation.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import Component from 'vue-class-component'
|
||||
import routes, { AppRoute } from '@/routes'
|
||||
import { Mixins, Watch } from 'vue-property-decorator'
|
||||
import { mdiLinkVariant, mdiViewDashboardOutline } from '@mdi/js'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { PrinterStateKlipperConfig } from '@/store/printer/types'
|
||||
import { GuiNavigationStateEntry } from '@/store/gui/navigation/types'
|
||||
|
||||
export interface NaviPoint {
|
||||
type: 'link' | 'route'
|
||||
title: string
|
||||
to?: string
|
||||
href?: string
|
||||
target?: string
|
||||
icon: string
|
||||
position: number
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class NavigationMixin extends Mixins(BaseMixin) {
|
||||
private customNaviLinks: NaviPoint[] = []
|
||||
|
||||
get countPrinters() {
|
||||
return this.$store.getters['farm/countPrinters']
|
||||
}
|
||||
|
||||
get routesNaviPoints(): NaviPoint[] {
|
||||
const points: NaviPoint[] = []
|
||||
|
||||
if (this.countPrinters) {
|
||||
points.push({
|
||||
title: this.$t('App.Printers'),
|
||||
icon: mdiViewDashboardOutline,
|
||||
to: '/allPrinters',
|
||||
position: 0,
|
||||
} as NaviPoint)
|
||||
}
|
||||
|
||||
routes
|
||||
.filter((element) => {
|
||||
return element.showInNavi && this.showInNavi(element)
|
||||
})
|
||||
.forEach((element) => {
|
||||
const [position, visible] = this.getUiSettings({
|
||||
type: 'route',
|
||||
title: element.title ?? 'unknown',
|
||||
visible: true,
|
||||
position: element.position ?? 999,
|
||||
})
|
||||
|
||||
points.push({
|
||||
type: 'route',
|
||||
title: this.$t(`Router.${element.title}`),
|
||||
icon: element.icon,
|
||||
to: element.path,
|
||||
position,
|
||||
visible,
|
||||
} as NaviPoint)
|
||||
})
|
||||
|
||||
if (this.customNaviLinks.length) {
|
||||
this.customNaviLinks.forEach((element) => {
|
||||
const [position, visible] = this.getUiSettings({
|
||||
type: 'link',
|
||||
title: element.title ?? 'unknown',
|
||||
visible: element.visible ?? true,
|
||||
position: element.position ?? 999,
|
||||
})
|
||||
|
||||
points.push({
|
||||
type: 'link',
|
||||
title: element.title,
|
||||
icon: element.icon,
|
||||
href: element.href,
|
||||
target: element.target,
|
||||
position,
|
||||
visible,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
get naviPoints(): NaviPoint[] {
|
||||
return this.routesNaviPoints.sort((a, b) => a.position - b.position)
|
||||
}
|
||||
|
||||
get visibleNaviPoints(): NaviPoint[] {
|
||||
return this.naviPoints.filter((entry) => entry.visible)
|
||||
}
|
||||
|
||||
get uiSettings(): GuiNavigationStateEntry[] {
|
||||
return this.$store.state.gui.navigation.entries
|
||||
}
|
||||
|
||||
get klippy_state(): string {
|
||||
return this.$store.state.server.klippy_state
|
||||
}
|
||||
|
||||
get boolNaviWebcam(): boolean {
|
||||
return this.$store.state.gui.uiSettings.boolWebcamNavi
|
||||
}
|
||||
|
||||
get moonrakerComponents(): string[] {
|
||||
return this.$store.state.server.components
|
||||
}
|
||||
|
||||
get registeredDirectories(): string[] {
|
||||
return this.$store.state.server.registered_directories
|
||||
}
|
||||
|
||||
get klipperConfigfileSettings(): PrinterStateKlipperConfig[] {
|
||||
return this.$store.state.printer.configfile?.settings ?? {}
|
||||
}
|
||||
|
||||
get sidebarNaviFile(): string {
|
||||
return this.$store.getters['files/getCustomNaviPoints']
|
||||
}
|
||||
|
||||
get webcamCount(): number {
|
||||
return this.$store.getters['gui/webcams/getWebcams'].length
|
||||
}
|
||||
|
||||
@Watch('sidebarNaviFile', { immediate: true })
|
||||
async sidebarNaviFileChanged(newVal: string) {
|
||||
this.customNaviLinks = []
|
||||
|
||||
// stop if no file is set
|
||||
if (!newVal) return
|
||||
|
||||
const content = await fetch(newVal)
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
window.console.error('Unable to parse .theme/navi.json.')
|
||||
throw err
|
||||
})
|
||||
|
||||
content.forEach((item: NaviPoint) => {
|
||||
this.customNaviLinks.push({
|
||||
title: item.title ?? 'Unknown',
|
||||
icon: item.icon ?? mdiLinkVariant,
|
||||
href: item.href ?? '#',
|
||||
target: item.target ?? undefined,
|
||||
position: item.position ?? 999,
|
||||
} as NaviPoint)
|
||||
})
|
||||
}
|
||||
|
||||
showInNavi(route: AppRoute): boolean {
|
||||
if (['shutdown', 'error', 'disconnected'].includes(this.klippy_state) && !route.alwaysShow) return false
|
||||
else if (route.title === 'Webcam' && this.webcamCount === 0) return false
|
||||
else if (route.moonrakerComponent && !this.moonrakerComponents.includes(route.moonrakerComponent)) return false
|
||||
else if (route.registeredDirectory && !this.registeredDirectories.includes(route.registeredDirectory))
|
||||
return false
|
||||
else if (route.klipperComponent && !(route.klipperComponent in this.klipperConfigfileSettings)) return false
|
||||
else if (route.klipperIsConnected && !this.klippyIsConnected) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getUiSettings(entry: GuiNavigationStateEntry): [number, boolean] {
|
||||
const index = this.uiSettings.findIndex((point) => {
|
||||
return point.title === entry.title && point.type === entry.type
|
||||
})
|
||||
|
||||
if (index === -1) return [entry.position, entry.visible]
|
||||
|
||||
return [this.uiSettings[index].position, this.uiSettings[index].visible]
|
||||
}
|
||||
}
|
53
src/components/settings/SettingsNavigationTab.vue
Normal file
53
src/components/settings/SettingsNavigationTab.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<h3 class="text-h5 mb-3">{{ $t('Settings.NavigationTab.Navigation') }}</h3>
|
||||
<draggable v-model="sortableNaviPoints" handle=".handle" ghost-class="ghost" group="navigation-points">
|
||||
<settings-navigation-tab-item
|
||||
v-for="(naviPoint, index) in sortableNaviPoints"
|
||||
:key="index"
|
||||
class="dragable-item my-2 mx-0"
|
||||
:navi-point="naviPoint" />
|
||||
</draggable>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import NavigationMixin, { NaviPoint } from '@/components/mixins/navigation'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import SettingsNavigationTabItem from '@/components/settings/SettingsNavigationTabItem.vue'
|
||||
|
||||
@Component({
|
||||
components: { SettingsNavigationTabItem, SettingsRow, draggable },
|
||||
})
|
||||
export default class SettingsNavigationTab extends Mixins(NavigationMixin, BaseMixin) {
|
||||
get sortableNaviPoints() {
|
||||
return this.naviPoints.filter((naviPoint) => naviPoint.position > 0)
|
||||
}
|
||||
|
||||
set sortableNaviPoints(newVal: NaviPoint[]) {
|
||||
// update store with new positions
|
||||
newVal.forEach((naviPoint, index) => {
|
||||
this.$store.dispatch('gui/navigation/updatePos', {
|
||||
type: naviPoint.type,
|
||||
title: naviPoint.title,
|
||||
visible: naviPoint.visible,
|
||||
position: index + 1,
|
||||
})
|
||||
})
|
||||
|
||||
// upload to moonraker db
|
||||
this.$store.dispatch('gui/navigation/upload')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dragable-item {
|
||||
background-color: #282828;
|
||||
}
|
||||
</style>
|
62
src/components/settings/SettingsNavigationTabItem.vue
Normal file
62
src/components/settings/SettingsNavigationTabItem.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<v-row class="dragable-item my-2 mx-0">
|
||||
<v-col class="col-auto pr-0 d-flex py-2">
|
||||
<v-icon class="handle">{{ mdiDragVertical }}</v-icon>
|
||||
</v-col>
|
||||
<v-col class="py-2">
|
||||
<settings-row :title="title" :sub-title="subtitle" :dynamic-slot-width="true">
|
||||
<v-icon :color="checkboxColor" v-html="checkboxIcon" @click="changeVisibility" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import NavigationMixin, { NaviPoint } from '@/components/mixins/navigation'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { mdiDragVertical, mdiCheckboxMarked, mdiCheckboxBlankOutline } from '@mdi/js'
|
||||
|
||||
@Component({
|
||||
components: { SettingsRow, draggable },
|
||||
})
|
||||
export default class SettingsNavigationTab extends Mixins(NavigationMixin, BaseMixin) {
|
||||
mdiDragVertical = mdiDragVertical
|
||||
|
||||
@Prop({ type: Object, required: true }) naviPoint!: NaviPoint
|
||||
|
||||
get title() {
|
||||
return this.naviPoint.title
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.naviPoint.type === 'link') return `URL: ${this.naviPoint.href ?? 'Unknown'}`
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
get checkboxColor() {
|
||||
if (this.naviPoint.visible) return 'primary'
|
||||
|
||||
return 'grey lighten-1'
|
||||
}
|
||||
|
||||
get checkboxIcon() {
|
||||
if (this.naviPoint.visible) return mdiCheckboxMarked
|
||||
|
||||
return mdiCheckboxBlankOutline
|
||||
}
|
||||
|
||||
changeVisibility() {
|
||||
this.$store.dispatch('gui/navigation/changeVisibility', this.naviPoint)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dragable-item {
|
||||
background-color: #282828;
|
||||
}
|
||||
</style>
|
@ -65,10 +65,6 @@
|
||||
<v-switch v-model="boolBigThumbnail" hide-details class="mt-0"></v-switch>
|
||||
</settings-row>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<settings-row :title="$t('Settings.UiSettingsTab.ShowWebcamInNavigation').toString()">
|
||||
<v-switch v-model="boolWebcamInNavigation" hide-details class="mt-0"></v-switch>
|
||||
</settings-row>
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<settings-row
|
||||
:title="$t('Settings.UiSettingsTab.DisplayCANCEL_PRINT').toString()"
|
||||
:sub-title="$t('Settings.UiSettingsTab.DisplayCANCEL_PRINTDescription').toString()"
|
||||
@ -215,14 +211,6 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin) {
|
||||
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.boolBigThumbnail', value: newVal })
|
||||
}
|
||||
|
||||
get boolWebcamInNavigation() {
|
||||
return this.$store.state.gui.uiSettings.boolWebcamNavi ?? false
|
||||
}
|
||||
|
||||
set boolWebcamInNavigation(newVal) {
|
||||
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.boolWebcamNavi', value: newVal })
|
||||
}
|
||||
|
||||
get displayCancelPrint() {
|
||||
return this.$store.state.gui.uiSettings.displayCancelPrint
|
||||
}
|
||||
|
89
src/components/ui/SidebarItem.vue
Normal file
89
src/components/ui/SidebarItem.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip right :open-delay="500" :disabled="navigationStyle !== 'iconsOnly'">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-list-item
|
||||
router
|
||||
:to="to"
|
||||
:href="href"
|
||||
:target="target"
|
||||
class="small-list-item"
|
||||
v-bind="attrs"
|
||||
v-on="on">
|
||||
<v-list-item-icon class="my-3 mr-3 menu-item-icon">
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title tile class="menu-item-title">
|
||||
{{ title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<span>{{ title }}</span>
|
||||
</v-tooltip>
|
||||
<v-divider v-if="borderBottom" class="my-1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Component from 'vue-class-component'
|
||||
import { Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { NaviPoint } from '@/components/mixins/navigation'
|
||||
|
||||
@Component
|
||||
export default class SidebarItem extends Mixins(BaseMixin) {
|
||||
@Prop({ type: Object, required: true }) item!: NaviPoint
|
||||
|
||||
get navigationStyle() {
|
||||
return this.$store.state.gui.uiSettings.navigationStyle
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.item.icon
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.item.title
|
||||
}
|
||||
|
||||
get to() {
|
||||
return this.item.to ?? undefined
|
||||
}
|
||||
|
||||
get href() {
|
||||
return this.item.href ?? undefined
|
||||
}
|
||||
|
||||
get target() {
|
||||
return this.item.target ?? undefined
|
||||
}
|
||||
|
||||
get borderBottom() {
|
||||
return this.item.to === '/allPrinters'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.small-list-item {
|
||||
height: var(--sidebar-menu-item-height);
|
||||
}
|
||||
|
||||
.active-nav-item {
|
||||
border-right: 4px solid var(--v-primary-base);
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.menu-item-title {
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
</style>
|
@ -911,6 +911,9 @@
|
||||
"UnableToLoadLight": "Unable to load light",
|
||||
"UnableToLoadPreset": "Unable to load preset"
|
||||
},
|
||||
"NavigationTab": {
|
||||
"Navigation": "Navigation"
|
||||
},
|
||||
"PresetsTab": {
|
||||
"AddPreset": "add preset",
|
||||
"Cooldown": "Cooldown",
|
||||
@ -1036,7 +1039,6 @@
|
||||
"PowerDeviceName": "Printer power device",
|
||||
"PowerDeviceNameDescription": "Select which Moonraker power device should be used to power on the printer.",
|
||||
"Primary": "Primary",
|
||||
"ShowWebcamInNavigation": "Show Webcam in navigation",
|
||||
"UiSettings": "UI-Settings"
|
||||
},
|
||||
"Update": "update",
|
||||
|
@ -28,6 +28,7 @@ const routes: AppRoute[] = [
|
||||
component: Dashboard,
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
position: 10,
|
||||
},
|
||||
{
|
||||
title: 'Printers',
|
||||
@ -43,6 +44,7 @@ const routes: AppRoute[] = [
|
||||
component: Webcam,
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
position: 20,
|
||||
},
|
||||
{
|
||||
title: 'Console',
|
||||
@ -52,6 +54,7 @@ const routes: AppRoute[] = [
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
klipperIsConnected: true,
|
||||
position: 30,
|
||||
},
|
||||
{
|
||||
title: 'Heightmap',
|
||||
@ -61,6 +64,7 @@ const routes: AppRoute[] = [
|
||||
alwaysShow: false,
|
||||
showInNavi: true,
|
||||
klipperComponent: 'bed_mesh',
|
||||
position: 40,
|
||||
},
|
||||
{
|
||||
title: 'G-Code Files',
|
||||
@ -70,6 +74,7 @@ const routes: AppRoute[] = [
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
registeredDirectory: 'gcodes',
|
||||
position: 50,
|
||||
},
|
||||
{
|
||||
title: 'G-Code Viewer',
|
||||
@ -78,6 +83,7 @@ const routes: AppRoute[] = [
|
||||
component: () => import('../pages/Viewer.vue'),
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
position: 60,
|
||||
},
|
||||
{
|
||||
title: 'History',
|
||||
@ -87,6 +93,7 @@ const routes: AppRoute[] = [
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
moonrakerComponent: 'history',
|
||||
position: 70,
|
||||
},
|
||||
{
|
||||
title: 'Timelapse',
|
||||
@ -96,6 +103,7 @@ const routes: AppRoute[] = [
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
moonrakerComponent: 'timelapse',
|
||||
position: 80,
|
||||
},
|
||||
{
|
||||
title: 'Machine',
|
||||
@ -104,6 +112,7 @@ const routes: AppRoute[] = [
|
||||
component: Machine,
|
||||
alwaysShow: true,
|
||||
showInNavi: true,
|
||||
position: 90,
|
||||
},
|
||||
{
|
||||
title: null,
|
||||
@ -130,4 +139,5 @@ export interface AppRoute {
|
||||
klipperComponent?: string
|
||||
klipperIsConnected?: boolean
|
||||
children?: AppRoute[]
|
||||
position?: number
|
||||
}
|
||||
|
@ -199,11 +199,14 @@ export const getters: GetterTree<FileState, any> = {
|
||||
|
||||
const file = directory?.childrens?.find(
|
||||
(element: FileStateFile) =>
|
||||
element.filename !== undefined &&
|
||||
element.filename.substr(0, element.filename.lastIndexOf('.')) === acceptName &&
|
||||
acceptExtensions.includes(element.filename.substr(element.filename.lastIndexOf('.') + 1))
|
||||
element.filename?.slice(0, element.filename?.lastIndexOf('.')) === acceptName &&
|
||||
acceptExtensions.includes(element.filename?.slice(element.filename?.lastIndexOf('.') + 1))
|
||||
)
|
||||
return file ? rootGetters['socket/getUrl'] + '/server/files/config/' + themeDir + '/' + file.filename : null
|
||||
if (!file) return null
|
||||
|
||||
return `${rootGetters['socket/getUrl']}/server/files/config/${themeDir}/${
|
||||
file.filename
|
||||
}?timestamp=${file.modified.getTime()}`
|
||||
},
|
||||
|
||||
getSidebarLogo: (state, getters) => {
|
||||
@ -234,6 +237,13 @@ export const getters: GetterTree<FileState, any> = {
|
||||
return getters['getThemeFileUrl'](acceptName, acceptExtensions) ?? null
|
||||
},
|
||||
|
||||
getCustomNaviPoints: (state, getters) => {
|
||||
const acceptName = 'navi'
|
||||
const acceptExtensions = ['json']
|
||||
|
||||
return getters['getThemeFileUrl'](acceptName, acceptExtensions) ?? null
|
||||
},
|
||||
|
||||
getCustomFavicons: (state, getters) => {
|
||||
const acceptName16 = 'favicon-32x32'
|
||||
const acceptName32 = 'favicon-32x32'
|
||||
|
@ -10,10 +10,11 @@ import { console } from '@/store/gui/console'
|
||||
import { gcodehistory } from '@/store/gui/gcodehistory'
|
||||
import { macros } from '@/store/gui/macros'
|
||||
import { miscellaneous } from '@/store/gui/miscellaneous'
|
||||
import { navigation } from '@/store/gui/navigation'
|
||||
import { notifications } from '@/store/gui/notifications'
|
||||
import { presets } from '@/store/gui/presets'
|
||||
import { remoteprinters } from '@/store/gui/remoteprinters'
|
||||
import { webcams } from '@/store/gui/webcams'
|
||||
import { notifications } from '@/store/gui/notifications'
|
||||
|
||||
export const getDefaultState = (): GuiState => {
|
||||
return {
|
||||
@ -141,6 +142,9 @@ export const getDefaultState = (): GuiState => {
|
||||
showGCodePanel: false,
|
||||
cncMode: false,
|
||||
},
|
||||
navigation: {
|
||||
entries: [],
|
||||
},
|
||||
uiSettings: {
|
||||
logo: defaultLogoColor,
|
||||
primary: defaultPrimaryColor,
|
||||
@ -152,7 +156,6 @@ export const getDefaultState = (): GuiState => {
|
||||
boolBigThumbnail: true,
|
||||
boolWideNavDrawer: false,
|
||||
boolHideUploadAndPrintButton: false,
|
||||
boolWebcamNavi: false,
|
||||
navigationStyle: 'iconsAndText',
|
||||
powerDeviceName: null,
|
||||
hideSaveConfigForBedMash: false,
|
||||
@ -263,6 +266,7 @@ export const gui: Module<GuiState, any> = {
|
||||
gcodehistory,
|
||||
macros,
|
||||
miscellaneous,
|
||||
navigation,
|
||||
notifications,
|
||||
presets,
|
||||
remoteprinters,
|
||||
|
27
src/store/gui/navigation/actions.ts
Normal file
27
src/store/gui/navigation/actions.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ActionTree } from 'vuex'
|
||||
import { RootState } from '@/store/types'
|
||||
import Vue from 'vue'
|
||||
import { GuiNavigationState, GuiNavigationStateEntry } from '@/store/gui/navigation/types'
|
||||
|
||||
export const actions: ActionTree<GuiNavigationState, RootState> = {
|
||||
reset({ commit }) {
|
||||
commit('reset')
|
||||
},
|
||||
|
||||
upload({ state }) {
|
||||
Vue.$socket.emit('server.database.post_item', {
|
||||
namespace: 'mainsail',
|
||||
key: 'navigation.entries',
|
||||
value: state.entries,
|
||||
})
|
||||
},
|
||||
|
||||
updatePos({ commit }, payload: GuiNavigationStateEntry) {
|
||||
commit('updatePos', payload)
|
||||
},
|
||||
|
||||
changeVisibility({ commit, dispatch }, payload: GuiNavigationStateEntry) {
|
||||
commit('changeVisibility', payload)
|
||||
dispatch('upload')
|
||||
},
|
||||
}
|
5
src/store/gui/navigation/getters.ts
Normal file
5
src/store/gui/navigation/getters.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { GetterTree } from 'vuex'
|
||||
import { GuiNavigationState } from './types'
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const getters: GetterTree<GuiNavigationState, any> = {}
|
23
src/store/gui/navigation/index.ts
Normal file
23
src/store/gui/navigation/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Module } from 'vuex'
|
||||
import { actions } from '@/store/gui/navigation/actions'
|
||||
import { mutations } from '@/store/gui/navigation/mutations'
|
||||
import { getters } from '@/store/gui/navigation/getters'
|
||||
import { GuiNavigationState } from '@/store/gui/navigation/types'
|
||||
|
||||
export const getDefaultState = (): GuiNavigationState => {
|
||||
return {
|
||||
entries: [],
|
||||
}
|
||||
}
|
||||
|
||||
// initial state
|
||||
const state = getDefaultState()
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const navigation: Module<GuiNavigationState, any> = {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
}
|
62
src/store/gui/navigation/mutations.ts
Normal file
62
src/store/gui/navigation/mutations.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { getDefaultState } from './index'
|
||||
import { MutationTree } from 'vuex'
|
||||
import { GuiNavigationState, GuiNavigationStateEntry } from './types'
|
||||
import Vue from 'vue'
|
||||
|
||||
export const mutations: MutationTree<GuiNavigationState> = {
|
||||
reset(state) {
|
||||
Object.assign(state, getDefaultState())
|
||||
},
|
||||
|
||||
updatePos(state, payload: GuiNavigationStateEntry) {
|
||||
const index = state.entries.findIndex((entry) => {
|
||||
return entry.type === payload.type && entry.title === payload.title
|
||||
})
|
||||
|
||||
// update existing entry
|
||||
if (index !== -1) {
|
||||
state.entries[index].position = payload.position
|
||||
return
|
||||
}
|
||||
|
||||
// create new entry
|
||||
const newEntry: GuiNavigationStateEntry = {
|
||||
type: payload.type,
|
||||
title: payload.title,
|
||||
visible: payload.visible,
|
||||
position: payload.position,
|
||||
}
|
||||
// copy old array
|
||||
const entries = [...state.entries]
|
||||
// add new entry
|
||||
entries.push(newEntry)
|
||||
// set new array
|
||||
Vue.set(state, 'entries', entries)
|
||||
},
|
||||
|
||||
changeVisibility(state, payload: GuiNavigationStateEntry) {
|
||||
const index = state.entries.findIndex((entry) => {
|
||||
return entry.type === payload.type && entry.title === payload.title
|
||||
})
|
||||
|
||||
// update existing entry
|
||||
if (index !== -1) {
|
||||
state.entries[index].visible = !payload.visible
|
||||
return
|
||||
}
|
||||
|
||||
// create new entry
|
||||
const newEntry: GuiNavigationStateEntry = {
|
||||
type: payload.type,
|
||||
title: payload.title,
|
||||
visible: !payload.visible,
|
||||
position: payload.position,
|
||||
}
|
||||
// copy old array
|
||||
const entries = [...state.entries]
|
||||
// add new entry
|
||||
entries.push(newEntry)
|
||||
// set new array
|
||||
Vue.set(state, 'entries', entries)
|
||||
},
|
||||
}
|
10
src/store/gui/navigation/types.ts
Normal file
10
src/store/gui/navigation/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface GuiNavigationState {
|
||||
entries: GuiNavigationStateEntry[]
|
||||
}
|
||||
|
||||
export interface GuiNavigationStateEntry {
|
||||
type: 'route' | 'link'
|
||||
title: string
|
||||
visible: boolean
|
||||
position: number
|
||||
}
|
@ -5,6 +5,7 @@ import { GuiRemoteprintersState } from '@/store/gui/remoteprinters/types'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { GuiNotificationState } from '@/store/gui/notifications/types'
|
||||
import { FileStateFile, FileStateGcodefile } from '@/store/files/types'
|
||||
import { GuiNavigationState } from '@/store/gui/navigation/types'
|
||||
|
||||
export interface GuiState {
|
||||
general: {
|
||||
@ -90,6 +91,7 @@ export interface GuiState {
|
||||
cncMode: boolean
|
||||
}
|
||||
macros?: GuiMacrosState
|
||||
navigation: GuiNavigationState
|
||||
notifications?: GuiNotificationState
|
||||
presets?: GuiPresetsState
|
||||
remoteprinters?: GuiRemoteprintersState
|
||||
@ -104,7 +106,6 @@ export interface GuiState {
|
||||
boolBigThumbnail: boolean
|
||||
boolWideNavDrawer: boolean
|
||||
boolHideUploadAndPrintButton: boolean
|
||||
boolWebcamNavi: boolean
|
||||
navigationStyle: 'iconsAndText' | 'iconsOnly'
|
||||
powerDeviceName: string | null
|
||||
hideSaveConfigForBedMash: boolean
|
||||
|
Loading…
x
Reference in New Issue
Block a user