feat: notifications (#738)

Co-authored-by: th33xitus <th33xitus@googlemail.com>
This commit is contained in:
Stefan Dej 2022-04-10 23:20:19 +02:00 committed by GitHub
parent e2caa99f0b
commit d830493acc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1037 additions and 405 deletions

View File

@ -41,7 +41,7 @@
<the-connecting-dialog v-else></the-connecting-dialog>
<the-update-dialog></the-update-dialog>
<the-editor></the-editor>
<the-timelapse-rendering-snackbar>-</the-timelapse-rendering-snackbar>
<the-timelapse-rendering-snackbar></the-timelapse-rendering-snackbar>
</v-app>
</template>

View File

@ -1,96 +0,0 @@
<style scoped></style>
<template>
<v-menu
v-if="throttledStateFlags.length"
v-model="showMenu"
bottom
:offset-y="true"
:close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn :color="currentFlags.length ? 'error' : 'warning'" icon tile class="mr-3" v-bind="attrs" v-on="on">
<v-icon>{{ mdiRaspberryPi }}</v-icon>
</v-btn>
</template>
<v-list min-width="300" max-width="600">
<template v-if="currentFlags.length">
<v-subheader class="" style="height: auto">
{{ $t('App.ThrottledStates.HeadlineCurrentFlags') }}
</v-subheader>
<v-list-item v-for="flag in currentFlags" :key="flag" two-line>
<v-list-item-content class="py-0">
<v-list-item-title>{{ $t(`App.ThrottledStates.Title${convertName(flag)}`) }}</v-list-item-title>
<v-list-item-subtitle class="text-wrap">
{{ $t(`App.ThrottledStates.Description${convertName(flag)}`) }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
<template v-if="previouslyFlags.length">
<v-divider v-if="currentFlags.length" class="my-2"></v-divider>
<v-subheader class="" style="height: auto">
{{ $t('App.ThrottledStates.HeadlinePreviouslyFlags') }}
</v-subheader>
<v-list-item v-for="flag in previouslyFlags" :key="flag" two-line>
<v-list-item-content class="py-0">
<v-list-item-title>{{ $t(`App.ThrottledStates.Title${convertName(flag)}`) }}</v-list-item-title>
<v-list-item-subtitle class="text-wrap">
{{ $t(`App.ThrottledStates.Description${convertName(flag)}`) }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from './mixins/base'
import { mdiRaspberryPi } from '@mdi/js'
@Component
export default class TheThrottledStates extends Mixins(BaseMixin) {
mdiRaspberryPi = mdiRaspberryPi
private showMenu = false
get throttledStateFlags() {
return this.$store.state.server.throttled_state.flags.filter((flag: string) => {
return flag !== '?'
})
/*return [
'Under-Voltage Detected',
'Frequency Capped',
'Currently Throttled',
'Temperature Limit Active',
'Previously Under-Volted',
'Previously Frequency Capped',
'Previously Throttled',
'Previously Temperature Limited'
]*/
}
get currentFlags() {
return this.throttledStateFlags.filter((flag: string) => {
return !flag.startsWith('Previously ')
})
}
get previouslyFlags() {
return this.throttledStateFlags.filter((flag: string) => {
return flag.startsWith('Previously ')
})
}
convertName(flag: string): string {
flag = flag.replace(/ /g, '').replace(/-/g, '')
flag = flag.charAt(0).toUpperCase() + flag.slice(1)
return flag
}
}
</script>

View File

@ -47,7 +47,6 @@
<v-toolbar-title class="text-no-wrap ml-0 pl-2 mr-2">{{ printerName }}</v-toolbar-title>
<printer-selector v-if="countPrinters"></printer-selector>
<v-spacer></v-spacer>
<the-throttled-states></the-throttled-states>
<input
ref="fileUploadAndStart"
type="file"
@ -95,6 +94,7 @@
<v-icon class="mr-md-2">{{ mdiAlertCircleOutline }}</v-icon>
<span class="d-none d-md-inline">{{ $t('App.TopBar.EmergencyStop') }}</span>
</v-btn>
<the-notification-menu></the-notification-menu>
<the-settings-menu></the-settings-menu>
<the-top-corner-menu></the-top-corner-menu>
</v-app-bar>
@ -142,10 +142,10 @@ import axios from 'axios'
import { formatFilesize } from '@/plugins/helpers'
import TheTopCornerMenu from '@/components/TheTopCornerMenu.vue'
import TheSettingsMenu from '@/components/TheSettingsMenu.vue'
import TheThrottledStates from '@/components/TheThrottledStates.vue'
import Panel from '@/components/ui/Panel.vue'
import PrinterSelector from '@/components/ui/PrinterSelector.vue'
import MainsailLogo from '@/components/ui/MainsailLogo.vue'
import TheNotificationMenu from '@/components/notifications/TheNotificationMenu.vue'
import { topbarHeight } from '@/store/variables'
import { mdiAlertCircleOutline, mdiContentSave, mdiFileUpload, mdiClose, mdiCloseThick } from '@mdi/js'
@ -165,11 +165,11 @@ type uploadSnackbar = {
@Component({
components: {
Panel,
TheThrottledStates,
TheSettingsMenu,
TheTopCornerMenu,
PrinterSelector,
MainsailLogo,
TheNotificationMenu,
},
})
export default class TheTopbar extends Mixins(BaseMixin) {

View File

@ -40,7 +40,8 @@ export default class BaseMixin extends Vue {
}
get printer_state(): string {
const printer_state = this.$store.state.printer.print_stats?.state ?? ''
const printer_state =
this.$store.state.printer.print_stats?.state ?? this.$store.state.printer.idle_timeout?.state ?? ''
const timelapse_pause = this.$store.state.printer['gcode_macro TIMELAPSE_TAKE_FRAME']?.is_paused ?? false
return printer_state === 'paused' && timelapse_pause ? 'printing' : printer_state
}

View File

@ -0,0 +1,156 @@
<template>
<v-alert text :color="alertColor" border="left">
<v-row align="start">
<v-col class="grow">
<div class="notification-menu-entry__headline mb-1 text-subtitle-1">
<template v-if="'url' in entry">
<a :class="`text-decoration-none ${alertColor}--text`" :href="entry.url" target="_blank">
<v-icon small :class="`${alertColor}--text pb-1`">
{{ mdiLinkVariant }}
</v-icon>
{{ entry.title }}
</a>
</template>
<template v-else>
<span :class="`${alertColor}--text`">{{ entry.title }}</span>
</template>
</div>
<p class="text-body-2 mb-0 text--disabled font-weight-light" v-html="formatedText"></p>
</v-col>
<v-col
v-if="entry.priority !== 'critical'"
class="shrink pl-0 pb-0 pt-1 pr-2 d-flex flex-column align-self-stretch justify-space-between">
<v-btn v-if="entryType === 'announcement'" icon plain :color="alertColor" class="mb-2" @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
<v-btn v-else icon plain :color="alertColor" class="mb-2" @click="dismiss('reboot', null)">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon plain retain-focus-on-click :color="alertColor" class="pb-1" @click="expand = !expand">
<v-icon>{{ mdiBellOffOutline }}</v-icon>
</v-btn>
</v-col>
</v-row>
<v-row v-if="entry.priority !== 'critical'">
<v-expand-transition>
<div v-show="expand" class="pt-1" style="width: 100%">
<v-divider class="pb-1 ml-2"></v-divider>
<div class="text-right py-1" style="font-size: 0.875rem">
<span class="text--disabled text-caption font-weight-light">
{{ $t('App.Notifications.Remind') }}
</span>
<template v-if="entryType === 'announcement'">
<v-btn
:color="alertColor"
x-small
plain
text
outlined
class="mx-1"
@click="dismiss('time', 60 * 60)">
1H
</v-btn>
<v-btn
:color="alertColor"
x-small
plain
text
outlined
class="mx-1"
@click="dismiss('time', 60 * 60 * 24)">
1D
</v-btn>
<v-btn
:color="alertColor"
x-small
plain
text
outlined
class="mx-1"
@click="dismiss('time', 60 * 60 * 24 * 7)">
7D
</v-btn>
</template>
<template v-else>
<v-btn
:color="alertColor"
x-small
plain
text
outlined
class="mx-1"
@click="dismiss('reboot', null)">
{{ $t('App.Notifications.NextReboot') }}
</v-btn>
<v-btn :color="alertColor" x-small plain text outlined class="mx-1" @click="close">
{{ $t('App.Notifications.Never') }}
</v-btn>
</template>
</div>
</div>
</v-expand-transition>
</v-row>
</v-alert>
</template>
<script lang="ts">
import BaseMixin from '@/components/mixins/base'
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
import { mdiClose, mdiLinkVariant, mdiBellOffOutline } from '@mdi/js'
import { GuiNotificationStateEntry } from '@/store/gui/notifications/types'
@Component({
components: {},
})
export default class NotificationMenuEntry extends Mixins(BaseMixin) {
mdiClose = mdiClose
mdiLinkVariant = mdiLinkVariant
mdiBellOffOutline = mdiBellOffOutline
private expand = false
@Prop({ required: true })
declare readonly entry: GuiNotificationStateEntry
@Prop({ default: true })
declare readonly parentState: boolean
get formatedText() {
return this.entry.description.replace(/\[([^\]]+)\]\(([^)]+)\)/, '<a href="$2" target="_blank">$1</a>')
}
get alertColor() {
if (this.entry.priority === 'critical') return 'error'
if (this.entry.priority === 'high') return 'warning'
return 'info'
}
get entryType() {
const posFirstSlash = this.entry.id.indexOf('/')
if (posFirstSlash === -1) return ''
return this.entry.id.slice(0, posFirstSlash)
}
close() {
this.$store.dispatch('gui/notifications/close', { id: this.entry.id })
}
dismiss(type: 'time' | 'reboot', time: number | null) {
this.$store.dispatch('gui/notifications/dismiss', { id: this.entry.id, type, time })
}
@Watch('parentState')
parentStateUpdate(newVal: boolean) {
if (!newVal) this.expand = false
}
}
</script>
<style scoped>
.notification-menu-entry__headline {
line-height: 1.2;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<v-menu
v-model="boolMenu"
bottom
:left="!isMobile"
offset-y
:close-on-click="true"
:close-on-content-click="false"
origin="center center"
transition="slide-y-transition"
:min-width="isMobile ? '100%' : null">
<template #activator="{ on, attrs }">
<v-btn icon tile class="minwidth-0" v-bind="attrs" v-on="on">
<v-badge
:content="notifications.length <= 9 ? notifications.length : '9+'"
:value="notifications.length > 0"
:color="colorBadge"
overlap>
<v-icon>{{ attrs['aria-expanded'] === 'false' ? mdiBellOutline : mdiBell }}</v-icon>
</v-badge>
</v-btn>
</template>
<v-card flat :min-width="300" :max-width="isMobile ? null : 400">
<template v-if="notifications.length">
<overlay-scrollbars class="announcement-menu__scrollbar">
<v-card-text>
<template v-for="(entry, index) in notifications">
<notification-menu-entry
:key="entry.id"
:entry="entry"
:class="index < notifications.length - 1 ? '' : 'mb-0'"
:parent-state="boolMenu" />
</template>
</v-card-text>
</overlay-scrollbars>
<template v-if="notifications.length > 1">
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="primary" class="mr-2" @click="dismissAll">
<v-icon left>{{ mdiCloseBoxMultipleOutline }}</v-icon>
{{ $t('App.Notifications.DismissAll') }}
</v-btn>
</v-card-actions>
</template>
</template>
<template v-else>
<v-card-text class="text-center">
<span class="text-disabled">{{ $t('App.Notifications.NoNotification') }}</span>
</v-card-text>
</template>
</v-card>
</v-menu>
</template>
<script lang="ts">
import BaseMixin from '@/components/mixins/base'
import { Component, Mixins } from 'vue-property-decorator'
import NotificationMenuEntry from '@/components/notifications/NotificationMenuEntry.vue'
import { mdiBell, mdiBellOutline, mdiCloseBoxMultipleOutline } from '@mdi/js'
import { GuiNotificationStateEntry } from '@/store/gui/notifications/types'
@Component({
components: { NotificationMenuEntry },
})
export default class TheNotificationMenu extends Mixins(BaseMixin) {
mdiBell = mdiBell
mdiBellOutline = mdiBellOutline
mdiCloseBoxMultipleOutline = mdiCloseBoxMultipleOutline
private boolMenu = false
get notifications() {
return this.$store.getters['gui/notifications/getNotifications'] ?? []
}
get existsCriticalAnnouncements() {
return this.notifications.filter((entry: GuiNotificationStateEntry) => entry.priority === 'critical').length > 0
}
get existsHighAnnouncements() {
return this.notifications.filter((entry: GuiNotificationStateEntry) => entry.priority === 'high').length > 0
}
get countNormalAnnouncements() {
return this.notifications.filter((entry: GuiNotificationStateEntry) => entry.priority === 'normal').length
}
get colorBadge() {
if (this.existsCriticalAnnouncements) return 'error'
if (this.existsHighAnnouncements) return 'warning'
return 'primary'
}
dismissAll() {
this.notifications.forEach(async (entry: GuiNotificationStateEntry) => {
if (entry.id.startsWith('announcement')) {
await this.$store.dispatch('gui/notifications/close', { id: entry.id })
} else {
await this.$store.dispatch('gui/notifications/dismiss', { id: entry.id, type: 'reboot', time: null })
}
})
}
}
</script>
<style scoped>
.announcement-menu__scrollbar {
max-height: 500px;
}
</style>

View File

@ -1,44 +0,0 @@
<template>
<panel
v-if="socketIsConnected && dependencies.length"
:icon="mdiAlertCircle"
:title="$tc('Panels.DependenciesPanel.Dependency', dependencies.length) + ' (' + dependencies.length + ')'"
:collapsible="true"
card-class="dependencies-panel"
toolbar-color="orange darken-2">
<v-card-text v-for="(dependency, index) in dependencies" :key="index" :class="index > 0 ? 'py-0' : 'pt-3 pb-0'">
<v-divider v-if="index" class="my-2"></v-divider>
<v-row>
<v-col>
<p class="mb-0 orange--text">
{{
$t('Panels.DependenciesPanel.DependencyDescription', {
name: dependency.serviceName,
installedVersion: dependency.installedVersion,
neededVersion: dependency.neededVersion,
})
}}
</p>
</v-col>
</v-row>
</v-card-text>
<v-card-actions></v-card-actions>
</panel>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import Panel from '@/components/ui/Panel.vue'
import { mdiAlertCircle } from '@mdi/js'
@Component({
components: { Panel },
})
export default class DependenciesPanel extends Mixins(BaseMixin) {
mdiAlertCircle = mdiAlertCircle
get dependencies() {
return this.$store.getters['getDependencies'] ?? []
}
}
</script>

View File

@ -1,81 +0,0 @@
<template>
<panel
v-if="klipperReadyForGui && warnings.length"
:icon="mdiAlertCircle"
:title="$t('Panels.KlipperWarningsPanel.KlipperWarnings') + ' (' + warnings.length + ')'"
:collapsible="true"
card-class="klipper-warnings-panel"
toolbar-color="orange darken-2">
<v-card-text v-for="(warning, index) in warnings" :key="index" :class="index > 0 ? 'py-0' : 'pt-3 pb-0'">
<v-divider v-if="index" class="my-2"></v-divider>
<v-row>
<v-col>
<p v-if="warning.type === 'deprecated_option'" class="orange--text mb-0">
{{
$t('Panels.KlipperWarningsPanel.DeprecatedOption', {
section: warning.section,
option: warning.option,
})
}}
</p>
<p v-else-if="warning.type === 'deprecated_value'" class="orange--text mb-0">
{{
$t('Panels.KlipperWarningsPanel.DeprecatedValue', {
section: warning.section,
option: warning.option,
value: warning.value,
})
}}
</p>
<p v-else class="orange--text mb-0">{{ warning.message }}</p>
</v-col>
<v-col class="col-auto d-flex align-center">
<a :href="getDocsLink(warning)" target="_blank" class="text-decoration-none">
<v-icon>{{ mdiInformation }}</v-icon>
</a>
</v-col>
</v-row>
</v-card-text>
<v-divider class="mt-3"></v-divider>
<v-card-actions class="justify-start">
<v-btn small :href="apiUrl + '/server/files/klipper.log'" target="_blank" class="ml-2 primary--text">
<v-icon class="mr-2" small>{{ mdiDownload }}</v-icon>
{{ $t('Panels.KlipperWarningsPanel.DownloadLog') }}
</v-btn>
</v-card-actions>
</panel>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import BaseMixin from '../mixins/base'
import { Mixins } from 'vue-property-decorator'
import { caseInsensitiveSort } from '@/plugins/helpers'
import Panel from '@/components/ui/Panel.vue'
import { mdiAlertCircle, mdiDownload, mdiInformation } from '@mdi/js'
@Component({
components: { Panel },
})
export default class KlipperWarningsPanelPanel extends Mixins(BaseMixin) {
mdiAlertCircle = mdiAlertCircle
mdiInformation = mdiInformation
mdiDownload = mdiDownload
get warnings() {
let warnings = this.$store.state.printer.configfile?.warnings ?? []
return caseInsensitiveSort(warnings, 'option')
}
getDocsLink(warning: { type: string; option: string; value: string }) {
let url = 'https://docs.mainsail.xyz/faq/klipper_warnings/' + warning.type
if (warning.type === 'deprecated_option' && warning.option.startsWith('default_parameter'))
url += '#default_parameter'
else if (warning.type === 'deprecated_option') url += '#' + warning.option
else if (warning.type === 'deprecated_value') url += '#' + warning.value
return url
}
}
</script>

View File

@ -30,7 +30,7 @@
<v-divider class="mb-2"></v-divider>
</template>
<v-card-actions class="justify-center pb-3">
<v-btn small href="https://docs.mainsail.xyz/necessary-configuration" target="_blank">
<v-btn small href="https://docs.mainsail.xyz/configuration" target="_blank">
<v-icon small class="mr-1">{{ mdiInformation }}</v-icon>
{{ $t('Panels.MinSettingsPanel.MoreInformation') }}
</v-btn>

View File

@ -1,58 +0,0 @@
<template>
<panel
v-if="failedComponents.length || warnings.length"
:icon="mdiAlertCircle"
:title="$t('Panels.MoonrakerStatePanel.MoonrakerWarnings')"
:collapsible="true"
card-class="moonraker-state-panel"
toolbar-color="orange darken-2">
<v-card-text v-if="failedComponents.length">
<v-row>
<v-col>
<p class="orange--text">{{ $t('Panels.MoonrakerStatePanel.MoonrakerErrorInfo') }}</p>
<p class="mb-2 orange--text">{{ $t('Panels.MoonrakerStatePanel.FollowingPluginHasAnError') }}</p>
<ul class="mt-0 pt-0">
<li v-for="component in failedComponents" :key="component" class="orange--text">
<code>{{ component }}</code>
</li>
</ul>
</v-col>
</v-row>
</v-card-text>
<v-divider v-if="failedComponents.length || warnings.length"></v-divider>
<v-card-text v-for="(warning, index) in warnings" :key="warning" :class="index > 0 ? 'py-0' : 'pt-3 pb-0'">
<v-divider v-if="index" class="my-2"></v-divider>
<p class="orange--text mb-0">{{ warning }}</p>
</v-card-text>
<v-divider class="mt-3"></v-divider>
<v-card-actions class="justify-start">
<v-btn small :href="apiUrl + '/server/files/moonraker.log'" target="_blank" class="ml-2 primary--text">
<v-icon class="mr-2" small>{{ mdiDownload }}</v-icon>
{{ $t('Panels.MoonrakerStatePanel.DownloadLog') }}
</v-btn>
</v-card-actions>
</panel>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import BaseMixin from '../mixins/base'
import { Mixins } from 'vue-property-decorator'
import Panel from '@/components/ui/Panel.vue'
import { mdiAlertCircle, mdiDownload } from '@mdi/js'
@Component({
components: { Panel },
})
export default class MoonrakerStatePanel extends Mixins(BaseMixin) {
mdiDownload = mdiDownload
mdiAlertCircle = mdiAlertCircle
get failedComponents() {
return this.$store.state.server.failed_components ?? []
}
get warnings() {
return this.$store.state.server.warnings ?? []
}
}
</script>

View File

@ -6,11 +6,8 @@
<template>
<div>
<dependencies-panel></dependencies-panel>
<min-settings-panel></min-settings-panel>
<moonraker-state-panel></moonraker-state-panel>
<klippy-state-panel></klippy-state-panel>
<klipper-warnings-panel></klipper-warnings-panel>
<panel
v-if="klipperState === 'ready'"
:icon="mdiInformation"
@ -368,11 +365,8 @@ import Component from 'vue-class-component'
import { Mixins, Watch } from 'vue-property-decorator'
import { thumbnailSmallMin, thumbnailSmallMax, thumbnailBigMin } from '@/store/variables'
import BaseMixin from '@/components/mixins/base'
import DependenciesPanel from '@/components/panels/DependenciesPanel.vue'
import MinSettingsPanel from '@/components/panels/MinSettingsPanel.vue'
import MoonrakerStatePanel from '@/components/panels/MoonrakerStatePanel.vue'
import KlippyStatePanel from '@/components/panels/KlippyStatePanel.vue'
import KlipperWarningsPanel from '@/components/panels/KlipperWarningsPanel.vue'
import StatusPanelExcludeObject from '@/components/panels/StatusPanelExcludeObject.vue'
import Panel from '@/components/ui/Panel.vue'
import {
@ -391,11 +385,8 @@ import {
@Component({
components: {
DependenciesPanel,
KlipperWarningsPanel,
KlippyStatePanel,
MinSettingsPanel,
MoonrakerStatePanel,
Panel,
StatusPanelExcludeObject,
},

View File

@ -1,5 +1,11 @@
{
"App": {
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "Funktionen \"{option}\" i sektion \"{section}\" er forældet.",
"DeprecatedValue": "Værdien \"{value}\" i muligheden \"{option}\" i sektion \"{section}\" er forældet."
}
},
"NumberInput": {
"GreaterOrEqualError": "Skal være større eller lig med {min}!",
"MustBeBetweenError": "Skal være mellem {min} og {max}!",
@ -15,8 +21,6 @@
"DescriptionPreviouslyUnderVolted": "rPI strømforsyning faldt til under 4,65V mindst en gang siden sidste opstart.",
"DescriptionTemperatureLimitActive": "rPi uC (3A+/3B+ only) temperatur er i øjeblikket over advarselsgrænsen (standard 60°C).",
"DescriptionUnderVoltageDetected": "rPI strømforsyning i øjeblikket under 4,65V",
"HeadlineCurrentFlags": "\"Lige nu\" varsel",
"HeadlinePreviouslyFlags": "\"Tidligere\" varsel",
"TitleCurrentlyThrottled": "Begrænset i øjeblikket",
"TitleFrequencyCapped": "Maks. frekvens formindsket",
"TitlePreviouslyFrequencyCapped": "Maks. frekvens formindsket tidligere",
@ -438,12 +442,6 @@
"SwitchToPrinter": "Skift til printer",
"WebcamOff": "Sluk"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Funktionen \"{option}\" i sektion \"{section}\" er forældet.",
"DeprecatedValue": "Værdien \"{value}\" i muligheden \"{option}\" i sektion \"{section}\" er forældet.",
"DownloadLog": "Download log",
"KlipperWarnings": "Klipper advarsler"
},
"KlippyStatePanel": {
"FirmwareRestart": "Genstart alt",
"KlipperCheck": "Check at Klipper service kører og at en UDS (Unix Domain Socket) er konfigureret.",
@ -499,12 +497,6 @@
"Empty": "Tom"
}
},
"MoonrakerStatePanel": {
"DownloadLog": "Download log",
"FollowingPluginHasAnError": "Følgende plugin har en fejl:",
"MoonrakerErrorInfo": "En fejl blev fundet under indlæsning af moonraker komponenter. Tjek logfilen og ret fejlen.",
"MoonrakerWarnings": "Moonraker advarsler"
},
"PowerControlPanel": {
"Error": "Fejl",
"Off": "Sluk",

View File

@ -1,5 +1,28 @@
{
"App": {
"Notifications": {
"DependencyName": "Abhängigkeit: {name}",
"DependencyDescription": "Die momentane {name} Version unterstützt nicht alle Funktionen von Mainsail. Aktualisiere {name} mindestens auf Version {neededVersion}.",
"DismissAll": "Dismiss all",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "Beim Laden der Moonraker-Komponenten wurde ein Fehler festgestellt. Bitte prüfe die Logdatei und behebe das Problem.",
"MoonrakerWarning": "Moonraker Warnung",
"UnparsedConfigOption": "Nicht erkannte Config-Option '{option}: {value}' in Abschnitt [{section}] entdeckt. Dies kann eine Option sein, die nicht mehr verfügbar ist, oder das Ergebnis eines Moduls sein, das nicht geladen werden konnte. In Zukunft wird dies zu einem Startfehler führen.",
"UnparsedConfigSection": "Nicht erkannter Config-Abschnitt [{section}] gefunden. Dies kann das Ergebnis einer Komponente sein, die nicht geladen werden konnte. In Zukunft wird dies zu einem Startfehler führen."
},
"KlipperWarnings": {
"DeprecatedOption": "Option '{option}' im Abschnitt '{section}' ist veraltet und wird in einem zukünftigen Release entfernt.",
"DeprecatedOptionHeadline": "Veralterte Klipper Option",
"DeprecatedValue": "Wert '{value}' in Option '{option}' im Abschnitt '{section}' ist veraltet und wird in einem zukünftigen Release entfernt.",
"DeprecatedValueHeadline": "Veralteter Klipper Wert",
"KlipperWarning": "Klipper Warnung"
},
"Never": "nie",
"NextReboot": "nächsten Reboot",
"NoNotification": "Keine Benachrichtigung vorhanden",
"Remind": "Errinnere:"
},
"NumberInput": {
"GreaterOrEqualError": "Muss größer oder gleich {min} sein!",
"MustBeBetweenError": "Muss zwischen {min} und {max} liegen!",
@ -15,8 +38,6 @@
"DescriptionPreviouslyUnderVolted": "rPI-Versorgungsspannung ist seit dem letzten Einschalten mindestens einmal unter 4,65 V gefallen.",
"DescriptionTemperatureLimitActive": "Die Temperatur des rPi uC (nur 3A+/3B+) liegt derzeit über dem Soft-Limit (Standard 60C).",
"DescriptionUnderVoltageDetected": "rPI-Versorgungsspannung derzeit unter 4,65V",
"HeadlineCurrentFlags": "Derzeitige flags",
"HeadlinePreviouslyFlags": "Bisherige flags",
"TitleCurrentlyThrottled": "Drosselung aktiv",
"TitleFrequencyCapped": "Frequenz begrenzt",
"TitlePreviouslyFrequencyCapped": "Vorh. Frequenzbegrenzung registriert",
@ -433,21 +454,11 @@
"Z": "Z",
"ZTilt": "Z Tilt"
},
"DependenciesPanel": {
"Dependency": "Abhängigkeit | Abhängigkeiten",
"DependencyDescription": "Deine momentane {name} Version unterstützt nicht alle Funktionen von Mainsail. Aktualisiere {name} mindestens auf Version {neededVersion}."
},
"FarmPrinterPanel": {
"ReconnectToPrinter": "Neu verbinden",
"SwitchToPrinter": "Zum Drucker wechseln",
"WebcamOff": "Aus"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Option '{option}' im Abschnitt '{section}' ist veraltet und wird in einem zukünftigen Release entfernt.",
"DeprecatedValue": "Wert '{value}' in Option '{option}' im Abschnitt '{section}' ist veraltet und wird in einem zukünftigen Release entfernt.",
"DownloadLog": "Logdatei herunterladen",
"KlipperWarnings": "Klipper Warnungen"
},
"KlippyStatePanel": {
"FirmwareRestart": "Firmware Neustart",
"KlipperCheck": "Bitte überprüfen Sie, ob der Klipper-Dienst läuft und ein UDS (Unix Domain Socket) konfiguriert ist.",
@ -503,12 +514,6 @@
"Empty": "Leer"
}
},
"MoonrakerStatePanel": {
"DownloadLog": "Log herunterladen",
"FollowingPluginHasAnError": "Das folgende Plugin hat einen Fehler:",
"MoonrakerErrorInfo": "Beim Laden der Moonraker-Komponenten wurde ein Fehler festgestellt. Bitte prüfe die Logdatei und behebe das Problem.",
"MoonrakerWarnings": "Moonraker Warnungen"
},
"PowerControlPanel": {
"Error": "Fehler",
"Off": "Aus",

View File

@ -1,5 +1,28 @@
{
"App": {
"Notifications": {
"DependencyName": "Dependency: {name}",
"DependencyDescription": "The current {name} version does not support all features of Mainsail. Update {name} to at least {neededVersion}.",
"DismissAll": "Dismiss all",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "An error was detected while loading the moonraker component '{component}'. Please check the logfile and fix the issue.",
"MoonrakerWarning": "Moonraker warning",
"UnparsedConfigOption": "Unparsed config option '{option}: {value}' detected in section [{section}]. This may be an option no longer available or could be the result of a module that failed to load. In the future this will result in a startup error.",
"UnparsedConfigSection": "Unparsed config section [{section}] detected. This may be the result of a component that failed to load. In the future this will result in a startup error."
},
"KlipperWarnings": {
"DeprecatedOption": "Option '{option}' in section '{section}' is deprecated and will be removed in a future release.",
"DeprecatedOptionHeadline": "Deprecated Klipper Option",
"DeprecatedValue": "Value '{value}' in option '{option}' in section '{section}' is deprecated and will be removed in a future release.",
"DeprecatedValueHeadline": "Deprecated Klipper Value",
"KlipperWarning": "Klipper warning"
},
"Never": "never",
"NextReboot": "next reboot",
"NoNotification": "No Notification available",
"Remind": "Remind:"
},
"NumberInput": {
"GreaterOrEqualError": "Must be grater or equal than {min}!",
"MustBeBetweenError": "Must be between {min} and {max}!",
@ -15,8 +38,6 @@
"DescriptionPreviouslyUnderVolted": "rPI supply voltage dropped below 4.65V at least once since the last power-on.",
"DescriptionTemperatureLimitActive": "rPi uC (3A+/3B+ only) temperature is currently above the soft limit (default 60C).",
"DescriptionUnderVoltageDetected": "rPI supply voltage currently below 4.65V",
"HeadlineCurrentFlags": "Current Flags",
"HeadlinePreviouslyFlags": "Previously Flags",
"TitleCurrentlyThrottled": "Currently Throttled",
"TitleFrequencyCapped": "Frequency Capped",
"TitlePreviouslyFrequencyCapped": "Previously Frequency Capped",
@ -434,21 +455,11 @@
"Z": "Z",
"ZTilt": "Z Tilt"
},
"DependenciesPanel": {
"Dependency": "Dependency | Dependencies",
"DependencyDescription": "Your current {name} version does not support all features of Mainsail. Update {name} to at least {neededVersion}."
},
"FarmPrinterPanel": {
"ReconnectToPrinter": "Reconnect",
"SwitchToPrinter": "Switch to Printer",
"WebcamOff": "Off"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Option '{option}' in section '{section}' is deprecated and will be removed in a future release.",
"DeprecatedValue": "Value '{value}' in option '{option}' in section '{section}' is deprecated and will be removed in a future release.",
"DownloadLog": "download log",
"KlipperWarnings": "Klipper Warnings"
},
"KlippyStatePanel": {
"FirmwareRestart": "Firmware Restart",
"KlipperCheck": "Please check if the Klipper service is running and an UDS (Unix Domain Socket) is configured.",
@ -504,12 +515,6 @@
"Empty": "Empty"
}
},
"MoonrakerStatePanel": {
"DownloadLog": "Download Log",
"FollowingPluginHasAnError": "Following plugin has an error:",
"MoonrakerErrorInfo": "An error was detected while loading the moonraker components. Please check the logfile and fix the issue.",
"MoonrakerWarnings": "Moonraker Warnings"
},
"PowerControlPanel": {
"Error": "Error",
"Off": "Off",

View File

@ -1,5 +1,11 @@
{
"App": {
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "La opción '{option}' en la sección '{section}' está discontinuada y será removida en la próxima versión.",
"DeprecatedValue": "El valor '{value}' en la opción '{option}' en la sección '{section}' está discontinuado y será removido en la próxima versión."
}
},
"NumberInput": {
"GreaterOrEqualError": "¡Debe ser mayor o igual a {min}!",
"MustBeBetweenError": "¡Debe estar entre {min} y {max}!",
@ -420,12 +426,6 @@
"SwitchToPrinter": "Cambiar a impresora",
"WebcamOff": "Apagar"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "La opción '{option}' en la sección '{section}' está discontinuada y será removida en la próxima versión.",
"DeprecatedValue": "El valor '{value}' en la opción '{option}' en la sección '{section}' está discontinuado y será removido en la próxima versión.",
"DownloadLog": "Descargar registro",
"KlipperWarnings": "Alertas de Klipper"
},
"KlippyStatePanel": {
"FirmwareRestart": "Reiniciar Firmware",
"KlipperCheck": "Verifique que el servicio Klipper está corriendo y que un UDS (Unix Domain Socket) esta configurado.",

View File

@ -1,6 +1,12 @@
{
"_last_update:": "09.01.2022",
"App": {
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "L'option '{option}' dans la section '{section}' est obsolète.",
"DeprecatedValue": "La valeur '{value}' dans l'option '{option}' dans la section '{section}' est obsolète."
}
},
"Printers": "Imprimantes",
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "rPi ARM core(s) sont actuellement réduits",
@ -404,12 +410,6 @@
"SwitchToPrinter": "Changer d'imprimante",
"WebcamOff": "Arrêt"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "L'option '{option}' dans la section '{section}' est obsolète.",
"DeprecatedValue": "La valeur '{value}' dans l'option '{option}' dans la section '{section}' est obsolète.",
"DownloadLog": "téléchargement du fichier log",
"KlipperWarnings": "Avertissements Klipper"
},
"KlippyStatePanel": {
"FirmwareRestart": "Redémarrage Firmware",
"KlipperCheck": "Contrôlez que le sevice Klipper est actif et qu'un UDS (Unix Domain Socket) est configuré",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "Nyomtatók",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "'{section}' / '{option}' opcióját leírtuk, és a következő verzióban már nem lesz benne.",
"DeprecatedValue": "'{section}' / '{option}' / Value '{value}' opcióját leírtuk, és a következő verzióban már nem lesz benne."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "Az rPi ARM mag(ok) jelenleg túlterheltek.",
"DescriptionFrequencyCapped": "Az rPi ARM max frekvenciája jelenleg 1,2 GHz -re korlátozódik.",
@ -403,12 +409,6 @@
"SwitchToPrinter": "Váltás a nyomtatóra",
"WebcamOff": "Ki"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "'{section}' / '{option}' opcióját leírtuk, és a következő verzióban már nem lesz benne.",
"DeprecatedValue": "'{section}' / '{option}' / Value '{value}' opcióját leírtuk, és a következő verzióban már nem lesz benne.",
"DownloadLog": "log letöltése",
"KlipperWarnings": "Klipper Figyelmeztetések"
},
"KlippyStatePanel": {
"FirmwareRestart": "Firmware újraindítása",
"KlipperCheck": "Kérjük, ellenőrizd, a Klipper szolgáltatás fut-e, konfigurálva van-e UDS (Unix Domain Socket).",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "Stampanti",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "L'opzione '{option}' nella sezione '{section}' è obsoleta e sarà rimossa in una versione futura.",
"DeprecatedValue": "Il valore '{value}' in '{option}' nella sezione '{section}' è obsoleto e sarà rimosso in una versione futura."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "Uno o più core ARM dell'rPI sono attualmente rallentati.",
"DescriptionFrequencyCapped": "La frequenza massima del processore ARM dell'rPI è attualmente limitata a 1,2 GHz.",
@ -403,12 +409,6 @@
"SwitchToPrinter": "Passa alla Stampante",
"WebcamOff": "Spenta"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "L'opzione '{option}' nella sezione '{section}' è obsoleta e sarà rimossa in una versione futura.",
"DeprecatedValue": "Il valore '{value}' in '{option}' nella sezione '{section}' è obsoleto e sarà rimosso in una versione futura.",
"DownloadLog": "scarica log",
"KlipperWarnings": "Avvisi di Klipper"
},
"KlippyStatePanel": {
"FirmwareRestart": "Riavvio Firmware",
"KlipperCheck": "Controlla se il servizio Klipper è in esecuzione e se è configurato un UDS (Unix Domain Socket).",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "Printers",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "Optie '{option}' in sectie '{section}' is verouderd en wordt in een toekomstige versie verwijderd.",
"DeprecatedValue": "Waarde '{value}' in optie '{option}' in sectie '{section}' is verouderd en wordt in een toekomstige versie verwijderd."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "rPi ARM core(s) worden momenteel gethrottled.",
"DescriptionFrequencyCapped": "rPi ARM max frequency is momenteel beperkt tot 1.2 GHz.",
@ -406,12 +412,6 @@
"SwitchToPrinter": "Wissel naar Printer",
"WebcamOff": "Uit"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Optie '{option}' in sectie '{section}' is verouderd en wordt in een toekomstige versie verwijderd.",
"DeprecatedValue": "Waarde '{value}' in optie '{option}' in sectie '{section}' is verouderd en wordt in een toekomstige versie verwijderd.",
"DownloadLog": "log downloaden",
"KlipperWarnings": "Klipper Waarschuwingen"
},
"KlippyStatePanel": {
"FirmwareRestart": "Firmware Herstart",
"KlipperCheck": "Controleer of de Klipper service draait en een UDS (Unix Domain Socket) geconfigureerd is.",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "Drukarki",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "Opcja '{option}' w sekcji '{section}' jest przestarzała i zostanie usunięta w przyszłych wydaniach.",
"DeprecatedValue": "Ustawienie '{value}' w opcji '{option}' w sekcji '{section}' jest przestarzała i zostanie usunięta w przyszłych wydaniach."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "Częstotliwość rdzeni rPi jest aktualnie obniżona.",
"DescriptionFrequencyCapped": "Taktowanie rPi ograniczone do 1.2 GHz.",
@ -403,12 +409,6 @@
"SwitchToPrinter": "Przełącz do drukarki",
"WebcamOff": "Wyłącz"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Opcja '{option}' w sekcji '{section}' jest przestarzała i zostanie usunięta w przyszłych wydaniach.",
"DeprecatedValue": "Ustawienie '{value}' w opcji '{option}' w sekcji '{section}' jest przestarzała i zostanie usunięta w przyszłych wydaniach.",
"DownloadLog": "Pobierz logi",
"KlipperWarnings": "Ostrzeżenia Klippera"
},
"KlippyStatePanel": {
"FirmwareRestart": "Ponowne uruchomienie oprogramowania",
"KlipperCheck": "Sprawdź , czy Klipper jest uruchomiony oraz czy UDS (Unix Domain Socket) został skonfigurowany poprawnie.",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "Принтер",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "Опция '{option}' в разделе '{section}' устарела и будет удалена в будущем выпуске.",
"DeprecatedValue": "Значение '{value}' в опции '{option}' в секции '{section}' устарело и будет удалено в будущем релизе."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "ARM-ядро(ядра) rPi в настоящее время дросселируется.",
"DescriptionFrequencyCapped": "Максимальная частота rPi ARM в настоящее время ограничена 1,2 ГГц.",
@ -406,12 +412,6 @@
"SwitchToPrinter": "Переключение на принтер",
"WebcamOff": "Офф"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "Опция '{option}' в разделе '{section}' устарела и будет удалена в будущем выпуске.",
"DeprecatedValue": "Значение '{value}' в опции '{option}' в секции '{section}' устарело и будет удалено в будущем релизе.",
"DownloadLog": "Загрузить файл журнала",
"KlipperWarnings": "Предупреждения о клиперах"
},
"KlippyStatePanel": {
"FirmwareRestart": "Перезапуск прошивки",
"KlipperCheck": "Проверьте, запущена ли служба Klipper и настроен ли UDS (Unix Domain Socket).",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "列印機組",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "{section}' 中的部分選項 '{option}' 已棄用,將在未來版本中刪除。",
"DeprecatedValue": "不推薦使用'{section}' 部分的選項'{option}' 中的值'{value}',並將在未來版本中刪除。"
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "樹莓派ARM核心目前已被限制.",
"DescriptionFrequencyCapped": "樹莓派ARM最大頻率目前限制為1.2 GHz.",
@ -351,12 +357,6 @@
"SwitchToPrinter": "切換到列印機",
"WebcamOff": "關閉"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "{section}' 中的部分選項 '{option}' 已棄用,將在未來版本中刪除。",
"DeprecatedValue": "不推薦使用'{section}' 部分的選項'{option}' 中的值'{value}',並將在未來版本中刪除。",
"DownloadLog": "下載日誌",
"KlipperWarnings": "Klipper 警告"
},
"KlippyStatePanel": {
"FirmwareRestart": "韌體重新啟動",
"KlipperCheck": "請檢查Klipper服務是否運行並且已經設定UDSUnix Domain Socket。",

View File

@ -1,6 +1,12 @@
{
"App": {
"Printers": "打印机组",
"Notifications": {
"KlipperWarnings": {
"DeprecatedOption": "选项 '{option}' 在章节 '{section}' 已经弃用,在未来版本会被移除.",
"DeprecatedValue": "数值 '{value}' 在选项 '{option}' 中的章节'{section}' 已经弃用,在未来版本会被移除."
}
},
"ThrottledStates": {
"DescriptionCurrentlyThrottled": "rPi ARM 核心当前被限制.",
"DescriptionFrequencyCapped": "rPi ARM 最高频率限制再 1.2 GHz.",
@ -403,12 +409,6 @@
"SwitchToPrinter": "切换到打印机",
"WebcamOff": "关闭"
},
"KlipperWarningsPanel": {
"DeprecatedOption": "选项 '{option}' 在章节 '{section}' 已经弃用,在未来版本会被移除.",
"DeprecatedValue": "数值 '{value}' 在选项 '{option}' 中的章节'{section}' 已经弃用,在未来版本会被移除.",
"DownloadLog": "下载记录",
"KlipperWarnings": "Klipper 警告"
},
"KlippyStatePanel": {
"FirmwareRestart": "Firmware 重启",
"KlipperCheck": "请检查Klipper服务是否运行并且已经设置UDS(Unix Domain Socket).",

View File

@ -30,7 +30,7 @@ Vue.use(VueMeta)
import VueLoadImage from 'vue-load-image'
Vue.component('VueLoadImage', VueLoadImage)
//vue-toast-notification
//vue-toast-notifications
import VueToast from 'vue-toast-notification'
import 'vue-toast-notification/dist/theme-sugar.css'
import { WebSocketPlugin } from '@/plugins/webSocketClient'

View File

@ -91,7 +91,6 @@ import MacrosPanel from '@/components/panels/MacrosPanel.vue'
import MiniconsolePanel from '@/components/panels/MiniconsolePanel.vue'
import MinSettingsPanel from '@/components/panels/MinSettingsPanel.vue'
import MiscellaneousPanel from '@/components/panels/MiscellaneousPanel.vue'
import MoonrakerStatePanel from '@/components/panels/MoonrakerStatePanel.vue'
import PrintsettingsPanel from '@/components/panels/PrintsettingsPanel.vue'
import StatusPanel from '@/components/panels/StatusPanel.vue'
import ToolsPanel from '@/components/panels/ToolsPanel.vue'
@ -109,7 +108,6 @@ import kebabCase from 'lodash.kebabcase'
MiniconsolePanel,
MinSettingsPanel,
MiscellaneousPanel,
MoonrakerStatePanel,
PrintsettingsPanel,
StatusPanel,
ToolsPanel,

View File

@ -411,4 +411,8 @@ export const actions: ActionTree<GuiState, RootState> = {
dispatch('saveSetting', { name: `gcodeViewer.klipperCache.${key}`, value })
})
},
announcementDismissFlag(_, payload) {
window.console.log(payload)
},
}

View File

@ -12,6 +12,7 @@ import { macros } from '@/store/gui/macros'
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 {
@ -234,6 +235,7 @@ export const gui: Module<GuiState, any> = {
console,
gcodehistory,
macros,
notifications,
presets,
remoteprinters,
webcams,

View File

@ -0,0 +1,89 @@
import { ActionTree } from 'vuex'
import { GuiNotificationState, GuiNotificationStateDismissEntry } from './types'
import { RootState } from '../../types'
import Vue from 'vue'
export const actions: ActionTree<GuiNotificationState, RootState> = {
reset({ commit }) {
commit('reset')
},
upload({ state }) {
Vue.$socket.emit('server.database.post_item', {
namespace: 'mainsail',
key: 'notifications.dismiss',
value: state.dismiss,
})
},
close({ dispatch }, payload) {
const posFirstSlash = payload.id.indexOf('/')
if (posFirstSlash === -1) return
const category = payload.id.slice(0, posFirstSlash)
const id = payload.id.slice(posFirstSlash + 1)
if (category === 'announcement') {
dispatch('server/announcements/close', { entry_id: id }, { root: true })
return
}
dispatch('storeDismiss', {
entry_id: id,
category,
type: 'ever',
time: null,
})
},
dismiss({ dispatch }, payload) {
const posFirstSlash = payload.id.indexOf('/')
if (posFirstSlash === -1) return
const category = payload.id.slice(0, posFirstSlash)
const id = payload.id.slice(posFirstSlash + 1)
if (category === 'announcement') {
dispatch('server/announcements/dismiss', { entry_id: id, time: payload.time }, { root: true })
return
}
dispatch('storeDismiss', {
entry_id: id,
category,
type: payload.type,
time: payload.time,
})
},
async storeDismiss(
{ commit, dispatch, state },
payload: { entry_id: string; category: string; type: string; time: number | null }
) {
let date = new Date().getTime()
if (payload.type === 'time') {
date = new Date().getTime() + (payload.time ?? 0) * 1000
}
const newDismiss: GuiNotificationStateDismissEntry = {
id: payload.entry_id,
category: payload.category,
type: payload.type,
date,
}
if (
state.dismiss.filter(
(dismiss) =>
dismiss.id === newDismiss.id &&
dismiss.category === newDismiss.category &&
dismiss.type === newDismiss.type
).length
) {
await commit('removeDismiss', newDismiss)
}
await commit('addDismiss', newDismiss)
await dispatch('upload')
},
}

View File

@ -0,0 +1,299 @@
import { GetterTree } from 'vuex'
import { GuiNotificationState, GuiNotificationStateDismissEntry, GuiNotificationStateEntry } from './types'
import { ServerAnnouncementsStateEntry } from '@/store/server/announcements/types'
import i18n from '@/plugins/i18n.js'
import { RootStateDependency } from '@/store/types'
import { camelize } from '@/plugins/helpers'
import { sha256 } from 'js-sha256'
import { PrinterStateKlipperConfigWarning } from '@/store/printer/types'
export const getters: GetterTree<GuiNotificationState, any> = {
getNotifications: (state, getters) => {
let notifications: GuiNotificationStateEntry[] = []
// moonraker announcements
notifications = notifications.concat(getters['getNotificationsAnnouncements'])
// rpi flag notifications
notifications = notifications.concat(getters['getNotificationsFlags'])
// mainsail dependencies
notifications = notifications.concat(getters['getNotificationsDependencies'])
// moonraker warnings
notifications = notifications.concat(getters['getNotificationsMoonrakerWarnings'])
// moonraker failed compontents
notifications = notifications.concat(getters['getNotificationsMoonrakerFailedComponents'])
// klipper warnings
notifications = notifications.concat(getters['getNotificationsKlipperWarnings'])
const mapType = {
normal: 2,
high: 1,
critical: 0,
}
return notifications.sort((a, b) => {
if (mapType[a.priority] < mapType[b.priority]) return -1
if (mapType[a.priority] > mapType[b.priority]) return 1
return b.date.getTime() - a.date.getTime()
})
},
getNotificationsAnnouncements: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
// moonraker announcements
const announcements = rootGetters['server/announcements/getAnnouncements']
if (announcements.length) {
announcements.forEach((entry: ServerAnnouncementsStateEntry) => {
notifications.push({
id: 'announcement/' + entry.entry_id,
priority: entry.priority,
title: entry.title,
description: entry.description,
date: entry.date,
dismissed: entry.dismissed,
url: entry.url,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getNotificationsFlags: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
// get all current flags
let flags = rootGetters['server/getThrottledStateFlags']
if (flags.length) {
const date = rootState.server.system_boot_at ?? new Date()
// get all dismissed flags and convert it to a string[]
const flagDismisses = rootGetters['gui/notifications/getDismissByCategory']('flag').map(
(dismiss: GuiNotificationStateDismissEntry) => {
return dismiss.id
}
)
// filter all dismissed flags
flags = flags.filter((flag: string) => !flagDismisses.includes(flag))
// add all flags to the notifications array
flags.forEach((flag: string) => {
notifications.push({
id: 'flag/' + flag,
priority: flag.startsWith('Previously') ? 'high' : 'critical',
title: i18n.t(`App.ThrottledStates.Title${flag}`),
description: i18n.t(`App.ThrottledStates.Description${flag}`),
date,
dismissed: false,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getNotificationsDependencies: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
let dependencies = rootGetters['getDependencies']
if (dependencies.length) {
const date = rootState.server.system_boot_at ?? new Date()
// get all dismissed dependencies and convert it to a string[]
const flagDismisses = rootGetters['gui/notifications/getDismissByCategory']('dependency').map(
(dismiss: GuiNotificationStateDismissEntry) => {
return dismiss.id
}
)
// filter all dismissed dependencies
dependencies = dependencies.filter(
(dependency: RootStateDependency) =>
!flagDismisses.includes(`${dependency.serviceName}/${dependency.neededVersion}`)
)
dependencies.forEach((dependency: RootStateDependency) => {
notifications.push({
id: `dependency/${dependency.serviceName}/${dependency.neededVersion}`,
priority: 'high',
title: i18n.t('App.Notifications.DependencyName', { name: dependency.serviceName }).toString(),
description: i18n
.t('App.Notifications.DependencyDescription', {
name: dependency.serviceName,
installedVersion: dependency.installedVersion,
neededVersion: dependency.neededVersion,
})
.toString(),
date,
dismissed: false,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getNotificationsMoonrakerWarnings: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
let warnings = rootState.server.warnings ?? []
if (warnings.length) {
const date = rootState.server.system_boot_at ?? new Date()
// get all dismissed moonraker warnings and convert it to a string[]
const warningsDismisses = rootGetters['gui/notifications/getDismissByCategory']('moonrakerWarning').map(
(dismiss: GuiNotificationStateDismissEntry) => {
return dismiss.id
}
)
// filter all dismissed warnings
warnings = warnings.filter((warning: string) => !warningsDismisses.includes(sha256(warning)))
warnings.forEach((warning: string) => {
let description = warning
// add possible translations
if (warning.startsWith('Unparsed config option')) {
const warningRegExp = RegExp(/'(?<option>.+): (?<value>.+)'.+\[(?<section>.+)\]/)
const output = warningRegExp.exec(warning)?.groups ?? { option: '', section: '', value: '' }
description = i18n.t('App.Notifications.MoonrakerWarnings.UnparsedConfigOption', output).toString()
} else if (warning.startsWith('Unparsed config section')) {
const warningRegExp = RegExp(/\[(?<section>.+)\]/)
const output = warningRegExp.exec(warning)?.groups ?? { section: '' }
description = i18n.t('App.Notifications.MoonrakerWarnings.UnparsedConfigSection', output).toString()
}
notifications.push({
id: `moonrakerWarning/${sha256(warning)}`,
priority: 'high',
title: i18n.t('App.Notifications.MoonrakerWarnings.MoonrakerWarning').toString(),
description: description,
date,
dismissed: false,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getNotificationsMoonrakerFailedComponents: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
let failedCompontents = rootState.server.failed_components ?? []
if (failedCompontents.length) {
const date = rootState.server.system_boot_at ?? new Date()
// get all dismissed failed components and convert it to a string[]
const flagDismisses = rootGetters['gui/notifications/getDismissByCategory']('moonrakerFailedComponent').map(
(dismiss: GuiNotificationStateDismissEntry) => {
return dismiss.id
}
)
// filter all dismissed failed components
failedCompontents = failedCompontents.filter((component: string) => !flagDismisses.includes(component))
failedCompontents.forEach((component: string) => {
notifications.push({
id: `moonrakerFailedComponent/${component}`,
priority: 'high',
title: i18n.t('App.Notifications.MoonrakerWarnings.MoonrakerComponent', { component }).toString(),
description: i18n
.t('App.Notifications.MoonrakerWarnings.MoonrakerFailedComponentDescription', { component })
.toString(),
date,
dismissed: false,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getNotificationsKlipperWarnings: (state, getters, rootState, rootGetters) => {
const notifications: GuiNotificationStateEntry[] = []
let warnings = (rootState.printer.configfile?.warnings ?? []) as PrinterStateKlipperConfigWarning[]
if (warnings.length) {
const date = rootState.server.system_boot_at ?? new Date()
// get all dismissed klipper warnings and convert it to a string[]
const warningsDismisses = rootGetters['gui/notifications/getDismissByCategory']('klipperWarning').map(
(dismiss: GuiNotificationStateDismissEntry) => {
return dismiss.id
}
)
// filter all dismissed warnings
warnings = warnings.filter((warning) => !warningsDismisses.includes(sha256(warning.message)))
warnings.forEach((warning) => {
let title = i18n.t('App.Notifications.KlipperWarnings.KlipperWarning').toString()
let description = warning.message
// add possible translations
if (warning.type === 'deprecated_value') {
title = i18n.t('App.Notifications.KlipperWarnings.DeprecatedValueHeadline').toString()
description = i18n.t('App.Notifications.KlipperWarnings.DeprecatedValue', warning).toString()
} else if (warning.type === 'deprecated_option') {
title = i18n.t('App.Notifications.KlipperWarnings.DeprecatedOptionHeadline').toString()
description = i18n.t('App.Notifications.KlipperWarnings.DeprecatedOption', warning).toString()
}
// generate url to mainsail docs to fix this warning
let url = 'https://docs.mainsail.xyz/faq/klipper_warnings/' + warning.type
if (warning.type === 'deprecated_option' && warning.option.startsWith('default_parameter'))
url += '#default_parameter'
else if (warning.type === 'deprecated_option') url += '#' + warning.option
else if (warning.type === 'deprecated_value') url += '#' + warning.value
notifications.push({
id: `klipperWarning/${sha256(warning.message)}`,
priority: 'high',
title: title,
description: description,
date,
url,
dismissed: false,
} as GuiNotificationStateEntry)
})
}
return notifications
},
getDismiss: (state, getters, rootState) => {
const currentTime = new Date()
const systemBootAt = rootState.server.system_boot_at ?? new Date()
let dismisses = [...state.dismiss]
dismisses = dismisses.filter((dismiss) => {
if (dismiss.type === 'reboot') {
return systemBootAt.getTime() < dismiss.date
}
if (dismiss.type === 'time') {
return currentTime.getTime() < dismiss.date
}
return true
})
return dismisses
},
getDismissByCategory: (state, getters) => (category: string) => {
let dismisses = getters.getDismiss
dismisses = dismisses.filter((dismiss: GuiNotificationStateDismissEntry) => dismiss.category === category)
return dismisses
},
}

View File

@ -0,0 +1,22 @@
import { GuiNotificationState } from './types'
import { Module } from 'vuex'
import { actions } from './actions'
import { mutations } from './mutations'
import { getters } from './getters'
export const getDefaultState = (): GuiNotificationState => {
return {
dismiss: [],
}
}
// initial state
const state = getDefaultState()
export const notifications: Module<GuiNotificationState, any> = {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@ -0,0 +1,28 @@
import { getDefaultState } from './index'
import { MutationTree } from 'vuex'
import { GuiNotificationState } from './types'
import Vue from 'vue'
export const mutations: MutationTree<GuiNotificationState> = {
reset(state) {
Object.assign(state, getDefaultState())
},
addDismiss(state, payload) {
const dismiss = [...state.dismiss]
dismiss.push(payload)
Vue.set(state, 'dismiss', dismiss)
},
removeDismiss(state, payload) {
const dismiss = [...state.dismiss]
const index = dismiss.findIndex(
(dismiss) =>
dismiss.id === payload.id && dismiss.category === payload.category && dismiss.type === payload.type
)
if (index !== -1) dismiss.splice(index)
Vue.set(state, 'dismiss', dismiss)
},
}

View File

@ -0,0 +1,20 @@
export interface GuiNotificationState {
dismiss: GuiNotificationStateDismissEntry[]
}
export interface GuiNotificationStateEntry {
id: string
priority: 'normal' | 'high' | 'critical'
title: string
description: string
date: Date
dismissed: boolean
url?: string
}
export interface GuiNotificationStateDismissEntry {
id: string
category: string
type: string
date: number
}

View File

@ -3,6 +3,7 @@ import { GuiConsoleState } from '@/store/gui/console/types'
import { GuiPresetsState } from '@/store/gui/presets/types'
import { GuiRemoteprintersState } from '@/store/gui/remoteprinters/types'
import { ServerHistoryStateJob } from '@/store/server/history/types'
import { GuiNotificationState } from '@/store/gui/notifications/types'
export interface GuiState {
general: {
@ -79,6 +80,7 @@ export interface GuiState {
}
}
macros?: GuiMacrosState
notifications?: GuiNotificationState
presets?: GuiPresetsState
remoteprinters?: GuiRemoteprintersState
uiSettings: {

View File

@ -285,9 +285,12 @@ export const getters: GetterTree<PrinterState, RootState> = {
max_power: undefined,
}
if ('settings' in state.configfile && key.toLowerCase() in state.configfile.settings) {
if (
'configfile' in state &&
'settings' in state.configfile &&
key.toLowerCase() in state.configfile.settings
) {
if ('off_below' in settings) tmp.off_below = settings?.off_below ?? 0
if ('max_power' in settings) tmp.max_power = settings?.max_power ?? 1
}

View File

@ -171,3 +171,11 @@ export interface PrinterStateMcu {
measured_max_temp: number | null
}
}
export interface PrinterStateKlipperConfigWarning {
message: string
option: string
section: string
type: 'deprecated_value' | 'deprecated_option'
value: string
}

View File

@ -84,6 +84,10 @@ export const actions: ActionTree<ServerState, RootState> = {
initProcStats({ commit }, payload) {
if (payload.throttled_state !== null) commit('setThrottledState', payload.throttled_state)
if (payload.system_uptime) {
const system_boot_at = new Date(new Date().getTime() - payload.system_uptime * 1000)
commit('setSystemBootAt', system_boot_at)
}
},
updateProcStats({ commit }, payload) {

View File

@ -0,0 +1,45 @@
import Vue from 'vue'
import { ActionTree } from 'vuex'
import { RootState } from '@/store/types'
import { ServerAnnouncementsState } from './types'
export const actions: ActionTree<ServerAnnouncementsState, RootState> = {
reset({ commit }) {
commit('reset')
},
init() {
Vue.$socket.emit('server.announcements.list', {}, { action: 'server/announcements/getList' })
},
getList({ commit }, payload) {
if ('entries' in payload) {
const entries = payload.entries.map((entry: any) => {
const date = new Date(entry.date * 1000)
const date_dismissed = payload.date_dismissed ? new Date(entry.date_dismissed * 1000) : null
const dismiss_wake = payload.dismiss_wake ? new Date(entry.dismiss_wake * 1000) : null
return { ...entry, date, date_dismissed, dismiss_wake }
})
commit('setEntries', entries)
}
if ('feeds' in payload) commit('setFeeds', payload.feeds)
},
getDismissed({ commit }, payload) {
commit('setDismissed', { entry_id: payload.entry_id, status: true })
},
getWaked({ commit }, payload) {
commit('setDismissed', { entry_id: payload.entry_id, status: false })
},
close(_, payload) {
Vue.$socket.emit('server.announcements.dismiss', { entry_id: payload.entry_id })
},
dismiss(_, payload) {
Vue.$socket.emit('server.announcements.dismiss', { entry_id: payload.entry_id, wake_time: payload.time })
},
}

View File

@ -0,0 +1,9 @@
import { GetterTree } from 'vuex'
import { ServerAnnouncementsState, ServerAnnouncementsStateEntry } from './types'
// eslint-disable-next-line
export const getters: GetterTree<ServerAnnouncementsState, any> = {
getAnnouncements: (state) => {
return state.entries.filter((entry: ServerAnnouncementsStateEntry) => !entry.dismissed)
},
}

View File

@ -0,0 +1,24 @@
import { Module } from 'vuex'
import { ServerAnnouncementsState } from '@/store/server/announcements/types'
import { actions } from '@/store/server/announcements/actions'
import { mutations } from '@/store/server/announcements/mutations'
import { getters } from '@/store/server/announcements/getters'
export const getDefaultState = (): ServerAnnouncementsState => {
return {
entries: [],
feeds: [],
}
}
// initial state
const state = getDefaultState()
// eslint-disable-next-line
export const announcements: Module<ServerAnnouncementsState, any> = {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@ -0,0 +1,32 @@
import { getDefaultState } from './index'
import { MutationTree } from 'vuex'
import { ServerAnnouncementsState } from './types'
import Vue from 'vue'
export const mutations: MutationTree<ServerAnnouncementsState> = {
reset(state) {
Object.assign(state, getDefaultState())
},
setEntries(state, payload) {
Vue.set(state, 'entries', payload)
},
setFeeds(state, payload) {
Vue.set(state, 'feeds', payload)
},
setDismissed(state, payload) {
const entries = [...state.entries]
const index = entries.findIndex((entry) => entry.entry_id === payload.entry_id)
if (index > -1) {
entries[index].dismissed = payload.status
if (!payload.status) {
entries[index].date_dismissed = null
entries[index].dismiss_wake = null
} else entries[index].date_dismissed = new Date()
}
Vue.set(state, 'entries', entries)
},
}

View File

@ -0,0 +1,18 @@
export interface ServerAnnouncementsState {
entries: ServerAnnouncementsStateEntry[]
feeds: string[]
}
export interface ServerAnnouncementsStateEntry {
entry_id: string
url: string
title: string
description: string
priority: 'normal' | 'high'
date: Date
dismissed: boolean
date_dismissed: Date | null
dismiss_wake: Date | null
source: string
feed: string
}

View File

@ -159,4 +159,25 @@ export const getters: GetterTree<ServerState, any> = {
return interfaces
},
getThrottledStateFlags: (state) => {
let flags = state.throttled_state.flags.filter((flag: string) => flag !== '?')
/*let flags = [
'Under-Voltage Detected',
'Frequency Capped',
'Currently Throttled',
'Temperature Limit Active',
'Previously Under-Volted',
'Previously Frequency Capped',
'Previously Throttled',
'Previously Temperature Limited',
]*/
flags = flags.map((flag) => {
flag = flag.replace(/ /g, '').replace(/-/g, '')
return flag.charAt(0).toUpperCase() + flag.slice(1)
})
return flags
},
}

View File

@ -10,6 +10,7 @@ import { updateManager } from '@/store/server/updateManager'
import { history } from '@/store/server/history'
import { timelapse } from '@/store/server/timelapse'
import { jobQueue } from '@/store/server/jobQueue'
import { announcements } from '@/store/server/announcements'
// create getDefaultState
export const getDefaultState = (): ServerState => {
@ -26,6 +27,7 @@ export const getDefaultState = (): ServerState => {
events: [],
config: {},
system_info: null,
system_boot_at: null,
cpu_temp: 0,
moonraker_stats: null,
throttled_state: {
@ -56,5 +58,6 @@ export const server: Module<ServerState, any> = {
history,
timelapse,
jobQueue,
announcements,
},
}

View File

@ -147,6 +147,10 @@ export const mutations: MutationTree<ServerState> = {
if (payload && 'flags' in payload) Vue.set(state.throttled_state, 'flags', payload.flags)
},
setSystemBootAt(state, payload) {
Vue.set(state, 'system_boot_at', payload)
},
addRootDirectory(state, payload) {
state.registered_directories.push(payload.name)
},

View File

@ -31,7 +31,9 @@ export interface ServerState {
network: {
[key: string]: ServerStateNetwork
}
system_uptime: number | null
} | null
system_boot_at: Date | null
moonraker_stats: {
cpu_usage: number
mem_units: string

View File

@ -110,6 +110,18 @@ export const actions: ActionTree<SocketState, RootState> = {
dispatch('server/jobQueue/getEvent', payload.params[0], { root: true })
break
case 'notify_announcement_update':
dispatch('server/announcements/getList', payload.params[0], { root: true })
break
case 'notify_announcement_dismissed':
dispatch('server/announcements/getDismissed', payload.params[0], { root: true })
break
case 'notify_announcement_wake':
dispatch('server/announcements/getWaked', payload.params[0], { root: true })
break
default:
if (payload.result !== 'ok' && payload.error?.message)
window.console.error('JSON-RPC: ' + payload.error.message)

View File

@ -2,7 +2,7 @@ export const defaultLogoColor = '#D41216'
export const defaultPrimaryColor = '#2196f3'
export const minKlipperVersion = 'v0.10.0-271'
export const minMoonrakerVersion = 'v0.7.1-449'
export const minMoonrakerVersion = 'v0.7.1-486'
export const colorArray = ['#F44336', '#8e379d', '#03DAC5', '#3F51B5', '#ffde03', '#009688', '#E91E63']
@ -24,7 +24,7 @@ export const validGcodeExtensions = ['.gcode', '.g', '.gco', '.ufp', '.nc']
/*
* List of initable server components
*/
export const initableServerComponents = ['history', 'power', 'updateManager', 'timelapse', 'jobQueue']
export const initableServerComponents = ['history', 'power', 'updateManager', 'timelapse', 'jobQueue', 'announcements']
/*
* List of required klipper config modules