Ctrl K
ComunidadGuías…

src/App.vue

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<template>
  <v-app
    class="fluidd"
    :class="{ 'no-pointer-events': dragState }"
  >
    <app-tools-drawer v-model="toolsdrawer" />
    <app-nav-drawer v-model="navdrawer" />

    <inline-svg
      v-if="showBackgroundLogo && !isMobileViewport"
      :src="logoSrc"
      class="background-logo"
    />

    <app-bar
      @toolsdrawer="handleToolsDrawerChange"
      @navdrawer="handleNavDrawerChange"
    />

    <flash-message
      v-if="flashMessageState"
      v-model="flashMessageState.open"
      :text="flashMessageState.text"
      :type="flashMessageState.type"
      :timeout="flashMessageState.timeout"
    />

    <v-btn
      v-if="isMobileViewport && socketReady"
      fab
      fixed
      bottom
      left
      width="36"
      height="36"
      class="ml-2 mb-2"
      color="error"
      style="z-index: 2000"
      :aria-label="$t('app.general.tooltip.estop')"
      @click="emergencyStop()"
    >
      <v-icon>$estop</v-icon>
    </v-btn>

    <v-main :style="customBackgroundImageStyle">
      <v-container
        v-if="socketReady"
        fluid
        :class="[['single', 'double', 'triple', 'quad'][columnCount - 1]]"
        class="constrained-width pa-2 pa-sm-4"
      >
        <v-row v-if="(!klippyReady || hasWarnings) && !inLayout">
          <v-col>
            <klippy-status-card />
          </v-col>
        </v-row>

        <router-view />
      </v-container>

      <login v-else-if="socketAuthenticating" />

      <socket-disconnected v-else />

      <register-service-worker />

      <socket-stalled-snackbar v-if="socketReady" />

      <template v-if="socketConnected">
        <file-system-download-dialog />
        <file-system-upload-dialog />
        <updating-dialog
          :updating="versionUpdating"
          :responses="versionResponses"
          :title-updating="$t('app.version.status.updating')"
          :title-finished="$t('app.version.status.finished')"
          @close="handleVersionDialogClose"
        />
        <updating-dialog
          :updating="firmwareUpdating"
          :responses="firmwareResponses"
          :title-updating="$t('app.firmware.status.updating')"
          :title-finished="$t('app.firmware.status.finished')"
          @close="handleFirmwareDialogClose"
        />
        <spool-selection-dialog />
        <action-command-prompt-dialog />
        <keyboard-shortcuts-dialog />
        <manual-probe-dialog />
        <bed-screws-adjust-dialog />
        <screws-tilt-adjust-dialog />
        <mmu-edit-ttg-map-dialog />
        <afc-print-start-dialog />
      </template>
    </v-main>

    <app-footer />

    <app-drag-overlay
      v-model="dragState"
      :message="$t('app.file_system.overlay.drag_files_folders_upload')"
      icon="$fileUpload"
    />
  </v-app>
</template>

<script lang="ts">
import { Component, Mixins, Watch } from 'vue-property-decorator'
import { EventBus } from '@/eventBus'
import StateMixin from '@/mixins/state'
import FilesMixin from '@/mixins/files'
import BrowserMixin from '@/mixins/browser'
import type { LinkPropertyHref, MetaPropertyName } from 'vue-meta'
import FileSystemDownloadDialog from '@/components/widgets/filesystem/FileSystemDownloadDialog.vue'
import FileSystemUploadDialog from '@/components/widgets/filesystem/FileSystemUploadDialog.vue'
import SpoolSelectionDialog from '@/components/widgets/spoolman/SpoolSelectionDialog.vue'
import type { FlashMessage } from '@/types'
import { getFilesFromDataTransfer, hasFilesInDataTransfer } from '@/util/file-system-entry'
import type { ThemeConfig } from '@/store/config/types'
import ActionCommandPromptDialog from '@/components/common/ActionCommandPromptDialog.vue'
import KeyboardShortcutsDialog from '@/components/common/KeyboardShortcutsDialog.vue'
import { eventTargetIsContentEditable, keyboardEventToKeyboardShortcut } from '@/util/event-helpers'
import MmuEditTtgMapDialog from './components/widgets/mmu/MmuEditTtgMapDialog.vue'
import AfcPrintStartDialog from './components/widgets/afc/dialogs/AfcPrintStartDialog.vue'
import type { UpdateResponse } from '@/store/version/types'
import type { ConsoleLogEntry } from '@/store/console/types'

