import { Component, Mixins, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
-import { validGcodeExtensions } from '@/store/variables'
+import { defaultBigThumbnailBackground, validGcodeExtensions } from '@/store/variables'
import { formatFilesize, formatPrintTime, sortFiles } from '@/plugins/helpers'
import { FileStateFile, FileStateGcodefile } from '@/store/files/types'
import Panel from '@/components/ui/Panel.vue'
@@ -1101,6 +1102,18 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
this.$store.dispatch('gui/saveSetting', { name: 'view.gcodefiles.countPerPage', value: newVal })
}
+ get bigThumbnailBackground() {
+ return this.$store.state.gui.uiSettings.bigThumbnailBackground ?? defaultBigThumbnailBackground
+ }
+
+ get bigThumbnailTooltipColor() {
+ if (defaultBigThumbnailBackground.toLowerCase() === this.bigThumbnailBackground.toLowerCase()) {
+ return undefined
+ }
+
+ return this.bigThumbnailBackground
+ }
+
getStatusIcon(status: string | null) {
return this.$store.getters['server/history/getPrintStatusIcon'](status)
}
@@ -1322,7 +1335,7 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
await addElementToItems('gcodes/' + this.currentPath, this.selectedFiles)
const date = new Date()
- const timestamp = `${date.getFullYear()}${date.getMonth()}${date.getDay()}-${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
+ const timestamp = `${date.getFullYear()}${date.getMonth()}${date.getDate()}-${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
this.$socket.emit(
'server.files.zip',
diff --git a/src/components/panels/Machine/ConfigFilesPanel.vue b/src/components/panels/Machine/ConfigFilesPanel.vue
index b0463caf0b..1357163606 100644
--- a/src/components/panels/Machine/ConfigFilesPanel.vue
+++ b/src/components/panels/Machine/ConfigFilesPanel.vue
@@ -1050,7 +1050,7 @@ export default class ConfigFilesPanel extends Mixins(BaseMixin) {
const date = new Date()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
- const day = date.getDay().toString().padStart(2, '0')
+ const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
diff --git a/src/components/panels/Machine/UpdatePanel/GitCommitsList.vue b/src/components/panels/Machine/UpdatePanel/GitCommitsList.vue
index fcfa06235a..688ff0e4da 100644
--- a/src/components/panels/Machine/UpdatePanel/GitCommitsList.vue
+++ b/src/components/panels/Machine/UpdatePanel/GitCommitsList.vue
@@ -83,7 +83,7 @@ export default class GitCommitsList extends Mixins(BaseMixin) {
if (
commitDate.getFullYear() !== lastCommitDate.getFullYear() ||
commitDate.getMonth() !== lastCommitDate.getMonth() ||
- commitDate.getDay() !== lastCommitDate.getDay()
+ commitDate.getDate() !== lastCommitDate.getDate()
) {
output.push({
date: commitDate,
diff --git a/src/components/panels/Spoolman/SpoolmanPanelActiveSpool.vue b/src/components/panels/Spoolman/SpoolmanPanelActiveSpool.vue
new file mode 100644
index 0000000000..52dafdd208
--- /dev/null
+++ b/src/components/panels/Spoolman/SpoolmanPanelActiveSpool.vue
@@ -0,0 +1,129 @@
+
+
+
+ #{{ id }} | {{ vendor }}
+
+ {{ name }}
+
+ {{ subtitle }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/panels/SpoolmanPanel.vue b/src/components/panels/SpoolmanPanel.vue
new file mode 100644
index 0000000000..054a8786d7
--- /dev/null
+++ b/src/components/panels/SpoolmanPanel.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+ {{ mdiSwapVertical }}
+
+
+
+
+ {{ mdiDotsVertical }}
+
+
+
+
+
+ {{ mdiEject }}
+ {{ $t('Panels.SpoolmanPanel.EjectSpool') }}
+
+
+
+
+ {{ mdiOpenInNew }}
+ {{ $t('Panels.SpoolmanPanel.OpenSpoolManager') }}
+
+
+
+
+
+
+
+
+ {{ $t('Panels.SpoolmanPanel.NoActiveSpool') }}
+
+ {{ $t('Panels.SpoolmanPanel.SelectSpool') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/panels/Status/Gcodefiles.vue b/src/components/panels/Status/Gcodefiles.vue
index 4b5064f7d5..b65a32e488 100644
--- a/src/components/panels/Status/Gcodefiles.vue
+++ b/src/components/panels/Status/Gcodefiles.vue
@@ -20,7 +20,11 @@
@click="showDialog(item)">
-
+
+ @closeDialog="closeDialog" />
@@ -241,6 +245,7 @@ import {
mdiCloseThick,
} from '@mdi/js'
import Panel from '@/components/ui/Panel.vue'
+import { defaultBigThumbnailBackground } from '@/store/variables'
interface dialogRenameObject {
show: boolean
@@ -356,6 +361,18 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
return `width: ${this.contentTdWidth}px;`
}
+ get bigThumbnailBackground() {
+ return this.$store.state.gui.uiSettings.bigThumbnailBackground ?? defaultBigThumbnailBackground
+ }
+
+ get bigThumbnailTooltipColor() {
+ if (defaultBigThumbnailBackground.toLowerCase() === this.bigThumbnailBackground.toLowerCase()) {
+ return undefined
+ }
+
+ return this.bigThumbnailBackground
+ }
+
showContextMenu(e: any, item: FileStateGcodefile) {
if (!this.contextMenu.shown) {
e?.preventDefault()
diff --git a/src/components/panels/Status/JobqueueEntry.vue b/src/components/panels/Status/JobqueueEntry.vue
index 2f12f94f4f..6a1a6ea72f 100644
--- a/src/components/panels/Status/JobqueueEntry.vue
+++ b/src/components/panels/Status/JobqueueEntry.vue
@@ -5,7 +5,11 @@
@contextmenu="showContextMenu($event, item)">
-
+
@@ -116,6 +120,7 @@ import BaseMixin from '@/components/mixins/base'
import { ServerJobQueueStateJob } from '@/store/server/jobQueue/types'
import { mdiChevronDown, mdiChevronUp, mdiCloseThick, mdiCounter, mdiFile, mdiPlay, mdiPlaylistRemove } from '@mdi/js'
import NumberInput from '@/components/inputs/NumberInput.vue'
+import { defaultBigThumbnailBackground } from '@/store/variables'
@Component({
components: { NumberInput },
})
@@ -192,6 +197,18 @@ export default class StatusPanelJobqueueEntry extends Mixins(BaseMixin) {
return output
}
+ get bigThumbnailBackground() {
+ return this.$store.state.gui.uiSettings.bigThumbnailBackground ?? defaultBigThumbnailBackground
+ }
+
+ get bigThumbnailTooltipColor() {
+ if (defaultBigThumbnailBackground.toLowerCase() === this.bigThumbnailBackground.toLowerCase()) {
+ return undefined
+ }
+
+ return this.bigThumbnailBackground
+ }
+
formatPrintTime(totalSeconds: number) {
if (totalSeconds) {
let output = ''
diff --git a/src/components/panels/Status/PrintstatusThumbnail.vue b/src/components/panels/Status/PrintstatusThumbnail.vue
index 9603883ae9..43fdf91e5c 100644
--- a/src/components/panels/Status/PrintstatusThumbnail.vue
+++ b/src/components/panels/Status/PrintstatusThumbnail.vue
@@ -7,6 +7,7 @@
tabindex="-1"
class="d-flex align-end statusPanel-big-thumbnail"
height="200"
+ :style="thumbnailStyle"
@focus="focusBigThumbnail"
@blur="blurBigThumbnail">
-
- {{ mdiFile }}
+
+
+
+
+ {{ mdiFile }}
+
-
+
-
-
- {{ mdiFile }}
+
+
+
+
+
+ {{ mdiFile }}
+
@@ -77,13 +86,11 @@
import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
-import { thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables'
+import { defaultBigThumbnailBackground, thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables'
import { mdiFileOutline, mdiFile } from '@mdi/js'
import { Debounce } from 'vue-debounce-decorator'
-@Component({
- components: {},
-})
+@Component({})
export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) {
mdiFileOutline = mdiFileOutline
mdiFile = mdiFile
@@ -178,6 +185,18 @@ export default class StatusPanelPrintstatusThumbnail extends Mixins(BaseMixin) {
return this.current_filename && setting && this.thumbnailBig
}
+ get bigThumbnailBackground() {
+ return this.$store.state.gui.uiSettings.bigThumbnailBackground ?? defaultBigThumbnailBackground
+ }
+
+ get thumbnailStyle() {
+ if (defaultBigThumbnailBackground.toLowerCase() !== this.bigThumbnailBackground.toLowerCase()) {
+ return { backgroundColor: this.bigThumbnailBackground }
+ }
+
+ return {}
+ }
+
focusBigThumbnail() {
if (this.$refs.bigThumbnail) {
const clientWidth = this.$refs.bigThumbnail.$el.clientWidth
diff --git a/src/components/panels/Temperature/TemperaturePanelList.vue b/src/components/panels/Temperature/TemperaturePanelList.vue
index 023859d2ae..c149fd3881 100644
--- a/src/components/panels/Temperature/TemperaturePanelList.vue
+++ b/src/components/panels/Temperature/TemperaturePanelList.vue
@@ -34,6 +34,13 @@
:key="objectName"
:object-name="objectName"
:is-responsive-mobile="el.is.mobile ?? false" />
+
+
+
@@ -70,6 +77,14 @@ export default class TemperaturePanelList extends Mixins(BaseMixin) {
return this.$store.state.printer?.heaters?.available_sensors ?? []
}
+ get available_monitors() {
+ return this.$store.state.printer?.heaters?.available_monitors ?? []
+ }
+
+ get monitors() {
+ return this.available_monitors.sort(this.sortObjectName)
+ }
+
get temperature_fans() {
return this.available_sensors
.filter((name: string) => name.startsWith('temperature_fan') && !name.startsWith('temperature_fan _'))
@@ -84,6 +99,10 @@ export default class TemperaturePanelList extends Mixins(BaseMixin) {
return this.$store.state.gui.view.tempchart.hideMcuHostSensors ?? false
}
+ get hideMonitors(): boolean {
+ return this.$store.state.gui.view.tempchart.hideMonitors ?? false
+ }
+
get temperature_sensors() {
return this.available_sensors
.filter((fullName: string) => {
diff --git a/src/components/panels/Temperature/TemperaturePanelListItem.vue b/src/components/panels/Temperature/TemperaturePanelListItem.vue
index 960876af7c..311e7b1403 100644
--- a/src/components/panels/Temperature/TemperaturePanelListItem.vue
+++ b/src/components/panels/Temperature/TemperaturePanelListItem.vue
@@ -68,6 +68,7 @@ import { convertName } from '@/plugins/helpers'
import {
mdiFan,
mdiFire,
+ mdiMemory,
mdiPrinter3dNozzle,
mdiPrinter3dNozzleAlert,
mdiRadiator,
@@ -131,6 +132,9 @@ export default class TemperaturePanelListItem extends Mixins(BaseMixin) {
// show heater_generic icon
if (this.objectName.startsWith('heater_generic')) return mdiFire
+ // show heater_generic icon
+ if (this.objectName.startsWith('tmc')) return mdiMemory
+
// show fan icon, if it is a fan
if (this.isFan) return mdiFan
@@ -138,7 +142,7 @@ export default class TemperaturePanelListItem extends Mixins(BaseMixin) {
}
get color() {
- return this.$store.getters['printer/tempHistory/getDatasetColor'](this.objectName) ?? ''
+ return this.$store.getters['printer/tempHistory/getDatasetColor'](this.objectName) ?? '#FFFFFF'
}
get iconColor() {
diff --git a/src/components/panels/Temperature/TemperaturePanelSettings.vue b/src/components/panels/Temperature/TemperaturePanelSettings.vue
index c00f80b903..08cd332259 100644
--- a/src/components/panels/Temperature/TemperaturePanelSettings.vue
+++ b/src/components/panels/Temperature/TemperaturePanelSettings.vue
@@ -20,6 +20,13 @@
hide-details
:label="$t('Panels.TemperaturePanel.HideMcuHostSensors')" />
+
+
+
diff --git a/src/components/panels/Timelapse/TimelapseFilesPanel.vue b/src/components/panels/Timelapse/TimelapseFilesPanel.vue
index bf9313287d..06456d7f5b 100644
--- a/src/components/panels/Timelapse/TimelapseFilesPanel.vue
+++ b/src/components/panels/Timelapse/TimelapseFilesPanel.vue
@@ -746,7 +746,7 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
await addElementToItems(this.currentPath, this.selectedFiles)
const date = new Date()
- const timestamp = `${date.getFullYear()}${date.getMonth()}${date.getDay()}-${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
+ const timestamp = `${date.getFullYear()}${date.getMonth()}${date.getDate()}-${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
this.$socket.emit(
'server.files.zip',
diff --git a/src/components/panels/Timelapse/TimelapseStatusPanel.vue b/src/components/panels/Timelapse/TimelapseStatusPanel.vue
index 5ac64a4c53..2129fc85cb 100644
--- a/src/components/panels/Timelapse/TimelapseStatusPanel.vue
+++ b/src/components/panels/Timelapse/TimelapseStatusPanel.vue
@@ -202,10 +202,11 @@ import BaseMixin from '@/components/mixins/base'
import SettingsRow from '@/components/settings/SettingsRow.vue'
import Panel from '@/components/ui/Panel.vue'
import { mdiFile, mdiInformation, mdiTextBoxSearchOutline, mdiCloseThick } from '@mdi/js'
+import WebcamMixin from '@/components/mixins/webcam'
@Component({
components: { Panel, SettingsRow },
})
-export default class TimelapseStatusPanel extends Mixins(BaseMixin) {
+export default class TimelapseStatusPanel extends Mixins(BaseMixin, WebcamMixin) {
mdiInformation = mdiInformation
mdiFile = mdiFile
mdiCloseThick = mdiCloseThick
@@ -351,14 +352,13 @@ export default class TimelapseStatusPanel extends Mixins(BaseMixin) {
}
get webcamStyle() {
- let transforms = []
- const scale = [0, 180].includes(this.camSettings.rotate) ? 1 : this.scale
- this.camSettings?.flipX ? transforms.push(`scaleX(-${scale})`) : transforms.push(`scaleX(${scale})`)
- this.camSettings?.flipY ? transforms.push(`scaleY(-${scale})`) : transforms.push(`scaleY(${scale})`)
- if (this.camSettings.rotate !== 0) transforms.push(`rotate(${this.camSettings.rotate}deg)`)
- if (transforms.length) return { transform: transforms.join(' ') }
-
- return {}
+ return {
+ transform: this.generateTransform(
+ this.camSettings.flip_horizontal ?? false,
+ this.camSettings.flip_vertical ?? false,
+ this.camSettings.rotation ?? 0
+ ),
+ }
}
startRender() {
diff --git a/src/components/panels/WebcamPanel.vue b/src/components/panels/WebcamPanel.vue
index 9ed38b4889..652c3e5c89 100644
--- a/src/components/panels/WebcamPanel.vue
+++ b/src/components/panels/WebcamPanel.vue
@@ -8,8 +8,8 @@
:collapsible="$route.fullPath !== '/cam'"
card-class="webcam-panel"
:margin-bottom="currentPage !== 'page'">
-
-
+
+
@@ -33,7 +33,7 @@
{{ convertWebcamIcon(webcam.icon) }}
-
+
@@ -77,6 +77,10 @@ export default class WebcamPanel extends Mixins(BaseMixin, WebcamMixin) {
return this.$store.getters['gui/webcams/getWebcams']
}
+ get showSwitch() {
+ return this.webcams.length > 1
+ }
+
// id changed to name with the refactoring of using moonraker webcam API
get currentCamId(): string {
if (this.webcams.length === 1) return this.webcams[0].name ?? 'all'
diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue
index bfd44fc0ee..f8339aca33 100644
--- a/src/components/settings/SettingsUiSettingsTab.vue
+++ b/src/components/settings/SettingsUiSettingsTab.vue
@@ -65,6 +65,32 @@
+
+
+ {{ mdiRestart }}
+
+
+
+
+
+
+
+
+
@@ -224,7 +250,7 @@ import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import SettingsRow from '@/components/settings/SettingsRow.vue'
-import { defaultLogoColor, defaultPrimaryColor } from '@/store/variables'
+import { defaultLogoColor, defaultPrimaryColor, defaultBigThumbnailBackground } from '@/store/variables'
import { Debounce } from 'vue-debounce-decorator'
import { mdiRestart, mdiTimerOutline } from '@mdi/js'
import { ServerPowerStateDevice } from '@/store/server/power/types'
@@ -238,6 +264,7 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin) {
defaultLogoColor = defaultLogoColor
defaultPrimaryColor = defaultPrimaryColor
+ defaultBigThumbnailBackground = defaultBigThumbnailBackground
get logoColor() {
return this.$store.state.gui.uiSettings.logo
@@ -263,6 +290,14 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin) {
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.boolBigThumbnail', value: newVal })
}
+ get bigThumbnailBackground() {
+ return this.$store.state.gui.uiSettings.bigThumbnailBackground
+ }
+
+ set bigThumbnailBackground(newVal) {
+ this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.bigThumbnailBackground', value: newVal })
+ }
+
get displayCancelPrint() {
return this.$store.state.gui.uiSettings.displayCancelPrint
}
@@ -468,5 +503,10 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin) {
updatePrimaryColor(newVal: any) {
this.primaryColor = this.clearColorObject(newVal)
}
+
+ @Debounce(500)
+ updateBigThumbnailBackground(newVal: any) {
+ this.bigThumbnailBackground = this.clearColorObject(newVal)
+ }
}
diff --git a/src/components/ui/SpoolIcon.vue b/src/components/ui/SpoolIcon.vue
new file mode 100644
index 0000000000..d0b65e6c84
--- /dev/null
+++ b/src/components/ui/SpoolIcon.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
diff --git a/src/components/webcams/streamers/WebrtcCameraStreamer.vue b/src/components/webcams/streamers/WebrtcCameraStreamer.vue
index a73646ffea..16140d76a8 100644
--- a/src/components/webcams/streamers/WebrtcCameraStreamer.vue
+++ b/src/components/webcams/streamers/WebrtcCameraStreamer.vue
@@ -39,7 +39,6 @@ export default class WebrtcCameraStreamer extends Mixins(BaseMixin, WebcamMixin)
get url() {
const baseUrl = this.camSettings.stream_url
let url = new URL(baseUrl, this.printerUrl === null ? this.hostUrl.toString() : this.printerUrl)
- url.port = this.hostPort.toString()
if (baseUrl.startsWith('http') || baseUrl.startsWith('://')) url = new URL(baseUrl)
diff --git a/src/components/webcams/streamers/WebrtcMediaMTX.vue b/src/components/webcams/streamers/WebrtcMediaMTX.vue
index 8d2ce39fcd..257a93808e 100644
--- a/src/components/webcams/streamers/WebrtcMediaMTX.vue
+++ b/src/components/webcams/streamers/WebrtcMediaMTX.vue
@@ -23,27 +23,36 @@ import BaseMixin from '@/components/mixins/base'
import { GuiWebcamStateWebcam } from '@/store/gui/webcams/types'
import WebcamMixin from '@/components/mixins/webcam'
+interface OfferData {
+ iceUfrag: string
+ icePwd: string
+ medias: string[]
+}
+
@Component
-export default class WebrtcRTSPSimpleServer extends Mixins(BaseMixin, WebcamMixin) {
+export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
@Prop({ required: true }) readonly camSettings!: GuiWebcamStateWebcam
@Prop({ default: null }) readonly printerUrl!: string | null
@Ref() declare video: HTMLVideoElement
- // webrtc player vars
- private terminated: boolean = false
- private ws: WebSocket | null = null
private pc: RTCPeerConnection | null = null
- private restartTimeoutTimer: any = null
+ private restartTimeout: any = null
private status: string = 'connecting'
+ private eTag: string | null = null
+ private queuedCandidates: RTCIceCandidate[] = []
+ private offerData: OfferData = {
+ iceUfrag: '',
+ icePwd: '',
+ medias: [],
+ }
+ private RESTART_PAUSE = 2000
// stop the video and close the streams if the component is going to be destroyed so we don't leave hanging streams
beforeDestroy() {
this.terminate()
// clear any potentially open restart timeout
- if (this.restartTimeoutTimer) {
- clearTimeout(this.restartTimeoutTimer)
- }
+ if (this.restartTimeout) clearTimeout(this.restartTimeout)
}
get webcamStyle() {
@@ -58,9 +67,9 @@ export default class WebrtcRTSPSimpleServer extends Mixins(BaseMixin, WebcamMixi
get url() {
let baseUrl = this.camSettings.stream_url
- if (baseUrl.startsWith('http')) {
- baseUrl = baseUrl.replace('http', 'ws') + 'ws'
- }
+ if (!baseUrl.endsWith('/')) baseUrl += '/'
+
+ baseUrl = new URL('whep', baseUrl).toString()
return this.convertUrl(baseUrl, this.printerUrl)
}
@@ -87,140 +96,246 @@ export default class WebrtcRTSPSimpleServer extends Mixins(BaseMixin, WebcamMixi
this.start()
}
+ log(msg: string, obj?: any) {
+ if (obj) {
+ window.console.log(`[WebRTC mediamtx] ${msg}`, obj)
+ return
+ }
+
+ window.console.log(`[WebRTC mediamtx] ${msg}`)
+ }
+
// webrtc player methods
- // adapated from sample player in https://github.com/mrlt8/docker-wyze-bridge
- start() {
- // unterminate we're starting again.
- this.terminated = false
+ // adapted from https://github.com/bluenviron/mediamtx/blob/main/internal/core/webrtc_read_index.html
- // clear any potentially open restart timeout
- if (this.restartTimeoutTimer) {
- clearTimeout(this.restartTimeoutTimer)
- this.restartTimeoutTimer = null
- }
+ unquoteCredential = (v: any) => JSON.parse(`"${v}"`)
- window.console.log('[webcam-rtspsimpleserver] web socket connecting')
+ // eslint-disable-next-line no-undef
+ linkToIceServers(links: string | null): RTCIceServer[] {
+ if (links === null) return []
- // test if the url is valid
- try {
- const url = new URL(this.url)
+ return links.split(', ').map((link) => {
+ const m: RegExpMatchArray | null = link.match(
+ /^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i
+ )
- // break if url protocol is not ws
- if (!url.protocol.startsWith('ws')) {
- console.log('[webcam-rtspsimpleserver] invalid URL (no ws protocol)')
- return
+ // break if match is null
+ if (m === null) return { urls: '' }
+
+ // eslint-disable-next-line no-undef
+ const ret: RTCIceServer = {
+ urls: [m[1]],
+ }
+
+ if (m.length > 3) {
+ ret.username = this.unquoteCredential(m[3])
+ ret.credential = this.unquoteCredential(m[4])
+ ret.credentialType = 'password'
}
- } catch (err) {
- console.log('[webcam-rtspsimpleserver] invalid URL')
- return
- }
- // open websocket connection
- this.ws = new WebSocket(this.url)
+ return ret
+ })
+ }
- this.ws.onerror = (event) => {
- window.console.log('[webcam-rtspsimpleserver] websocket error', event)
- this.ws?.close()
- this.ws = null
+ parseOffer(offer: string) {
+ const ret: OfferData = {
+ iceUfrag: '',
+ icePwd: '',
+ medias: [],
}
- this.ws.onclose = (event) => {
- console.log('[webcam-rtspsimpleserver] websocket closed', event)
- this.ws = null
- this.scheduleRestart()
+ for (const line of offer.split('\r\n')) {
+ if (line.startsWith('m=')) {
+ ret.medias.push(line.slice('m='.length))
+ } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
+ ret.iceUfrag = line.slice('a=ice-ufrag:'.length)
+ } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {
+ ret.icePwd = line.slice('a=ice-pwd:'.length)
+ }
}
- this.ws.onmessage = (msg: MessageEvent) => this.webRtcOnIceServers(msg)
+ return ret
}
- terminate() {
- this.terminated = true
+ generateSdpFragment(offerData: OfferData, candidates: RTCIceCandidate[]) {
+ // I don't found a specification for this, but it seems to be the only way to make it work
+ const candidatesByMedia: any = {}
+ for (const candidate of candidates) {
+ const mid = candidate.sdpMLineIndex
+ if (mid === null) continue
- try {
- this.video.pause()
- } catch (err) {
- // ignore -- make sure we close down the sockets anyway
+ // create the array if it doesn't exist
+ if (!(mid in candidatesByMedia)) candidatesByMedia[mid] = []
+ candidatesByMedia[mid].push(candidate)
}
- this.ws?.close()
- this.pc?.close()
- }
+ let frag = 'a=ice-ufrag:' + offerData.iceUfrag + '\r\n' + 'a=ice-pwd:' + offerData.icePwd + '\r\n'
+ let mid = 0
- webRtcOnIceServers(msg: MessageEvent) {
- if (this.ws === null) return
+ for (const media of offerData.medias) {
+ if (candidatesByMedia[mid] !== undefined) {
+ frag += 'm=' + media + '\r\n' + 'a=mid:' + mid + '\r\n'
- const iceServers = JSON.parse(msg.data)
- this.pc = new RTCPeerConnection({
- iceServers,
- })
-
- this.ws.onmessage = (msg: MessageEvent) => this.webRtcOnRemoteDescription(msg)
- this.pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) => this.webRtcOnIceCandidate(evt)
+ for (const candidate of candidatesByMedia[mid]) {
+ frag += 'a=' + candidate.candidate + '\r\n'
+ }
+ }
- this.pc.oniceconnectionstatechange = () => {
- if (this.pc === null) return
+ mid++
+ }
- window.console.log('[webcam-rtspsimpleserver] peer connection state:', this.pc.iceConnectionState)
+ return frag
+ }
- this.status = (this.pc?.iceConnectionState ?? '').toString()
+ start() {
+ this.log('requesting ICE servers from ' + this.url)
- if (['failed', 'disconnected'].includes(this.status)) {
+ fetch(this.url, {
+ method: 'OPTIONS',
+ })
+ .then((res) => this.onIceServers(res))
+ .catch((err) => {
+ this.log('error: ' + err)
this.scheduleRestart()
- }
- }
+ })
+ }
- this.pc.ontrack = (evt: RTCTrackEvent) => {
- window.console.log('[webcam-rtspsimpleserver] new track ' + evt.track.kind)
- this.video.srcObject = evt.streams[0]
- }
+ onIceServers(res: Response) {
+ const iceServers = this.linkToIceServers(res.headers.get('Link'))
+ this.log('ice servers:', iceServers)
+
+ this.pc = new RTCPeerConnection({
+ iceServers,
+ })
const direction = 'sendrecv'
this.pc.addTransceiver('video', { direction })
this.pc.addTransceiver('audio', { direction })
- this.pc.createOffer().then((desc: any) => {
- if (this.pc === null || this.ws === null) return
+ this.pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) => this.onLocalCandidate(evt)
+ this.pc.oniceconnectionstatechange = () => this.onConnectionState()
- this.pc.setLocalDescription(desc)
+ this.pc.ontrack = (evt) => {
+ this.log('new track:', evt.track.kind)
+ this.video.srcObject = evt.streams[0]
+ }
+
+ this.pc.createOffer().then((offer) => this.onLocalOffer(offer))
+ }
- window.console.log('[webcam-rtspsimpleserver] sending offer')
- this.ws.send(JSON.stringify(desc))
+ // eslint-disable-next-line no-undef
+ onLocalOffer(offer: RTCSessionDescriptionInit) {
+ this.offerData = this.parseOffer(offer.sdp ?? '')
+ this.pc?.setLocalDescription(offer)
+
+ fetch(this.url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/sdp',
+ },
+ body: offer.sdp,
})
+ .then((res) => {
+ if (res.status !== 201) throw new Error('bad status code')
+ this.eTag = res.headers.get('ETag')
+
+ // fallback for MediaMTX v1.0.x with broken ETag header
+ if (res.headers.has('E-Tag')) this.eTag = res.headers.get('E-Tag')
+
+ return res.text()
+ })
+ .then((sdp) => {
+ this.onRemoteAnswer(
+ new RTCSessionDescription({
+ type: 'answer',
+ sdp,
+ })
+ )
+ })
+ .catch((err) => {
+ this.log(err)
+ this.scheduleRestart()
+ })
+ }
+
+ onRemoteAnswer(answer: RTCSessionDescription) {
+ if (this.restartTimeout !== null) return
+
+ // this.pc.setRemoteDescription(new RTCSessionDescription(answer));
+ this.pc?.setRemoteDescription(answer)
+
+ if (this.queuedCandidates.length !== 0) {
+ this.sendLocalCandidates(this.queuedCandidates)
+ this.queuedCandidates = []
+ }
}
- webRtcOnRemoteDescription(msg: any) {
- if (this.pc === null || this.ws === null) return
+ onConnectionState() {
+ if (this.restartTimeout !== null) return
+
+ this.status = this.pc?.iceConnectionState ?? ''
+ this.log('peer connection state:', this.status)
- this.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.data)))
- this.ws.onmessage = (msg: any) => this.webRtcOnRemoteCandidate(msg)
+ switch (this.status) {
+ case 'disconnected':
+ this.scheduleRestart()
+ }
}
- webRtcOnIceCandidate(evt: RTCPeerConnectionIceEvent) {
- if (this.ws === null) return
+ onLocalCandidate(evt: RTCPeerConnectionIceEvent) {
+ if (this.restartTimeout !== null) return
- if (evt.candidate?.candidate !== '') {
- this.ws.send(JSON.stringify(evt.candidate))
+ if (evt.candidate !== null) {
+ if (this.eTag === '') {
+ this.queuedCandidates.push(evt.candidate)
+ return
+ }
+
+ this.sendLocalCandidates([evt.candidate])
}
}
- webRtcOnRemoteCandidate(msg: any) {
- if (this.pc === null) return
+ sendLocalCandidates(candidates: RTCIceCandidate[]) {
+ fetch(this.url, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/trickle-ice-sdpfrag',
+ 'If-Match': this.eTag,
+ // eslint-disable-next-line no-undef
+ } as HeadersInit,
+ body: this.generateSdpFragment(this.offerData, candidates),
+ })
+ .then((res) => {
+ if (res.status !== 204) throw new Error('bad status code')
+ })
+ .catch((err) => {
+ this.log(err)
+ this.scheduleRestart()
+ })
+ }
+
+ terminate() {
+ this.log('terminating')
- this.pc.addIceCandidate(JSON.parse(msg.data))
+ if (this.pc !== null) {
+ this.pc.close()
+ this.pc = null
+ }
}
scheduleRestart() {
- this.ws?.close()
- this.ws = null
-
- this.pc?.close()
- this.pc = null
+ if (this.restartTimeout !== null) return
- if (this.terminated) return
+ this.terminate()
- this.restartTimeoutTimer = setTimeout(() => {
+ this.restartTimeout = window.setTimeout(() => {
+ this.log('scheduling restart')
+ this.restartTimeout = null
this.start()
- }, 2000)
+ }, this.RESTART_PAUSE)
+
+ this.eTag = ''
+ this.queuedCandidates = []
}
}
diff --git a/src/locales/de.json b/src/locales/de.json
index ab2e2df80f..8d0290c999 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -148,6 +148,7 @@
"StartPrint": {
"Cancel": "abbrechen",
"DoYouWantToStartFilename": "Willst du {filename} starten?",
+ "DoYouWantToStartFilenameFilament": "Willst du {filename} mit dem folgenden Filament starten?",
"Headline": "Starte Job",
"Print": "drucken",
"Timelapse": "Zeitraffer"
@@ -495,6 +496,7 @@
"CommittedOnDate": "eingereicht am {date}",
"CommittedYesterday": "eingereicht gestern",
"ConfigChanges": "Konfigurationsänderungen",
+ "Corrupt": "Korrupt",
"CountPackagesCanBeUpgraded": "{count} Pakete können aktualisiert werden",
"Detached": "abgetrennt",
"Dirty": "kompromittiert",
@@ -509,10 +511,6 @@
"LinkToGithub": "Link zu GitHub",
"MoonrakerUpdateQuestion": "Dadurch wird die Moonraker-API aktualisiert. Möglicherweise sind Änderungen an der Datei moonraker.conf erforderlich, damit die Maschine wieder verwendet werden kann.",
"MoreCommitsInfo": "Hier können maximal 30 Commits angezeigt werden. Um alle Commits zu sehen, klicke bitte auf den folgenden Link:",
- "Notification": {
- "Detached": "Ein abgetrennter Zustand ist weder ein Fehler noch ein Problem. Es bedeutet nur, dass im lokalen Repository zusätzliche Commits existieren, die im entfernten Repository nicht vorhanden sind.",
- "Dirty": "Das lokale Repository wurde geändert und kann in diesem Zustand nicht aktualisiert werden. Bitte stelle wieder ein saubers Repository her."
- },
"OSPackages": "OS-Pakete",
"SoftRecovery": "einfache Wiederherstellung",
"StartUpdate": "Aktualisierung starten",
@@ -591,6 +589,7 @@
"Send": "senden"
},
"MiniconsolePanel": {
+ "Autoscroll": "Autoscroll",
"Headline": "Konsole",
"HideTemperatures": "Temperaturmeldungen ausblenden",
"HideTimelapse": "Timelapse ausblenden",
@@ -623,6 +622,32 @@
"On": "An",
"PowerControl": "Stromversorgung"
},
+ "SpoolmanPanel": {
+ "Cancel": "Abbrechen",
+ "ChangeSpool": "Rolle wechseln",
+ "DaysAgo": "vor {days} Tagen",
+ "EjectSpool": "Rolle auswerfen",
+ "EjectSpoolQuestion": "Sind Sie sicher, dass Sie die Filamentrolle auswerfen wollen?",
+ "Filament": "Filament",
+ "FilamentTypeMismatch": "Das Material der aktiven Filamentrolle ({spoolType}) stimmt nicht mit dem Material des G-Codes ({fileType}) überein.",
+ "Headline": "Spoolman",
+ "LastUsed": "Zuletzt verwendet",
+ "Location": "Standort",
+ "Material": "Material",
+ "Never": "Nie",
+ "NoActiveSpool": "Die Filamentverfolgung ist inaktiv. Um zu beginnen, wählen Sie bitte eine Rolle aus.",
+ "NoResults": "Keine Rolle mit den aktuellen Suchkriterien gefunden.",
+ "NoSpools": "Keine Rolle verfügbar",
+ "NoSpoolSelected": "Keine Rolle ausgewählt. Bitte wählen Sie eine Spule aus, sonst kann dieser Druck nicht verfolgt werden.",
+ "OpenSpoolManager": "Rollenverwaltung öffnen",
+ "Refresh": "aktualisieren",
+ "Search": "Suchen",
+ "SelectSpool": "Rolle auswählen",
+ "Today": "Heute",
+ "TooLessFilament": "Die aktuelle Rolle hat möglicherweise nicht genug Filament für diesen Druck. ({spoolWeight}g von {fileWeight}g)",
+ "Weight": "Gewicht",
+ "Yesterday": "Gestern"
+ },
"StatusPanel": {
"CancelPrint": "Druck abbrechen",
"ClearPrintStats": "Druckstatistiken löschen",
@@ -685,6 +710,7 @@
},
"Headline": "Temperaturen",
"HideMcuHostSensors": "Host/MCU Sensoren ausblenden",
+ "HideMonitors": "Monitore ausblenden",
"Max": "max",
"Min": "min",
"Name": "Name",
@@ -727,9 +753,7 @@
"SaveConfig": "SAVE CONFIG",
"SaveInfoDescription": "Der neue Z-Versatz wurde berechnet und registriert. Auf \"SAVE CONFIG\", klicken um den neuen Z-Versatz in der printer.cfg zu speichern und Klipper neu zu starten.",
"SaveInfoDescriptionPrint": "Der neue Z-Versatz wurde berechnet und registriert. Nach dem Drucken auf \"SAVE CONFIG\" in der oberen Leiste klicken um den neuen Z-Versatz in der printer.cfg zu speichern und Klipper neu zu starten.",
- "SaveInfoHeadline": "Information",
- "ToEndstop": "bis Endschalter",
- "ToProbe": "bis Sensor"
+ "SaveInfoHeadline": "Information"
}
},
"PowerDeviceChangeDialog": {
@@ -884,6 +908,7 @@
"DbConsoleHistory": "Verlauf der Konsole",
"DbHistoryJobs": "Historie Druckvorgänge",
"DbHistoryTotals": "Historie Gesamtzähler",
+ "DBNavigation": "Navigation",
"DbTimelapseSettings": "Zeitraffer Einstellungen",
"DbView": "Ansichtseinstellungen",
"EstimateValues": {
@@ -891,6 +916,7 @@
"File": "Datei",
"Slicer": "Slicer"
},
+ "Everything": "Alle",
"FactoryDialog": "Bitte wähle alle Abschnitte die du zurücksetzen möchtest:",
"FactoryReset": "Werkseinstellungen",
"General": "Allgemein",
@@ -1063,6 +1089,7 @@
"UiSettingsTab": {
"BedScrewsDialog": "Hilfsfenster für Bettschraubenjustierung",
"BedScrewsDialogDescription": "Zeige ein Hilfsfenster für die Bettschraubenjustierung an.",
+ "BigThumbnailBackground": "Hintergrundfarbe vom großen Vorschaubild",
"BoolBigThumbnail": "Große Vorschaubilder",
"BoolBigThumbnailDescription": "Zeige ein großes Thumbnail in der Status-Anzeige während eines Drucks.",
"BoolHideUploadAndPrintButton": "\"Hochladen & Drucken\" Schaltfläche ausblenden",
@@ -1144,7 +1171,7 @@
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcJanus": "WebRTC (janus-gateway)",
- "WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
+ "WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
},
"Timelapse": {
diff --git a/src/locales/en.json b/src/locales/en.json
index 6e03f54bd3..d37a316405 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -137,7 +137,6 @@
"TryAgain": "try again"
},
"Console": {
- "Autoscroll": "Autoscroll",
"CommandList": "Command list",
"Empty": "Empty",
"HideTemperatures": "Hide temperatures",
@@ -149,6 +148,7 @@
"StartPrint": {
"Cancel": "Cancel",
"DoYouWantToStartFilename": "Do you want to start {filename}?",
+ "DoYouWantToStartFilenameFilament": "Do you want to start {filename} with the following filament?",
"Headline": "Start Job",
"Print": "print",
"Timelapse": "Timelapse"
@@ -622,6 +622,32 @@
"On": "On",
"PowerControl": "Power Control"
},
+ "SpoolmanPanel": {
+ "Cancel": "Cancel",
+ "ChangeSpool": "Change Spool",
+ "DaysAgo": "{days} days ago",
+ "EjectSpool": "Eject spool",
+ "EjectSpoolQuestion": "Are you sure to eject the filament spool?",
+ "Filament": "Filament",
+ "FilamentTypeMismatch": "The material of the active spool ({spoolType}) does not match the material of the G-Code ({fileType}).",
+ "Headline": "Spoolman",
+ "LastUsed": "Last Used",
+ "Location": "Location",
+ "Material": "Material",
+ "Never": "Never",
+ "NoActiveSpool": "Filament tracking is inactive. To get started, please select a spool.",
+ "NoResults": "No spool found with the current search criteria.",
+ "NoSpools": "No spools available",
+ "NoSpoolSelected": "No spool selected. Please select a spool or this print will not be tracked.",
+ "OpenSpoolManager": "open Spool Manager",
+ "Refresh": "refresh",
+ "Search": "Search",
+ "SelectSpool": "Select Spool",
+ "Today": "Today",
+ "TooLessFilament": "The current spool may not have enough filament for this print. ({spoolWeight}g of {fileWeight}g)",
+ "Weight": "Weight",
+ "Yesterday": "Yesterday"
+ },
"StatusPanel": {
"CancelPrint": "Cancel print",
"ClearPrintStats": "Clear print stats",
@@ -684,6 +710,7 @@
},
"Headline": "Temperatures",
"HideMcuHostSensors": "Hide Host/MCU Sensors",
+ "HideMonitors": "Hide Monitors",
"Max": "max",
"Min": "min",
"Name": "Name",
@@ -726,9 +753,7 @@
"SaveConfig": "SAVE CONFIG",
"SaveInfoDescription": "The new Z-Offset has been calculated and registered. Click on \"SAVE CONFIG\" to save it to the printer.cfg and restart Klipper.",
"SaveInfoDescriptionPrint": "The new Z-Offset has been calculated and registered. After printing, click on \"SAVE CONFIG\" in the top bar to save it to the printer.cfg and restart Klipper.",
- "SaveInfoHeadline": "Information",
- "ToEndstop": "to Endstop",
- "ToProbe": "to Probe"
+ "SaveInfoHeadline": "Information"
}
},
"PowerDeviceChangeDialog": {
@@ -1064,6 +1089,7 @@
"UiSettingsTab": {
"BedScrewsDialog": "Bed Screws Dialog",
"BedScrewsDialogDescription": "Display helper dialog for BED_SCREWS_ADJUST.",
+ "BigThumbnailBackground": "Large thumbnail background color",
"BoolBigThumbnail": "Large thumbnail",
"BoolBigThumbnailDescription": "Display a large thumbnail in the status panel during a print.",
"BoolHideUploadAndPrintButton": "Hide Upload and Print Button",
@@ -1145,7 +1171,7 @@
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcJanus": "WebRTC (janus-gateway)",
- "WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
+ "WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
},
"Timelapse": {
diff --git a/src/locales/tr.json b/src/locales/tr.json
index b5f51e8ec6..812ec8b8d0 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -1132,7 +1132,7 @@
"Webcams": "Web Kameraları",
"WebrtcCameraStreamer": "WebRTC (kamera-streamer)",
"WebrtcJanus": "WebRTC (janus-gateway)",
- "WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
+ "WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
},
"Timelapse": {
diff --git a/src/locales/zh.json b/src/locales/zh.json
index c1564fe305..7662f90cb1 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -1,6 +1,10 @@
{
"App": {
"Notifications": {
+ "BrowserWarnings": {
+ "Description": "{name} 已过时且没有完整支持。当前版本是{version},但是Mainsail需要{minVersion}或更高版本。",
+ "Headline": "过时的浏览器"
+ },
"DependencyDescription": "当前版本的{name}并不支持Mainsail的全部功能。{name}需要至少更新到{neededVersion}版本。",
"DependencyName": "依赖:{name}",
"DismissAll": "全部忽略",
@@ -29,7 +33,7 @@
"MustBeBetweenError": "必须在{min}和{max}之间!",
"NoEmptyAllowedError": "输入值不能为空!"
},
- "Printers": "打印机集群",
+ "Printers": "打印机群组",
"TheServiceWorker": {
"DescriptionNeedUpdate": "本地缓存已过时,需要进行更新。请点击下面的按钮来更新缓存。",
"TitleNeedUpdate": "PWA需要更新",
@@ -144,6 +148,7 @@
"StartPrint": {
"Cancel": "取消",
"DoYouWantToStartFilename": "是否开始打印{filename}?",
+ "DoYouWantToStartFilenameFilament": "是否用以下耗材开始打印{filename}?",
"Headline": "开始任务",
"Print": "打印",
"Timelapse": "延时摄影"
@@ -193,7 +198,7 @@
"ExtruderTemp": "挤出机温度",
"Filament": "耗材",
"FilamentName": "耗材名称",
- "FilamentType": "耗材规格",
+ "FilamentType": "耗材类型",
"FilamentUsage": "耗材用量",
"FilamentWeight": "耗材重量",
"Files": "每页显示文件数",
@@ -219,7 +224,7 @@
"Preheat": "预热",
"PrintedFiles": "已打印文件",
"PrintStart": "开始打印",
- "PrintTime": "打印时间",
+ "PrintTime": "打印时长",
"RefreshCurrentDirectory": "刷新当前路径",
"Rename": "重命名",
"RenameDirectory": "重命名文件夹",
@@ -316,7 +321,7 @@
"History": {
"AddNote": "添加便条",
"AllJobs": "全部",
- "AvgPrinttime": "平均打印时间",
+ "AvgPrinttime": "平均打印时长",
"Cancel": "取消",
"Chart": "图表",
"CreateNote": "新建便条",
@@ -329,58 +334,58 @@
"EndTime": "结束时间",
"EstimatedFilament": "预估耗材用量",
"EstimatedFilamentWeight": "预估耗材重量",
- "EstimatedTime": "预估时长",
+ "EstimatedTime": "预估打印时长",
"FilamentCalc": "耗材预估长度",
"FilamentUsage": "耗材用量",
- "FilamentUsed": "耗材使用长度",
+ "FilamentUsed": "实际耗材用量",
"Filename": "文件名",
"Filesize": "文件大小",
"FirstLayerBedTemp": "首层热床温度",
- "FirstLayerExtTemp": "首层仓内温度",
+ "FirstLayerExtTemp": "首层挤出温度",
"FirstLayerHeight": "首层高度",
"HistoryFilamentUsage": "耗材用量",
- "HistoryPrinttimeAVG": "打印时间",
+ "HistoryPrinttimeAVG": "打印时长",
"JobDetails": "任务详情",
"Jobs": "每页显示任务数",
- "LastModified": "上次修改",
+ "LastModified": "修改日期",
"LayerHeight": "层高",
- "LongestPrinttime": "最长打印时间",
+ "LongestPrinttime": "最长打印时长",
"Note": "便条",
"ObjectHeight": "物体高度",
"PrintDuration": "打印耗时",
"PrintHistory": "打印历史",
- "PrintTime": "打印时长",
- "PrinttimeAvg": "平均打印时间",
+ "PrintTime": "实际打印时长",
+ "PrinttimeAvg": "平均打印时长",
"Reprint": "重新打印",
"Save": "保存",
"Search": "搜索",
"SelectedFilamentUsed": "已选中的任务耗材使用量",
"SelectedJobs": "已选中的任务",
- "SelectedPrinttime": "已选中的任务打印时间",
+ "SelectedPrinttime": "已选中的任务打印时长",
"Slicer": "切片软件",
"SlicerVersion": "切片软件版本",
"StartTime": "开始时间",
"Statistics": "统计",
- "Status": "状态",
+ "Status": "任务状态",
"StatusValues": {
- "cancelled": "取消",
- "completed": "完成",
- "error": "错误",
+ "cancelled": "取消打印",
+ "completed": "完成打印",
+ "error": "故障",
"in_progress": "打印中",
"klippy_disconnect": "Klippy失去连接",
- "klippy_shutdown": "Klippy已关闭",
+ "klippy_shutdown": "Klippy关闭",
"Others": "其他",
- "server_exit": "服务器退出"
+ "server_exit": "服务终止"
},
"Table": "表格",
"TitleExportHistory": "导出历史记录",
"TitleRefreshHistory": "刷新历史记录",
"TitleSettings": "设置",
- "TotalDuration": "总时间",
+ "TotalDuration": "总消耗时长",
"TotalFilamentUsed": "耗材使用总量",
"TotalJobs": "任务次数",
- "TotalPrinttime": "总打印时间",
- "TotalTime": "总消耗时间"
+ "TotalPrinttime": "总打印时长",
+ "TotalTime": "总消耗时长"
},
"JobQueue": {
"AllJobs": "全部任务",
@@ -422,7 +427,7 @@
"FreeDisk": "剩余磁盘空间",
"HiddenFiles": "显示隐藏文件",
"HideBackupFiles": "隐藏备份文件",
- "LastModified": "上次修改",
+ "LastModified": "修改日期",
"Name": "名称",
"RefreshDirectory": "刷新文件夹",
"Rename": "重命名",
@@ -491,7 +496,7 @@
"CommittedOnDate": "提交于{date}",
"CommittedYesterday": "昨天提交",
"ConfigChanges": "查看变更日志",
- "Corrupt": "破坏",
+ "Corrupt": "异常",
"CountPackagesCanBeUpgraded": "有{count}个软件包可以更新",
"Detached": "独立分支",
"Dirty": "被修改",
@@ -540,8 +545,8 @@
"FirmwareRetractionSettings": {
"RetractLength": "回抽长度",
"RetractSpeed": "回抽速度",
- "UnretractExtraLength": "回料长度",
- "UnretractSpeed": "回料速度"
+ "UnretractExtraLength": "装填长度",
+ "UnretractSpeed": "装填速度"
},
"Headline": "挤出机",
"LoadFilament": "装载耗材",
@@ -561,7 +566,7 @@
"WebcamOff": "关闭"
},
"KlippyStatePanel": {
- "CheckKlippyAndUdsAddress": "请检查Klipper服务是否启动并且moonraker.conf已经添加了klippy_uds_address配置。",
+ "CheckKlippyAndUdsAddress": "请检查Klipper服务是否启动。",
"FirmwareRestart": "重启Klipper固件",
"MoonrakerCannotConnect": "Moonraker无法连接到Klipper !",
"PowerOn": "开启电源",
@@ -584,6 +589,7 @@
"Send": "发送"
},
"MiniconsolePanel": {
+ "Autoscroll": "自动滚动",
"Headline": "控制台",
"HideTemperatures": "隐藏温度变化消息",
"HideTimelapse": "隐藏延时摄影消息",
@@ -616,6 +622,32 @@
"On": "打开",
"PowerControl": "电源控制"
},
+ "SpoolmanPanel": {
+ "Cancel": "取消",
+ "ChangeSpool": "更换料盘",
+ "DaysAgo": "{days}天前",
+ "EjectSpool": "弹出料盘",
+ "EjectSpoolQuestion": "你确定要弹出耗材料盘吗?",
+ "Filament": "耗材",
+ "FilamentTypeMismatch": "已激活料盘的材料({spoolType})与G-Code的材料({fileType})不匹配。",
+ "Headline": "Spoolman",
+ "LastUsed": "最后使用",
+ "Location": "位置",
+ "Material": "材料",
+ "Never": "从未",
+ "NoActiveSpool": "耗材跟踪未激活。要开始,请选择一个料盘。",
+ "NoResults": "未找到符合当前搜索条件的料盘。",
+ "NoSpools": "没有可用料盘",
+ "NoSpoolSelected": "没有选择料盘。请选择一个料盘,否则将无法跟踪此打印。",
+ "OpenSpoolManager": "打开料盘管理器",
+ "Refresh": "刷新",
+ "Search": "搜索",
+ "SelectSpool": "选择料盘",
+ "Today": "今天",
+ "TooLessFilament": "当前料盘可能没有足够的耗材用于此次打印。(剩余{spoolWeight}克/需要{fileWeight}克)",
+ "Weight": "重量",
+ "Yesterday": "昨天"
+ },
"StatusPanel": {
"CancelPrint": "取消打印",
"ClearPrintStats": "清除打印统计数据",
@@ -678,6 +710,7 @@
},
"Headline": "温度",
"HideMcuHostSensors": "隐藏Host/MCU传感器",
+ "HideMonitors": "隐藏监视器",
"Max": "最高",
"Min": "最低",
"Name": "名称",
@@ -720,9 +753,7 @@
"SaveConfig": "保存配置",
"SaveInfoDescription": "已经计算生成新的Z轴偏移。点击\"保存配置\"后会自动保存到printer.cfg中并重启Klipper。",
"SaveInfoDescriptionPrint": "已经计算生成新的Z轴偏移。打印完成后点击\"保存配置\"后会自动保存到printer.cfg中并重启Klipper。",
- "SaveInfoHeadline": "信息",
- "ToEndstop": "距离限位",
- "ToProbe": "距离Probe传感器"
+ "SaveInfoHeadline": "信息"
}
},
"PowerDeviceChangeDialog": {
@@ -875,6 +906,8 @@
"CannotReadJson": "无法读取/解析备份文件。",
"DateFormat": "日期格式",
"DbConsoleHistory": "控制台历史记录",
+ "DbHistoryJobs": "历史任务",
+ "DbHistoryTotals": "历史总计",
"DBNavigation": "侧边栏",
"DbTimelapseSettings": "延时摄影设置",
"DbView": "视图设置",
@@ -956,7 +989,7 @@
"UnableToLoadPreset": "无法加载预设"
},
"NavigationTab": {
- "Navigation": "导航"
+ "Navigation": "侧边栏"
},
"PresetsTab": {
"AddPreset": "添加预设",
@@ -1042,10 +1075,10 @@
"Timelapse": "延时摄影",
"TravelSpeed": "移动速度",
"TravelSpeedDescription": "打印头移动到停靠位置和恢复到打印位置时的运动速度",
- "UnretractDistance": "回料长度",
- "UnretractDistanceDescription": "挤出机回抽后重新回料的长度",
- "UnretractSpeed": "回料速度",
- "UnretractSpeedDescription": "挤出机回抽后重新回料的速度",
+ "UnretractDistance": "装填长度",
+ "UnretractDistanceDescription": "挤出机回抽后重新装填的长度",
+ "UnretractSpeed": "装填速度",
+ "UnretractSpeedDescription": "挤出机回抽后重新装填的速度",
"VariableFps": "可变FPS",
"VariableFpsDescription": "如果开启,输出视频的帧率会根据目标时长来计算",
"VariableFpsMax": "可变FPS最大值",
@@ -1055,7 +1088,8 @@
},
"UiSettingsTab": {
"BedScrewsDialog": "底板螺丝对话框",
- "BedScrewsDialogDescription": "显示 BED_SCREWS_ADJUST 的辅助程序对话框。",
+ "BedScrewsDialogDescription": "显示BED_SCREWS_ADJUST辅助对话框。",
+ "BigThumbnailBackground": "大缩略图背景颜色",
"BoolBigThumbnail": "大缩略图",
"BoolBigThumbnailDescription": "打印时在状态框中显示大缩略图",
"BoolHideUploadAndPrintButton": "隐藏\"上传并打印\"按钮",
@@ -1086,18 +1120,18 @@
"LockSlidersDescription": "在允许更改之前需要解锁触摸屏上的滑动条",
"Logo": "Logo颜色",
"ManualProbeDialog": "手动Probe助手对话框",
- "ManualProbeDialogDescription": "显示 PROBE_CALIBRATE 或 Z_ENDSTOP_CALIBRATE 的辅助程序对话框。",
- "NavigationStyle": "导航栏样式",
- "NavigationStyleDescription": "更改导航栏外观",
+ "ManualProbeDialogDescription": "显示PROBE_CALIBRATE或Z_ENDSTOP_CALIBRATE辅助对话框。",
+ "NavigationStyle": "侧边栏样式",
+ "NavigationStyleDescription": "更改侧边栏外观",
"NavigationStyleIconsAndText": "图标+文字",
"NavigationStyleIconsOnly": "仅图标",
"PowerDeviceName": "打印机电源设备",
"PowerDeviceNameDescription": "请选择可以控制打印机供电的Moonraker电源设备。",
"Primary": "高亮颜色",
"ScrewsTiltAdjustDialog": "螺丝倾斜调整对话框",
- "ScrewsTiltAdjustDialogDescription": "显示 SCREWS_TILT_CALCULATE 的辅助程序对话框。",
+ "ScrewsTiltAdjustDialogDescription": "显示SCREWS_TILT_CALCULATE辅助对话框。",
"TempchartHeight": "温度图表高度",
- "TempchartHeightDescription": "修改Dashboard上温度图表的高度。",
+ "TempchartHeightDescription": "修改控制面板上温度图表的高度。",
"UiSettings": "UI设置"
},
"Update": "更新",
@@ -1121,7 +1155,7 @@
"Ipstream": "IP摄像头",
"JMuxerStream": "原始h264视频流 (jmuxer)",
"Mjpegstreamer": "MJPEG-Streamer",
- "MjpegstreamerAdaptive": "Adaptive MJPEG-Streamer (试验性)",
+ "MjpegstreamerAdaptive": "自适应MJPEG-Streamer(试验性)",
"Name": "名称",
"NameAlreadyExists": "名称已经存在",
"Required": "需要填写名称",
@@ -1130,14 +1164,14 @@
"Service": "服务",
"TargetFPS": "目标FPS",
"UpdateWebcam": "更新摄像头",
- "UrlSnapshot": "快照地址(URL)",
- "UrlStream": "视频流地址(URL)",
+ "UrlSnapshot": "快照URL",
+ "UrlStream": "视频流URL",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "垂直",
"Webcams": "摄像头",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcJanus": "WebRTC (janus-gateway)",
- "WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
+ "WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
},
"Timelapse": {
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
index 86fdd025b0..6d8d7d326e 100644
--- a/src/pages/Dashboard.vue
+++ b/src/pages/Dashboard.vue
@@ -91,6 +91,7 @@ import MacrosPanel from '@/components/panels/MacrosPanel.vue'
import MiniconsolePanel from '@/components/panels/MiniconsolePanel.vue'
import MinSettingsPanel from '@/components/panels/MinSettingsPanel.vue'
import MiscellaneousPanel from '@/components/panels/MiscellaneousPanel.vue'
+import SpoolmanPanel from '@/components/panels/SpoolmanPanel.vue'
import StatusPanel from '@/components/panels/StatusPanel.vue'
import ToolheadControlPanel from '@/components/panels/ToolheadControlPanel.vue'
import TemperaturePanel from '@/components/panels/TemperaturePanel.vue'
@@ -106,6 +107,7 @@ import WebcamPanel from '@/components/panels/WebcamPanel.vue'
MiniconsolePanel,
MinSettingsPanel,
MiscellaneousPanel,
+ SpoolmanPanel,
StatusPanel,
ToolheadControlPanel,
TemperaturePanel,
diff --git a/src/store/gui/getters.ts b/src/store/gui/getters.ts
index 039b9846cc..c26e42a906 100644
--- a/src/store/gui/getters.ts
+++ b/src/store/gui/getters.ts
@@ -72,6 +72,11 @@ export const getters: GetterTree = {
allPanels = allPanels.filter((name) => name !== 'webcam')
}
+ // remove spoolman panel, if no spoolman component exists in moonraker
+ if (!rootState.server.components.includes('spoolman')) {
+ allPanels = allPanels.filter((name) => name !== 'spoolman')
+ }
+
return allPanels
},
diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts
index c484e21da2..47512562cf 100644
--- a/src/store/gui/index.ts
+++ b/src/store/gui/index.ts
@@ -3,7 +3,7 @@ import { Module } from 'vuex'
import { actions } from '@/store/gui/actions'
import { mutations } from '@/store/gui/mutations'
import { getters } from '@/store/gui/getters'
-import { defaultLogoColor, defaultPrimaryColor } from '@/store/variables'
+import { defaultLogoColor, defaultPrimaryColor, defaultBigThumbnailBackground } from '@/store/variables'
// load modules
import { console } from '@/store/gui/console'
@@ -155,6 +155,7 @@ export const getDefaultState = (): GuiState => {
confirmOnEmergencyStop: false,
confirmOnPowerDeviceChange: false,
boolBigThumbnail: true,
+ bigThumbnailBackground: defaultBigThumbnailBackground,
boolWideNavDrawer: false,
boolHideUploadAndPrintButton: false,
navigationStyle: 'iconsAndText',
@@ -239,6 +240,7 @@ export const getDefaultState = (): GuiState => {
boolTempchart: true,
hiddenDataset: [],
hideMcuHostSensors: false,
+ hideMonitors: false,
autoscale: false,
datasetSettings: {},
},
diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts
index 8eba4690b1..7ad7e91248 100644
--- a/src/store/gui/types.ts
+++ b/src/store/gui/types.ts
@@ -105,6 +105,7 @@ export interface GuiState {
confirmOnEmergencyStop: boolean
confirmOnPowerDeviceChange: boolean
boolBigThumbnail: boolean
+ bigThumbnailBackground: string
boolWideNavDrawer: boolean
boolHideUploadAndPrintButton: boolean
navigationStyle: 'iconsAndText' | 'iconsOnly'
@@ -165,6 +166,7 @@ export interface GuiState {
boolTempchart: boolean
hiddenDataset: string[]
hideMcuHostSensors: boolean
+ hideMonitors: boolean
autoscale: boolean
datasetSettings: any
}
diff --git a/src/store/printer/actions.ts b/src/store/printer/actions.ts
index 8a89be88e6..a2af4be528 100644
--- a/src/store/printer/actions.ts
+++ b/src/store/printer/actions.ts
@@ -57,7 +57,12 @@ export const actions: ActionTree = {
if (Object.keys(subscripts).length > 0)
Vue.$socket.emit('printer.objects.subscribe', { objects: subscripts }, { action: 'printer/getInitData' })
- else Vue.$socket.emit('server.temperature_store', {}, { action: 'printer/tempHistory/init' })
+ else
+ Vue.$socket.emit(
+ 'server.temperature_store',
+ { include_monitors: true },
+ { action: 'printer/tempHistory/init' }
+ )
dispatch('socket/removeInitModule', 'printer/initSubscripts', { root: true })
},
@@ -70,7 +75,7 @@ export const actions: ActionTree = {
dispatch('getData', payload)
- Vue.$socket.emit('server.temperature_store', {}, { action: 'printer/tempHistory/init' })
+ Vue.$socket.emit('server.temperature_store', { include_monitors: true }, { action: 'printer/tempHistory/init' })
setTimeout(() => {
dispatch('initExtruderCanExtrude')
diff --git a/src/store/printer/getters.ts b/src/store/printer/getters.ts
index 38253d0f67..6186c9fa64 100644
--- a/src/store/printer/getters.ts
+++ b/src/store/printer/getters.ts
@@ -372,6 +372,10 @@ export const getters: GetterTree = {
return state.heaters?.available_sensors ?? []
},
+ getAvailableMonitors: (state) => {
+ return state.heaters?.available_monitors ?? []
+ },
+
getFilamentSensors: (state) => {
const sensorObjectNames = ['filament_switch_sensor', 'filament_motion_sensor']
const sensors: PrinterStateFilamentSensors[] = []
diff --git a/src/store/printer/tempHistory/actions.ts b/src/store/printer/tempHistory/actions.ts
index 6b09d0d1cb..f4bf566e3f 100644
--- a/src/store/printer/tempHistory/actions.ts
+++ b/src/store/printer/tempHistory/actions.ts
@@ -28,6 +28,7 @@ export const actions: ActionTree = {
const now = new Date()
const allHeaters = rootGetters['printer/getAvailableHeaters'] ?? []
const allSensors = rootGetters['printer/getAvailableSensors'] ?? []
+ const allMonitors = rootGetters['printer/getAvailableMonitors'] ?? []
const maxHistory = rootGetters['printer/tempHistory/getTemperatureStoreSize']
if (payload !== undefined) {
@@ -44,7 +45,7 @@ export const actions: ActionTree = {
}
// break if sensor doesn't exist anymore or start with a _
- if (!allSensors.includes(key) || nameOnly.startsWith('_')) {
+ if (!(allSensors.includes(key) || allMonitors.includes(key)) || nameOnly.startsWith('_')) {
delete payload[key]
return
}
@@ -54,7 +55,7 @@ export const actions: ActionTree = {
if (datasetKey + 's' in datasetValues) {
const length = maxHistory - datasetValues[datasetKey + 's'].length
datasetValues[datasetKey + 's'] = [
- ...Array.from({ length }, () => 0),
+ ...Array.from({ length }, () => null),
...datasetValues[datasetKey + 's'],
]
}
@@ -64,7 +65,8 @@ export const actions: ActionTree = {
})
// add missing heaters/sensors
- allSensors.forEach((key: string) => {
+ const allEntries = allSensors.concat(allMonitors)
+ allEntries.forEach((key: string) => {
// break if sensor is already in the cache
if (key in payload) return
@@ -85,15 +87,15 @@ export const actions: ActionTree = {
powers?: number[]
speeds?: number[]
} = {
- temperatures: Array(maxHistory).fill(0),
+ temperatures: Array(maxHistory).fill(null),
}
if (allHeaters.includes(key)) {
- addValues.targets = Array(maxHistory).fill(0)
- addValues.powers = Array(maxHistory).fill(0)
+ addValues.targets = Array(maxHistory).fill(null)
+ addValues.powers = Array(maxHistory).fill(null)
} else if (['temperature_fan'].includes(sensorType)) {
- addValues.targets = Array(maxHistory).fill(0)
- addValues.speeds = Array(maxHistory).fill(0)
+ addValues.targets = Array(maxHistory).fill(null)
+ addValues.speeds = Array(maxHistory).fill(null)
}
importData[key] = { ...addValues }
@@ -239,7 +241,11 @@ export const actions: ActionTree = {
window.console.debug('update Source', t0-state.timeLastUpdate)
}*/
- if (rootState?.printer?.heaters?.available_sensors?.length) {
+ const allSensors = rootGetters['printer/getAvailableSensors'] ?? []
+ const allMonitors = rootGetters['printer/getAvailableMonitors'] ?? []
+ const items = allSensors.concat(allMonitors)
+
+ if (items.length) {
const now = new Date()
if (state.source.length) {
@@ -255,14 +261,15 @@ export const actions: ActionTree = {
date: now,
}
- rootState.printer.heaters.available_sensors.forEach((name: string) => {
+ items.forEach((name: string) => {
if (!(rootState.printer && name in rootState.printer)) return
const printerObject = { ...rootState.printer[name] }
datasetTypes.forEach((attrKey) => {
if (!(attrKey in printerObject)) return
- let value = Math.round(printerObject[attrKey] * 10) / 10
+ let value = printerObject[attrKey]
+ if (value !== null) value = Math.round(printerObject[attrKey] * 10) / 10
if (datasetTypesInPercents.includes(attrKey))
value = Math.round(printerObject[attrKey] * 1000) / 1000
diff --git a/src/store/printer/tempHistory/getters.ts b/src/store/printer/tempHistory/getters.ts
index 53ed0d318d..d9b591efd6 100644
--- a/src/store/printer/tempHistory/getters.ts
+++ b/src/store/printer/tempHistory/getters.ts
@@ -145,6 +145,21 @@ export const getters: GetterTree = {
})
}
+ // hide Monitors, if the option is set to true
+ const hideMonitors = rootState.gui?.view?.tempchart?.hideMonitors ?? false
+ if (hideMonitors) {
+ const monitors = rootState.printer?.heaters?.available_monitors ?? []
+
+ Object.keys(selected)
+ .filter((seriesName) => {
+ const datasetName = seriesName.slice(0, seriesName.lastIndexOf('-'))
+ return monitors.includes(datasetName)
+ })
+ .forEach((seriesName) => {
+ selected[seriesName] = false
+ })
+ }
+
return selected
},
diff --git a/src/store/server/index.ts b/src/store/server/index.ts
index 95717ac7c6..643e9ce4c2 100644
--- a/src/store/server/index.ts
+++ b/src/store/server/index.ts
@@ -11,6 +11,7 @@ import { history } from '@/store/server/history'
import { timelapse } from '@/store/server/timelapse'
import { jobQueue } from '@/store/server/jobQueue'
import { announcements } from '@/store/server/announcements'
+import { spoolman } from '@/store/server/spoolman'
// create getDefaultState
export const getDefaultState = (): ServerState => {
@@ -59,5 +60,6 @@ export const server: Module = {
timelapse,
jobQueue,
announcements,
+ spoolman,
},
}
diff --git a/src/store/server/spoolman/actions.ts b/src/store/server/spoolman/actions.ts
new file mode 100644
index 0000000000..6553cd8d2e
--- /dev/null
+++ b/src/store/server/spoolman/actions.ts
@@ -0,0 +1,132 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { RootState } from '@/store/types'
+import { ServerSpoolmanState } from '@/store/server/spoolman/types'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init({ dispatch }) {
+ Vue.$socket.emit('server.spoolman.get_spool_id', {}, { action: 'server/spoolman/getActiveSpoolId' })
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: '/v1/info',
+ },
+ { action: 'server/spoolman/getInfo' }
+ )
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: '/v1/health',
+ },
+ { action: 'server/spoolman/getHealth' }
+ )
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: '/v1/vendor',
+ },
+ { action: 'server/spoolman/getVendors' }
+ )
+
+ dispatch('socket/addInitModule', 'server/spoolman/getActiveSpoolId', { root: true })
+ dispatch('socket/addInitModule', 'server/spoolman/getHealth', { root: true })
+ dispatch('socket/addInitModule', 'server/spoolman/getInfo', { root: true })
+ dispatch('socket/addInitModule', 'server/spoolman/getVendors', { root: true })
+
+ dispatch('socket/removeInitModule', 'server/spoolman/init', { root: true })
+ },
+
+ getActiveSpoolId({ commit, dispatch }, payload) {
+ commit('setActiveSpoolId', payload.spool_id)
+ dispatch('socket/removeInitModule', 'server/spoolman/getActiveSpoolId', { root: true })
+
+ // also set active spool to null, if spool_id is 0
+ if (payload.spool_id === 0) {
+ commit('setActiveSpool', null)
+ return
+ }
+
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: `/v1/spool/${payload.spool_id}`,
+ },
+ { action: 'server/spoolman/getActiveSpool' }
+ )
+ },
+
+ getActiveSpool({ commit }, payload) {
+ if ('requestParams' in payload) delete payload.requestParams
+
+ commit('setActiveSpool', payload)
+ },
+
+ getHealth({ commit, dispatch }, payload) {
+ delete payload.requestParams
+ commit('setHealth', payload.status)
+ dispatch('socket/removeInitModule', 'server/spoolman/getHealth', { root: true })
+ },
+
+ getInfo({ commit, dispatch }, payload) {
+ delete payload.requestParams
+ commit('setInfo', payload)
+ dispatch('socket/removeInitModule', 'server/spoolman/getInfo', { root: true })
+ },
+
+ getVendors({ commit, dispatch }, payload) {
+ delete payload.requestParams
+ commit(
+ 'setVendors',
+ Object.entries(payload).map((value) => value)
+ )
+ dispatch('socket/removeInitModule', 'server/spoolman/getVendors', { root: true })
+ },
+
+ refreshSpools({ dispatch }) {
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: '/v1/spool',
+ },
+ { action: 'server/spoolman/getSpools' }
+ )
+
+ dispatch('socket/addLoading', 'refreshSpools', { root: true })
+ },
+
+ getSpools({ commit, dispatch }, payload) {
+ if ('requestParams' in payload) delete payload.requestParams
+ const spools = Object.entries(payload).map((value) => value[1])
+ commit('setSpools', spools)
+
+ dispatch('socket/removeLoading', 'refreshSpools', { root: true })
+ },
+
+ setActiveSpool(_, id: number | null) {
+ Vue.$socket.emit('server.spoolman.post_spool_id', {
+ spool_id: id,
+ })
+ },
+
+ refreshActiveSpool({ state }) {
+ if (state.active_spool_id === null) return
+
+ Vue.$socket.emit(
+ 'server.spoolman.proxy',
+ {
+ request_method: 'GET',
+ path: `/v1/spool/${state.active_spool_id}`,
+ },
+ { action: 'server/spoolman/getActiveSpool' }
+ )
+ },
+}
diff --git a/src/store/server/spoolman/getters.ts b/src/store/server/spoolman/getters.ts
new file mode 100644
index 0000000000..0cdbc78581
--- /dev/null
+++ b/src/store/server/spoolman/getters.ts
@@ -0,0 +1,5 @@
+import { GetterTree } from 'vuex'
+import { ServerSpoolmanState } from './types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {}
diff --git a/src/store/server/spoolman/index.ts b/src/store/server/spoolman/index.ts
new file mode 100644
index 0000000000..8c894c942e
--- /dev/null
+++ b/src/store/server/spoolman/index.ts
@@ -0,0 +1,34 @@
+import { Module } from 'vuex'
+import { ServerSpoolmanState } from '@/store/server/spoolman/types'
+import { actions } from '@/store/server/spoolman/actions'
+import { mutations } from '@/store/server/spoolman/mutations'
+import { getters } from '@/store/server/spoolman/getters'
+
+export const getDefaultState = (): ServerSpoolmanState => {
+ return {
+ health: '',
+ info: {
+ automatic_backups: false,
+ backups_dir: '',
+ data_dir: '',
+ debug_mode: false,
+ version: '',
+ },
+ active_spool_id: null,
+ active_spool: null,
+ vendors: [],
+ feeds: [],
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const spoolman: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/server/spoolman/mutations.ts b/src/store/server/spoolman/mutations.ts
new file mode 100644
index 0000000000..f1a9f40937
--- /dev/null
+++ b/src/store/server/spoolman/mutations.ts
@@ -0,0 +1,34 @@
+import { getDefaultState } from './index'
+import { MutationTree } from 'vuex'
+import { ServerSpoolmanState } from './types'
+import Vue from 'vue'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ setActiveSpoolId(state, payload) {
+ Vue.set(state, 'active_spool_id', payload)
+ },
+
+ setActiveSpool(state, payload) {
+ Vue.set(state, 'active_spool', payload)
+ },
+
+ setHealth(state, payload) {
+ Vue.set(state, 'health', payload)
+ },
+
+ setInfo(state, payload) {
+ Vue.set(state, 'info', payload)
+ },
+
+ setVendors(state, payload) {
+ Vue.set(state, 'vendors', payload)
+ },
+
+ setSpools(state, payload) {
+ Vue.set(state, 'spools', payload)
+ },
+}
diff --git a/src/store/server/spoolman/types.ts b/src/store/server/spoolman/types.ts
new file mode 100644
index 0000000000..fd2af037fb
--- /dev/null
+++ b/src/store/server/spoolman/types.ts
@@ -0,0 +1,52 @@
+export interface ServerSpoolmanState {
+ health: string
+ info: {
+ automatic_backups: boolean
+ backups_dir: string
+ data_dir: string
+ debug_mode: boolean
+ version: string
+ }
+ active_spool_id: number | null
+ active_spool: ServerSpoolmanStateSpool | null
+ vendors: ServerSpoolmanStateVendor[]
+ feeds: string[]
+}
+
+export interface ServerSpoolmanStateVendor {
+ id: number
+ registered: string
+ name: string
+}
+
+export interface ServerSpoolmanStateFilament {
+ id: number
+ registered: string
+ name: string
+ comment?: string
+ color_hex: string
+ density: number
+ diameter: number
+ material: string
+ price: number
+ settings_bed_temp: number
+ settings_extruder_temp: number
+ spool_weight: number
+ weight: number
+ vendor: ServerSpoolmanStateVendor
+}
+
+export interface ServerSpoolmanStateSpool {
+ id: number
+ registered: string
+ archived: boolean
+ filament: ServerSpoolmanStateFilament
+ first_used: string
+ last_used: string
+ remaining_length: number
+ remaining_weight: number
+ used_length: number
+ used_weight: number
+ location?: string
+ comment?: string
+}
diff --git a/src/store/socket/actions.ts b/src/store/socket/actions.ts
index a231fa0512..a58a016d13 100644
--- a/src/store/socket/actions.ts
+++ b/src/store/socket/actions.ts
@@ -128,6 +128,10 @@ export const actions: ActionTree = {
dispatch('gui/webcams/initStore', payload.params[0], { root: true })
break
+ case 'notify_active_spool_set':
+ dispatch('server/spoolman/getActiveSpoolId', payload.params[0], { root: true })
+ break
+
default:
window.console.debug(payload)
}
diff --git a/src/store/variables.ts b/src/store/variables.ts
index df540bde7b..5673c35ce4 100644
--- a/src/store/variables.ts
+++ b/src/store/variables.ts
@@ -1,8 +1,9 @@
export const defaultLogoColor = '#D41216'
export const defaultPrimaryColor = '#2196f3'
+export const defaultBigThumbnailBackground = '#1e1e1e'
-export const minKlipperVersion = 'v0.11.0-97'
-export const minMoonrakerVersion = 'v0.8.0-38'
+export const minKlipperVersion = 'v0.11.0-257'
+export const minMoonrakerVersion = 'v0.8.0-137'
export const minBrowserVersions = [{ name: 'safari', version: '16.5.2' }]
export const colorArray = ['#F44336', '#8e379d', '#03DAC5', '#3F51B5', '#ffde03', '#009688', '#E91E63']
@@ -25,7 +26,15 @@ export const validGcodeExtensions = ['.gcode', '.g', '.gco', '.ufp', '.nc']
/*
* List of initable server components
*/
-export const initableServerComponents = ['history', 'power', 'updateManager', 'timelapse', 'jobQueue', 'announcements']
+export const initableServerComponents = [
+ 'history',
+ 'power',
+ 'updateManager',
+ 'timelapse',
+ 'jobQueue',
+ 'announcements',
+ 'spoolman',
+]
/*
* List of required klipper config modules
@@ -78,6 +87,7 @@ export const allDashboardPanels = [
'machine-settings',
'miniconsole',
'miscellaneous',
+ 'spoolman',
'temperature',
'webcam',
]
| |