Skip to content

feat: Automatic print start dialog on upload #2223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<the-editor />
<the-timelapse-rendering-snackbar />
<the-fullscreen-upload />
<the-launch-file-handler />
<the-upload-snackbar />
<the-manual-probe-dialog />
<the-bed-screws-dialog />
Expand All @@ -38,6 +39,7 @@ import TheEditor from '@/components/TheEditor.vue'
import { panelToolbarHeight, topbarHeight, navigationItemHeight } from '@/store/variables'
import TheTimelapseRenderingSnackbar from '@/components/TheTimelapseRenderingSnackbar.vue'
import TheFullscreenUpload from '@/components/TheFullscreenUpload.vue'
import TheLaunchFileHandler from '@/components/TheLaunchFileHandler.vue'
import TheUploadSnackbar from '@/components/TheUploadSnackbar.vue'
import TheManualProbeDialog from '@/components/dialogs/TheManualProbeDialog.vue'
import TheBedScrewsDialog from '@/components/dialogs/TheBedScrewsDialog.vue'
Expand All @@ -59,6 +61,7 @@ Component.registerHooks(['metaInfo'])
TheTopbar,
TheSidebar,
TheFullscreenUpload,
TheLaunchFileHandler,
TheUploadSnackbar,
TheManualProbeDialog,
TheBedScrewsDialog,
Expand Down
27 changes: 6 additions & 21 deletions src/components/TheFullscreenUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
<script lang="ts">
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import FilesMixin from '@/components/mixins/files'
import Component from 'vue-class-component'
import { validGcodeExtensions } from '@/store/variables'
import { mdiTrayArrowDown } from '@mdi/js'