@Component<App>({
  metaInfo () {
    return {
      title: this.pageTitle,
      link: this.pageIcon,
      meta: this.pageMeta
    }
  },
  components: {
    SpoolSelectionDialog,
    FileSystemDownloadDialog,
    FileSystemUploadDialog,
    ActionCommandPromptDialog,
    KeyboardShortcutsDialog,
    MmuEditTtgMapDialog,
    AfcPrintStartDialog
  }
})
export default class App extends Mixins(StateMixin, FilesMixin, BrowserMixin) {
  toolsdrawer: boolean | null = null
  navdrawer: boolean | null = null
  dragState = false
  customBackgroundImageStyle: Record<string, string> = {}

  flashMessageState: FlashMessage = {
    open: false,
    text: undefined,
    type: undefined
  }

  get theme (): ThemeConfig {
    return this.$typedState.config.uiSettings.theme
  }

  get showBackgroundLogo () {
    return this.theme.backgroundLogo
  }

  get logoSrc () {
    return `${import.meta.env.BASE_URL}${this.theme.logo.src}`
  }

  get primaryColor (): string {
    return this.$vuetify.theme.currentTheme.primary?.toString() ?? ''
  }

  get primaryOffsetColor (): string {
    return this.$vuetify.theme.currentTheme['primary-offset']?.toString() ?? ''
  }

  get versionUpdating (): boolean {
    return this.$typedState.version.status?.busy ?? false
  }

  get versionResponses (): UpdateResponse[] {
    return this.$typedGetters['version/getResponses']
  }

  handleVersionDialogClose () {
    this.$typedCommit('version/setClearUpdateResponse')
  }

  get firmwareUpdating (): boolean {
    return this.$typedGetters['firmware/isBusy']
  }

  get firmwareResponses (): ConsoleLogEntry[] {
    return this.$typedGetters['firmware/getResponses']
  }

  handleFirmwareDialogClose () {
    this.$typedCommit('firmware/setClearResponses')
  }

  get inLayout (): boolean {
    return (this.$typedState.config.layoutMode)
  }

  get columnCount (): number {
    return this.$typedState.config.containerColumnCount
  }

  get fileDropRoot () {
    return this.$route.meta?.fileDropRoot
  }

  get progress (): number {
    const progress: number = this.$typedGetters['printer/getPrintProgress']
    return Math.floor(progress * 100)
  }

  get pageTitle () {
    const instanceName: string = this.$typedState.config.uiSettings.general.instanceName

    if (!this.socketReady) {
      return instanceName
    }

    const progress = this.printerPrinting
      ? `[${this.progress}%]`
      : ''
    const pageName = this.$route.name
      ? this.$t(`app.general.title.${this.$route.name}`)
      : ''

    const parts = [
      progress,
      instanceName,
      pageName
    ]

    return parts
      .filter(part => part)
      .join(' | ')
  }

  get pageIcon (): LinkPropertyHref[] {
    const iconDataUrl = this.printInProgressIconDataUrl || this.defaultIconDataUrl

    return [
      {
        rel: 'icon',
        type: 'image/svg+xml',
        sizes: '32x32',
        href: iconDataUrl
      },
      {
        rel: 'icon',
        type: 'image/svg+xml',
        sizes: '16x16',
        href: iconDataUrl
      }
    ]
  }

  get pageMeta (): MetaPropertyName[] {
    return [
      {
        name: 'theme-color',
        content: this.primaryColor
      }
    ]
  }

