feat: add jobs to queue in batches (#1253)

fixes https://github.com/mainsail-crew/mainsail/issues/920
This commit is contained in:
Stefan Dej 2023-02-11 22:30:52 +01:00 committed by GitHub
parent c904b7ba71
commit b3ce868dec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 563 additions and 273 deletions

View File

@ -324,6 +324,13 @@
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
{{ $t('Files.AddToQueue') }}
</v-list-item>
<v-list-item
v-if="!contextMenu.item.isDirectory && moonrakerComponents.includes('job_queue')"
:disabled="!isGcodeFile(contextMenu.item)"
@click="openAddBatchToQueueDialog(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
{{ $t('Files.AddBatchToQueue') }}
</v-list-item>
<v-list-item
v-if="contextMenu.item.preheat_gcode !== null"
:disabled="['error', 'printing', 'paused'].includes(printer_state)"
@ -496,6 +503,53 @@
</v-card-actions>
</panel>
</v-dialog>
<v-dialog v-model="dialogAddBatchToQueue.show" max-width="400">
<panel
:title="$t('Files.AddToQueue').toString()"
card-class="gcode-files-add-to-queue-dialog"
:icon="mdiPlaylistPlus"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="dialogAddBatchToQueue.show = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<v-text-field
ref="inputFieldAddToQueueCount"
v-model="dialogAddBatchToQueue.count"
:label="$t('Files.Count')"
required
hide-spin-buttons
type="number"
:rules="countInputRules"
@keyup.enter="addBatchToQueueAction">
<template #append-outer>
<div class="_spin_button_group">
<v-btn class="mt-n3" icon plain small @click="dialogAddBatchToQueue.count++">
<v-icon>{{ mdiChevronUp }}</v-icon>
</v-btn>
<v-btn
:disabled="dialogAddBatchToQueue.count <= 1"
class="mb-n3"
icon
plain
small
@click="dialogAddBatchToQueue.count--">
<v-icon>{{ mdiChevronDown }}</v-icon>
</v-btn>
</div>
</template>
</v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="" text @click="dialogAddBatchToQueue.show = false">{{ $t('Files.Cancel') }}</v-btn>
<v-btn color="primary" text @click="addBatchToQueueAction">{{ $t('Files.AddToQueue') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</div>
</template>
@ -509,6 +563,8 @@ import Panel from '@/components/ui/Panel.vue'
import SettingsRow from '@/components/settings/SettingsRow.vue'
import draggable from 'vuedraggable'
import {
mdiChevronDown,
mdiChevronUp,
mdiDragVertical,
mdiCheckboxBlankOutline,
mdiCheckboxMarked,
@ -552,6 +608,12 @@ interface dialogPrintFile {
item: FileStateGcodefile
}
interface dialogAddBatchToQueue {
show: boolean
count: number
item: FileStateGcodefile
}
interface dialogRenameObject {
show: boolean
newName: string
@ -572,6 +634,8 @@ interface tableColumnSetting {
components: { StartPrintDialog, Panel, SettingsRow, draggable },
})
export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
mdiChevronDown = mdiChevronDown
mdiChevronUp = mdiChevronUp
mdiFile = mdiFile
mdiFileDocumentMultipleOutline = mdiFileDocumentMultipleOutline
mdiMagnify = mdiMagnify
@ -646,6 +710,12 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
item: { ...this.contextMenu.item },
}
private dialogAddBatchToQueue: dialogAddBatchToQueue = {
show: false,
count: 1,
item: { ...this.contextMenu.item },
}
private dialogRenameFile: dialogRenameObject = {
show: false,
newName: '',
@ -671,6 +741,10 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
(value: string) => !!value || this.$t('Files.InvalidNameEmpty'),
(value: string) => !this.existsFilename(value) || this.$t('Files.InvalidNameAlreadyExists'),
]
private countInputRules = [
(value: string) => !!value || this.$t('JobQueue.InvalidCountEmpty'),
(value: string) => parseInt(value) > 0 || this.$t('JobQueue.InvalidCountGreaterZero'),
]
existsFilename(name: string) {
return this.files.findIndex((file: FileStateFile) => file.filename === name) >= 0
@ -1097,11 +1171,31 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
this.currentPath = this.currentPath.slice(0, this.currentPath.lastIndexOf('/'))
}
addToQueue(item: FileStateGcodefile | FileStateFile) {
async addToQueue(item: FileStateGcodefile) {
let filename = [this.currentPath, item.filename].join('/')
if (filename.startsWith('/')) filename = filename.slice(1)
this.$store.dispatch('server/jobQueue/addToQueue', [filename])
await this.$store.dispatch('server/jobQueue/addToQueue', [filename])
}
openAddBatchToQueueDialog(item: FileStateGcodefile) {
this.dialogAddBatchToQueue.show = true
this.dialogAddBatchToQueue.count = 1
this.dialogAddBatchToQueue.item = item
}
async addBatchToQueueAction() {
let filename = [this.currentPath, this.dialogAddBatchToQueue.item.filename].join('/')
if (filename.startsWith('/')) filename = filename.slice(1)
const array: string[] = []
for (let i = 0; i < this.dialogAddBatchToQueue.count; i++) {
array.push(filename)
}
await this.$store.dispatch('server/jobQueue/addToQueue', array)
this.dialogAddBatchToQueue.show = false
}
changeMetadataVisible(name: string, value: boolean) {
@ -1336,6 +1430,15 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
}
</script>
<style scoped>
._spin_button_group {
width: 24px;
margin-top: -6px;
margin-left: -6px;
margin-bottom: -6px;
}
</style>
<style>
/*noinspection CssUnusedSymbol*/
.files-table .v-data-table-header__icon {

View File

@ -1,6 +1,10 @@
<template>
<div>
<panel ref="jobqueuePanel" :icon="mdiTrayFull" :title="$t('JobQueue.JobQueue')" card-class="jobqueue-panel">
<panel
ref="jobqueuePanel"
:icon="mdiTrayFull"
:title="$t('JobQueue.JobQueue').toString()"
card-class="jobqueue-panel">
<template #buttons>
<v-btn
v-if="queueState === 'paused'"
@ -48,98 +52,29 @@
</template>
<template #item="{ item }">
<tr
:key="item.job_id"
v-longpress:600="(e) => showContextMenu(e, item)"
class="file-list-cursor user-select-none"
@contextmenu="showContextMenu($event, item)">
<td class="pr-0 text-center" style="width: 32px">
<template v-if="getSmallThumbnail(item) && getBigThumbnail(item)">
<v-tooltip
v-if="!item.isDirectory && getSmallThumbnail(item) && getBigThumbnail(item)"
top
content-class="tooltip__content-opacity1">
<template #activator="{ on, attrs }">
<vue-load-image>
<img
slot="image"
:src="getSmallThumbnail(item)"
width="32"
height="32"
v-bind="attrs"
v-on="on" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
</template>
<template v-else-if="getSmallThumbnail(item)">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<template v-else>
<v-icon>{{ mdiFile }}</v-icon>
</template>
</td>
<td class=" ">
<div class="d-block text-truncate" :style="styleContentTdWidth">{{ item.filename }}</div>
<small v-if="existMetadata(item)">{{ getDescription(item) }}</small>
</td>
</tr>
<jobqueue-entry :key="item.job_id" :item="item" :content-td-width="contentTdWidth" />
</template>
</v-data-table>
<resize-observer @notify="handleResize" />
</panel>
<v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
<v-list>
<v-list-item @click="deleteJob(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPlaylistRemove }}</v-icon>
{{ $t('JobQueue.RemoveFromQueue') }}
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { ServerHistoryStateJob } from '@/store/server/history/types'
import { formatFilesize, formatPrintTime } from '@/plugins/helpers'
import Panel from '@/components/ui/Panel.vue'
import { ServerJobQueueStateJob } from '@/store/server/jobQueue/types'
import { mdiPlay, mdiPause, mdiFile, mdiPlaylistRemove, mdiTrayFull } from '@mdi/js'
import { mdiPlay, mdiPause, mdiTrayFull } from '@mdi/js'
import JobqueueEntry from '@/components/panels/Status/JobqueueEntry.vue'
@Component({
components: { Panel },
components: { JobqueueEntry, Panel },
})
export default class JobqueuePanel extends Mixins(BaseMixin) {
mdiPlay = mdiPlay
mdiPause = mdiPause
mdiFile = mdiFile
mdiPlaylistRemove = mdiPlaylistRemove
mdiTrayFull = mdiTrayFull
formatFilesize = formatFilesize
private contentTdWidth = 100
private contextMenu = {
shown: false,
touchTimer: undefined,
x: 0,
y: 0,
item: {},
}
declare $refs: {
jobqueuePanel: any
@ -161,27 +96,6 @@ export default class JobqueuePanel extends Mixins(BaseMixin) {
this.$store.dispatch('gui/saveSetting', { name: 'view.jobqueue.countPerPage', value: newVal })
}
get styleContentTdWidth() {
return `width: ${this.contentTdWidth}px;`
}
showContextMenu(e: any, item: ServerHistoryStateJob) {
if (!this.contextMenu.shown) {
e?.preventDefault()
this.contextMenu.shown = true
this.contextMenu.x = e?.clientX || e?.pageX || window.screenX / 2
this.contextMenu.y = e?.clientY || e?.pageY || window.screenY / 2
this.contextMenu.item = item
this.$nextTick(() => {
this.contextMenu.shown = true
})
}
}
deleteJob(item: ServerJobQueueStateJob) {
this.$store.dispatch('server/jobQueue/deleteFromQueue', [item.job_id])
}
startJobqueue() {
this.$store.dispatch('server/jobQueue/start')
}
@ -190,35 +104,6 @@ export default class JobqueuePanel extends Mixins(BaseMixin) {
this.$store.dispatch('server/jobQueue/pause')
}
getSmallThumbnail(item: ServerJobQueueStateJob) {
return this.$store.getters['server/jobQueue/getSmallThumbnail'](item)
}
getBigThumbnail(item: ServerJobQueueStateJob) {
return this.$store.getters['server/jobQueue/getBigThumbnail'](item)
}
getDescription(item: ServerJobQueueStateJob) {
let output = ''
output += this.$t('Files.Filament') + ': '
if (item.metadata?.filament_total || item.metadata.filament_weight_total) {
if (item.metadata?.filament_total) output += item.metadata.filament_total.toFixed() + ' mm'
if (item.metadata?.filament_total && item.metadata.filament_weight_total) output += ' / '
if (item.metadata?.filament_weight_total) output += item.metadata.filament_weight_total.toFixed(2) + ' g'
} else output += '--'
output += ', ' + this.$t('Files.PrintTime') + ': '
if (item.metadata?.estimated_time) output += formatPrintTime(item.metadata.estimated_time)
else output += '--'
return output
}
existMetadata(item: ServerJobQueueStateJob) {
return item?.metadata?.metadataPulled
}
mounted() {
this.calcContentTdWidth()
}
@ -235,7 +120,7 @@ export default class JobqueuePanel extends Mixins(BaseMixin) {
}
</script>
<style>
<style scoped>
.jobqueue-panel {
position: relative;
}

View File

@ -83,6 +83,12 @@
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
{{ $t('Files.AddToQueue') }}
</v-list-item>
<v-list-item
v-if="moonrakerComponents.includes('job_queue')"
@click="openAddBatchToQueueDialog(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
{{ $t('Files.AddBatchToQueue') }}
</v-list-item>
<v-list-item
v-if="contextMenu.item.preheat_gcode !== null"
:disabled="['error', 'printing', 'paused'].includes(printer_state)"
@ -137,6 +143,53 @@
</v-card-actions>
</panel>
</v-dialog>
<v-dialog v-model="dialogAddBatchToQueue.show" max-width="400">
<panel
:title="$t('Files.AddToQueue').toString()"
card-class="gcode-files-add-to-queue-dialog"
:icon="mdiPlaylistPlus"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="dialogAddBatchToQueue.show = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<v-text-field
ref="inputFieldAddToQueueCount"
v-model="dialogAddBatchToQueue.count"
:label="$t('Files.Count')"
required
hide-spin-buttons
type="number"
:rules="countInputRules"
@keyup.enter="addBatchToQueueAction">
<template #append-outer>
<div class="_spin_button_group">
<v-btn class="mt-n3" icon plain small @click="dialogAddBatchToQueue.count++">
<v-icon>{{ mdiChevronUp }}</v-icon>
</v-btn>
<v-btn
:disabled="dialogAddBatchToQueue.count <= 1"
class="mb-n3"
icon
plain
small
@click="dialogAddBatchToQueue.count--">
<v-icon>{{ mdiChevronDown }}</v-icon>
</v-btn>
</div>
</template>
</v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="" text @click="dialogAddBatchToQueue.show = false">{{ $t('Files.Cancel') }}</v-btn>
<v-btn color="primary" text @click="addBatchToQueueAction">{{ $t('Files.AddToQueue') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</v-card>
</template>
@ -148,6 +201,8 @@ import ControlMixin from '@/components/mixins/control'
import { FileStateGcodefile } from '@/store/files/types'
import StartPrintDialog from '@/components/dialogs/StartPrintDialog.vue'
import {
mdiChevronDown,
mdiChevronUp,
mdiFile,
mdiPlay,
mdiPlaylistPlus,
@ -166,12 +221,20 @@ interface dialogRenameObject {
item: FileStateGcodefile
}
interface dialogAddBatchToQueue {
show: boolean
count: number
item: FileStateGcodefile
}
@Component({
components: {
StartPrintDialog,
},
})
export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixin) {
mdiChevronDown = mdiChevronDown
mdiChevronUp = mdiChevronUp
mdiFile = mdiFile
mdiPlay = mdiPlay
mdiPlaylistPlus = mdiPlaylistPlus
@ -199,6 +262,7 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
last_status: null,
last_start_time: null,
last_total_duration: null,
preheat_gcode: null,
}
private currentPath = ''
private contentTdWidth = 100
@ -222,6 +286,17 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
item: { ...this.dialogFile },
}
private dialogAddBatchToQueue: dialogAddBatchToQueue = {
show: false,
count: 1,
item: { ...this.contextMenu.item },
}
private countInputRules = [
(value: string) => !!value || this.$t('JobQueue.InvalidCountEmpty'),
(value: string) => parseInt(value) > 0 || this.$t('JobQueue.InvalidCountGreaterZero'),
]
get gcodeFiles() {
let gcodes = this.$store.getters['files/getAllGcodes'] ?? []
gcodes = gcodes
@ -343,6 +418,26 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
this.$store.dispatch('server/jobQueue/addToQueue', [item.filename])
}
openAddBatchToQueueDialog(item: FileStateGcodefile) {
this.dialogAddBatchToQueue.show = true
this.dialogAddBatchToQueue.count = 1
this.dialogAddBatchToQueue.item = item
}
async addBatchToQueueAction() {
let filename = [this.currentPath, this.dialogAddBatchToQueue.item.filename].join('/')
if (filename.startsWith('/')) filename = filename.slice(1)
const array: string[] = []
for (let i = 0; i < this.dialogAddBatchToQueue.count; i++) {
array.push(filename)
}
await this.$store.dispatch('server/jobQueue/addToQueue', array)
this.dialogAddBatchToQueue.show = false
}
view3D(item: FileStateGcodefile) {
this.$router.push({ path: '/viewer', query: { filename: 'gcodes/' + item.filename } })
}
@ -419,8 +514,15 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
}
</script>
<style lang="scss" scoped>
<style scoped>
.filesGcodeCard {
position: relative;
}
._spin_button_group {
width: 24px;
margin-top: -6px;
margin-left: -6px;
margin-bottom: -6px;
}
</style>

View File

@ -5,77 +5,27 @@
hide-default-footer
class="dashboard-jobqueue-table"
sort-by="time_added"
mobile-breakpoint="0"
@current-items="setFirst">
mobile-breakpoint="0">
<template #no-data>
<div class="text-center">{{ $t('Panels.StatusPanel.EmptyJobqueue') }}</div>
</template>
<template #item="{ item }">
<tr
<template #item="{ item, index }">
<jobqueue-entry
:key="item.job_id"
v-longpress:600="(e) => showContextMenu(e, item)"
class="cursor-pointer"
@contextmenu="showContextMenu($event, item)">
<td class="pr-0 text-center" style="width: 32px">
<template v-if="getSmallThumbnail(item) && getBigThumbnail(item)">
<v-tooltip
v-if="!item.isDirectory && getSmallThumbnail(item) && getBigThumbnail(item)"
top
content-class="tooltip__content-opacity1">
<template #activator="{ on, attrs }">
<vue-load-image>
<img
slot="image"
:src="getSmallThumbnail(item)"
width="32"
height="32"
v-bind="attrs"
v-on="on" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
</template>
<template v-else-if="getSmallThumbnail(item)">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<template v-else>
<v-icon>{{ mdiFile }}</v-icon>
</template>
</td>
<td class="pr-2">
<template v-if="item.isFirst && !printerIsPrinting">
<v-btn icon color="success" class="float-right minwidth-0 mt-1" @click="startJobqueue">
<v-icon>{{ mdiPlay }}</v-icon>
</v-btn>
</template>
<div class="d-block text-truncate" :style="styleContentTdWidth">{{ item.filename }}</div>
<small v-if="existMetadata(item)">{{ getDescription(item) }}</small>
</td>
</tr>
:item="item"
:isFirst="index === 0"
:content-td-width="contentTdWidth" />
</template>
<template v-if="jobs.length > jobsTable.length" #body.append>
<template v-if="jobsRest.length" #body.append>
<tr>
<td class="pr-0 text-center" style="width: 32px">
<v-icon>{{ mdiFileMultiple }}</v-icon>
</td>
<td class="pr-2">
{{
$tc('Panels.StatusPanel.JobqueueMoreFiles', jobs.length - jobsTable.length, {
count: jobs.length - jobsTable.length,
$tc('Panels.StatusPanel.JobqueueMoreFiles', restJobsLength, {
count: restJobsLength,
})
}}
<br />
@ -85,14 +35,6 @@
</template>
</v-data-table>
<resize-observer @notify="handleResize" />
<v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
<v-list>
<v-list-item @click="removeFromJobqueue(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPlaylistRemove }}</v-icon>
{{ $t('JobQueue.RemoveFromQueue') }}
</v-list-item>
</v-list>
</v-menu>
</v-card>
</template>
@ -101,24 +43,15 @@ import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { ServerJobQueueStateJob } from '@/store/server/jobQueue/types'
import { mdiFile, mdiPlay, mdiFileMultiple, mdiPlaylistRemove } from '@mdi/js'
import { mdiFileMultiple } from '@mdi/js'
import JobqueueEntry from '@/components/panels/Status/JobqueueEntry.vue'
@Component({
components: {},
components: { JobqueueEntry },
})
export default class StatusPanelJobqueue extends Mixins(BaseMixin) {
mdiFile = mdiFile
mdiPlay = mdiPlay
mdiFileMultiple = mdiFileMultiple
mdiPlaylistRemove = mdiPlaylistRemove
private contentTdWidth = 100
private contextMenu = {
shown: false,
touchTimer: undefined,
x: 0,
y: 0,
item: {},
}
declare $refs: {
filesJobqueue: any
@ -136,15 +69,27 @@ export default class StatusPanelJobqueue extends Mixins(BaseMixin) {
return this.jobs.slice(5)
}
get restJobsLength() {
let count = 0
this.jobsRest.forEach((item: ServerJobQueueStateJob) => {
count += (item.combinedIds?.length ?? 0) + 1
})
return count
}
get descriptionRestJobs() {
let filamentLength = 0
let filamentWeight = 0
let printTime = 0
this.jobsRest.forEach((item: ServerJobQueueStateJob) => {
if (item.metadata?.filament_total) filamentLength += item.metadata?.filament_total
if (item.metadata?.filament_weight_total) filamentWeight += item.metadata?.filament_weight_total
if (item.metadata?.estimated_time) printTime = item.metadata.estimated_time
const count = (item.combinedIds?.length ?? 0) + 1
if (item.metadata?.filament_total) filamentLength += item.metadata?.filament_total * count
if (item.metadata?.filament_weight_total) filamentWeight += item.metadata?.filament_weight_total * count
if (item.metadata?.estimated_time) printTime = item.metadata.estimated_time * count
})
let output = ''
@ -163,49 +108,6 @@ export default class StatusPanelJobqueue extends Mixins(BaseMixin) {
return output
}
get styleContentTdWidth() {
return `width: ${this.contentTdWidth}px;`
}
getSmallThumbnail(item: ServerJobQueueStateJob) {
return this.$store.getters['server/jobQueue/getSmallThumbnail'](item)
}
getBigThumbnail(item: ServerJobQueueStateJob) {
return this.$store.getters['server/jobQueue/getBigThumbnail'](item)
}
getDescription(item: ServerJobQueueStateJob) {
let output = ''
output += this.$t('Panels.StatusPanel.Filament') + ': '
if (item.metadata?.filament_total || item.metadata.filament_weight_total) {
if (item.metadata?.filament_total) output += item.metadata.filament_total.toFixed() + ' mm'
if (item.metadata?.filament_total && item.metadata.filament_weight_total) output += ' / '
if (item.metadata?.filament_weight_total) output += item.metadata.filament_weight_total.toFixed(2) + ' g'
} else output += '--'
output += ', ' + this.$t('Panels.StatusPanel.PrintTime') + ': '
if (item.metadata?.estimated_time) output += this.formatPrintTime(item.metadata.estimated_time)
else output += '--'
return output
}
existMetadata(item: ServerJobQueueStateJob) {
return item?.metadata?.metadataPulled
}
setFirst(currItems: ServerJobQueueStateJob[]) {
// first check that actually exists values
if (currItems.length) {
// toggle all to false
currItems.forEach((x: ServerJobQueueStateJob) => (x.isFirst = false))
// just set first to true
currItems[0].isFirst = true
}
}
formatPrintTime(totalSeconds: number) {
if (totalSeconds) {
let output = ''
@ -232,27 +134,10 @@ export default class StatusPanelJobqueue extends Mixins(BaseMixin) {
return '--'
}
showContextMenu(e: any, item: ServerJobQueueStateJob) {
if (!this.contextMenu.shown) {
e?.preventDefault()
this.contextMenu.shown = true
this.contextMenu.x = e?.clientX || e?.pageX || window.screenX / 2
this.contextMenu.y = e?.clientY || e?.pageY || window.screenY / 2
this.contextMenu.item = item
this.$nextTick(() => {
this.contextMenu.shown = true
})
}
}
startJobqueue() {
this.$store.dispatch('server/jobQueue/start')
}
removeFromJobqueue(item: ServerJobQueueStateJob) {
this.$store.dispatch('server/jobQueue/deleteFromQueue', [item.job_id])
}
mounted() {
setTimeout(() => {
this.calcContentTdWidth()

View File

@ -0,0 +1,271 @@
<template>
<tr
v-longpress:600="(e) => showContextMenu(e, item)"
class="cursor-pointer"
@contextmenu="showContextMenu($event, item)">
<td class="pr-0 text-center" style="width: 32px">
<template v-if="smallThumbnail && bigThumbnail">
<v-tooltip v-if="smallThumbnail && bigThumbnail" top content-class="tooltip__content-opacity1">
<template #activator="{ on, attrs }">
<vue-load-image>
<img slot="image" :src="smallThumbnail" width="32" height="32" v-bind="attrs" v-on="on" />
<div slot="preloader">
<v-progress-circular indeterminate color="primary" />
</div>
<div slot="error">
<v-icon>{{ mdiFile }}</v-icon>
</div>
</vue-load-image>
</template>
<span><img :src="bigThumbnail" width="250" /></span>
</v-tooltip>
</template>
<template v-else-if="smallThumbnail">
<vue-load-image>
<img slot="image" :src="smallThumbnail" width="32" height="32" />
<div slot="preloader">
<v-progress-circular indeterminate color="primary" />
</div>
<div slot="error">
<v-icon>{{ mdiFile }}</v-icon>
</div>
</vue-load-image>
</template>
<template v-else>
<v-icon>{{ mdiFile }}</v-icon>
</template>
</td>
<td class="pr-2">
<template v-if="isFirst && !printerIsPrinting">
<v-btn icon color="success" class="float-right minwidth-0 mt-1" @click="startJobqueue">
<v-icon>{{ mdiPlay }}</v-icon>
</v-btn>
</template>
<div class="d-block text-truncate" :style="styleContentTdWidth">
<strong v-if="item.combinedIds.length">{{ item.combinedIds.length + 1 }}x</strong>
{{ item.filename }}
</div>
<small v-if="item?.metadata?.metadataPulled">{{ description }}</small>
</td>
<v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
<v-list>
<v-list-item @click="openChangeCountDialog(contextMenu.item)">
<v-icon class="mr-1">{{ mdiCounter }}</v-icon>
{{ $t('JobQueue.ChangeCount') }}
</v-list-item>
<v-list-item @click="removeFromJobqueue(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPlaylistRemove }}</v-icon>
{{ $t('JobQueue.RemoveFromQueue') }}
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="dialogChangeCount.show" max-width="400">
<panel
:title="$t('JobQueue.ChangeCount').toString()"
:icon="mdiCounter"
card-class="jobqueue-change-count-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="dialogChangeCount.show = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<v-text-field
ref="inputFieldAddToQueueCount"
v-model="dialogChangeCount.count"
:label="$t('JobQueue.Count')"
required
:rules="countInputRules"
hide-spin-buttons
type="number"
@keyup.enter="changeCountAction">
<template #append-outer>
<div class="_spin_button_group">
<v-btn class="mt-n3" icon plain small @click="dialogChangeCount.count++">
<v-icon>{{ mdiChevronUp }}</v-icon>
</v-btn>
<v-btn
:disabled="dialogChangeCount.count <= 1"
class="mb-n3"
icon
plain
small
@click="dialogChangeCount.count--">
<v-icon>{{ mdiChevronDown }}</v-icon>
</v-btn>
</div>
</template>
</v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="" text @click="dialogChangeCount.show = false">{{ $t('JobQueue.Cancel') }}</v-btn>
<v-btn color="primary" text @click="changeCountAction">{{ $t('JobQueue.ChangeCount') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</tr>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import { Mixins, Prop } from 'vue-property-decorator'
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'
@Component({
components: { NumberInput },
})
export default class StatusPanelJobqueueEntry extends Mixins(BaseMixin) {
mdiChevronDown = mdiChevronDown
mdiChevronUp = mdiChevronUp
mdiCloseThick = mdiCloseThick
mdiCounter = mdiCounter
mdiFile = mdiFile
mdiPlay = mdiPlay
mdiPlaylistRemove = mdiPlaylistRemove
@Prop({ type: Object, required: true }) declare item: ServerJobQueueStateJob
@Prop({ type: Number, required: true }) declare contentTdWidth: number
@Prop({ type: Boolean, default: false }) declare isFirst: boolean
private contextMenu: {
shown: boolean
x: number
y: number
item: ServerJobQueueStateJob | any
} = {
shown: false,
x: 0,
y: 0,
item: {},
}
private dialogChangeCount: {
show: boolean
count: number
item: ServerJobQueueStateJob | any
} = {
show: false,
count: 1,
item: {},
}
private countInputRules = [
(value: string) => !!value || this.$t('JobQueue.InvalidCountEmpty'),
(value: string) => parseInt(value) > 0 || this.$t('JobQueue.InvalidCountGreaterZero'),
]
declare $refs: {
filesJobqueue: any
}
get styleContentTdWidth() {
return `width: ${this.contentTdWidth}px;`
}
get smallThumbnail() {
return this.$store.getters['server/jobQueue/getSmallThumbnail'](this.item)
}
get bigThumbnail() {
return this.$store.getters['server/jobQueue/getBigThumbnail'](this.item)
}
get description() {
let output = ''
output += this.$t('Panels.StatusPanel.Filament') + ': '
if (this.item.metadata?.filament_total || this.item.metadata.filament_weight_total) {
if (this.item.metadata?.filament_total) output += this.item.metadata.filament_total.toFixed() + ' mm'
if (this.item.metadata?.filament_total && this.item.metadata.filament_weight_total) output += ' / '
if (this.item.metadata?.filament_weight_total)
output += this.item.metadata.filament_weight_total.toFixed(2) + ' g'
} else output += '--'
output += ', ' + this.$t('Panels.StatusPanel.PrintTime') + ': '
if (this.item.metadata?.estimated_time) output += this.formatPrintTime(this.item.metadata.estimated_time)
else output += '--'
return output
}
formatPrintTime(totalSeconds: number) {
if (totalSeconds) {
let output = ''
const days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= 3600 * 24
output += days + 'd'
}
const hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
if (hours) output += ' ' + hours + 'h'
const minutes = Math.floor(totalSeconds / 60)
if (minutes) output += ' ' + minutes + 'm'
const seconds = totalSeconds % 60
if (seconds) output += ' ' + seconds.toFixed(0) + 's'
return output
}
return '--'
}
showContextMenu(e: any, item: ServerJobQueueStateJob) {
if (!this.contextMenu.shown) {
e?.preventDefault()
this.contextMenu.shown = true
this.contextMenu.x = e?.clientX || e?.pageX || window.screenX / 2
this.contextMenu.y = e?.clientY || e?.pageY || window.screenY / 2
this.contextMenu.item = item
this.$nextTick(() => {
this.contextMenu.shown = true
})
}
}
startJobqueue() {
this.$store.dispatch('server/jobQueue/start')
}
removeFromJobqueue(item: ServerJobQueueStateJob) {
const ids = [...(item.combinedIds ?? [])]
ids.push(item.job_id)
this.$store.dispatch('server/jobQueue/deleteFromQueue', ids)
}
openChangeCountDialog(item: ServerJobQueueStateJob) {
this.dialogChangeCount.show = true
this.dialogChangeCount.count = (item.combinedIds?.length ?? 0) + 1
this.dialogChangeCount.item = item
}
changeCountAction() {
this.$store.dispatch('server/jobQueue/changeCount', {
job_id: this.dialogChangeCount.item.job_id,
count: this.dialogChangeCount.count,
})
this.dialogChangeCount.show = false
}
}
</script>
<style lang="scss" scoped>
.filesJobqueue {
position: relative;
}
._spin_button_group {
width: 24px;
margin-top: -6px;
margin-left: -6px;
margin-bottom: -6px;
}
</style>

View File

@ -133,7 +133,7 @@ export default class StatusPanel extends Mixins(BaseMixin) {
}
get jobsCount() {
return this.jobs?.length ?? 0
return this.$store.getters['server/jobQueue/getJobsCount'] ?? 0
}
get current_filename() {

View File

@ -164,11 +164,13 @@
"Yes": "Yes"
},
"Files": {
"AddBatchToQueue": "Add batch to Queue",
"AddToQueue": "Add to Queue",
"AllFiles": "All",
"BedTemp": "Bed Temp.",
"Cancel": "Cancel",
"ChamberTemp": "Chamber Temp.",
"Count": "Count",
"Create": "Create",
"CreateNewDirectory": "Create new Directory",
"CurrentPath": "Current path",
@ -369,7 +371,12 @@
},
"JobQueue": {
"AllJobs": "All Jobs",
"ChangeCount": "Change count",
"Cancel": "Cancel",
"Count": "Count",
"Empty": "Empty",
"InvalidCountEmpty": "Input must not be empty!",
"InvalidCountGreaterZero": "Input must be greater than 0!",
"JobQueue": "Job Queue",
"Jobs": "Jobs",
"Pause": "Pause",

View File

@ -1,7 +1,7 @@
import Vue from 'vue'
import { ActionTree } from 'vuex'
import { RootState } from '@/store/types'
import { ServerJobQueueState } from '@/store/server/jobQueue/types'
import { ServerJobQueueState, ServerJobQueueStateJob } from '@/store/server/jobQueue/types'
export const actions: ActionTree<ServerJobQueueState, RootState> = {
reset({ commit }) {
@ -28,6 +28,31 @@ export const actions: ActionTree<ServerJobQueueState, RootState> = {
Vue.$socket.emit('server.job_queue.post_job', { filenames: filenames })
},
changeCount({ getters }, payload: { job_id: string; count: number }) {
const filenames: string[] = []
const jobs = getters['getJobs']
jobs.forEach((job: ServerJobQueueStateJob) => {
if (job.job_id === payload.job_id) {
for (let i = 0; i < payload.count; i++) {
filenames.push(job.filename)
}
return
}
const count = (job.combinedIds?.length ?? 0) + 1
for (let i = 0; i < count; i++) {
filenames.push(job.filename)
}
})
Vue.$socket.emit('server.job_queue.post_job', {
filenames,
reset: true,
})
},
deleteFromQueue(_, job_ids: string[]) {
Vue.$socket.emit('server.job_queue.delete_job', { job_ids })
},

View File

@ -10,10 +10,17 @@ export const getters: GetterTree<ServerJobQueueState, any> = {
state.queued_jobs.forEach((queuedJob) => {
const job = { ...queuedJob }
if (jobs.length && jobs[jobs.length - 1].filename === job.filename) {
jobs[jobs.length - 1].combinedIds?.push(job.job_id)
return
}
const file = rootGetters['files/getFile']('gcodes/' + job.filename)
if (!file?.metadataPulled)
Vue.$socket.emit('server.files.metadata', { filename: job.filename }, { action: 'files/getMetadata' })
job['metadata'] = file
job.metadata = file
job.combinedIds = []
jobs.push(job)
})
@ -21,6 +28,10 @@ export const getters: GetterTree<ServerJobQueueState, any> = {
return jobs
},
getJobsCount: (state) => {
return state.queued_jobs.length
},
getSmallThumbnail: (state, getters, rootState, rootGetters) => (item: ServerJobQueueStateJob) => {
if (item?.metadata?.thumbnails?.length) {
const thumbnail = item?.metadata?.thumbnails.find(

View File

@ -10,4 +10,5 @@ export interface ServerJobQueueStateJob {
time_in_queue: number
metadata?: any
isFirst?: boolean
combinedIds?: string[]
}