@Component
export default class TheFullscreenUpload extends Mixins(BaseMixin) {
export default class TheFullscreenUpload extends Mixins(BaseMixin, FilesMixin) {
mdiTrayArrowDown = mdiTrayArrowDown
private visible = false

Expand Down Expand Up @@ -77,28 +77,13 @@ export default class TheFullscreenUpload extends Mixins(BaseMixin) {
if (e.dataTransfer?.files?.length) {
const files = [...e.dataTransfer.files]

await this.$store.dispatch('socket/addLoading', { name: 'gcodeUpload' })
await this.$store.dispatch('files/uploadSetCurrentNumber', 0)
await this.$store.dispatch('files/uploadSetMaxNumber', files.length)

for (const file of files) {
const extensionPos = file.name.lastIndexOf('.')
const extension = file.name.slice(extensionPos)
const isGcode = validGcodeExtensions.includes(extension)

await this.uploadFiles(files, (file) => {
const isGcode = this.isGcodeFilename(file.name)
let path = ''
if (this.currentRoute === '/files' && isGcode) path = this.currentPathGcodes
else if (this.currentRoute === '/config' && !isGcode) path = this.currentPathConfig

const root = isGcode ? 'gcodes' : 'config'
await this.$store.dispatch('files/uploadIncrementCurrentNumber')
const result = await this.$store.dispatch('files/uploadFile', { file, path, root })

if (result !== false)
this.$toast.success(this.$t('Files.SuccessfullyUploaded', { filename: result }).toString())
}

await this.$store.dispatch('socket/removeLoading', { name: 'gcodeUpload' })
return { path, root: isGcode ? 'gcodes' : 'config' }
})
}
}
}
Expand Down
142 changes: 142 additions & 0 deletions src/components/TheLaunchFileHandler.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<start-print-dialog
v-if="showPrintDialog"
:bool="showPrintDialog"
:file="file"
current-path=""
@closeDialog="onCloseDialog" />
</template>

<script lang="ts">
import { Mixins, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import FilesMixin from '@/components/mixins/files'
import Component from 'vue-class-component'
import StartPrintDialog from '@/components/dialogs/StartPrintDialog.vue'
import { FileStateGcodefile, FileStateSimpleFile } from '@/store/files/types'
import { launchCount } from '@/plugins/pwaLaunchCount'

@Component({
components: {
StartPrintDialog,
},
})
export default class TheLaunchFileHandler extends Mixins(BaseMixin, FilesMixin) {
item: FileStateGcodefile | null = null
private uploadedFile: null | { path: string; filename: string } = null

get fileReady() {
return this.file && this.file.metadataPulled
}

get file() {
if (!this.uploadedFile) return null
const { path, filename } = this.uploadedFile
const files = this.$store.getters['files/getGcodeFiles'](path ? `/${path}` : path, false, true)
return files.find((f: FileStateGcodefile) => f.filename === filename)
}

get canPrint() {
return !this.isUpdating && !['error', 'printing', 'paused'].includes(this.printer_state)
}

get isUpdating() {
return Boolean(this.$store.state.server.updateManager.updateResponse.application ?? '')
}

get showPrintDialog() {
return this.fileReady && this.canPrint
}

@Watch('file')
onFileChange(file: FileStateGcodefile) {
if (file && !file.metadataPulled && !file.metadataRequested) {
const filename = ['gcodes', file.path, file.filename].filter(Boolean).join('/')
this.$store.dispatch('files/requestMetadata', [{ filename }])
}
}

get latestGcodeFile(): FileStateSimpleFile | null {
return this.$store.state.files.latestGcodeFile as FileStateSimpleFile | null
}

get showPrintOnUpload(): boolean {
return this.$store.state.gui.uiSettings.showPrintOnUpload
}

@Watch('latestGcodeFile')
onLatestGcodeFileChange(file: FileStateSimpleFile | null) {
const latestKnownGcodeFileTime = Number(localStorage.getItem('latestKnownGcodeFileTime') ?? 0)
if (file && file.modified > latestKnownGcodeFileTime) {
localStorage.setItem('latestKnownGcodeFileTime', String(file.modified))
if (latestKnownGcodeFileTime > 0 && this.showPrintOnUpload)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing i've kind of been on the fence with here is perhaps maybe we should add an additional condition that the modified timestamp is within the last 5 minutes or so? The reason being, if I've got multiple instances of mainsail that I use with the "showPrintOnUpload" setting enabled, then lets say i send a print and go to mainsail, the dialog is presented, and i go ahead an hit print (or cancel). Then later I open another mainsail instance with the setting turned on, it would see there is a new gcode file with a timestamp newer than what it had saved in its localStorage, so it would present the dialog for the other instance.

Or alternatively, perhaps we save the latestKnownGcodeFileTime in the moonraker db, so we can track the latest one any of the mainsail instances have seen? The main caveat there is AFAICT the moonraker db stuff isn't actively updated (nor do I see a way to subscribe to changes to the db), so that means if there are multiple instances already open, they could have stale latestKnownGcodeFileTime attributes.

this.uploadedFile = { path: file.path, filename: file.filename }
}
}

mounted() {
window.launchQueue?.setConsumer(this.onLaunch)
if (this.latestGcodeFile) this.onLatestGcodeFileChange(this.latestGcodeFile)
}

beforeDestroy() {
window.launchQueue?.setConsumer(() => undefined)
}

/**
* Determine if the launch is a new file launch. This is somewhat quirky because the launch queue is is re-sent
* when the page is reloaded, so to prevent re-processing the same launch when the page is reloaded, we calculate a
* launch key, save it to session storage, and compare it to the last launch key. And we also check the launch count
* to see if this is the first launch or a subsequent launch. This is needed because the launch queue is re-sent
* when the page is reloaded, so we need to differentiate between the first launch and subsequent launches.
*
* @param files The files to check.
*/
isNewFileLaunch(files: File[]) {
const lastLaunchKey = sessionStorage.getItem('launchKey')
const launchKey = files
.map(({ name, lastModified, size }: File) => [name, lastModified, size].join('::'))
.sort()
.join(',')
sessionStorage.setItem('launchKey', launchKey)

return launchCount.value > 1 || launchKey !== lastLaunchKey
}

isGcodeFileHandle(file: FileSystemHandle): file is FileSystemFileHandle {
if (file.kind !== 'file') return false
return this.isGcodeFilename(file.name) //
}

onCloseDialog() {
this.uploadedFile = null
}

async onLaunch(launchParams: LaunchParams) {
// increment the launch count
launchCount.value++

// Nothing to do when the queue is empty.
if (!launchParams.files || !launchParams.files.length) {
return
}

// Filter to only gcode files and get the files from the file handles
const files = await Promise.all(
launchParams.files.filter(this.isGcodeFileHandle).map((file: FileSystemFileHandle) => file.getFile())
)

if (!this.isNewFileLaunch(files)) {
console.log('Repeated first launch key, ignoring because the page was probably just reloaded.')
return
}
this.uploadedFile = null

const root = 'gcodes'
const path = ''
const uploadedFiles = await this.uploadFiles(files, { root, path })

if (uploadedFiles.length > 0) this.uploadedFile = { path, filename: uploadedFiles[0].name }
}
}
</script>
46 changes: 46 additions & 0 deletions src/components/mixins/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { validGcodeExtensions } from '@/store/variables'
import Vue from 'vue'
import Component from 'vue-class-component'

type FileUploadRoot = 'gcodes' | 'config'
type FileUploadDestination = { root: FileUploadRoot; path?: string }
type FileUploadDestinationGetter = (file: File) => FileUploadDestination

@Component
export default class FilesMixins extends Vue {
loadingKeyForRoot(root?: FileUploadRoot) {
if (root === 'config') {
return 'configFileUpload'
}
return 'gcodeUpload'
}

isGcodeFilename(filename: string) {
const extensionPos = filename.lastIndexOf('.')
const extension = filename.slice(extensionPos)
return validGcodeExtensions.includes(extension)
}

async uploadFiles(files: File[], destination: Partial<FileUploadDestination> | FileUploadDestinationGetter = {}) {
const loadingKey = this.loadingKeyForRoot(typeof destination === 'object' ? destination.root : 'gcodes')
await this.$store.dispatch('socket/addLoading', { name: loadingKey })
await this.$store.dispatch('files/uploadSetCurrentNumber', 0)
await this.$store.dispatch('files/uploadSetMaxNumber', files.length)

const uploadedFiles = []
for (const file of files) {
await this.$store.dispatch('files/uploadIncrementCurrentNumber')
const { path = '', root } = typeof destination === 'function' ? destination(file) : destination
const pathWithoutPrefix = path.slice(0, 1) === '/' ? path.slice(1) : path
const result = await this.$store.dispatch('files/uploadFile', { file, path: pathWithoutPrefix, root })

if (result !== false) {
this.$toast.success(this.$t('Files.SuccessfullyUploaded', { filename: result }).toString())
uploadedFiles.push(file)
}
}

await this.$store.dispatch('socket/removeLoading', { name: loadingKey })
return uploadedFiles
}
}
15 changes: 15 additions & 0 deletions src/components/settings/SettingsUiSettingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,13 @@
:dynamic-slot-width="true">
<v-switch v-model="hideOtherInstances" hide-details class="mt-0" />
</settings-row>
<v-divider class="my-2" />
<settings-row
:title="$t('Settings.UiSettingsTab.ShowPrintOnUpload')"
:sub-title="$t('Settings.UiSettingsTab.ShowPrintOnUploadDescription')"
:dynamic-slot-width="true">
<v-switch v-model="showPrintOnUpload" hide-details class="mt-0" />
</settings-row>
</v-card-text>
</v-card>
</div>
Expand Down Expand Up @@ -711,6 +718,14 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin, ThemeMixin)
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.hideOtherInstances', value: newVal })
}

