-
-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(StatusPanel): add option to show history in StatusPanel (#2055)
Co-authored-by: rackrick <[email protected]>
- Loading branch information
Showing
8 changed files
with
392 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<template> | ||
<v-card class="history" flat> | ||
<template v-if="jobsCombined.length"> | ||
<v-row class="mx-0 mt-0 pb-3"> | ||
<v-col class="history-list"> | ||
<status-panel-history-entry v-for="job in jobsCombined" :key="job.job_id" :job="job" /> | ||
</v-col> | ||
</v-row> | ||
</template> | ||
<div v-else> | ||
<p class="body-2 my-3 text-center text--disabled">{{ $t('Panels.StatusPanel.EmptyHistory') }}</p> | ||
</div> | ||
</v-card> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import Component from 'vue-class-component' | ||
import { Mixins } from 'vue-property-decorator' | ||
import BaseMixin from '@/components/mixins/base' | ||
import StatusPanelHistoryEntry from '@/components/panels/Status/HistoryEntry.vue' | ||
import { ServerHistoryStateJob, ServerHistoryStateJobWithCount } from '@/store/server/history/types' | ||
@Component({ | ||
components: { StatusPanelHistoryEntry }, | ||
}) | ||
export default class StatusPanelHistory extends Mixins(BaseMixin) { | ||
get jobs(): ServerHistoryStateJob[] { | ||
return this.$store.state.server.history.jobs ?? [] | ||
} | ||
get maxLength() { | ||
return this.$store.state.gui.uiSettings.dashboardHistoryLimit ?? 5 | ||
} | ||
get jobsCombined() { | ||
const jobs: ServerHistoryStateJobWithCount[] = [] | ||
for (const job of this.jobs) { | ||
if (jobs.length === 0) { | ||
jobs.push({ ...job, count: 1 }) | ||
continue | ||
} | ||
const lastJob = jobs[jobs.length - 1] | ||
const lastJobUuid = lastJob.metadata.uuid ?? null | ||
const jobUuid = job.metadata.uuid ?? null | ||
if (lastJobUuid === jobUuid && lastJob.status === job.status) { | ||
lastJob.filament_used += job.filament_used | ||
lastJob.print_duration += job.print_duration | ||
lastJob.total_duration += job.total_duration | ||
lastJob.count += 1 | ||
continue | ||
} | ||
if (jobs.length >= this.maxLength) break | ||
jobs.push({ ...job, count: 1 }) | ||
} | ||
return jobs | ||
} | ||
startJobqueue() { | ||
this.$store.dispatch('server/jobQueue/start') | ||
} | ||
} | ||
</script> | ||
|
||
<style scoped> | ||
.history-list .history-list-entry + .history-list-entry { | ||
border-top: 1px solid rgba(255, 255, 255, 0.12); | ||
} | ||
.theme--light .history-list > .history-list-entry + .history-list-entry { | ||
border-top: 1px solid rgba(0, 0, 0, 0.12); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
<template> | ||
<v-row | ||
v-longpress:600="(e) => openContextMenu(e)" | ||
class="history-list-entry d-flex flex-row flex-nowrap cursor-pointer" | ||
@contextmenu="openContextMenu($event)"> | ||
<v-col class="col-auto d-flex flex-column justify-center pr-0 py-0"> | ||
<v-tooltip | ||
v-if="smallThumbnail" | ||
top | ||
:disabled="!bigThumbnail" | ||
content-class="tooltip__content-opacity1" | ||
:color="bigThumbnailTooltipColor"> | ||
<template #activator="{ on, attrs }"> | ||
<vue-load-image class="text-center width-32"> | ||
<img | ||
slot="image" | ||
:src="smallThumbnail" | ||
:width="32" | ||
:height="32" | ||
:alt="job.filename" | ||
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" :alt="job.filename" /></span> | ||
</v-tooltip> | ||
<v-icon v-else>{{ mdiFile }}</v-icon> | ||
</v-col> | ||
<v-col class="py-1" style="min-width: 0; font-size: 0.875em"> | ||
<div class="text-truncate"> | ||
<strong v-if="job.count > 1">{{ job.count }}x</strong> | ||
{{ job.filename }} | ||
</div> | ||
<small v-if="description" class="text-truncate">{{ description }}</small> | ||
</v-col> | ||
<v-col class="col-auto d-flex flex-column justify-center pa-0 pr-3"> | ||
<v-tooltip top> | ||
<template #activator="{ on, attrs }"> | ||
<span v-bind="attrs" v-on="on"> | ||
<v-icon small :color="statusColor" :disabled="!job.exists"> | ||
{{ statusIcon }} | ||
</v-icon> | ||
</span> | ||
</template> | ||
<span>{{ statusName }}</span> | ||
</v-tooltip> | ||
</v-col> | ||
<v-menu v-model="showContextMenu" :position-x="contextMenuX" :position-y="contextMenuY" absolute offset-y> | ||
<v-list> | ||
<v-list-item v-if="job.exists" :disabled="printerIsPrinting || !klipperReadyForGui" @click="startPrint"> | ||
<v-icon class="mr-1">{{ mdiPrinter }}</v-icon> | ||
{{ $t('History.Reprint') }} | ||
</v-list-item> | ||
<v-list-item v-if="job.exists && isJobQueueAvailable" @click="addToQueue"> | ||
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon> | ||
{{ $t('Files.AddToQueue') }} | ||
</v-list-item> | ||
<v-list-item v-if="job.exists && isJobQueueAvailable" @click="addBatchToQueueDialogBool = true"> | ||
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon> | ||
{{ $t('Files.AddBatchToQueue') }} | ||
</v-list-item> | ||
<v-list-item class="red--text" @click="deleteJob"> | ||
<v-icon class="mr-1" color="error">{{ mdiDelete }}</v-icon> | ||
{{ $t('History.Delete') }} | ||
</v-list-item> | ||
</v-list> | ||
</v-menu> | ||
<add-batch-to-queue-dialog | ||
:is-visible="addBatchToQueueDialogBool" | ||
:show-toast="true" | ||
:filename="job.filename" | ||
@close="addBatchToQueueDialogBool = false" /> | ||
</v-row> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import Component from 'vue-class-component' | ||
import { Mixins, Prop } from 'vue-property-decorator' | ||
import BaseMixin from '@/components/mixins/base' | ||
import { mdiCloseThick, mdiDelete, mdiFile, mdiPlaylistPlus, mdiPrinter } from '@mdi/js' | ||
import { defaultBigThumbnailBackground, thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables' | ||
import { ServerHistoryStateJobWithCount } from '@/store/server/history/types' | ||
import { FileStateFileThumbnail } from '@/store/files/types' | ||
import { formatPrintTime } from '@/plugins/helpers' | ||
@Component | ||
export default class StatusPanelHistoryEntry extends Mixins(BaseMixin) { | ||
mdiCloseThick = mdiCloseThick | ||
mdiDelete = mdiDelete | ||
mdiFile = mdiFile | ||
mdiPlaylistPlus = mdiPlaylistPlus | ||
mdiPrinter = mdiPrinter | ||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJobWithCount | ||
showContextMenu = false | ||
contextMenuX = 0 | ||
contextMenuY = 0 | ||
addBatchToQueueDialogBool = false | ||
get smallThumbnail() { | ||
if ((this.job.metadata?.thumbnails?.length ?? 0) < 1) return false | ||
const thumbnail = this.job.metadata?.thumbnails?.find( | ||
(thumb) => | ||
thumb.width >= thumbnailSmallMin && | ||
thumb.width <= thumbnailSmallMax && | ||
thumb.height >= thumbnailSmallMin && | ||
thumb.height <= thumbnailSmallMax | ||
) | ||
return thumbnail ? this.createThumbnailUrl(thumbnail) : false | ||
} | ||
get bigThumbnail() { | ||
if ((this.job.metadata?.thumbnails?.length ?? 0) < 1) return false | ||
const thumbnail = this.job.metadata?.thumbnails?.find((thumb) => thumb.width >= thumbnailBigMin) | ||
return thumbnail ? this.createThumbnailUrl(thumbnail) : false | ||
} | ||
get statusIcon() { | ||
return this.$store.getters['server/history/getPrintStatusIcon'](this.job.status) | ||
} | ||
get statusColor() { | ||
return this.$store.getters['server/history/getPrintStatusIconColor'](this.job.status) | ||
} | ||
get statusName() { | ||
// check if translation exists | ||
if (!this.$t(`History.StatusValues.${this.job.status}`, 'en')) return this.job.status.replace(/_/g, ' ') | ||
return this.$t(`History.StatusValues.${this.job.status}`) | ||
} | ||
get description() { | ||
const outputArray = [] | ||
const filamentArray = [] | ||
let filament = '--' | ||
if (this.filamentLength) filamentArray.push(this.filamentLength) | ||
if (this.filamentWeight) filamentArray.push(this.filamentWeight) | ||
if (filamentArray.length) filament = filamentArray.join(' / ') | ||
outputArray.push(`${this.$t('Panels.StatusPanel.Filament')}: ${filament}`) | ||
if (this.estimatedTime !== '--') | ||
outputArray.push(`${this.$t('Panels.StatusPanel.PrintTime')}: ${this.estimatedTime}`) | ||
else if (this.totalTime) outputArray.push(`${this.$t('Panels.StatusPanel.TotalTime')}: ${this.totalTime}`) | ||
return outputArray.join(', ') | ||
} | ||
get filamentLength() { | ||
const length = this.job.filament_used | ||
if (length === 0) return null | ||
if (length >= 1000) return (length / 1000).toFixed(1) + ' m' | ||
return length.toFixed(0) + ' mm' | ||
} | ||
get filamentWeight() { | ||
const metadataFilamentLength = this.job.metadata?.filament_total ?? 0 | ||
const metadataFilamentWeight = this.job.metadata?.filament_weight_total ?? 0 | ||
if (metadataFilamentLength === 0 || metadataFilamentWeight === 0) return null | ||
const specificWeight = metadataFilamentWeight / metadataFilamentLength | ||
const weight = this.job.filament_used * specificWeight | ||
if (weight === 0) return null | ||
if (weight >= 1000) return (length / 1000).toFixed(1) + ' kg' | ||
return weight.toFixed(0) + ' g' | ||
} | ||
get estimatedTime() { | ||
let totalSeconds = this.job.print_duration ?? 0 | ||
if (totalSeconds == 0) return '--' | ||
return formatPrintTime(totalSeconds) | ||
} | ||
get totalTime() { | ||
let totalSeconds: number = this.job.total_duration ?? 0 | ||
if (totalSeconds === 0) return null | ||
return formatPrintTime(totalSeconds) | ||
} | ||
get bigThumbnailBackground() { | ||
return this.$store.state.gui.uiSettings.bigThumbnailBackground ?? defaultBigThumbnailBackground | ||
} | ||
get bigThumbnailTooltipColor() { | ||
if (defaultBigThumbnailBackground.toLowerCase() === this.bigThumbnailBackground.toLowerCase()) { | ||
return undefined | ||
} | ||
return this.bigThumbnailBackground | ||
} | ||
get isJobQueueAvailable() { | ||
return this.moonrakerComponents.includes('job_queue') | ||
} | ||
openContextMenu(e: any) { | ||
e?.preventDefault() | ||
if (this.showContextMenu) { | ||
this.showContextMenu = false | ||
return | ||
} | ||
this.showContextMenu = true | ||
this.contextMenuX = e?.clientX || e?.pageX || window.screenX / 2 | ||
this.contextMenuY = e?.clientY || e?.pageY || window.screenY / 2 | ||
} | ||
startPrint() { | ||
if (!this.job.exists) return | ||
this.$socket.emit('printer.print.start', { filename: this.job.filename }) | ||
} | ||
addToQueue() { | ||
this.$store.dispatch('server/jobQueue/addToQueue', [this.job.filename]) | ||
this.$toast.info(this.$t('History.AddToQueueSuccessful', { filename: this.job.filename }).toString()) | ||
} | ||
deleteJob() { | ||
this.$socket.emit( | ||
'server.history.delete_job', | ||
{ uid: this.job.job_id }, | ||
{ action: 'server/history/getDeletedJobs' } | ||
) | ||
} | ||
createThumbnailUrl(thumbnail: FileStateFileThumbnail) { | ||
let relative_url = '' | ||
if (this.job.filename.lastIndexOf('/') !== -1) { | ||
relative_url = this.job.filename.substring(0, this.job.filename.lastIndexOf('/') + 1) | ||
} | ||
return `${this.apiUrl}/server/files/gcodes/${encodeURI(relative_url + thumbnail.relative_path)}?timestamp=${ | ||
this.job.metadata.modified | ||
}` | ||
} | ||
} | ||
</script> | ||
|
||
<style scoped> | ||
.width-32 { | ||
width: 32px; | ||
} | ||
</style> |
Oops, something went wrong.