diff --git a/src/components/TheSettingsMenu.vue b/src/components/TheSettingsMenu.vue index f989aae967..ad951fdcf9 100644 --- a/src/components/TheSettingsMenu.vue +++ b/src/components/TheSettingsMenu.vue @@ -81,6 +81,7 @@ import SettingsGCodeViewerTab from '@/components/settings/SettingsGCodeViewerTab import SettingsEditorTab from '@/components/settings/SettingsEditorTab.vue' import SettingsTimelapseTab from '@/components/settings/SettingsTimelapseTab.vue' import SettingsNavigationTab from '@/components/settings/SettingsNavigationTab.vue' +import SettingsTempgroupsTab from '@/components/settings/SettingsTempgroupsTab.vue' import Panel from '@/components/ui/Panel.vue' import { @@ -101,6 +102,7 @@ import { mdiDipSwitch, mdiMenu, mdiGrid, + mdiThermometer, } from '@mdi/js' import SettingsMiscellaneousTab from '@/components/settings/SettingsMiscellaneousTab.vue' import SettingsHeightmapTab from '@/components/settings/SettingsHeightmapTab.vue' @@ -123,6 +125,7 @@ import SettingsHeightmapTab from '@/components/settings/SettingsHeightmapTab.vue SettingsMiscellaneousTab, SettingsNavigationTab, SettingsHeightmapTab, + SettingsTempgroupsTab, }, }) export default class TheSettingsMenu extends Mixins(BaseMixin) { @@ -211,6 +214,11 @@ export default class TheSettingsMenu extends Mixins(BaseMixin) { name: 'heightmap', title: this.$t('Settings.HeightmapTab.Heightmap'), }, + { + icon: mdiThermometer, + name: 'tempgroups', + title: this.$t('Settings.TempgroupsTab.TempGroups'), + }, ] if (this.moonrakerComponents.includes('timelapse')) { diff --git a/src/components/charts/TempChart.vue b/src/components/charts/TempChart.vue index 2fe69636a6..d4030b4c5f 100644 --- a/src/components/charts/TempChart.vue +++ b/src/components/charts/TempChart.vue @@ -14,7 +14,7 @@ + + diff --git a/src/components/panels/TemperaturePanel.vue b/src/components/panels/TemperaturePanel.vue index 4acf4e9e86..24d466b60d 100644 --- a/src/components/panels/TemperaturePanel.vue +++ b/src/components/panels/TemperaturePanel.vue @@ -10,10 +10,11 @@ - + + @@ -31,9 +32,10 @@ import Panel from '@/components/ui/Panel.vue' import Responsive from '@/components/ui/Responsive.vue' import { mdiCloseThick, mdiThermometerLines } from '@mdi/js' import TemperaturePanelPresets from '@/components/panels/Temperature/TemperaturePanelPresets.vue' +import TemperaturePanelTabs from '@/components/panels/Temperature/TemperaturePanelTabs.vue' @Component({ - components: { Panel, TempChart, TemperatureInput, Responsive, TemperaturePanelPresets }, + components: { Panel, TempChart, TemperatureInput, Responsive, TemperaturePanelPresets, TemperaturePanelTabs }, }) export default class TemperaturePanel extends Mixins(BaseMixin, ControlMixin) { mdiCloseThick = mdiCloseThick @@ -45,5 +47,13 @@ export default class TemperaturePanel extends Mixins(BaseMixin, ControlMixin) { get boolTempchart(): boolean { return this.$store.state.gui.view.tempchart.boolTempchart ?? false } + + get shouldShowTabs(): boolean { + return this.$store.getters['gui/tempgroups/shouldShowTabs'] ?? false + } + + get sensorFilter(): string[] | null { + return this.$store.getters['gui/tempgroups/getActiveSensorFilter'] ?? null + } } diff --git a/src/components/settings/SettingsTempgroupsTab.vue b/src/components/settings/SettingsTempgroupsTab.vue new file mode 100644 index 0000000000..727b9b1598 --- /dev/null +++ b/src/components/settings/SettingsTempgroupsTab.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/components/settings/SettingsTempgroupsTabEdit.vue b/src/components/settings/SettingsTempgroupsTabEdit.vue new file mode 100644 index 0000000000..eff0ba110d --- /dev/null +++ b/src/components/settings/SettingsTempgroupsTabEdit.vue @@ -0,0 +1,345 @@ + + + diff --git a/src/locales/en.json b/src/locales/en.json index 0381e5cda5..57b7e7f9db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1021,6 +1021,7 @@ "Unknown": "Unknown" }, "TemperaturePanel": { + "All": "All", "AutoscaleChart": "Autoscale Chart", "Avg": "Avg", "Cooldown": "Cooldown", @@ -1381,6 +1382,30 @@ "UseConfigJson": "InstanceDB = JSON detected. Please use the config.json to modify the printers list." }, "Store": "store", + "TempgroupsTab": { + "Add": "add", + "AddGroup": "add group", + "AvailableSensors": "Available Sensors", + "CountSensors": "no sensors added | {count} sensor | {count} sensors", + "DisplayName": "Display name", + "EditDisplayName": "Edit display name", + "EditGroup": "Edit Group", + "General": "General", + "Groups": "Groups", + "GroupSensors": "Group Sensors", + "Name": "Name", + "NameExists": "Name already exists", + "NameRequired": "Name is required", + "NoAvailableSensors": "No available sensors.", + "NoGroups": "No groups found...", + "NoSensorsInGroup": "No sensors in this group.", + "RemoveSensor": "Remove sensor from group", + "Search": "Search", + "ShowAllTab": "Show 'All' tab", + "ShowChart": "Show chart", + "TempGroups": "Temp Groups", + "UnknownGroup": "Unknown Group" + }, "TimelapseTab": { "Autorender": "Autorender", "AutorenderDescription": "If enabled, the timelapse video will automatically render at the end of the print", @@ -1581,6 +1606,21 @@ "WebrtcMediaMTX": "WebRTC (MediaMTX)" } }, + "Tempgroups": { + "AddGroup": "Add Group", + "AvailableSensors": "Available sensors", + "CreateGroup": "Create Group", + "DeleteGroup": "Delete Group", + "DeleteGroupQuestion": "Do you really want to delete the group \"{name}\"?", + "DisplayName": "Display Name", + "EditGroup": "Edit Group", + "GroupName": "Group Name", + "NoSensorsAdded": "No sensors added to this group.", + "SensorsInGroup": "Sensors in this group", + "ShowAllTab": "Show \"All\" tab", + "ShowChart": "Show Chart", + "Title": "Temperature Groups" + }, "Timelapse": { "AllFiles": "All", "Autorender": "Auto-render", diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts index 4c6dfb97a4..4de3472940 100644 --- a/src/store/gui/index.ts +++ b/src/store/gui/index.ts @@ -21,6 +21,7 @@ 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 { tempgroups } from '@/store/gui/tempgroups' import { webcams } from '@/store/gui/webcams' import { heightmap } from '@/store/gui/heightmap' @@ -336,6 +337,7 @@ export const gui: Module = { notifications, presets, remoteprinters, + tempgroups, webcams, heightmap, }, diff --git a/src/store/gui/tempgroups/actions.ts b/src/store/gui/tempgroups/actions.ts new file mode 100644 index 0000000000..38438cb1ce --- /dev/null +++ b/src/store/gui/tempgroups/actions.ts @@ -0,0 +1,113 @@ +import { ActionTree } from 'vuex' +import { RootState } from '@/store/types' +import { v4 as uuidv4 } from 'uuid' +import Vue from 'vue' +import { GuiTempgroupsState, GuiTempgroup, GuiTempgroupSensor } from '@/store/gui/tempgroups/types' + +export const actions: ActionTree = { + reset({ commit }) { + commit('reset') + }, + + saveSetting({ dispatch }, payload: { name: string; value: any }) { + dispatch( + 'gui/saveSetting', + { + name: 'tempgroups.' + payload.name, + value: payload.value, + }, + { root: true } + ) + }, + + setActiveGroup({ commit, dispatch }, groupId: string | null) { + commit('setActiveGroup', groupId) + dispatch('saveSetting', { name: 'activeGroupId', value: groupId }) + }, + + setShowAllTab({ commit, dispatch }, show: boolean) { + commit('setShowAllTab', show) + dispatch('saveSetting', { name: 'showAllTab', value: show }) + }, + + groupUpload({ state }, id: string) { + Vue.$socket.emit('server.database.post_item', { + namespace: 'mainsail', + key: 'tempgroups.groups.' + id, + value: state.groups[id], + }) + }, + + async groupStore({ commit, state, dispatch }, payload: { values: Partial }) { + const id = uuidv4() + const maxPosition = Object.values(state.groups).reduce((max, g) => Math.max(max, g.position), -1) + + const group: GuiTempgroup = { + id, + name: payload.values.name || 'New Group', + position: maxPosition + 1, + color: payload.values.color, + showChart: payload.values.showChart ?? true, + sensors: payload.values.sensors ?? [], + } + + commit('groupStore', { id, values: group }) + await dispatch('groupUpload', id) + + return id + }, + + groupUpdate({ commit, dispatch }, payload: { id: string; values: Partial }) { + commit('groupUpdate', payload) + dispatch('groupUpload', payload.id) + }, + + groupDelete({ commit }, id: string) { + commit('groupDelete', id) + Vue.$socket.emit('server.database.delete_item', { + namespace: 'mainsail', + key: 'tempgroups.groups.' + id, + }) + }, + + addSensorToGroup({ commit, dispatch }, payload: { id: string; sensorName: string; displayName?: string }) { + const sensor: GuiTempgroupSensor = { + pos: 0, + name: payload.sensorName, + displayName: payload.displayName, + } + + commit('addSensorToGroup', { id: payload.id, sensor }) + dispatch('groupUpload', payload.id) + }, + + updateSensorInGroup( + { commit, dispatch }, + payload: { + id: string + sensorName: string + option: keyof GuiTempgroupSensor + value: any + } + ) { + commit('updateSensorInGroup', payload) + dispatch('groupUpload', payload.id) + }, + + removeSensorFromGroup({ commit, dispatch }, payload: { id: string; sensorName: string }) { + commit('removeSensorFromGroup', payload) + dispatch('groupUpload', payload.id) + }, + + reorderSensorsInGroup({ commit, dispatch }, payload: { id: string; sensors: GuiTempgroupSensor[] }) { + commit('reorderSensorsInGroup', payload) + dispatch('groupUpload', payload.id) + }, + + reorderGroups({ commit, dispatch }, orderedIds: string[]) { + commit('reorderGroups', orderedIds) + orderedIds.forEach((id) => { + dispatch('groupUpload', id) + }) + }, +} diff --git a/src/store/gui/tempgroups/getters.ts b/src/store/gui/tempgroups/getters.ts new file mode 100644 index 0000000000..bd2a1993c5 --- /dev/null +++ b/src/store/gui/tempgroups/getters.ts @@ -0,0 +1,71 @@ +import { GetterTree } from 'vuex' +import { GuiTempgroupsState, GuiTempgroup } from '@/store/gui/tempgroups/types' +import { RootState } from '@/store/types' + +// eslint-disable-next-line +export const getters: GetterTree = { + getSortedGroups(state): GuiTempgroup[] { + return Object.values(state.groups).sort((a, b) => a.position - b.position) + }, + + getActiveGroup(state): GuiTempgroup | null { + if (!state.activeGroupId) return null + return state.groups[state.activeGroupId] || null + }, + + getGroup: + (state) => + (id: string): GuiTempgroup | null => { + return state.groups[id] || null + }, + + getActiveSensorFilter(state): string[] | null { + if (!state.activeGroupId) return null + + const group = state.groups[state.activeGroupId] + if (!group) return null + + return group.sensors + .slice() + .sort((a, b) => a.pos - b.pos) + .map((s) => s.name) + }, + + isSensorInAnyGroup: + (state) => + (sensorName: string): boolean => { + return Object.values(state.groups).some((group) => group.sensors.some((s) => s.name === sensorName)) + }, + + getSensorDisplayName: + (state) => + (sensorName: string): string => { + for (const group of Object.values(state.groups)) { + const sensor = group.sensors.find((s) => s.name === sensorName) + if (sensor?.displayName) { + return sensor.displayName + } + } + return sensorName + }, + + getUngroupedSensors(state, getters, rootState): string[] { + const allSensors: string[] = [] + + const heaters = rootState.printer?.heaters?.available_heaters || [] + allSensors.push(...heaters) + + const sensors = rootState.printer?.heaters?.available_sensors || [] + sensors.forEach((sensor: string) => { + if (!allSensors.includes(sensor)) { + allSensors.push(sensor) + } + }) + + return allSensors.filter((name) => !getters.isSensorInAnyGroup(name)) + }, + + shouldShowTabs(state): boolean { + return Object.keys(state.groups).length > 0 + }, +} diff --git a/src/store/gui/tempgroups/index.ts b/src/store/gui/tempgroups/index.ts new file mode 100644 index 0000000000..99042f76e0 --- /dev/null +++ b/src/store/gui/tempgroups/index.ts @@ -0,0 +1,24 @@ +import { Module } from 'vuex' +import { actions } from '@/store/gui/tempgroups/actions' +import { mutations } from '@/store/gui/tempgroups/mutations' +import { getters } from '@/store/gui/tempgroups/getters' +import { GuiTempgroupsState } from '@/store/gui/tempgroups/types' + +export const getDefaultState = (): GuiTempgroupsState => { + return { + showAllTab: true, + activeGroupId: null, + groups: {}, + } +} + +const state = getDefaultState() + +// eslint-disable-next-line +export const tempgroups: Module = { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/store/gui/tempgroups/mutations.ts b/src/store/gui/tempgroups/mutations.ts new file mode 100644 index 0000000000..695cfc177a --- /dev/null +++ b/src/store/gui/tempgroups/mutations.ts @@ -0,0 +1,110 @@ +import { getDefaultState } from './index' +import { MutationTree } from 'vuex' +import Vue from 'vue' +import { GuiTempgroupsState, GuiTempgroup, GuiTempgroupSensor } from '@/store/gui/tempgroups/types' + +export const mutations: MutationTree = { + reset(state) { + Object.assign(state, getDefaultState()) + }, + + setActiveGroup(state, groupId: string | null) { + state.activeGroupId = groupId + }, + + setShowAllTab(state, show: boolean) { + state.showAllTab = show + }, + + groupStore(state, payload: { id: string; values: GuiTempgroup }) { + Vue.set(state.groups, payload.id, payload.values) + }, + + groupUpdate(state, payload: { id: string; values: Partial }) { + if (payload.id in state.groups) { + const group = { ...state.groups[payload.id] } + Object.assign(group, payload.values) + Vue.set(state.groups, payload.id, group) + } + }, + + groupDelete(state, groupId: string) { + if (groupId in state.groups) { + Vue.delete(state.groups, groupId) + } + + if (state.activeGroupId === groupId) { + state.activeGroupId = null + } + }, + + addSensorToGroup(state, payload: { id: string; sensor: GuiTempgroupSensor }) { + const group = state.groups[payload.id] + if (group) { + const sensors = [...(group.sensors ?? [])] + const maxPos = sensors.reduce((max, s) => Math.max(max, s.pos), -1) + sensors.push({ ...payload.sensor, pos: maxPos + 1 }) + + Vue.set(state.groups[payload.id], 'sensors', sensors) + } + }, + + updateSensorInGroup( + state, + payload: { + id: string + sensorName: string + option: keyof GuiTempgroupSensor + value: any + } + ) { + const group = state.groups[payload.id] + if (group) { + const sensors = [...(group.sensors ?? [])] + const sensorIndex = sensors.findIndex((s) => s.name === payload.sensorName) + if (sensorIndex !== -1) { + sensors[sensorIndex] = { + ...sensors[sensorIndex], + [payload.option]: payload.value, + } + Vue.set(state.groups[payload.id], 'sensors', sensors) + } + } + }, + + removeSensorFromGroup(state, payload: { id: string; sensorName: string }) { + const group = state.groups[payload.id] + if (group) { + const sensors = [...(group.sensors ?? [])] + const deletedIndex = sensors.findIndex((s) => s.name === payload.sensorName) + if (deletedIndex !== -1) { + const oldPos = sensors[deletedIndex].pos + sensors.splice(deletedIndex, 1) + + sensors + .filter((s) => s.pos > oldPos) + .forEach((s) => { + s.pos = s.pos - 1 + }) + } + + Vue.set(state.groups[payload.id], 'sensors', sensors) + } + }, + + reorderSensorsInGroup(state, payload: { id: string; sensors: GuiTempgroupSensor[] }) { + const group = state.groups[payload.id] + if (group) { + const reorderedSensors = payload.sensors.map((s, i) => ({ ...s, pos: i })) + Vue.set(state.groups[payload.id], 'sensors', reorderedSensors) + } + }, + + reorderGroups(state, orderedIds: string[]) { + orderedIds.forEach((id, index) => { + if (state.groups[id]) { + Vue.set(state.groups[id], 'position', index) + } + }) + }, +} diff --git a/src/store/gui/tempgroups/types.ts b/src/store/gui/tempgroups/types.ts new file mode 100644 index 0000000000..402d4daa31 --- /dev/null +++ b/src/store/gui/tempgroups/types.ts @@ -0,0 +1,22 @@ +export interface GuiTempgroupsState { + showAllTab: boolean + activeGroupId: string | null + groups: { + [key: string]: GuiTempgroup + } +} + +export interface GuiTempgroup { + id: string + name: string + position: number + color?: string + showChart: boolean + sensors: GuiTempgroupSensor[] +} + +export interface GuiTempgroupSensor { + pos: number + name: string + displayName?: string +} diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts index dccd2627fa..f81f08c937 100644 --- a/src/store/gui/types.ts +++ b/src/store/gui/types.ts @@ -2,6 +2,7 @@ import { GuiMacrosState } from '@/store/gui/macros/types' import { GuiConsoleState } from '@/store/gui/console/types' import { GuiPresetsState } from '@/store/gui/presets/types' import { GuiRemoteprintersState } from '@/store/gui/remoteprinters/types' +import { GuiTempgroupsState } from '@/store/gui/tempgroups/types' import { ServerHistoryStateJob } from '@/store/server/history/types' import { GuiNotificationState } from '@/store/gui/notifications/types' import { FileStateFile, FileStateGcodefile } from '@/store/files/types' @@ -98,6 +99,7 @@ export interface GuiState { notifications?: GuiNotificationState presets?: GuiPresetsState remoteprinters?: GuiRemoteprintersState + tempgroups?: GuiTempgroupsState uiSettings: { mode: 'dark' | 'light' theme: string