get showPrintOnUpload() {
return this.$store.state.gui.uiSettings.showPrintOnUpload ?? false
}

set showPrintOnUpload(newVal) {
this.$store.dispatch('gui/saveSettingInLocalStorage', { name: 'uiSettings.showPrintOnUpload', value: newVal })
}

clearColorObject(color: any): string {
if (typeof color === 'object' && 'hex' in color) color = color.hex
if (color.length > 7) color = color.substr(0, 7)
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,8 @@
"ProgressAsFaviconDescription": "Change the Mainsail logo favicon to a progress circle.",
"ScrewsTiltAdjustDialog": "Screws Tilt Adjust Dialog",
"ScrewsTiltAdjustDialogDescription": "Display helper dialog for SCREWS_TILT_CALCULATE.",
"ShowPrintOnUpload": "Show Print on Upload",
"ShowPrintOnUploadDescription": "Display the show print dialog after uploading a file. This setting is local to the browser.",
"TempchartHeight": "Height Temperature Chart",
"TempchartHeightDescription": "Modify the height of the temperature chart on the Dashboard.",
"Theme": "Theme",
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/pwaLaunchCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ref } from 'vue'

// This file is used to track the number of times the PWA has been launched
// via launchQueue. This has been extracted from the launch handler because
// if it is present in the launch handler, it will be reset during hot reload.
// This throws off the functionality that determines if it is a new launch,
// causing new launches to be treated as a relaunch and get ignored.
export const launchCount = ref(0)
1 change: 1 addition & 0 deletions src/store/farm/printer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const getDefaultState = (): FarmPrinterState => {
settings: {},
databases: [],
current_file: {
path: '',
isDirectory: false,
filename: '',
modified: new Date(),
Expand Down
1 change: 1 addition & 0 deletions src/store/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const getDefaultState = (): FileState => {
percent: 0,
speed: 0,
},
latestGcodeFile: null,
}
}

