-
+
+
{{ $t('App.Notifications.Remind') }}
-
-
- 1H
-
-
- 1D
-
-
- 7D
-
-
-
-
- {{ $t('App.Notifications.NextReboot') }}
-
-
- {{ $t('App.Notifications.Never') }}
-
-
+
+ {{ reminder.text }}
+
+
@@ -101,6 +83,13 @@ 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'
+import { TranslateResult } from 'vue-i18n'
+import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
+
+interface ReminderOption {
+ text: string | TranslateResult
+ clickFunction: Function
+}
@Component({
components: {},
@@ -110,7 +99,8 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
mdiLinkVariant = mdiLinkVariant
mdiBellOffOutline = mdiBellOffOutline
- private expand = false
+ expand = false
+ showMaintenanceDetails = false
@Prop({ required: true })
declare readonly entry: GuiNotificationStateEntry
@@ -139,6 +129,49 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
return this.entry.id.slice(0, posFirstSlash)
}
+ get maintenanceEntry() {
+ if (this.entryType !== 'maintenance') return null
+
+ const id = this.entry.id.replace('maintenance/', '')
+ const entries = this.$store.getters['gui/maintenance/getEntries']
+
+ return entries.find((entry: GuiMaintenanceStateEntry) => entry.id === id)
+ }
+
+ get reminderTimes() {
+ let output: ReminderOption[] = [
+ {
+ text: this.$t('App.Notifications.NextReboot'),
+ clickFunction: () => this.dismiss('reboot', null),
+ },
+ { text: this.$t('App.Notifications.Never'), clickFunction: () => this.close() },
+ ]
+
+ if (['announcement', 'maintenance'].includes(this.entryType)) {
+ output = []
+ output.push({
+ text: this.$t('App.Notifications.OneHourShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60),
+ })
+ output.push({
+ text: this.$t('App.Notifications.OneDayShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60 * 24),
+ })
+ output.push({
+ text: this.$t('App.Notifications.OneWeekShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60 * 24 * 7),
+ })
+ }
+
+ return output
+ }
+
+ xButtonAction() {
+ if (this.entryType === 'announcement') return this.close()
+
+ this.dismiss('reboot', null)
+ }
+
close() {
this.$store.dispatch('gui/notifications/close', { id: this.entry.id })
}
@@ -157,10 +190,12 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
diff --git a/src/components/panels/HistoryStatisticsPanel.vue b/src/components/panels/HistoryStatisticsPanel.vue
index a282fe979..a2099cca2 100644
--- a/src/components/panels/HistoryStatisticsPanel.vue
+++ b/src/components/panels/HistoryStatisticsPanel.vue
@@ -9,65 +9,20 @@
-
-
- {{ $t('History.SelectedPrinttime') }} |
- {{ formatPrintTime(selectedPrintTime) }} |
-
-
- {{ $t('History.LongestPrinttime') }} |
- {{ formatPrintTime(selectedLongestPrintTime) }} |
-
-
- {{ $t('History.AvgPrinttime') }} |
- {{ formatPrintTime(selectedAvgPrintTime) }} |
-
-
- {{ $t('History.SelectedFilamentUsed') }} |
- {{ Math.round(selectedFilamentUsed / 100) / 10 }} m |
-
-
- {{ $t('History.SelectedJobs') }} |
- {{ selectedJobs.length }} |
-
-
-
-
- {{ $t('History.TotalPrinttime') }} |
- {{ formatPrintTime(totalPrintTime) }} |
-
-
- {{ $t('History.LongestPrinttime') }} |
- {{ formatPrintTime(longestPrintTime) }} |
-
-
- {{ $t('History.AvgPrinttime') }} |
- {{ formatPrintTime(avgPrintTime) }} |
-
-
- {{ $t('History.TotalFilamentUsed') }} |
- {{ Math.round(totalFilamentUsed / 100) / 10 }} m |
-
-
- {{ $t('History.TotalJobs') }} |
- {{ totalJobsCount }} |
-
-
+
+ {{ total.title }} |
+ {{ total.value }} |
+
-
-
+
+
-
- {{ $t('History.Chart') }}
-
-
- {{ $t('History.Table') }}
-
+ {{ $t('History.Chart') }}
+ {{ $t('History.Table') }}
@@ -88,16 +43,12 @@
-
-
+
+
-
- {{ $t('History.FilamentUsage') }}
-
-
- {{ $t('History.PrinttimeAvg') }}
-
+ {{ $t('History.FilamentUsage') }}
+ {{ $t('History.PrinttimeAvg') }}
@@ -113,17 +64,20 @@ import Panel from '@/components/ui/Panel.vue'
import HistoryFilamentUsage from '@/components/charts/HistoryFilamentUsage.vue'
import HistoryPrinttimeAvg from '@/components/charts/HistoryPrinttimeAvg.vue'
import HistoryAllPrintStatusChart from '@/components/charts/HistoryAllPrintStatusChart.vue'
-import { ServerHistoryStateJob } from '@/store/server/history/types'
+import { ServerHistoryStateJob, ServerHistoryStateJobAuxiliaryTotal } from '@/store/server/history/types'
import { mdiChartAreaspline, mdiDatabaseArrowDownOutline } from '@mdi/js'
+import { formatPrintTime } from '@/plugins/helpers'
+import HistoryMixin from '@/components/mixins/history'
@Component({
components: { Panel, HistoryFilamentUsage, HistoryPrinttimeAvg, HistoryAllPrintStatusChart },
})
-export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
+export default class HistoryStatisticsPanel extends Mixins(BaseMixin, HistoryMixin) {
mdiChartAreaspline = mdiChartAreaspline
mdiDatabaseArrowDownOutline = mdiDatabaseArrowDownOutline
+ formatPrintTime = formatPrintTime
get selectedJobs() {
- return this.$store.state.gui.view.history.selectedJobs ?? []
+ return this.$store.getters['server/history/getSelectedJobs']
}
get existsSelectedJobs() {
@@ -131,9 +85,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get totalPrintTime() {
- return 'total_print_time' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_print_time
- : 0
+ return this.$store.state.server.history.job_totals?.total_print_time ?? 0
}
get selectedPrintTime() {
@@ -147,9 +99,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get longestPrintTime() {
- return 'longest_print' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.longest_print
- : 0
+ return this.$store.state.server.history.job_totals?.longest_print ?? 0
}
get selectedLongestPrintTime() {
@@ -177,9 +127,13 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get totalFilamentUsed() {
- return 'total_filament_used' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_filament_used
- : 0
+ return this.$store.state.server.history.job_totals?.total_filament_used ?? 0
+ }
+
+ get totalFilamentUsedFormat() {
+ const value = Math.round(this.totalFilamentUsed / 100) / 10
+
+ return `${value} m`
}
get selectedFilamentUsed() {
@@ -192,10 +146,14 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
return filamentUsed
}
+ get selectedFilamentUsedFormat() {
+ const value = Math.round(this.selectedFilamentUsed / 100) / 10
+
+ return `${value} m`
+ }
+
get totalJobsCount() {
- return 'total_jobs' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_jobs
- : 0
+ return this.$store.state.server.history.job_totals?.total_jobs ?? 0
}
get toggleChart() {
@@ -218,30 +176,116 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
return this.$store.state.server.history.all_loaded ?? false
}
- refreshHistory() {
- this.$store.dispatch('socket/addLoading', { name: 'historyLoadAll' })
+ get selectedTotals() {
+ const output: { title: string; value: string }[] = [
+ {
+ title: this.$t('History.SelectedPrinttime') as string,
+ value: this.formatPrintTime(this.selectedPrintTime, false),
+ },
+ {
+ title: this.$t('History.LongestPrinttime') as string,
+ value: this.formatPrintTime(this.selectedLongestPrintTime, false),
+ },
+ {
+ title: this.$t('History.AvgPrinttime') as string,
+ value: this.formatPrintTime(this.selectedAvgPrintTime, false),
+ },
+ {
+ title: this.$t('History.SelectedFilamentUsed') as string,
+ value: this.selectedFilamentUsedFormat,
+ },
+ {
+ title: this.$t('History.SelectedJobs') as string,
+ value: this.selectedJobs.length.toString(),
+ },
+ ]
+
+ output.push(...this.auxiliarySelectedTotals)
+
+ return output
+ }
- this.$socket.emit('server.history.list', { start: 0, limit: 50 }, { action: 'server/history/getHistory' })
+ get auxiliarySelectedTotals() {
+ const output: { title: string; value: string }[] = []
+ this.moonrakerHistoryFields.forEach((historyField) => {
+ const value = this.selectedJobs.reduce((acc: number, job: ServerHistoryStateJob) => {
+ const historyFieldName = historyField.name.replace('history_field_', '')
+ const auxiliary_data = job.auxiliary_data?.find(
+ (auxiliary) => auxiliary.provider === historyField.provider && auxiliary.name === historyFieldName
+ )
+
+ if (!auxiliary_data || typeof auxiliary_data.value !== 'number') return acc
+
+ return acc + auxiliary_data.value
+ }, 0)
+
+ output.push({
+ title: historyField.desc,
+ value: `${Math.round(value * 1000) / 1000} ${historyField.unit}`,
+ })
+ })
+
+ return output
}
- formatPrintTime(totalSeconds: number) {
- if (totalSeconds) {
- let output = ''
+ get genericTotals() {
+ const output: { title: string; value: string }[] = [
+ {
+ title: this.$t('History.TotalPrinttime') as string,
+ value: this.formatPrintTime(this.totalPrintTime, false),
+ },
+ {
+ title: this.$t('History.LongestPrinttime') as string,
+ value: this.formatPrintTime(this.longestPrintTime, false),
+ },
+ {
+ title: this.$t('History.AvgPrinttime') as string,
+ value: this.formatPrintTime(this.avgPrintTime, false),
+ },
+ {
+ title: this.$t('History.TotalFilamentUsed') as string,
+ value: this.totalFilamentUsedFormat,
+ },
+ {
+ title: this.$t('History.TotalJobs') as string,
+ value: this.totalJobsCount.toString(),
+ },
+ ]
+
+ // Add auxiliary totals
+ output.push(...this.auxiliaryTotals)
+
+ return output
+ }
- const hours = Math.floor(totalSeconds / 3600)
- totalSeconds %= 3600
- if (hours) output += ' ' + hours + 'h'
+ get auxiliaryTotals() {
+ const auxiliaries = this.$store.state.server.history.auxiliary_totals ?? []
+ const output: { title: string; value: string }[] = []
+
+ auxiliaries.forEach((auxiliary: ServerHistoryStateJobAuxiliaryTotal) => {
+ const historyFieldName = `history_field_${auxiliary.field}`
+ const historyField = this.moonrakerHistoryFields.find(
+ (historyField) => historyField.provider === auxiliary.provider && historyField.name === historyFieldName
+ )
+ const value = Math.round((auxiliary.total ?? 0) * 1000) / 1000
+
+ output.push({
+ title: historyField?.desc ?? auxiliary.field,
+ value: `${value} ${historyField?.unit}`,
+ })
+ })
- const minutes = Math.floor(totalSeconds / 60)
- if (minutes) output += ' ' + minutes + 'm'
+ return output
+ }
- const seconds = totalSeconds % 60
- if (seconds) output += ' ' + seconds.toFixed(0) + 's'
+ get totals() {
+ return this.existsSelectedJobs ? this.selectedTotals : this.genericTotals
+ }
- return output
- }
+ refreshHistory() {
+ this.$store.dispatch('socket/addLoading', { name: 'historyLoadAll' })
- return '--'
+ this.$socket.emit('server.history.list', { start: 0, limit: 50 }, { action: 'server/history/getHistory' })
}
}
diff --git a/src/components/panels/Machine/SystemPanelHost.vue b/src/components/panels/Machine/SystemPanelHost.vue
index affe497bd..fe9a75d54 100644
--- a/src/components/panels/Machine/SystemPanelHost.vue
+++ b/src/components/panels/Machine/SystemPanelHost.vue
@@ -39,7 +39,7 @@
{{ $t('Machine.SystemPanel.Values.Load', { load: hostStats.load }) }},
- {{ $t('Machine.SystemPanel.Values.Memory', { memory: hostStats.memoryFormat }) }},
+ {{ $t('Machine.SystemPanel.Values.Memory', { memory: hostStats.memoryFormat }) }}
@@ -51,6 +51,7 @@
+ ,
{{
$t('Machine.SystemPanel.Values.Temp', {
temp: hostStats.tempSensor.temperature,
@@ -74,6 +75,7 @@
+ ,
{{
$t('Machine.SystemPanel.Values.Temp', {
temp: hostStats.tempSensor.temperature,
diff --git a/src/components/panels/Miscellaneous/MoonrakerSensor.vue b/src/components/panels/Miscellaneous/MoonrakerSensor.vue
new file mode 100644
index 000000000..e0b919518
--- /dev/null
+++ b/src/components/panels/Miscellaneous/MoonrakerSensor.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue b/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue
new file mode 100644
index 000000000..1ac9f31c2
--- /dev/null
+++ b/src/components/panels/Miscellaneous/MoonrakerSensorValue.vue
@@ -0,0 +1,92 @@
+
+
+ {{ symbol }}
+ {{ name }}:
+ {{ output }}
+
+
+
+
diff --git a/src/components/panels/MiscellaneousPanel.vue b/src/components/panels/MiscellaneousPanel.vue
index 9a671d075..976e4cf88 100644
--- a/src/components/panels/MiscellaneousPanel.vue
+++ b/src/components/panels/MiscellaneousPanel.vue
@@ -8,7 +8,7 @@
:collapsible="true"
card-class="miscellaneous-panel">
-
+
+ :multi="parseInt(object.scale)" />
-
+
-
+
+ :filament_detected="sensor.filament_detected" />
+
+
+
+
@@ -49,10 +53,11 @@ import BaseMixin from '@/components/mixins/base'
import MiscellaneousSlider from '@/components/inputs/MiscellaneousSlider.vue'
import MiscellaneousLight from '@/components/inputs/MiscellaneousLight.vue'
import FilamentSensor from '@/components/inputs/FilamentSensor.vue'
+import MoonrakerSensor from '@/components/panels/Miscellaneous/MoonrakerSensor.vue'
import Panel from '@/components/ui/Panel.vue'
import { mdiDipSwitch } from '@mdi/js'
@Component({
- components: { Panel, FilamentSensor, MiscellaneousSlider, MiscellaneousLight },
+ components: { Panel, FilamentSensor, MiscellaneousSlider, MiscellaneousLight, MoonrakerSensor },
})
export default class MiscellaneousPanel extends Mixins(BaseMixin) {
mdiDipSwitch = mdiDipSwitch
@@ -69,6 +74,10 @@ export default class MiscellaneousPanel extends Mixins(BaseMixin) {
return this.$store.getters['printer/getLights'] ?? []
}
+ get moonrakerSensors() {
+ return this.$store.getters['server/sensor/getSensors'] ?? []
+ }
+
get showMiscellaneousPanel() {
return (
this.klipperReadyForGui && (this.miscellaneous.length || this.filamentSensors.length || this.lights.length)
diff --git a/src/components/panels/Status/PrintstatusPrinting.vue b/src/components/panels/Status/PrintstatusPrinting.vue
index 2f19780ca..400b96d42 100644
--- a/src/components/panels/Status/PrintstatusPrinting.vue
+++ b/src/components/panels/Status/PrintstatusPrinting.vue
@@ -239,13 +239,13 @@ export default class StatusPanelPrintstatusPrinting extends Mixins(BaseMixin) {
}
formatDuration(seconds: number) {
- let prefix = seconds < 0 ? '-' : ''
+ const prefix = seconds < 0 ? '-' : ''
let absSeconds = Math.abs(seconds)
- let h = Math.floor(absSeconds / 3600)
+ const h = Math.floor(absSeconds / 3600)
absSeconds %= 3600
- let m = ('0' + Math.floor(absSeconds / 60)).slice(-2)
- let s = ('0' + (absSeconds % 60).toFixed(0)).slice(-2)
+ const m = ('0' + Math.floor(absSeconds / 60)).slice(-2)
+ const s = ('0' + Math.floor(absSeconds % 60)).slice(-2)
return prefix + h + ':' + m + ':' + s
}
diff --git a/src/components/panels/Status/PrintstatusThumbnail.vue b/src/components/panels/Status/PrintstatusThumbnail.vue
index 43fdf91e5..11207d17b 100644
--- a/src/components/panels/Status/PrintstatusThumbnail.vue
+++ b/src/components/panels/Status/PrintstatusThumbnail.vue
@@ -8,13 +8,11 @@
class="d-flex align-end statusPanel-big-thumbnail"
height="200"
:style="thumbnailStyle"
- @focus="focusBigThumbnail"
- @blur="blurBigThumbnail">
-
+ @focus="focus = true"
+ @blur="focus = false">
+
-
+
{{ mdiFileOutline }}
{{ current_filename }}
@@ -95,6 +93,9 @@ export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) {
mdiFileOutline = mdiFileOutline
mdiFile = mdiFile
+ focus = false
+ thumbnailFactor = 0
+
declare $refs: {
bigThumbnail: any
}
@@ -190,34 +191,59 @@ export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) {
}
get thumbnailStyle() {
+ let output: { height: string; backgroundColor?: string } = {
+ height: '200px',
+ }
+
+ if (!this.printstatusThumbnailZoom) {
+ output.height = '100%'
+ } else if (this.focus && this.thumbnailBlurHeight > 0) {
+ output.height = `${this.thumbnailBlurHeight}px`
+ }
+
if (defaultBigThumbnailBackground.toLowerCase() !== this.bigThumbnailBackground.toLowerCase()) {
- return { backgroundColor: this.bigThumbnailBackground }
+ output.backgroundColor = this.bigThumbnailBackground
+
+ return output
}
- return {}
+ return output
}
- focusBigThumbnail() {
- if (this.$refs.bigThumbnail) {
- const clientWidth = this.$refs.bigThumbnail.$el.clientWidth
- const thumbnailWidth = this.thumbnailBigWidth
- const factor = clientWidth / thumbnailWidth
+ get styleThumbnailOverlay() {
+ const style = {
+ backgroundColor: 'rgba(0, 0, 0, 0.3)',
+ backdropFilter: 'blur(3px)',
+ }
- this.$refs.bigThumbnail.$el.style.height = (this.thumbnailBigHeight * factor).toFixed() + 'px'
+ if (!this.$vuetify.theme.dark) {
+ style.backgroundColor = 'rgba(255, 255, 255, 0.3)'
}
+
+ return style
}
- blurBigThumbnail() {
- if (this.$refs.bigThumbnail) {
- this.$refs.bigThumbnail.$el.style.height = '200px'
- }
+ get thumbnailBlurHeight() {
+ if (this.thumbnailFactor === 0) return 0
+
+ return (this.thumbnailBigHeight * this.thumbnailFactor).toFixed()
+ }
+
+ get printstatusThumbnailZoom() {
+ return this.$store.state.gui.uiSettings.printstatusThumbnailZoom ?? true
+ }
+
+ calcThumbnailFactor() {
+ const thumbnailClientWidth = this.$refs.bigThumbnail?.$el.clientWidth ?? 0
+ if (!thumbnailClientWidth || !this.thumbnailBigWidth) this.thumbnailFactor = 0
+
+ return (this.thumbnailFactor = thumbnailClientWidth / this.thumbnailBigWidth)
}
@Debounce(200)
handleResize() {
this.$nextTick(() => {
- const isFocused = document.activeElement === this.$refs.bigThumbnail?.$el
- if (isFocused) this.focusBigThumbnail()
+ this.calcThumbnailFactor()
})
}
}
@@ -231,4 +257,9 @@ export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) {
.statusPanel-printstatus-thumbnail {
position: relative;
}
+
+.statusPanel-thumbnail-overlay {
+ background-color: rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(3px);
+}
diff --git a/src/components/panels/StatusPanel.vue b/src/components/panels/StatusPanel.vue
index ec8612bec..ba405f501 100644
--- a/src/components/panels/StatusPanel.vue
+++ b/src/components/panels/StatusPanel.vue
@@ -263,7 +263,7 @@ export default class StatusPanel extends Mixins(BaseMixin) {
icon: mdiLayersPlus,
loadingName: 'pauseAtLayer',
status: () => {
- if (this.multiFunctionButton || this.layer_count === null) return false
+ if (this.multiFunctionButton || !this.displayPauseAtLayerButton) return false
return ['paused', 'printing'].includes(this.printer_state)
},
@@ -319,7 +319,7 @@ export default class StatusPanel extends Mixins(BaseMixin) {
click: this.btnExcludeObject,
},
{
- text: this.$t('Panels.StatusPanel.PauseAtLayer.PauseAtLayer') + ' - ' + this.displayPauseAtLayerButton,
+ text: this.$t('Panels.StatusPanel.PauseAtLayer.PauseAtLayer'),
loadingName: 'pauseAtLayer',
icon: mdiLayersPlus,
status: () => this.displayPauseAtLayerButton,
diff --git a/src/components/settings/SettingsTimelapseTab.vue b/src/components/settings/SettingsTimelapseTab.vue
index 13cc39b70..b39f84d27 100644
--- a/src/components/settings/SettingsTimelapseTab.vue
+++ b/src/components/settings/SettingsTimelapseTab.vue
@@ -27,14 +27,19 @@
+ :sub-title="$t('Settings.TimelapseTab.CameraDescriptionWithSnapshotUrl')">
+
+ {{ $t('Settings.TimelapseTab.CameraWarningAlreadySet') }}
+ ({{ $t('Settings.TimelapseTab.CameraWarningAlreadySetSmall') }})
+
+ :disabled="blockedsettings.includes('camera') || availableSnapshotWebcams.length === 0" />
webcam.snapshot_url !== ''
+ )
+ }
+
get cameraOptions() {
- const webcams = this.$store.getters['gui/webcams/getWebcams']
- const output: any = []
-
- webcams
- .filter((webcam: GuiWebcamStateWebcam) => webcam.snapshot_url !== '')
- .forEach((webcam: GuiWebcamStateWebcam) => {
- output.push({
- text: webcam.name,
- value: webcam.name,
- })
+ let output: { text: string | TranslateResult; value: string | null }[] = []
+
+ if (this.availableSnapshotWebcams.length === 0) {
+ return [{ value: null, text: this.$t('Settings.TimelapseTab.NoWebcamFound') }]
+ }
+
+ this.availableSnapshotWebcams.forEach((webcam: GuiWebcamStateWebcam) => {
+ output.push({
+ text: webcam.name,
+ value: webcam.name,
})
+ })
- return caseInsensitiveSort(output, 'text')
+ output = caseInsensitiveSort(output, 'text')
+
+ if (this.camera === null) {
+ output.unshift({ value: null, text: this.$t('Settings.TimelapseTab.SelectWebcam') })
+ }
+
+ return output
}
get blockedsettings() {
@@ -853,6 +872,17 @@ export default class SettingsTimelapseTab extends Mixins(BaseMixin) {
}
get camera() {
+ const value = this.$store.state.server.timelapse.settings.camera ?? null
+
+ if (
+ value === null ||
+ this.blockedsettings.includes('snapshoturl') ||
+ this.availableSnapshotWebcams.length === 0 ||
+ this.availableSnapshotWebcams.find((webcam) => webcam.name === value) === undefined
+ ) {
+ return null
+ }
+
return this.$store.state.server.timelapse.settings.camera
}
diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue
index 143de9e91..617afa1bf 100644
--- a/src/components/settings/SettingsUiSettingsTab.vue
+++ b/src/components/settings/SettingsUiSettingsTab.vue
@@ -76,32 +76,41 @@
:dynamic-slot-width="true">
-
-
-
- {{ mdiRestart }}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ mdiRestart }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/webcams/streamers/MjpegstreamerAdaptive.vue b/src/components/webcams/streamers/MjpegstreamerAdaptive.vue
index b2dd821b2..39f04cc30 100644
--- a/src/components/webcams/streamers/MjpegstreamerAdaptive.vue
+++ b/src/components/webcams/streamers/MjpegstreamerAdaptive.vue
@@ -227,4 +227,8 @@ export default class MjpegstreamerAdaptive extends Mixins(BaseMixin, WebcamMixin
padding: 3px 10px;
border-top-left-radius: 5px;
}
+
+html.theme--light .webcamFpsOutput {
+ background: rgba(255, 255, 255, 0.7);
+}
diff --git a/src/locales/de.json b/src/locales/de.json
index f7e4b544d..b46e96466 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -13,8 +13,11 @@
"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",
+ "KlipperRuntimeWarning": "Klipper Laufzeitwarnung",
"KlipperWarning": "Klipper Warnung"
},
+ "MaintenanceReminder": "Wartungserinnerung",
+ "MaintenanceReminderText": "Wartung \"{name}\" ist fällig.",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "Beim Laden der Moonraker-Komponente '{component}' wurde ein Fehler festgestellt. Bitte prüfe die Logdatei und behebe das Problem.",
@@ -28,7 +31,11 @@
"NextReboot": "nächsten Reboot",
"NoNotification": "Keine Benachrichtigung vorhanden",
"Notifications": "Benachrichtigungen",
- "Remind": "Errinnere:"
+ "OneDayShort": "1T",
+ "OneHourShort": "1Std",
+ "OneWeekShort": "1W",
+ "Remind": "Erinnere:",
+ "ShowDetails": "Details anzeigen"
},
"NumberInput": {
"GreaterOrEqualError": "Muss größer oder gleich {min} sein!",
@@ -36,6 +43,9 @@
"NoEmptyAllowedError": "Feld darf nicht leer sein!"
},
"Printers": "Drucker",
+ "TextfieldWithCopy": {
+ "Copied": "Kopiert!"
+ },
"TheServiceWorker": {
"DescriptionNeedUpdate": "Der lokale Cache ist veraltet und muss aktualisiert werden. Bitte klicke auf den Button unten, um den Cache zu aktualisieren.",
"TitleNeedUpdate": "PWA benötigt ein Update",
@@ -146,6 +156,26 @@
"SendCode": "Code senden...",
"SetupConsole": "Einstellungen"
},
+ "CoolDownDialog": {
+ "AreYouSure": "Bist du sicher?",
+ "CoolDown": "Abkühlen",
+ "No": "Nein",
+ "Yes": "Ja"
+ },
+ "DevicesDialog": {
+ "CanBusInfo": "Nur nicht zugewiesene Knoten können erkannt werden. Es wird empfohlen, nur ein nicht zugewiesenes Gerät an den Can-Bus anzuschließen, um Kommunikationsprobleme zu vermeiden. Klicke hier für weitere Informationen:",
+ "ClickRefresh": "Klicke auf den Aktualisieren-Button, um die Geräte zu aktualisieren.",
+ "DevicePath": "Gerätepfad",
+ "Formats": "Formate",
+ "Headline": "Geräte",
+ "HideSystemEntries": "Systemeinträge ausblenden",
+ "LibcameraId": "Libcamera ID",
+ "NoDeviceFound": "Kein Gerät gefunden. Bitte überprüfe die Verbindung und klicke auf den Aktualisieren-Button.",
+ "PathByHardware": "Hardwarepfad",
+ "PathById": "ID-Pfad",
+ "Refresh": "Aktualisieren",
+ "Resolutions": "Auflösungen"
+ },
"Dialogs": {
"StartPrint": {
"Cancel": "abbrechen",
@@ -158,6 +188,7 @@
},
"Editor": {
"ConfigReference": "Konfig-Referenz",
+ "DeviceDialog": "Geräte",
"DontSave": "Nicht speichern",
"Downloading": "Herunterladen",
"FailedSave": "{filename} konnte nicht hochgeladen werden!",
@@ -316,22 +347,35 @@
"Wireframe": "Drahtgitter"
},
"History": {
+ "AddANote": "Eine Notiz hinzufügen",
+ "AddMaintenance": "Wartung hinzufügen",
"AddNote": "Notiz hinzufügen",
+ "AddToQueueSuccessful": "Datei {filename} zur Warteschlange hinzugefügt.",
"AllJobs": "Alle",
"AvgPrinttime": "Druckzeit - Ø",
"Cancel": "abbrechen",
"Chart": "Diagramm",
"CreateNote": "Notiz anlegen",
+ "DateBasedReminder": "Datum",
+ "DateBasedReminderDescription": "Diese Erinnerung basiert auf dem Datum.",
+ "Days": "Tage",
"Delete": "Löschen",
"DeleteSelectedQuestion": "Sollen wirklich {count} ausgewählte Jobs gelöscht werden?",
"DeleteSingleJobQuestion": "Soll der Job wirklich gelöscht werden?",
"Details": "Details",
+ "EditMaintenance": "Wartung bearbeiten",
"EditNote": "Notiz bearbeiten",
"Empty": "leer",
"EndTime": "Endzeit",
+ "EntryCreatedAt": "Erstellt am {date}.",
+ "EntryNextPerform": "Nächste Ausführung:",
+ "EntryPerformedAt": "Ausgeführt am {date}.",
+ "EntrySince": "Verwendet seit:",
"EstimatedFilament": "Geschätztes Filament",
"EstimatedFilamentWeight": "Geschätztes Filamentgewicht",
"EstimatedTime": "Geschätzte Zeit",
+ "FilamentBasedReminder": "Filament",
+ "FilamentBasedReminderDescription": "Diese Erinnerung basiert auf dem Filamentverbrauch.",
"FilamentCalc": "Geschätztes Filament",
"FilamentUsage": "Filamentverbrauch",
"FilamentUsed": "Filament verwendet - gesamt",
@@ -342,18 +386,35 @@
"FirstLayerHeight": "Erste Schicht Höhe",
"HistoryFilamentUsage": "Filament",
"HistoryPrinttimeAVG": "Drucke",
+ "Hours": "Stunden",
+ "InvalidNameEmpty": "Ungültiger Name. Name darf nicht leer sein!",
"JobDetails": "Job Details",
"Jobs": "Drucke",
"LastModified": "Zuletzt geändert",
"LayerHeight": "Schichthöhe",
"LoadCompleteHistory": "Lade vollständige Historie",
"LongestPrinttime": "Druckzeit - längste",
+ "Maintenance": "Wartung",
+ "MaintenanceEntries": "Wartungseinträge",
+ "Meter": "Meter",
+ "Name": "Name",
+ "NoReminder": "Keine Erinnerung",
"Note": "Notiz",
"ObjectHeight": "Objekthöhe",
+ "OneTime": "Einmalig",
+ "Perform": "ausführen",
+ "Performed": "ausgeführt",
+ "PerformedAndReschedule": "ausgeführt und neu planen",
+ "PerformMaintenance": "Wartung ausführen",
"PrintDuration": "Druckdauer",
"PrintHistory": "Historie",
+ "PrintJobs": "Drucke",
"PrintTime": "Druckzeit",
"PrinttimeAvg": "Druckzeit - Ø",
+ "PrinttimeBasedReminder": "Druckzeit",
+ "PrinttimeBasedReminderDescription": "Diese Erinnerung basiert auf der Druckzeit.",
+ "Reminder": "Erinnerung",
+ "Repeat": "Wiederholen",
"Reprint": "Erneut drucken",
"Save": "speichern",
"Search": "Suchen",
@@ -570,7 +631,9 @@
"KlippyStatePanel": {
"CheckKlippyAndUdsAddress": "Bitte überprüfen, ob der Klipper-Dienst läuft und klippy_uds_address in der moonraker.conf korrekt konfiguriert ist.",
"FirmwareRestart": "Firmware Neustart",
+ "KlipperLog": "Klipper Log",
"MoonrakerCannotConnect": "Moonraker kann keine Verbindung zu Klipper herstellen!",
+ "MoonrakerLog": "Moonraker Log",
"PowerOn": "Drucker einschalten",
"PrinterSwitchedOff": "Drucker ist ausgeschaltet",
"PrinterSwitchedOffDescription": "Der Drucker ist ausgeschaltet und es kann keine Verbindung zu Klipper hergestellt werden. Um den Drucker einzuschalten, auf die Schaltfläche unten klicken:",
@@ -800,6 +863,8 @@
"HostnameInvalid": "ungültiger Hostname/IP",
"HostnameIp": "Hostname/IP",
"HostnameRequired": "Hostname ist erforderlich",
+ "Name": "Name",
+ "Path": "Pfad",
"Port": "Port",
"PortRequired": "Port ist erforderlich",
"RememberToAdd": "Bitte denk daran, {cors} in moonraker.conf unter 'cors_domains' hinzuzufügen.",
@@ -916,7 +981,8 @@
"DbConsoleHistory": "Verlauf der Konsole",
"DbHistoryJobs": "Historie Druckvorgänge",
"DbHistoryTotals": "Historie Gesamtzähler",
- "DBNavigation": "Navigation",
+ "DbMaintenance": "Wartung",
+ "DbNavigation": "Navigation",
"DbTimelapseSettings": "Zeitraffer Einstellungen",
"DbView": "Ansichtseinstellungen",
"EstimateValues": {
@@ -1033,6 +1099,9 @@
"AddPrinter": "Drucker hinzufügen",
"EditPrinter": "Drucker bearbeiten",
"Hostname": "Hostname",
+ "Name": "Name",
+ "NameDescription": "Dieser Name wird nicht im GUI angezeigt und wird nur für Weiterleitungen verwendet.",
+ "Path": "Pfad",
"Port": "Port",
"RemotePrinters": "Remote Drucker",
"UpdatePrinter": "Drucker aktualisieren",
@@ -1119,6 +1188,8 @@
"BoolBigThumbnailDescription": "Zeige ein großes Thumbnail in der Status-Anzeige während eines Drucks.",
"BoolHideUploadAndPrintButton": "\"Hochladen & Drucken\" Schaltfläche ausblenden",
"BoolHideUploadAndPrintButtonDescription": "Blendet die \"Hochladen & Drucken\" Schaltfläche in der Kopfleiste ein oder aus.",
+ "ConfirmOnCoolDown": "Bestätigung für Abkühlen erforderlich",
+ "ConfirmOnCoolDownDescription": "Zeige vor dem Abkühlen einen Bestätigungsdialog.",
"ConfirmOnEmergencyStop": "Bestätigung für Notstopp erforderlich",
"ConfirmOnEmergencyStopDescription": "Zeige vor einem Notstop einen Bestätigungsdialog.",
"ConfirmOnPowerDeviceChange": "Bestätigung für Änderung der Stromversorgung von Geräten",
@@ -1153,6 +1224,8 @@
"PowerDeviceName": "Stromversorgung für Drucker",
"PowerDeviceNameDescription": "Wähle aus, welches Moonraker Power-Device zum Einschalten des Druckers verwendet werden soll.",
"Primary": "Primärfarbe",
+ "ProgressAsFavicon": "Fortschritt als Favicon anzeigen",
+ "ProgressAsFaviconDescription": "Ändere das Mainsail-Logo-Favicon in einen Fortschrittskreis.",
"ScrewsTiltAdjustDialog": "Hilfsfenster für Schrauben Neigunganpassung",
"ScrewsTiltAdjustDialogDescription": "Zeige ein Hilfsfenster für SCREWS_TILT_CALCULATE an.",
"TempchartHeight": "Temperatur-Chart Höhe",
diff --git a/src/locales/en.json b/src/locales/en.json
index 8c9ef596d..d43646d16 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -16,6 +16,8 @@
"KlipperRuntimeWarning": "Klipper runtime warning",
"KlipperWarning": "Klipper warning"
},
+ "MaintenanceReminder": "Maintenance Reminder",
+ "MaintenanceReminderText": "Maintenance \"{name}\" is due.",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "An error was detected while loading the moonraker component '{component}'. Please check the log file and fix the issue.",
@@ -29,7 +31,11 @@
"NextReboot": "next reboot",
"NoNotification": "No Notification available",
"Notifications": "Notifications",
- "Remind": "Remind:"
+ "OneDayShort": "1D",
+ "OneHourShort": "1H",
+ "OneWeekShort": "1W",
+ "Remind": "Remind:",
+ "ShowDetails": "show details"
},
"NumberInput": {
"GreaterOrEqualError": "Must be greater or equal than {min}!",
@@ -341,6 +347,8 @@
"Wireframe": "Wireframe"
},
"History": {
+ "AddANote": "Add a note",
+ "AddMaintenance": "Add Maintenance",
"AddNote": "Add note",
"AddToQueueSuccessful": "File {filename} added to Queue.",
"AllJobs": "All",
@@ -348,16 +356,26 @@
"Cancel": "Cancel",
"Chart": "Chart",
"CreateNote": "Create Note",
+ "DateBasedReminder": "Date",
+ "DateBasedReminderDescription": "This reminder is based on the date.",
+ "Days": "days",
"Delete": "Delete",
"DeleteSelectedQuestion": "Do you really want to delete {count} selected jobs?",
"DeleteSingleJobQuestion": "Do you really want to delete the job?",
"Details": "Details",
+ "EditMaintenance": "Edit Maintenance",
"EditNote": "Edit Note",
"Empty": "empty",
"EndTime": "End Time",
+ "EntryCreatedAt": "Created at {date}.",
+ "EntryNextPerform": "Next perform:",
+ "EntryPerformedAt": "Performed at {date}.",
+ "EntrySince": "Used since:",
"EstimatedFilament": "Estimated Filament",
"EstimatedFilamentWeight": "Estimated Filament Weight",
"EstimatedTime": "Estimated Time",
+ "FilamentBasedReminder": "Filament",
+ "FilamentBasedReminderDescription": "This reminder is based on the filament usage.",
"FilamentCalc": "Filament Calc",
"FilamentUsage": "Filament usage",
"FilamentUsed": "Filament Used",
@@ -368,18 +386,35 @@
"FirstLayerHeight": "First Layer Height",
"HistoryFilamentUsage": "Filament",
"HistoryPrinttimeAVG": "Prints",
+ "Hours": "hours",
+ "InvalidNameEmpty": "Invalid name. Name must not be empty!",
"JobDetails": "Job Details",
"Jobs": "Jobs",
"LastModified": "Last Modified",
"LayerHeight": "Layer Height",
"LoadCompleteHistory": "Load complete history",
"LongestPrinttime": "Longest Print Time",
+ "Maintenance": "Maintenance",
+ "MaintenanceEntries": "Maintenance Entries",
+ "Meter": "meter",
+ "Name": "Name",
+ "NoReminder": "No reminder",
"Note": "Note",
"ObjectHeight": "Object Height",
+ "OneTime": "One-Time",
+ "Perform": "perform",
+ "Performed": "performed",
+ "PerformedAndReschedule": "performed and reschedule",
+ "PerformMaintenance": "Perform Maintenance",
"PrintDuration": "Print Time",
"PrintHistory": "Print History",
+ "PrintJobs": "Print Jobs",
"PrintTime": "Print Time",
"PrinttimeAvg": "Print Time - Ø",
+ "PrinttimeBasedReminder": "Print Time",
+ "PrinttimeBasedReminderDescription": "This reminder is based on the print time.",
+ "Reminder": "Reminder",
+ "Repeat": "Repeat",
"Reprint": "Reprint",
"Save": "save",
"Search": "search",
@@ -946,7 +981,8 @@
"DbConsoleHistory": "Console History",
"DbHistoryJobs": "History Jobs",
"DbHistoryTotals": "History Totals",
- "DBNavigation": "Navigation",
+ "DbMaintenance": "Maintenance",
+ "DbNavigation": "Navigation",
"DbTimelapseSettings": "Timelapse Settings",
"DbView": "View Settings",
"EstimateValues": {
@@ -1076,7 +1112,9 @@
"Autorender": "Autorender",
"AutorenderDescription": "If enabled, the timelapse video will automatically render at the end of the print",
"Camera": "Camera",
- "CameraDescription": "Select which camera should be used",
+ "CameraDescriptionWithSnapshotUrl": "Select which camera (with snapshot URL) should be used",
+ "CameraWarningAlreadySet": "This value is already set in the Moonraker configuration file.",
+ "CameraWarningAlreadySetSmall": "snapshoturl in the [timelapse] section",
"ConstantRateFactor": "Constant Rate Factor",
"ConstantRateFactorDescription": "This configure quality vs file size of the rendered video. The range of the CRF scale is 0–51, where 0 is lossless, 23 is the default and 51 is worst quality possible. A lower value generally leads to higher quality and a subjectively sane range is 17–28. Consider 17 or 18 to be visually lossless.",
"duplicatelastframe": "Duplicate Last Frame",
@@ -1094,6 +1132,7 @@
"HyperlapseCycleDescription": "A snapshot will be taken any X seconds",
"Mode": "Mode",
"ModeDescription": "Select between Layer macro and Hyperlapse (time-based) mode",
+ "NoWebcamFound": "No Webcam available",
"OutputFramerate": "Output Framerate",
"OutputFramerateDescription": "Defines the framerate of the video. Note: this will be ignored if variable_fps is enabled",
"Parkhead": "Park Toolhead",
@@ -1124,6 +1163,7 @@
"RulesZeroAndPositive": "Value must be 0 or greater!",
"SaveFrames": "Save Frames",
"SaveFramesDescription": "Save the frames to a zip-file for external rendering",
+ "SelectWebcam": "Select webcam...",
"StreamDelayCompensation": "Stream Delay Compensation",
"StreamDelayCompensationDescription": "Delay frame capture",
"Targetlength": "Target Length",
@@ -1140,9 +1180,9 @@
"VariableFps": "Variable FPS",
"VariableFpsDescription": "If enabled, the framerate of the output video will be calculated based on target length",
"VariableFpsMax": "Variable FPS max",
- "VariableFpsMaxDescription": "",
+ "VariableFpsMaxDescription": "Maximum variable FPS value",
"VariableFpsMin": "Variable FPS min",
- "VariableFpsMinDescription": ""
+ "VariableFpsMinDescription": "Minimum variable FPS value"
},
"UiSettingsTab": {
"BedScrewsDialog": "Bed Screws Dialog",
@@ -1190,6 +1230,8 @@
"PowerDeviceName": "Printer power device",
"PowerDeviceNameDescription": "Select which Moonraker power device should be used to power on the printer.",
"Primary": "Primary",
+ "PrintstatusThumbnailZoom": "Large thumbnail zoom",
+ "PrintstatusThumbnailZoomDescription": "This will deactivate the zoom effect of the thumbnail in the status panel.",
"ProgressAsFavicon": "Show progress as favicon",
"ProgressAsFaviconDescription": "Change the Mainsail logo favicon to a progress circle.",
"ScrewsTiltAdjustDialog": "Screws Tilt Adjust Dialog",
diff --git a/src/locales/uk.json b/src/locales/uk.json
index 684818cc9..a42f3ceec 100644
--- a/src/locales/uk.json
+++ b/src/locales/uk.json
@@ -13,8 +13,11 @@
"DeprecatedOptionHeadline": "Застаріла опція Klipper",
"DeprecatedValue": "Значення '{value}' опції '{option}' у секції '{section}' застаріла і буде видалена у майбутньому випуску.",
"DeprecatedValueHeadline": "Застаріле значення Klipper",
+ "KlipperRuntimeWarning": "Попередження під час роботи Klipper",
"KlipperWarning": "Попередження Klipper"
},
+ "MaintenanceReminder": "Нагадування про технічне обслуговування",
+ "MaintenanceReminderText": "Термін технічного обслуговування \"{name}\".",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "Помилка була виявлена під час завантаження компонента Moonraker '{component}'. Будь ласка, перевірте файл журналу та виправте проблему.",
@@ -28,7 +31,11 @@
"NextReboot": "наступне перезавантаження",
"NoNotification": "Нема Повідомлень",
"Notifications": "Повідомлення",
- "Remind": "Нагадування:"
+ "OneDayShort": "1д",
+ "OneHourShort": "1год",
+ "OneWeekShort": "1тиж",
+ "Remind": "Нагадування:",
+ "ShowDetails": "показати деталі"
},
"NumberInput": {
"GreaterOrEqualError": "Повинен бути більшим або рівним, ніж {min}!",
@@ -123,14 +130,14 @@
}
},
"BedScrews": {
- "Abort": "перервати",
- "Accept": "прийняти",
- "Adjusted": "відрегульований",
- "Description": "Натисніть ADJUSTED, якщо поточний гвинт було відрегульовано. Натисніть ACCEPT, щоб продовжити без коригування.",
+ "Abort": "Перервати",
+ "Accept": "Прийняти",
+ "Adjusted": "Відкореговано",
+ "Description": "Натисніть ВІДКОРЕГОВАНО, якщо поточний гвинт було відрегульовано. Натисніть ПРИЙНЯТИ, щоб продовжити без коригування.",
"Headline": "Гвинти столу",
"ScrewAccepted": "Готові гвинти",
- "ScrewIndex": "Індекс гвинта",
- "ScrewName": "Ім'я гвинта",
+ "ScrewIndex": "Номер гвинта",
+ "ScrewName": "Назва гвинта",
"ScrewOutput": "{current} з {max}"
},
"ConnectionDialog": {
@@ -139,7 +146,7 @@
"Connecting": "Підключення до {host}",
"Failed": "Підключення не вдалося",
"Initializing": "Ініціалізація",
- "TryAgain": "спробуйте ще раз"
+ "TryAgain": "Спробуйте ще раз"
},
"Console": {
"CommandList": "Список команд",
@@ -149,6 +156,12 @@
"SendCode": "Надіслати код...",
"SetupConsole": "Консоль налаштування"
},
+ "CoolDownDialog": {
+ "AreYouSure": "Ти впевнений?",
+ "CoolDown": "Охолодження",
+ "No": "Ні",
+ "Yes": "Так"
+ },
"DevicesDialog": {
"CanBusInfo": "Можна виявити лише непризначені вузли. Рекомендується мати лише один непризначений пристрій, підключений до шини can, щоб уникнути проблем зі зв’язком. Щоб отримати детальнішу інформацію, натисніть на посилання:",
"ClickRefresh": "Натисніть кнопку оновити, щоб знайти пристрої.",
@@ -160,7 +173,7 @@
"NoDeviceFound": "Пристрій не знайдено. Перевірте підключення та натисніть кнопку оновити.",
"PathByHardware": "Фізичний шлях",
"PathById": "Шлях за ID",
- "Refresh": "оновити",
+ "Refresh": "Оновити",
"Resolutions": "Резолюції"
},
"Dialogs": {
@@ -169,7 +182,7 @@
"DoYouWantToStartFilename": "Ви хочете роздрукувати {filename}?",
"DoYouWantToStartFilenameFilament": "Хочете почати друк {filename} з наявного філаменту?",
"Headline": "Почати друк",
- "Print": "друк",
+ "Print": "Друк",
"Timelapse": "Таймлапс"
}
},
@@ -316,7 +329,7 @@
"InvalidNameAlreadyExists": "Ім'я профілю вже існує, будь ласка, виберіть інше ім'я профілю.",
"InvalidNameAscii": "Ім'я недійсне. Дозволяється лише символи ascii.",
"InvalidNameEmpty": "Введення не повинно бути порожнім!",
- "InvalidNameReserved": "Profile 'default' зарезервовано, будь ласка, виберіть інше ім'я профілю.",
+ "InvalidNameReserved": "Профіль 'default' зарезервовано, будь ласка, виберіть інше ім'я профілю.",
"Mesh": "Сітка",
"Name": "Ім'я",
"NoBedMeshHasBeenLoadedYet": "Жодна сітка для ліжка ще не була завантажена.",
@@ -334,6 +347,8 @@
"Wireframe": "Каркас"
},
"History": {
+ "AddANote": "Додати примітку",
+ "AddMaintenance": "Додати обслуговування",
"AddNote": "Додати коментар",
"AddToQueueSuccessful": "Файл {filename} додано до черги.",
"AllJobs": "ВСІ",
@@ -341,16 +356,26 @@
"Cancel": "Скасувати",
"Chart": "Графік",
"CreateNote": "Створити Примітку",
+ "DateBasedReminder": "Дата",
+ "DateBasedReminderDescription": "Це нагадування базується на даті.",
+ "Days": "днів",
"Delete": "Видалити",
"DeleteSelectedQuestion": "Ви дійсно хочете видалити {count} вибране завдання?",
"DeleteSingleJobQuestion": "Ви справді хочете видалити завдання?",
"Details": "Деталі",
+ "EditMaintenance": "Редагувати технічне обслуговування",
"EditNote": "Редагувати Примітку",
"Empty": "порожньо",
"EndTime": "Час Закінчення",
+ "EntryCreatedAt": "Створено {date}.",
+ "EntryNextPerform": "Наступне виконання:",
+ "EntryPerformedAt": "Виконано {date}.",
+ "EntrySince": "Використовується з:",
"EstimatedFilament": "Орієнтовна довжина прутка",
"EstimatedFilamentWeight": "Орієнтовна вага прутка",
"EstimatedTime": "Орієнтовний Час",
+ "FilamentBasedReminder": "Філамент",
+ "FilamentBasedReminderDescription": "Це нагадування базується на використанні філаменту.",
"FilamentCalc": "Калькулятор Прутка",
"FilamentUsage": "Використання Прутка",
"FilamentUsed": "Використано Прутка",
@@ -361,18 +386,36 @@
"FirstLayerHeight": "Висота першого шару",
"HistoryFilamentUsage": "Пруток",
"HistoryPrinttimeAVG": "Друк",
+ "Hours": "годин",
+ "InvalidNameEmpty": "Недійсне ім'я. Поле не повинно бути пустим!",
"JobDetails": "Деталі Завдань",
"Jobs": "Завдання",
"LastModified": "Дата Створення",
"LayerHeight": "Висота Шару",
"LoadCompleteHistory": "Завантажити повну історію",
"LongestPrinttime": "Найдовший Час Друку",
+ "Maintenance": "Технічне обслуговування",
+ "MaintenanceEntries": "Записи технічного обслуговування",
+ "Meter": "метр",
+ "Name": "Назва",
+ "NoReminder": "Нагадування відсутні",
"Note": "Примітка",
"ObjectHeight": "Висота Об'єкта",
+
+ "OneTime": "Одноразовий",
+ "Perform": "виконати",
+ "Performed": "виконано",
+ "PerformedAndReschedule": "виконано та переплановано",
+ "PerformMaintenance": "Виконати технічне обслуговування",
"PrintDuration": "Час Друку",
"PrintHistory": "Історія друку",
+ "PrintJobs": "Завдання друку",
"PrintTime": "Час Друку",
"PrinttimeAvg": "Час Друку - Ø",
+ "PrinttimeBasedReminder": "Час друку",
+ "PrinttimeBasedReminderDescription": "Це нагадування базується на часу друку.",
+ "Reminder": "Нагадування",
+ "Repeat": "Повторити",
"Reprint": "Передрукувати",
"Save": "зберегти",
"Search": "пошук",
@@ -589,7 +632,9 @@
"KlippyStatePanel": {
"CheckKlippyAndUdsAddress": "Будь ласка, перевірте, чи працює служба Klipper і чи правильно налаштовано klippy_uds_address у файлі moonraker.conf.",
"FirmwareRestart": "Перезапуск Прошивки",
+ "KlipperLog": "Журнал Klipper",
"MoonrakerCannotConnect": "Moonraker не може підключитися до Klipper!",
+ "MoonrakerLog": "Журнал Moonraker",
"PowerOn": "Увімкнено",
"PrinterSwitchedOff": "Принтер вимкнений",
"PrinterSwitchedOffDescription": "Принтер наразі вимкнено, і Klipper не може підключитися. Щоб увімкнути принтер, натисніть кнопку нижче:",
@@ -608,7 +653,7 @@
},
"MacrosPanel": {
"Headline": "Макрос",
- "Send": "відправити"
+ "Send": "Відправити"
},
"MiniconsolePanel": {
"Autoscroll": "Автопрокручування",
@@ -620,9 +665,9 @@
},
"MinSettingsPanel": {
"IncludeMainsailCfg": "Переконайтеся, що ви включили mainsail.cfg у свій файл printer.cfg.",
- "IsNotDefinedInConfig": "не визначається в конфігурації.",
+ "IsNotDefinedInConfig": "Відсутній імпорт в конфігурації.",
"MissingConfiguration": "Відсутня конфігурація",
- "MoreInformation": "більше інформації"
+ "MoreInformation": "Більше інформації"
},
"MiscellaneousPanel": {
"Headline": "Різне",
@@ -801,11 +846,11 @@
"Webcam": "Веб-камера"
},
"ScrewsTiltAdjust": {
- "Accept": "прийняти",
- "Base": "Основа",
- "ErrorText": "Щось пішло не так під час процесу взяття проби.",
- "Headline": "Гвинти регулювання нахилу",
- "Retry": "повторити спробу"
+ "Accept": "Прийняти",
+ "Base": "Базовий",
+ "ErrorText": "Щось пішло не так під час взяття проби.",
+ "Headline": "Регулювання нахилу взяттям проби",
+ "Retry": "Повторити спробу"
},
"SelectPrinterDialog": {
"AddPrinter": "Додати Принтер",
@@ -819,6 +864,8 @@
"HostnameInvalid": "Недійсне ім'я хоста/IP",
"HostnameIp": "Ім'я хоста/IP",
"HostnameRequired": "Необхідне ім'я хоста",
+ "Name": "Назва",
+ "Path": "Шлях",
"Port": "Порт",
"PortRequired": "Потрібен порт",
"RememberToAdd": "Будь ласка, не забудьте додати '{cors}' у moonraker.conf всередині 'cors_domains'.",
@@ -935,7 +982,8 @@
"DbConsoleHistory": "Історія Консолі",
"DbHistoryJobs": "Історія завдань",
"DbHistoryTotals": "Загальна Історія",
- "DBNavigation": "Навігація",
+ "DbMaintenance": "Технічне обслуговування",
+ "DbNavigation": "Навігація",
"DbTimelapseSettings": "Налаштування Таймлапсу",
"DbView": "Налаштування перегляду",
"EstimateValues": {
@@ -1052,6 +1100,9 @@
"AddPrinter": "Додати принтер",
"EditPrinter": "Редагувати принтер",
"Hostname": "Ім'я хоста/ІР-адреса",
+ "Name": "Назва",
+ "NameDescription": "Це ім’я не відображатиметься в графічному інтерфейсі і використовуватиметься лише для перенаправлення.",
+ "Path": "Шлях",
"Port": "Порт",
"RemotePrinters": "Принтери",
"UpdatePrinter": "Оновити принтер",
@@ -1138,6 +1189,8 @@
"BoolBigThumbnailDescription": "Відобразити велику мініатюру на панелі статусу під час друку.",
"BoolHideUploadAndPrintButton": "Приховати кнопку завантаження та друк",
"BoolHideUploadAndPrintButtonDescription": "Показати або приховати кнопку «Завантажити та друканути» у верхній панелі.",
+ "ConfirmOnCoolDown": "Вимагати підтвердження для Охолодження",
+ "ConfirmOnCoolDownDescription": "Показати діалогове вікно підтвердження для Охолодження",
"ConfirmOnEmergencyStop": "Вимагати підтвердження на АВАРІЙНУ ЗУПИНКУ",
"ConfirmOnEmergencyStopDescription": "Показати діалогове вікно підтвердження АВАРІЙНОЇ ЗУПИНКИ",
"ConfirmOnPowerDeviceChange": "Вимагати підтвердження змін живлення пристрою",
@@ -1172,6 +1225,8 @@
"PowerDeviceName": "Пристрій живлення принтера",
"PowerDeviceNameDescription": "Виберіть, який пристрій живлення Moonraker слід використовувати для живлення принтера.",
"Primary": "Основний Колір",
+ "ProgressAsFavicon": "Показати прогрес у вкладці браузера",
+ "ProgressAsFaviconDescription": "Змініть іконку логотипу Mainsail на іконку прогресу.",
"ScrewsTiltAdjustDialog": "Вікно налаштування гвинтів нахилу стола",
"ScrewsTiltAdjustDialogDescription": "Відобразити допоміжне вікно для SCREWS_TILT_CALCULATE.",
"TempchartHeight": "Висота графіку температур",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index f33d907d1..9ff1fadbf 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -13,8 +13,11 @@
"DeprecatedOptionHeadline": "Klipper选项已被弃用",
"DeprecatedValue": "在'{section}'标签中的'{option}'选项的参数'{value}'已被弃用,将在未来的版本中删除。",
"DeprecatedValueHeadline": "Klipper参数已被弃用",
+ "KlipperRuntimeWarning": "Klipper运行时警告",
"KlipperWarning": "Klipper警告"
},
+ "MaintenanceReminder": "保养提醒",
+ "MaintenanceReminderText": "\"{name}\" 已到保养期。",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker:{component}",
"MoonrakerFailedComponentDescription": "加载moonraker组件'{component}'时发生错误。请检查日志文件并修复此问题。",
@@ -28,7 +31,11 @@
"NextReboot": "下次重启",
"NoNotification": "没有可用的通知",
"Notifications": "通知",
- "Remind": "提醒:"
+ "OneDayShort": "1天",
+ "OneHourShort": "1小时",
+ "OneWeekShort": "1周",
+ "Remind": "提醒:",
+ "ShowDetails": "显示详情"
},
"NumberInput": {
"GreaterOrEqualError": "必须大于或等于{min}!",
@@ -149,6 +156,12 @@
"SendCode": "输入要执行的代码...",
"SetupConsole": "设置控制台"
},
+ "CoolDownDialog": {
+ "AreYouSure": "是否确定?",
+ "CoolDown": "降温",
+ "No": "取消",
+ "Yes": "确定"
+ },
"DevicesDialog": {
"CanBusInfo": "只有未分配的节点才能被检测到。建议仅连接一个未分配的设备到CAN总线,以避免通信问题。更多详情,请点击链接:",
"ClickRefresh": "点击刷新按钮以搜索设备。",
@@ -334,23 +347,35 @@
"Wireframe": "线框"
},
"History": {
- "AddNote": "添加便条",
+ "AddANote": "添加一条备注",
+ "AddMaintenance": "添加保养提醒",
+ "AddNote": "添加备注",
"AddToQueueSuccessful": "文件{filename}已添加到队列。",
"AllJobs": "全部",
"AvgPrinttime": "平均打印时长",
"Cancel": "取消",
"Chart": "图表",
- "CreateNote": "新建便条",
+ "CreateNote": "新建备注",
+ "DateBasedReminder": "日期",
+ "DateBasedReminderDescription": "本提醒基于日期。",
+ "Days": "天",
"Delete": "删除",
"DeleteSelectedQuestion": "你是否要删除已选中的{count}个任务?",
"DeleteSingleJobQuestion": "你是否要删除这个任务?",
"Details": "详情",
- "EditNote": "编辑便条",
+ "EditMaintenance": "编辑保养提醒",
+ "EditNote": "编辑备注",
"Empty": "没有内容",
"EndTime": "结束时间",
+ "EntryCreatedAt": "创建于 {date}。",
+ "EntryNextPerform": "下次执行:",
+ "EntryPerformedAt": "已于 {date} 执行。",
+ "EntrySince": "启用时间:",
"EstimatedFilament": "预估耗材用量",
"EstimatedFilamentWeight": "预估耗材重量",
"EstimatedTime": "预估打印时长",
+ "FilamentBasedReminder": "耗材",
+ "FilamentBasedReminderDescription": "本提醒基于耗材使用量。",
"FilamentCalc": "耗材预估长度",
"FilamentUsage": "耗材用量",
"FilamentUsed": "实际耗材消耗量",
@@ -361,18 +386,35 @@
"FirstLayerHeight": "首层高度",
"HistoryFilamentUsage": "耗材用量",
"HistoryPrinttimeAVG": "打印时长",
+ "Hours": "小时",
+ "InvalidNameEmpty": "无效名称。名称不能为空!",
"JobDetails": "任务详情",
"Jobs": "每页显示任务数",
"LastModified": "修改日期",
"LayerHeight": "层高",
"LoadCompleteHistory": "加载完整历史记录",
"LongestPrinttime": "最长打印时长",
- "Note": "便条",
+ "Maintenance": "保养",
+ "MaintenanceEntries": "保养",
+ "Meter": "米",
+ "Name": "名称",
+ "NoReminder": "不提醒",
+ "Note": "备注",
"ObjectHeight": "物体高度",
+ "OneTime": "一次性",
+ "Perform": "执行",
+ "Performed": "已执行",
+ "PerformedAndReschedule": "已执行并重新安排提醒",
+ "PerformMaintenance": "执行保养",
"PrintDuration": "打印耗时",
"PrintHistory": "打印历史",
+ "PrintJobs": "打印任务",
"PrintTime": "打印持续时间",
"PrinttimeAvg": "平均打印时长",
+ "PrinttimeBasedReminder": "打印时长",
+ "PrinttimeBasedReminderDescription": "本提醒基于打印时长",
+ "Reminder": "提醒",
+ "Repeat": "重复",
"Reprint": "重新打印",
"Save": "保存",
"Search": "搜索",
@@ -499,7 +541,7 @@
},
"UpdatePanel": {
"Abort": "中止",
- "AreYouSure": "你确定吗?",
+ "AreYouSure": "是否确定?",
"CheckForUpdates": "检查更新",
"Close": "关闭",
"CommitHistory": "查看提交记录",
@@ -589,7 +631,9 @@
"KlippyStatePanel": {
"CheckKlippyAndUdsAddress": "请检查Klipper服务是否启动。",
"FirmwareRestart": "重启Klipper固件",
+ "KlipperLog": "Klipper 日志",
"MoonrakerCannotConnect": "Moonraker无法连接到Klipper !",
+ "MoonrakerLog": "Moonraker 日志",
"PowerOn": "开启电源",
"PrinterSwitchedOff": "打印机电源已关闭",
"PrinterSwitchedOffDescription": "打印机电源已关闭,Klipper无法连接。点击下方按钮开启打印机电源:",
@@ -738,8 +782,8 @@
"Presets": "预设",
"SetupTemperatures": "配置温度",
"ShowChart": "显示图表",
- "ShowNameInChart": "在图表中显示{name} ",
- "ShowNameInList": "在列表中显示{name} ",
+ "ShowNameInChart": "在图表中显示{name}",
+ "ShowNameInList": "在列表中显示{name}",
"State": "状态",
"Target": "目标",
"TemperaturesInChart": "温度 [°C]",
@@ -784,8 +828,8 @@
"PowerDeviceChangeDialog": {
"AreYouSure": "是否确定?",
"No": "取消",
- "TurnDeviceOff": "关闭{device} ",
- "TurnDeviceOn": "打开{device} ",
+ "TurnDeviceOff": "关闭{device}",
+ "TurnDeviceOn": "打开{device}",
"Yes": "确定"
},
"Router": {
@@ -819,6 +863,8 @@
"HostnameInvalid": "不可用的主机名称/IP地址",
"HostnameIp": "主机名称/IP地址",
"HostnameRequired": "需要配置主机名称/IP地址",
+ "Name": "名称",
+ "Path": "路径",
"Port": "Moonraker服务端口号",
"PortRequired": "需要配置Moonraker服务端口号",
"RememberToAdd": "请在moonraker.conf的'cors_domains'标签中添加'{cors}'",
@@ -935,7 +981,8 @@
"DbConsoleHistory": "控制台历史记录",
"DbHistoryJobs": "历史任务",
"DbHistoryTotals": "历史总计",
- "DBNavigation": "侧边栏",
+ "DbMaintenance": "保养",
+ "DbNavigation": "侧边栏",
"DbTimelapseSettings": "延时摄影设置",
"DbView": "视图设置",
"EstimateValues": {
@@ -1052,6 +1099,9 @@
"AddPrinter": "添加打印机",
"EditPrinter": "编辑打印机",
"Hostname": "主机名称/IP地址",
+ "Name": "名称",
+ "NameDescription": "此名称仅用于重定向,并不会在GUI上显示。",
+ "Path": "路径",
"Port": "Moonraker服务端口",
"RemotePrinters": "打印机",
"UpdatePrinter": "更新打印机",
@@ -1138,6 +1188,8 @@
"BoolBigThumbnailDescription": "打印时在状态框中显示大缩略图",
"BoolHideUploadAndPrintButton": "隐藏\"上传并打印\"按钮",
"BoolHideUploadAndPrintButtonDescription": "在顶栏显示或者隐藏\"上传并打印\"按钮",
+ "ConfirmOnCoolDown": "冷却需要确认",
+ "ConfirmOnCoolDownDescription": "在按下\"冷却\"时显示确认对话框",
"ConfirmOnEmergencyStop": "紧急停止需要确认",
"ConfirmOnEmergencyStopDescription": "在按下\"紧急停止\"时显示确认对话框",
"ConfirmOnPowerDeviceChange": "控制设备电源时需要确认",
@@ -1172,6 +1224,8 @@
"PowerDeviceName": "打印机电源设备",
"PowerDeviceNameDescription": "请选择可以控制打印机供电的Moonraker电源设备。",
"Primary": "高亮颜色",
+ "ProgressAsFavicon": "显示进度到浏览器标签",
+ "ProgressAsFaviconDescription": "将浏览器标签的Mainsail图标更改为进度圆圈。",
"ScrewsTiltAdjustDialog": "螺丝倾斜调整对话框",
"ScrewsTiltAdjustDialogDescription": "显示SCREWS_TILT_CALCULATE辅助对话框。",
"TempchartHeight": "温度图表高度",
diff --git a/src/main.ts b/src/main.ts
index 93ebf69b0..7cff4da1c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -66,8 +66,11 @@ Vue.use(VueResize)
const initLoad = async () => {
try {
+ // get base url. by default, it is '/'
+ const base = import.meta.env.BASE_URL ?? '/'
+
//load config.json
- const res = await fetch('/config.json')
+ const res = await fetch(`${base}config.json`)
const file = (await res.json()) as Record
window.console.debug('Loaded config.json')
diff --git a/src/pages/History.vue b/src/pages/History.vue
index 84ef0fd35..4a304f4c4 100644
--- a/src/pages/History.vue
+++ b/src/pages/History.vue
@@ -17,11 +17,9 @@ import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import HistoryListPanel from '@/components/panels/HistoryListPanel.vue'
import HistoryStatisticsPanel from '@/components/panels/HistoryStatisticsPanel.vue'
+
@Component({
- components: {
- HistoryStatisticsPanel,
- HistoryListPanel,
- },
+ components: { HistoryListPanel, HistoryStatisticsPanel },
})
export default class PageHistory extends Mixins(BaseMixin) {}
diff --git a/src/plugins/helpers.ts b/src/plugins/helpers.ts
index 8cbd54a94..719408ce8 100644
--- a/src/plugins/helpers.ts
+++ b/src/plugins/helpers.ts
@@ -112,30 +112,30 @@ export const formatFrequency = (frequency: number): string => {
return Math.max(frequency, 0.1).toFixed() + units[i]
}
-export const formatPrintTime = (totalSeconds: number): string => {
- if (totalSeconds) {
- let output = ''
+export const formatPrintTime = (totalSeconds: number, boolDays = true): string => {
+ if (!totalSeconds) return '--'
+ const output: string[] = []
+
+ if (boolDays) {
const days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= 3600 * 24
- output += days + 'd'
+ output.push(`${days}d`)
}
+ }
- const hours = Math.floor(totalSeconds / 3600)
- totalSeconds %= 3600
- if (hours) output += ' ' + hours + 'h'
-
- const minutes = Math.floor(totalSeconds / 60)
- if (minutes) output += ' ' + minutes + 'm'
+ const hours = Math.floor(totalSeconds / 3600)
+ totalSeconds %= 3600
+ if (hours) output.push(`${hours}h`)
- const seconds = totalSeconds % 60
- if (seconds) output += ' ' + seconds.toFixed(0) + 's'
+ const minutes = Math.floor(totalSeconds / 60)
+ if (minutes) output.push(`${minutes}m`)
- return output
- }
+ const seconds = totalSeconds % 60
+ if (seconds) output.push(`${seconds.toFixed(0)}s`)
- return '--'
+ return output.join(' ')
}
export const sortFiles = (items: FileStateFile[] | null, sortBy: string[], sortDesc: boolean[]): FileStateFile[] => {
diff --git a/src/plugins/router.ts b/src/plugins/router.ts
index f211d1918..2d31ce432 100644
--- a/src/plugins/router.ts
+++ b/src/plugins/router.ts
@@ -3,6 +3,10 @@ import Vue from 'vue'
import routes from '@/routes'
Vue.use(VueRouter)
-const router = new VueRouter({ mode: 'history', routes })
+const router = new VueRouter({
+ base: import.meta.env.BASE_URL,
+ mode: 'history',
+ routes,
+})
export default router
diff --git a/src/store/actions.ts b/src/store/actions.ts
index ba07f8d86..05ddc62d7 100644
--- a/src/store/actions.ts
+++ b/src/store/actions.ts
@@ -19,6 +19,7 @@ export const actions: ActionTree = {
dispatch('socket/setSocket', {
hostname: printerSocket.hostname,
port: printerSocket.port,
+ path: printerSocket.path,
})
},
diff --git a/src/store/gui/actions.ts b/src/store/gui/actions.ts
index 1902d6e8a..ab362ee99 100644
--- a/src/store/gui/actions.ts
+++ b/src/store/gui/actions.ts
@@ -246,7 +246,7 @@ export const actions: ActionTree = {
}
for (const key of payload) {
- if (['webcams', 'timelapse'].includes(key)) {
+ if (['maintenance', 'timelapse', 'webcams'].includes(key)) {
const url = baseUrl + '?namespace=' + key
const response = await fetch(url)
diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts
index db1338865..6f94a5344 100644
--- a/src/store/gui/index.ts
+++ b/src/store/gui/index.ts
@@ -20,6 +20,7 @@ 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 { maintenance } from '@/store/gui/maintenance'
import { webcams } from '@/store/gui/webcams'
import { heightmap } from '@/store/gui/heightmap'
@@ -180,6 +181,7 @@ export const getDefaultState = (): GuiState => {
boolScrewsTiltAdjustDialog: true,
tempchartHeight: 250,
hideUpdateWarnings: false,
+ printstatusThumbnailZoom: true,
},
view: {
blockFileUpload: false,
@@ -250,6 +252,8 @@ export const getDefaultState = (): GuiState => {
'object_height',
],
selectedJobs: [],
+ showMaintenanceEntries: true,
+ showPrintJobs: true,
},
jobqueue: {
countPerPage: 10,
@@ -301,6 +305,7 @@ export const gui: Module = {
console,
gcodehistory,
macros,
+ maintenance,
miscellaneous,
navigation,
notifications,
diff --git a/src/store/gui/maintenance/actions.ts b/src/store/gui/maintenance/actions.ts
new file mode 100644
index 000000000..1f3530dff
--- /dev/null
+++ b/src/store/gui/maintenance/actions.ts
@@ -0,0 +1,185 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { GuiMaintenanceState, MaintenanceJson } from '@/store/gui/maintenance/types'
+import { RootState } from '@/store/types'
+import { v4 as uuidv4 } from 'uuid'
+import { themeDir } from '@/store/variables'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init() {
+ Vue.$socket.emit(
+ 'server.database.get_item',
+ { namespace: 'maintenance' },
+ { action: 'gui/maintenance/initStore' }
+ )
+ },
+
+ async initDb({ dispatch, rootGetters }) {
+ const baseUrl = rootGetters['socket/getUrl']
+ const url = `${baseUrl}/server/files/config/${themeDir}/maintenance.json?time=${Date.now()}`
+
+ const defaults: MaintenanceJson = await fetch(url)
+ .then((response) => {
+ if (response.status !== 200) return { entries: [] }
+
+ return response.json()
+ })
+ .catch((e) => {
+ window.console.error('maintenance.json cannot be parsed', e)
+ return { entries: [] }
+ })
+
+ // stop, when no entries are available/found
+ const entries = defaults.entries ?? []
+ if (entries?.length === 0) {
+ Vue.$socket.emit('server.database.post_item', {
+ namespace: 'maintenance',
+ key: uuidv4(),
+ value: {
+ name: 'MAINTENANCE_INIT',
+ },
+ })
+
+ return
+ }
+
+ const totals = await fetch(`${baseUrl}/server/history/totals`)
+ .then((response) => {
+ if (response.status !== 200) return {}
+
+ return response.json()
+ })
+ .then((response: any) => response.result?.job_totals ?? {})
+ .catch((e) => {
+ window.console.debug('History totals could not be loaded', e)
+ })
+
+ const total_filament = totals.total_filament_used ?? 0
+ const total_print_time = totals.total_print_time ?? 0
+ const date = new Date().getTime() / 1000
+
+ entries.forEach((entry) => {
+ dispatch('store', {
+ entry: {
+ name: entry.name,
+ note: entry.note ?? '',
+ start_time: date,
+ end_time: null,
+ start_filament: total_filament,
+ end_filament: null,
+ start_printtime: total_print_time,
+ end_printtime: null,
+ last_entry: null,
+
+ reminder: {
+ type: entry.reminder?.type ?? null,
+
+ filament: {
+ bool: entry.reminder?.filament?.bool ?? false,
+ value: entry.reminder?.filament?.value ?? null,
+ },
+
+ printtime: {
+ bool: entry.reminder?.printtime?.bool ?? false,
+ value: entry.reminder?.printtime?.value ?? null,
+ },
+
+ date: {
+ bool: entry.reminder?.date?.bool ?? false,
+ value: entry.reminder?.date?.value ?? null,
+ },
+ },
+ },
+ })
+ })
+ },
+
+ async initStore({ commit, dispatch }, payload) {
+ await commit('reset')
+
+ const entries = payload.value ?? {}
+ const initKey = Object.keys(entries).find((key) => entries[key]?.name === 'MAINTENANCE_INIT')
+ if (initKey) delete entries[initKey]
+
+ await commit('initStore', entries)
+ await dispatch('socket/removeInitModule', 'gui/maintenance/init', { root: true })
+ },
+
+ upload(_, payload) {
+ Vue.$socket.emit('server.database.post_item', {
+ namespace: 'maintenance',
+ key: payload.id,
+ value: payload.value,
+ })
+ },
+
+ store({ commit, dispatch, state }, payload) {
+ const id = uuidv4()
+
+ commit('store', { id, values: payload.entry })
+ dispatch('upload', {
+ id,
+ value: state.entries[id],
+ })
+ },
+
+ update({ commit, dispatch }, payload) {
+ const id = payload.id
+ delete payload.id
+
+ commit('update', {
+ id: id,
+ entry: payload,
+ })
+ dispatch('upload', {
+ id: id,
+ value: payload,
+ })
+ },
+
+ delete({ commit }, payload) {
+ commit('delete', payload)
+ Vue.$socket.emit('server.database.delete_item', { namespace: 'maintenance', key: payload })
+ },
+
+ perform({ dispatch, state, rootState }, payload: { id: string; note: string }) {
+ const entry = state.entries[payload.id]
+ if (!entry) return
+
+ const totalFilament = rootState.server?.history?.job_totals?.total_filament_used ?? 0
+ const totalPrintTime = rootState.server?.history?.job_totals?.total_print_time ?? 0
+
+ entry.id = payload.id
+ entry.end_time = Date.now() / 1000
+ entry.end_filament = totalFilament
+ entry.end_printtime = totalPrintTime
+ entry.perform_note = payload.note.trim() || null
+
+ dispatch('update', entry)
+
+ if (entry.reminder.type === 'repeat') {
+ const date = new Date()
+
+ dispatch('store', {
+ entry: {
+ name: entry.name,
+ note: entry.note,
+ // divided by 1000 to get seconds, because history entries are also in seconds
+ start_time: date.getTime() / 1000,
+ end_time: null,
+ start_filament: totalFilament,
+ end_filament: null,
+ start_printtime: totalPrintTime,
+ end_printtime: null,
+ last_entry: payload.id,
+
+ reminder: { ...entry.reminder },
+ },
+ })
+ }
+ },
+}
diff --git a/src/store/gui/maintenance/getters.ts b/src/store/gui/maintenance/getters.ts
new file mode 100644
index 000000000..2801a5e67
--- /dev/null
+++ b/src/store/gui/maintenance/getters.ts
@@ -0,0 +1,47 @@
+import { GetterTree } from 'vuex'
+import { GuiMaintenanceState, GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {
+ getEntries: (state) => {
+ const entries: GuiMaintenanceStateEntry[] = []
+
+ Object.keys(state.entries).forEach((id: string) => {
+ entries.push({ ...state.entries[id], id })
+ })
+
+ return entries
+ },
+
+ getOverdueEntries: (state, getters, rootState) => {
+ const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time ?? 0
+ const currentTotalFilamentUsed = rootState.server.history.job_totals.total_filament_used ?? 0
+ const currentDate = new Date().getTime() / 1000
+
+ const entries: GuiMaintenanceStateEntry[] = getters['getEntries'] ?? []
+
+ return entries.filter((entry) => {
+ if (entry.reminder.type === null || entry.end_time !== null) return false
+
+ if (entry.reminder.filament.bool) {
+ const end = entry.start_filament + (entry.reminder.filament.value ?? 0)
+
+ if (end <= currentTotalFilamentUsed) return true
+ }
+
+ if (entry.reminder.printtime.bool) {
+ const end = entry.start_printtime + (entry.reminder.printtime.value ?? 0) * 3600
+
+ if (end <= currentTotalPrintTime) return true
+ }
+
+ if (entry.reminder.date.bool) {
+ const end = entry.start_time + (entry.reminder.date.value ?? 0) * 24 * 60 * 60
+
+ if (end <= currentDate) return true
+ }
+
+ return false
+ })
+ },
+}
diff --git a/src/store/gui/maintenance/index.ts b/src/store/gui/maintenance/index.ts
new file mode 100644
index 000000000..fdfcc39b3
--- /dev/null
+++ b/src/store/gui/maintenance/index.ts
@@ -0,0 +1,23 @@
+import { Module } from 'vuex'
+import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
+import { actions } from '@/store/gui/maintenance/actions'
+import { mutations } from '@/store/gui/maintenance/mutations'
+import { getters } from '@/store/gui/maintenance/getters'
+
+export const getDefaultState = (): GuiMaintenanceState => {
+ return {
+ entries: {},
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const maintenance: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/gui/maintenance/mutations.ts b/src/store/gui/maintenance/mutations.ts
new file mode 100644
index 000000000..e7d559d9a
--- /dev/null
+++ b/src/store/gui/maintenance/mutations.ts
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import { MutationTree } from 'vuex'
+import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
+import { getDefaultState } from './index'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ initStore(state, payload) {
+ Vue.set(state, 'entries', payload)
+ },
+
+ store(state, payload) {
+ Vue.set(state.entries, payload.id, payload.values)
+ },
+
+ update(state, payload) {
+ if (!(payload.id in state.entries)) return
+
+ const entry = { ...state.entries[payload.id] }
+ Object.assign(entry, payload.entry)
+ Vue.set(state.entries, payload.id, entry)
+ },
+
+ delete(state, payload) {
+ if (payload in state.entries) {
+ Vue.delete(state.entries, payload)
+ }
+ },
+}
diff --git a/src/store/gui/maintenance/types.ts b/src/store/gui/maintenance/types.ts
new file mode 100644
index 000000000..cb63e8253
--- /dev/null
+++ b/src/store/gui/maintenance/types.ts
@@ -0,0 +1,67 @@
+export interface GuiMaintenanceState {
+ entries: {
+ [key: string]: GuiMaintenanceStateEntry
+ }
+}
+
+export interface GuiMaintenanceStateEntry {
+ id?: string
+ name: string
+ note: string
+ perform_note: string | null
+ start_time: number
+ end_time: number | null
+ start_filament: number
+ end_filament: number | null
+ start_printtime: number
+ end_printtime: number | null
+ last_entry: string | null
+
+ reminder: {
+ type: null | 'one-time' | 'repeat'
+
+ filament: {
+ bool: boolean
+ value: number | null
+ }
+
+ printtime: {
+ bool: boolean
+ value: number | null
+ }
+
+ date: {
+ bool: boolean
+ value: number | null
+ }
+ }
+}
+
+export interface HistoryListRowMaintenance extends GuiMaintenanceStateEntry {
+ type: 'maintenance'
+ select_id: string
+}
+
+export interface MaintenanceJson {
+ entries: MaintenanceJsonEntry[]
+}
+
+interface MaintenanceJsonEntry {
+ name: string
+ note?: string
+ reminder?: {
+ type: null | 'one-time' | 'repeat'
+ filament?: {
+ bool: boolean
+ value: number | null
+ }
+ printtime?: {
+ bool: boolean
+ value: number | null
+ }
+ date?: {
+ bool: boolean
+ value: number | null
+ }
+ }
+}
diff --git a/src/store/gui/notifications/getters.ts b/src/store/gui/notifications/getters.ts
index a8ddaa934..7364dd0a5 100644
--- a/src/store/gui/notifications/getters.ts
+++ b/src/store/gui/notifications/getters.ts
@@ -8,6 +8,7 @@ import { PrinterStateKlipperConfigWarning } from '@/store/printer/types'
import { detect } from 'detect-browser'
import semver from 'semver'
import { minBrowserVersions } from '@/store/variables'
+import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
export const getters: GetterTree = {
getNotifications: (state, getters) => {
@@ -34,6 +35,9 @@ export const getters: GetterTree = {
// klipper warnings
notifications = notifications.concat(getters['getNotificationsKlipperWarnings'])
+ // user-created reminders
+ notifications = notifications.concat(getters['getNotificationsOverdueMaintenance'])
+
// browser warnings
notifications = notifications.concat(getters['getNotificationsBrowserWarnings'])
@@ -363,6 +367,37 @@ export const getters: GetterTree = {
return notifications
},
+ getNotificationsOverdueMaintenance: (state, getters, rootState, rootGetters) => {
+ const notifications: GuiNotificationStateEntry[] = []
+ let entries: GuiMaintenanceStateEntry[] = rootGetters['gui/maintenance/getOverdueEntries']
+ if (entries.length == 0) return []
+
+ const date = rootState.server.system_boot_at ?? new Date()
+
+ // get all dismissed reminders and convert it to a string[]
+ const remindersDismisses = rootGetters['gui/notifications/getDismissByCategory']('maintenance').map(
+ (dismiss: GuiNotificationStateDismissEntry) => {
+ return dismiss.id
+ }
+ )
+
+ // filter all dismissed reminders
+ entries = entries.filter((entry) => !remindersDismisses.includes(entry.id))
+
+ entries.forEach((entry) => {
+ notifications.push({
+ id: `maintenance/${entry.id}`,
+ priority: 'high',
+ title: i18n.t('App.Notifications.MaintenanceReminder').toString(),
+ description: i18n.t('App.Notifications.MaintenanceReminderText', { name: entry.name }).toString(),
+ date,
+ dismissed: false,
+ })
+ })
+
+ return notifications
+ },
+
getDismiss: (state, getters, rootState) => {
const currentTime = new Date()
const systemBootAt = rootState.server.system_boot_at ?? new Date()
diff --git a/src/store/gui/reminders/actions.ts b/src/store/gui/reminders/actions.ts
new file mode 100644
index 000000000..2dc074945
--- /dev/null
+++ b/src/store/gui/reminders/actions.ts
@@ -0,0 +1,60 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { RootState } from '@/store/types'
+import { v4 as uuidv4 } from 'uuid'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init() {
+ Vue.$socket.emit('server.database.get_item', { namespace: 'reminders' }, { action: 'gui/reminders/initStore' })
+ },
+
+ async initStore({ commit, dispatch }, payload) {
+ await commit('reset')
+ await commit('initStore', payload)
+ await dispatch('socket/removeInitModule', 'gui/reminders/init', { root: true })
+ },
+
+ upload(_, payload) {
+ Vue.$socket.emit('server.database.post_item', { namespace: 'reminders', key: payload.id, value: payload.value })
+ },
+
+ store({ commit, dispatch, state }, payload) {
+ const id = uuidv4()
+
+ commit('store', { id, values: payload.values })
+ dispatch('upload', {
+ id,
+ value: state.reminders[id],
+ })
+ },
+
+ update({ commit, dispatch, state }, payload) {
+ commit('update', payload)
+ dispatch('upload', {
+ id: payload.id,
+ value: state.reminders[payload.id],
+ })
+ },
+
+ delete({ commit }, payload) {
+ commit('delete', payload)
+ Vue.$socket.emit('server.database.delete_item', { namespace: 'reminders', key: payload })
+ },
+
+ repeat({ dispatch, getters, state, rootState }, payload) {
+ if (!(payload.id in state.reminders)) return
+ const reminder = getters['getReminder'](payload.id)
+ const new_start_time = rootState.server?.history?.job_totals.total_print_time || 0
+ const snooze_epoch_time = Date.now()
+ dispatch('update', {
+ id: reminder.id,
+ snooze_print_hours_timestamps: [...reminder.snooze_print_hours_timestamps, new_start_time],
+ snooze_epoch_timestamps: [...reminder.snooze_epoch_timestamps, snooze_epoch_time],
+ })
+ },
+}
diff --git a/src/store/gui/reminders/getters.ts b/src/store/gui/reminders/getters.ts
new file mode 100644
index 000000000..40599e430
--- /dev/null
+++ b/src/store/gui/reminders/getters.ts
@@ -0,0 +1,29 @@
+import { GetterTree } from 'vuex'
+import { GuiRemindersState, GuiRemindersStateReminder } from '@/store/gui/reminders/types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {
+ getReminders: (state) => {
+ const reminders: GuiRemindersStateReminder[] = []
+
+ Object.keys(state.reminders).forEach((id: string) => {
+ reminders.push({ ...state.reminders[id], id })
+ })
+
+ return reminders
+ },
+
+ getReminder: (state, getters) => (id: string) => {
+ const reminders = getters['getReminders'] ?? []
+
+ return reminders.find((reminder: GuiRemindersStateReminder) => reminder.id === id)
+ },
+
+ getOverdueReminders: (state, getters, rootState) => {
+ const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time
+ const reminders: GuiRemindersStateReminder[] = getters['getReminders'] ?? []
+ return reminders.filter(
+ (reminder) => reminder.time_delta - (currentTotalPrintTime - reminder.start_total_print_time) < 0
+ )
+ },
+}
diff --git a/src/store/gui/reminders/index.ts b/src/store/gui/reminders/index.ts
new file mode 100644
index 000000000..227263eba
--- /dev/null
+++ b/src/store/gui/reminders/index.ts
@@ -0,0 +1,23 @@
+import { Module } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { actions } from '@/store/gui/reminders/actions'
+import { mutations } from '@/store/gui/reminders/mutations'
+import { getters } from '@/store/gui/reminders/getters'
+
+export const getDefaultState = (): GuiRemindersState => {
+ return {
+ reminders: {},
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const reminders: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/gui/reminders/mutations.ts b/src/store/gui/reminders/mutations.ts
new file mode 100644
index 000000000..992f2bdf5
--- /dev/null
+++ b/src/store/gui/reminders/mutations.ts
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import { MutationTree } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { getDefaultState } from './index'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ initStore(state, payload) {
+ Vue.set(state, 'reminders', payload.value)
+ },
+
+ store(state, payload) {
+ Vue.set(state.reminders, payload.id, payload.values)
+ },
+
+ update(state, payload) {
+ if (payload.id in state.reminders) {
+ const reminder = { ...state.reminders[payload.id] }
+ Object.assign(reminder, payload)
+ Vue.set(state.reminders, payload.id, reminder)
+ }
+ },
+
+ delete(state, payload) {
+ if (payload in state.reminders) {
+ Vue.delete(state.reminders, payload)
+ }
+ },
+}
diff --git a/src/store/gui/reminders/types.ts b/src/store/gui/reminders/types.ts
new file mode 100644
index 000000000..7edc70ca8
--- /dev/null
+++ b/src/store/gui/reminders/types.ts
@@ -0,0 +1,16 @@
+export interface GuiRemindersState {
+ reminders: {
+ [key: string]: GuiRemindersStateReminder
+ }
+}
+
+export interface GuiRemindersStateReminder {
+ id: string
+ name: string
+ start_total_print_time: number
+ time_delta: number
+ repeating: boolean
+ snooze_print_hours_timestamps: number[]
+ snooze_epoch_timestamps: number[]
+ remaining_print_time?: number
+}
diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts
index 5b71c4814..a1bef68e2 100644
--- a/src/store/gui/types.ts
+++ b/src/store/gui/types.ts
@@ -123,6 +123,7 @@ export interface GuiState {
boolScrewsTiltAdjustDialog: boolean
tempchartHeight: number
hideUpdateWarnings: boolean
+ printstatusThumbnailZoom: boolean
}
view: {
blockFileUpload: boolean
@@ -169,6 +170,8 @@ export interface GuiState {
hidePrintStatus: string[]
hideColums: string[]
selectedJobs: ServerHistoryStateJob[]
+ showMaintenanceEntries: boolean
+ showPrintJobs: boolean
}
jobqueue: {
countPerPage: number
diff --git a/src/store/printer/actions.ts b/src/store/printer/actions.ts
index a2af4be52..646953616 100644
--- a/src/store/printer/actions.ts
+++ b/src/store/printer/actions.ts
@@ -37,6 +37,7 @@ export const actions: ActionTree = {
)
commit('setData', {
+ app_name: payload.app ?? null,
hostname: payload.hostname,
software_version: payload.software_version,
cpu_info: payload.cpu_info,
diff --git a/src/store/printer/getters.ts b/src/store/printer/getters.ts
index 225b06bee..ba0204e27 100644
--- a/src/store/printer/getters.ts
+++ b/src/store/printer/getters.ts
@@ -141,24 +141,31 @@ export const getters: GetterTree = {
getMacros: (state) => {
const array: PrinterStateMacro[] = []
- const config = state.configfile?.config ?? {}
const settings = state.configfile?.settings ?? null
+ const printerGcodes = state.gcode?.commands ?? {}
- Object.keys(config)
- .filter((prop) => prop.toLowerCase().startsWith('gcode_macro'))
+ const prefix = 'gcode_macro '
+ const prefixLength = prefix.length
+
+ Object.keys(state)
+ .filter((prop) => prop.toLowerCase().startsWith(prefix))
.forEach((prop) => {
- const name = prop.replace('gcode_macro ', '')
+ const name = prop.slice(prefixLength)
+ const printerGcode = printerGcodes[name.toUpperCase()] ?? {}
+
+ // remove macros with a '_' as first char
if (name.startsWith('_')) return
+ // remove macros with rename_existing in the config
const propLower = prop.toLowerCase()
- const propSettings = settings[propLower]
+ const propSettings = settings[propLower] ?? {}
if ('rename_existing' in propSettings) return
const variables = state[prop] ?? {}
array.push({
name,
- description: settings[propLower].description ?? null,
+ description: printerGcode?.help ?? null,
prop: propSettings,
params: getMacroParams(propSettings),
variables,
@@ -409,7 +416,9 @@ export const getters: GetterTree = {
Object.keys(state).forEach((key) => {
if (key === 'mcu' || key.startsWith('mcu ')) {
const mcu = state[key]
- const versionOutput = (mcu.mcu_version ?? 'unknown').split('-').slice(0, 4).join('-')
+ let versionOutput = (mcu.mcu_version ?? 'unknown').split('-').slice(0, 4).join('-')
+
+ if ('app' in mcu && mcu.app !== 'Klipper') versionOutput = mcu.app + ' ' + versionOutput
let load = 0
if (mcu.last_stats?.mcu_task_avg && mcu.last_stats?.mcu_task_stddev) {
diff --git a/src/store/printer/tempHistory/getters.ts b/src/store/printer/tempHistory/getters.ts
index d9b591efd..ab74bbbe5 100644
--- a/src/store/printer/tempHistory/getters.ts
+++ b/src/store/printer/tempHistory/getters.ts
@@ -99,11 +99,12 @@ export const getters: GetterTree = {
const selected: legends = {}
const available_sensors = rootState.printer?.heaters?.available_sensors ?? []
+ const available_monitors = rootState.printer?.heaters?.available_monitors ?? []
const viewSettings = rootState.gui?.view?.tempchart?.datasetSettings ?? {}
Object.keys(viewSettings).forEach((key) => {
// break if this element doesn't exist in available_sensors
- if (!available_sensors.includes(key)) return
+ if (!available_sensors.includes(key) && !available_monitors.includes(key)) return
Object.keys(viewSettings[key]).forEach((attrKey) => {
// break if this element isn't a valid datasetType
diff --git a/src/store/server/actions.ts b/src/store/server/actions.ts
index 93aeb4ce3..75c11cc30 100644
--- a/src/store/server/actions.ts
+++ b/src/store/server/actions.ts
@@ -59,10 +59,14 @@ export const actions: ActionTree = {
dispatch('socket/addInitModule', 'gui/init', { root: true })
dispatch('gui/init', null, { root: true })
} else dispatch('gui/initDb', null, { root: true })
- if (payload.namespaces?.includes('webcams')) {
- dispatch('socket/addInitModule', 'gui/webcam/init', { root: true })
- dispatch('gui/webcams/init', null, { root: true })
- }
+ if (payload.namespaces?.includes('maintenance')) {
+ dispatch('socket/addInitModule', 'gui/maintenance/init', { root: true })
+ dispatch('gui/maintenance/init', null, { root: true })
+ } else dispatch('gui/maintenance/initDb', null, { root: true })
+
+ // init webcams
+ dispatch('socket/addInitModule', 'gui/webcam/init', { root: true })
+ dispatch('gui/webcams/init', null, { root: true })
commit('saveDbNamespaces', payload.namespaces)
diff --git a/src/store/server/getters.ts b/src/store/server/getters.ts
index d10bd4000..70decaaed 100644
--- a/src/store/server/getters.ts
+++ b/src/store/server/getters.ts
@@ -77,6 +77,10 @@ export const getters: GetterTree = {
version = rootState.printer?.software_version.split('-').slice(0, 4).join('-')
}
+ if (rootState.printer?.app_name) {
+ version = rootState.printer?.app_name + ' ' + version
+ }
+
let pythonVersion: null | string = null
if (state.system_info?.python?.version_string) {
const firstSpace = state.system_info?.python?.version_string.indexOf(' ')
@@ -108,7 +112,7 @@ export const getters: GetterTree = {
else if (memUsage && memUsage > 80) memUsageColor = 'warning'
let tempSensor = rootGetters['printer/getHostTempSensor']
- if (tempSensor === null) {
+ if (tempSensor === null && state.cpu_temp !== null) {
tempSensor = {
temperature: state.cpu_temp?.toFixed(0),
measured_min_temp: null,
diff --git a/src/store/server/history/actions.ts b/src/store/server/history/actions.ts
index 455fc8c4c..3a2eda0cf 100644
--- a/src/store/server/history/actions.ts
+++ b/src/store/server/history/actions.ts
@@ -19,6 +19,11 @@ export const actions: ActionTree = {
getTotals({ commit }, payload) {
commit('setTotals', payload.job_totals)
+
+ const auxiliary_totals = payload.auxiliary_totals ?? []
+ if (auxiliary_totals.length) {
+ commit('setAuxiliaryTotals', auxiliary_totals)
+ }
},
async getHistory({ commit, dispatch, state }, payload) {
diff --git a/src/store/server/history/getters.ts b/src/store/server/history/getters.ts
index 593ab593e..e56379912 100644
--- a/src/store/server/history/getters.ts
+++ b/src/store/server/history/getters.ts
@@ -1,14 +1,25 @@
import { GetterTree } from 'vuex'
import {
+ HistoryListRowJob,
ServerHistoryState,
ServerHistoryStateAllPrintStatusEntry,
ServerHistoryStateJob,
} from '@/store/server/history/types'
import { mdiAlertOutline, mdiCheckboxMarkedCircleOutline, mdiCloseCircleOutline, mdiProgressClock } from '@mdi/js'
import i18n from '@/plugins/i18n'
+import { HistoryListRowMaintenance } from '@/store/gui/maintenance/types'
+
+// I don't know why I cannot import the type from the HistoryListPanel, that's why I have to define it here again
+type HistoryListPanelRow = HistoryListRowJob | HistoryListRowMaintenance
// eslint-disable-next-line
export const getters: GetterTree = {
+ getSelectedJobs: (state, getters, rootState): ServerHistoryStateJob[] => {
+ const entries: HistoryListPanelRow[] = rootState.gui.view.history.selectedJobs ?? []
+
+ return entries.filter((entry) => entry.type === 'job') as ServerHistoryStateJob[]
+ },
+
getTotalPrintTime(state) {
let output = 0
@@ -150,11 +161,11 @@ export const getters: GetterTree = {
getSelectedPrintStatusArray(state, getters, rootState) {
const output: ServerHistoryStateAllPrintStatusEntry[] = []
- rootState.gui.view.history.selectedJobs.forEach((current: ServerHistoryStateJob) => {
+ getters.getSelectedJobs.forEach((current: ServerHistoryStateJob) => {
const index = output.findIndex((element) => element.name === current.status)
if (index !== -1) output[index].value += 1
else {
- const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en')
+ const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en').toString()
? i18n.t(`History.StatusValues.${current.status}`).toString()
: current.status
const itemStyle = {
@@ -192,7 +203,7 @@ export const getters: GetterTree = {
return output
},
- getFilamentUsageArray(state, getters, rootState) {
+ getFilamentUsageArray(state, getters) {
// eslint-disable-next-line
const output: any = []
const startDate = new Date()
@@ -202,9 +213,9 @@ export const getters: GetterTree = {
let jobsFiltered = [
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.filament_used > 0),
]
- if (rootState.gui.view.history.selectedJobs.length)
+ if (getters.getSelectedJobs.length)
jobsFiltered = [
- ...rootState.gui.view.history.selectedJobs.filter(
+ ...getters.getSelectedJobs.filter(
(job: ServerHistoryStateJob) =>
new Date(job.start_time * 1000) >= startDate && job.filament_used > 0
),
@@ -232,16 +243,16 @@ export const getters: GetterTree = {
})
},
- getPrinttimeAvgArray(state, getters, rootState) {
+ getPrinttimeAvgArray(state, getters) {
const output = [0, 0, 0, 0, 0]
const startDate = new Date(new Date().getTime() - 60 * 60 * 24 * 14 * 1000)
let jobsFiltered = [
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.status === 'completed'),
]
- if (rootState.gui.view.history.selectedJobs.length)
+ if (getters.getSelectedJobs.length)
jobsFiltered = [
- ...rootState.gui.view.history.selectedJobs.filter(
+ ...getters.getSelectedJobs.filter(
(job: ServerHistoryStateJob) =>
new Date(job.start_time * 1000) >= startDate && job.status === 'completed'
),
@@ -292,7 +303,7 @@ export const getters: GetterTree = {
// find jobs via metadata
const jobs = state.jobs.filter((job) => {
- return job.metadata?.size === filesize && Math.round(job.metadata?.modified * 1000) === modified
+ return job.metadata?.size === filesize && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
})
if (jobs.length) return jobs
if (job_id) return jobs.filter((job) => job.job_id === job_id)
@@ -303,7 +314,7 @@ export const getters: GetterTree = {
getPrintStatusByFilename: (state) => (filename: string, modified: number) => {
if (state.jobs.length) {
const job = state.jobs.find((job) => {
- return job.filename === filename && Math.round(job.metadata?.modified * 1000) === modified
+ return job.filename === filename && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
})
return job?.status ?? ''
@@ -354,7 +365,7 @@ export const getters: GetterTree = {
}
},
- getFilterdJobList: (state, getters, rootState) => {
+ getFilteredJobList: (state, getters, rootState) => {
const hideStatus = rootState.gui.view.history.hidePrintStatus
return state.jobs.filter((job: ServerHistoryStateJob) => {
diff --git a/src/store/server/history/index.ts b/src/store/server/history/index.ts
index 8e1acead3..c16abbfd5 100644
--- a/src/store/server/history/index.ts
+++ b/src/store/server/history/index.ts
@@ -15,6 +15,7 @@ export const getDefaultState = (): ServerHistoryState => {
longest_job: 0,
longest_print: 0,
},
+ auxiliary_totals: [],
all_loaded: false,
}
}
diff --git a/src/store/server/history/mutations.ts b/src/store/server/history/mutations.ts
index 9ee7a3129..d4df8fded 100644
--- a/src/store/server/history/mutations.ts
+++ b/src/store/server/history/mutations.ts
@@ -16,6 +16,10 @@ export const mutations: MutationTree = {
Vue.set(state, 'job_totals', payload)
},
+ setAuxiliaryTotals(state, payload) {
+ Vue.set(state, 'auxiliary_totals', payload)
+ },
+
setHistoryNotes(state, payload) {
const job = state.jobs.find((job) => job.job_id === payload.job_id)
if (job) Vue.set(job, 'note', payload.text)
diff --git a/src/store/server/history/types.ts b/src/store/server/history/types.ts
index 90b5ce7e8..d9e69e5fa 100644
--- a/src/store/server/history/types.ts
+++ b/src/store/server/history/types.ts
@@ -1,3 +1,5 @@
+import { FileStateFileThumbnail } from '@/store/files/types'
+
export interface ServerHistoryState {
jobs: ServerHistoryStateJob[]
job_totals: {
@@ -8,6 +10,7 @@ export interface ServerHistoryState {
longest_job: number
longest_print: number
}
+ auxiliary_totals: ServerHistoryStateJobAuxiliaryTotal[]
all_loaded: boolean
}
@@ -18,12 +21,57 @@ export interface ServerHistoryStateJob {
filament_used: number
filename: string
// eslint-disable-next-line
- metadata: any
+ metadata: {
+ print_start_time?: number
+ job_id?: number
+ size?: number
+ slicer?: string
+ slicer_version?: string
+ layer_count?: number
+ layer_height?: number
+ first_layer_height?: number
+ object_height?: number
+ filament_total?: number
+ filament_weight_total?: number
+ estimated_time?: number
+ thumbnails?: FileStateFileThumbnail[]
+ first_layer_bed_temp?: number
+ first_layer_extr_temp?: number
+ gcode_start_byte?: number
+ gcode_end_byte?: number
+ filename?: string
+ filesize?: number
+ modified?: number
+ uuid?: string
+ nozzle_diameter?: number
+ [key: string]: any
+ }
note?: string
print_duration: number
status: string
start_time: number
total_duration: number
+ auxiliary_data?: ServerHistoryStateJobAuxiliaryData[]
+}
+
+export interface ServerHistoryStateJobAuxiliaryData {
+ description: string
+ name: string
+ provider: string
+ units: string
+ value: number | number[]
+}
+
+export interface ServerHistoryStateJobAuxiliaryTotal {
+ field: string
+ maximum: number
+ provider: string
+ total: number
+}
+
+export interface HistoryListRowJob extends ServerHistoryStateJob {
+ type: 'job'
+ select_id: string
}
export interface ServerHistoryStateAllPrintStatusEntry {
diff --git a/src/store/server/index.ts b/src/store/server/index.ts
index 1fde111a3..5e90dc7dc 100644
--- a/src/store/server/index.ts
+++ b/src/store/server/index.ts
@@ -12,6 +12,7 @@ import { timelapse } from '@/store/server/timelapse'
import { jobQueue } from '@/store/server/jobQueue'
import { announcements } from '@/store/server/announcements'
import { spoolman } from '@/store/server/spoolman'
+import { sensor } from '@/store/server/sensor'
// create getDefaultState
export const getDefaultState = (): ServerState => {
@@ -62,5 +63,6 @@ export const server: Module = {
jobQueue,
announcements,
spoolman,
+ sensor,
},
}
diff --git a/src/store/server/sensor/actions.ts b/src/store/server/sensor/actions.ts
new file mode 100644
index 000000000..28addc301
--- /dev/null
+++ b/src/store/server/sensor/actions.ts
@@ -0,0 +1,26 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { ServerSensorState } from '@/store/server/sensor/types'
+import { RootState } from '@/store/types'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init() {
+ Vue.$socket.emit('server.sensors.list', {}, { action: 'server/sensor/getSensors' })
+ },
+
+ getSensors({ commit, dispatch }, payload) {
+ commit('setSensors', payload.sensors)
+
+ dispatch('socket/removeInitModule', 'server/sensor/init', { root: true })
+ },
+
+ updateSensors({ commit }, payload) {
+ Object.keys(payload).forEach((key) => {
+ commit('updateSensor', { key, value: payload[key] })
+ })
+ },
+}
diff --git a/src/store/server/sensor/getters.ts b/src/store/server/sensor/getters.ts
new file mode 100644
index 000000000..10028f160
--- /dev/null
+++ b/src/store/server/sensor/getters.ts
@@ -0,0 +1,9 @@
+import { GetterTree } from 'vuex'
+import { ServerSensorState } from '@/store/server/sensor/types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {
+ getSensors: (state) => {
+ return Object.keys(state.sensors)
+ },
+}
diff --git a/src/store/server/sensor/index.ts b/src/store/server/sensor/index.ts
new file mode 100644
index 000000000..c60aea2c9
--- /dev/null
+++ b/src/store/server/sensor/index.ts
@@ -0,0 +1,23 @@
+import { Module } from 'vuex'
+import { ServerSensorState } from '@/store/server/sensor/types'
+import { actions } from '@/store/server/sensor/actions'
+import { mutations } from '@/store/server/sensor/mutations'
+import { getters } from '@/store/server/sensor/getters'
+
+export const getDefaultState = (): ServerSensorState => {
+ return {
+ sensors: {},
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const sensor: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/server/sensor/mutations.ts b/src/store/server/sensor/mutations.ts
new file mode 100644
index 000000000..2e9a7b841
--- /dev/null
+++ b/src/store/server/sensor/mutations.ts
@@ -0,0 +1,20 @@
+import Vue from 'vue'
+import { getDefaultState } from './index'
+import { MutationTree } from 'vuex'
+import { ServerSensorState } from '@/store/server/sensor/types'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ setSensors(state, payload) {
+ Vue.set(state, 'sensors', payload)
+ },
+
+ updateSensor(state, payload) {
+ if (!(payload.key in state.sensors)) return
+
+ Vue.set(state.sensors[payload.key], 'values', payload.value)
+ },
+}
diff --git a/src/store/server/sensor/types.ts b/src/store/server/sensor/types.ts
new file mode 100644
index 000000000..e745b04d3
--- /dev/null
+++ b/src/store/server/sensor/types.ts
@@ -0,0 +1,14 @@
+export interface ServerSensorState {
+ sensors: {
+ [key: string]: ServerSensorStateSensor
+ }
+}
+
+export interface ServerSensorStateSensor {
+ friendly_name: string
+ id: string
+ type: string
+ values: {
+ [key: string]: number
+ }
+}
diff --git a/src/store/socket/actions.ts b/src/store/socket/actions.ts
index 50b66b16b..0a207d48b 100644
--- a/src/store/socket/actions.ts
+++ b/src/store/socket/actions.ts
@@ -135,6 +135,10 @@ export const actions: ActionTree = {
dispatch('server/spoolman/getActiveSpoolId', payload.params[0], { root: true })
break
+ case 'notify_sensor_update':
+ dispatch('server/sensor/updateSensors', payload.params[0], { root: true })
+ break
+
default:
window.console.debug(payload)
}
diff --git a/src/store/variables.ts b/src/store/variables.ts
index 11ee6d077..791e60855 100644
--- a/src/store/variables.ts
+++ b/src/store/variables.ts
@@ -38,6 +38,7 @@ export const initableServerComponents = [
'jobQueue',
'announcements',
'spoolman',
+ 'sensor',
]
/*