  get printInProgressIconDataUrl () {
    if (
      this.socketReady &&
      this.printerPrinting
    ) {
      const favIconSize = 64
      const primaryColor = this.primaryColor
      const secondaryColor = 'rgba(128, 128, 128, 0.3)'
      const canvas = document.createElement('canvas')
      const context = canvas.getContext('2d')

      if (context) {
        canvas.width = favIconSize
        canvas.height = favIconSize
        const percent = this.progress
        const centerX = canvas.width / 2
        const centerY = canvas.height / 2
        const lineWidth = 10
        const radius = favIconSize / 2 - lineWidth / 2
        const startAngle = 1.5 * Math.PI
        const endAngle = startAngle + (percent * 2 * Math.PI / 100)

        /* Draw the initial gray circle */
        context.moveTo(centerX, centerY)
        context.beginPath()
        context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false)
        context.strokeStyle = secondaryColor
        context.lineWidth = lineWidth
        context.stroke()
        context.closePath()

        /* Now draw the progress circle */
        context.moveTo(centerX, centerY)
        context.beginPath()
        context.arc(centerX, centerY, radius, startAngle, endAngle, false)
        context.strokeStyle = primaryColor
        context.lineWidth = lineWidth
        context.stroke()

        return canvas.toDataURL('image/png')
      }
    }
  }

  get defaultIconDataUrl () {
    const logoWithColor = `<svg width="56" height="56" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"><g><path fill="${this.primaryOffsetColor}" d="m14.853 33.757 11.617 9.66q.196.169.419.287l.002.001.044.023.017.009.03.014.027.013.021.01.037.016.01.005c.263.111.54.173.817.185h.01l.044.002h.104l.046-.002h.007a2.3 2.3 0 0 0 .818-.186l.008-.003.041-.018.016-.007.03-.015.032-.015.014-.007.044-.023.004-.002a2.3 2.3 0 0 0 .419-.287l10.86-9.031 5.646 3.727L28 56 9.243 37.398Zm.409-14.452 11.425 7.35c.407.267.86.4 1.313.4s.905-.133 1.312-.4l11.426-7.349 8.737 4.462L28 41.628 6.525 23.767zM28 0l24.42 8.993L28 24.699 3.58 8.99Z" /><path fill="${this.primaryColor}" d="m28 0-.135.05h.15v24.64L52.42 8.991Zm12.738 19.307-11.425 7.347-.06.04a2.4 2.4 0 0 1-1.237.36v14.56l21.459-17.846zm-.347 15.08-10.86 9.031q-.196.167-.418.285l-.006.002-.043.024-.013.007-.033.016-.03.014-.015.007-.041.018-.008.004c-.263.112-.54.173-.819.185h-.007l-.045.002h-.037v12.002l18.021-17.87Z" /></g></svg>`

    return `data:image/svg+xml;base64,${btoa(logoWithColor)}`
  }

  get customStyleSheet (): string | undefined {
    return this.$typedGetters['config/getCustomThemeFile']('custom', ['.css'])
  }

  @Watch('customStyleSheet')
  async onCustomStyleSheet (value: string | undefined) {
    if (!value) {
      return
    }

    const url = await this.createFileUrlWithToken(value, 'config')

    const oldCustomStylesheet = document.getElementById('customStylesheet')

    if (oldCustomStylesheet) {
      oldCustomStylesheet.setAttribute('href', url)
      return
    }

    const linkElement = document.createElement('link')

    linkElement.rel = 'stylesheet'
    linkElement.type = 'text/css'
    linkElement.id = 'customStylesheet'
    linkElement.href = url

    document.head.appendChild(linkElement)
  }

  get customBackgroundImage (): string | undefined {
    return this.$typedGetters['config/getCustomThemeFile']('background', ['.png', '.jpg', '.jpeg', '.gif'])
  }

  @Watch('customBackgroundImage')
  async onCustomBackgroundImage (value: string | undefined) {
    if (!value) {
      return
    }

    const url = await this.createFileUrlWithToken(value, 'config')

    this.customBackgroundImageStyle = {
      backgroundImage: `url(${url})`,
      backgroundSize: 'cover',
      backgroundAttachment: 'fixed',
      backgroundRepeat: 'no-repeat'
    }
  }

  get enableKeyboardShortcuts (): boolean {
    return this.$typedState.config.uiSettings.general.enableKeyboardShortcuts
  }

  mounted () {
    window.addEventListener('dragover', this.handleDragOver)
    window.addEventListener('dragenter', this.handleDragOver)
    window.addEventListener('dragleave', this.handleDragLeave)
    window.addEventListener('drop', this.handleDrop)
    window.addEventListener('keydown', this.handleKeyDown, false)

    // this.onLoadLocale(this.$i18n.locale)
    EventBus.bus.$on('flashMessage', (payload: FlashMessage) => {
      this.flashMessageState.text = (payload && payload.text) || undefined
      this.flashMessageState.type = (payload && payload.type) || undefined
      this.flashMessageState.timeout = (payload && payload.timeout !== undefined) ? payload.timeout : undefined
      this.flashMessageState.open = true
    })

    const legacyElementsSelectors = [
      "link[rel*='icon'][type='image/png']",
      "meta[name='theme-color']"
    ]

    for (const legacyElementsSelector of legacyElementsSelectors) {
      const legacyElements = document.querySelectorAll(legacyElementsSelector)

      legacyElements.forEach(item => {
        const parentElement = item.parentElement

        if (parentElement) {
          parentElement.removeChild(item)
        }
      })
    }
  }

  beforeDestroy () {
    window.removeEventListener('dragover', this.handleDragOver)
    window.removeEventListener('dragenter', this.handleDragOver)
    window.removeEventListener('dragleave', this.handleDragLeave)
    window.removeEventListener('drop', this.handleDrop)
    window.removeEventListener('keydown', this.handleKeyDown)
  }

  handleToolsDrawerChange () {
    this.toolsdrawer = !this.toolsdrawer
  }

  handleNavDrawerChange () {
    this.navdrawer = !this.navdrawer
  }

  handleDragOver (event: DragEvent) {
    if (
      this.socketReady &&
      this.fileDropRoot &&
      event.dataTransfer &&
      hasFilesInDataTransfer(event.dataTransfer)
    ) {
      event.preventDefault()

      this.dragState = true

      event.dataTransfer.dropEffect = 'copy'
    }
  }

  handleDragLeave (event: DragEvent) {
    if (this.fileDropRoot) {
      event.preventDefault()

      if (
        event.target instanceof HTMLElement &&
        event.target.className.includes('fluidd')
      ) {
        this.dragState = false
      }
    }
  }

  async handleDrop (event: DragEvent) {
    const root = this.fileDropRoot

    if (root) {
      event.preventDefault()

      this.dragState = false

      if (event.dataTransfer) {
        const files = await getFilesFromDataTransfer(event.dataTransfer)

        if (files) {
          const pathWithRoot: string = this.$typedGetters['files/getCurrentPathByRoot'](root)
          const path = pathWithRoot === root
            ? ''
            : pathWithRoot.substring(root.length + 1)

          const wait = `${this.$waits.onFileSystem}/${pathWithRoot}/`

          this.$typedDispatch('wait/addWait', wait)

          await this.uploadFiles(files, path, root, false)

          this.$typedDispatch('wait/removeWait', wait)
        }
      }
    }
  }

  handleKeyDown (event: KeyboardEvent) {
    if (!this.enableKeyboardShortcuts) {
      return
    }

    const shortcut = keyboardEventToKeyboardShortcut(event)

    if (
      this.isAppleDevice
        ? shortcut === 'Meta+Shift+e'
        : shortcut === 'Ctrl+Shift+E'
    ) {
      event.preventDefault()

      this.emergencyStop()

      return
    }

    if (
      !this.klippyReady ||
      eventTargetIsContentEditable(event)
    ) {
      return
    }

    switch (shortcut) {
      case 'Shift+C':
        if (
          this.printerPrinting ||
          this.printerPaused
        ) {
          event.preventDefault()

          this.cancelPrint()
        }
        break

      case 'Shift+P':
        if (this.printerPrinting) {
          event.preventDefault()

          this.pausePrint()
        }
        break

      case 'Shift+H':
        if (!this.printerPrinting) {
          event.preventDefault()

          this.homeAll()
        }
        break
    }
  }
}
</script>

<style lang="scss" scoped>
  .background-logo {
    pointer-events: none;
    position: fixed;
    width: 50%;
    height: auto;
    right: -10%;
    bottom: -20%;
    opacity: 8%;
  }
</style>