Expand Down
19 changes: 18 additions & 1 deletion src/store/files/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getDefaultState } from './index'
import { findDirectory } from '@/plugins/helpers'
import { MutationTree } from 'vuex'
import { FileState, FileStateFile } from '@/store/files/types'
import { allowedMetadata } from '@/store/variables'
import { allowedMetadata, validGcodeExtensions } from '@/store/variables'

export const mutations: MutationTree<FileState> = {
reset(state) {
Expand All @@ -12,6 +12,7 @@ export const mutations: MutationTree<FileState> = {

createRootDir(state, payload) {
state.filetree.push({
path: '',
isDirectory: true,
filename: payload.name,
modified: new Date(),
Expand Down Expand Up @@ -68,6 +69,20 @@ export const mutations: MutationTree<FileState> = {
const path = payload.item.path.substr(0, payload.item.path.lastIndexOf('/'))
const parent = findDirectory(state.filetree, (payload.item.root + '/' + path).split('/'))

if (payload.item.root === 'gcodes') {
const extension = filename.substring(filename.lastIndexOf('.'))
const lastModified = state.latestGcodeFile?.modified ?? 0
if (validGcodeExtensions.includes(extension) && payload.item.modified > lastModified) {
state.latestGcodeFile = {
path,
filename,
modified: payload.item.modified,
size: payload.item.size,
permissions: payload.item.permissions,
}
}
}

if (parent) {
const indexFile = parent.findIndex(
(element: FileStateFile) => !element.isDirectory && element.filename === filename
Expand All @@ -77,6 +92,7 @@ export const mutations: MutationTree<FileState> = {
const modified = new Date(payload.item.modified * 1000)

parent.push({
path,
isDirectory: false,
filename: filename,
modified: modified,
Expand Down Expand Up @@ -213,6 +229,7 @@ export const mutations: MutationTree<FileState> = {

if (parent) {
parent.push({
path,
isDirectory: true,
filename: dirname,
modified: payload.item.modified ?? new Date(),
Expand Down
Loading
Loading