diff --git a/src/App.vue b/src/App.vue index 29a4a2b71..8a24baf2e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -277,6 +277,7 @@ export default class App extends Mixins(BaseMixin) { @Watch('print_percent') print_percentChanged(newVal: number): void { this.drawFavicon(newVal) + this.refreshSpoolman() } @Watch('printerIsPrinting') @@ -284,6 +285,12 @@ export default class App extends Mixins(BaseMixin) { this.drawFavicon(this.print_percent) } + refreshSpoolman(): void { + if (this.moonrakerComponents.includes('spoolman')) { + this.$store.dispatch('server/spoolman/refreshActiveSpool', null, { root: true }) + } + } + appHeight() { this.$nextTick(() => { const doc = document.documentElement diff --git a/src/components/dialogs/SpoolmanChangeSpoolDialog.vue b/src/components/dialogs/SpoolmanChangeSpoolDialog.vue new file mode 100644 index 000000000..8c6254dd9 --- /dev/null +++ b/src/components/dialogs/SpoolmanChangeSpoolDialog.vue @@ -0,0 +1,174 @@ + + + + + + + {{ mdiCloseThick }} + + + + + + + {{ mdiRefresh }} + + + {{ mdiDatabase }} + + + + + + {{ $t('Panels.SpoolmanPanel.NoSpools') }} + + + {{ $t('Panels.SpoolmanPanel.NoResults') }} + + + + + + + + + + + + + diff --git a/src/components/dialogs/SpoolmanChangeSpoolDialogRow.vue b/src/components/dialogs/SpoolmanChangeSpoolDialogRow.vue new file mode 100644 index 000000000..c02fb14ff --- /dev/null +++ b/src/components/dialogs/SpoolmanChangeSpoolDialogRow.vue @@ -0,0 +1,104 @@ + + + + + + + {{ vendor }} - {{ name }} + + + {{ $t('Panels.SpoolmanPanel.Location') }}: {{ location }} + + + + {{ spool.comment }} + + + {{ material }} + {{ last_used }} + + {{ remaining_weight_format }} + / {{ total_weight_format }} + + + + + diff --git a/src/components/dialogs/SpoolmanEjectSpoolDialog.vue b/src/components/dialogs/SpoolmanEjectSpoolDialog.vue new file mode 100644 index 000000000..14200f2a0 --- /dev/null +++ b/src/components/dialogs/SpoolmanEjectSpoolDialog.vue @@ -0,0 +1,55 @@ + + + + + + {{ mdiCloseThick }} + + + + + + {{ $t('Panels.SpoolmanPanel.EjectSpoolQuestion') }} + + + + + + {{ $t('Panels.SpoolmanPanel.Cancel') }} + + {{ $t('Panels.SpoolmanPanel.EjectSpool') }} + + + + + + + diff --git a/src/components/dialogs/StartPrintDialog.vue b/src/components/dialogs/StartPrintDialog.vue index b2604b0e8..afcf1dcdb 100644 --- a/src/components/dialogs/StartPrintDialog.vue +++ b/src/components/dialogs/StartPrintDialog.vue @@ -1,23 +1,28 @@ - + - - {{ $t('Dialogs.StartPrint.Headline') }} + + + {{ $t('Dialogs.StartPrint.Headline') }} + - {{ $t('Dialogs.StartPrint.DoYouWantToStartFilename', { filename: file.filename }) }} + + {{ question }} + + - - + + - + - + - - {{ $t('Dialogs.StartPrint.Cancel') }} + + {{ $t('Dialogs.StartPrint.Cancel') }} + + + + + + {{ alert.text }} + + + + {{ buttonText }} + + + + + + + + + diff --git a/src/components/panels/Spoolman/SpoolmanPanelActiveSpool.vue b/src/components/panels/Spoolman/SpoolmanPanelActiveSpool.vue new file mode 100644 index 000000000..52dafdd20 --- /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 000000000..054a8786d --- /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/ui/SpoolIcon.vue b/src/components/ui/SpoolIcon.vue new file mode 100644 index 000000000..d0b65e6c8 --- /dev/null +++ b/src/components/ui/SpoolIcon.vue @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 6e03f54bd..f793ced0f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -149,6 +149,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 +623,33 @@ "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}).", + "FirstUsedOutput": "First used: {firstUsed}", + "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", diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue index 86fdd025b..6d8d7d326 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 039b9846c..c26e42a90 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/server/index.ts b/src/store/server/index.ts index 95717ac7c..643e9ce4c 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 000000000..6553cd8d2 --- /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 000000000..0cdbc7858 --- /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 000000000..8c894c942 --- /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 000000000..f1a9f4093 --- /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 000000000..fd2af037f --- /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 a231fa051..a58a016d1 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 df540bde7..27e536301 100644 --- a/src/store/variables.ts +++ b/src/store/variables.ts @@ -25,7 +25,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 +86,7 @@ export const allDashboardPanels = [ 'machine-settings', 'miniconsole', 'miscellaneous', + 'spoolman', 'temperature', 'webcam', ]
{{ $t('Panels.SpoolmanPanel.EjectSpoolQuestion') }}
+ {{ question }} +
{{ $t('Panels.SpoolmanPanel.NoActiveSpool') }}