feat: add multiselect to timelapse file manager (#1039)

This commit is contained in:
Stefan Dej 2022-08-23 23:36:56 +02:00 committed by GitHub
parent 99075fb259
commit cbb70759f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 13 deletions

View File

@ -32,6 +32,14 @@
dense dense
style="max-width: 300px"></v-text-field> style="max-width: 300px"></v-text-field>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn
v-if="selectedFiles.length"
:title="$t('Timelapse.Delete')"
color="error"
class="px-2 minwidth-0 ml-3"
@click="deleteSelectedDialog = true">
<v-icon>{{ mdiDelete }}</v-icon>
</v-btn>
<v-btn <v-btn
v-if="directoryPermissions.includes('w')" v-if="directoryPermissions.includes('w')"
:title="$t('Timelapse.CreateNewDirectory')" :title="$t('Timelapse.CreateNewDirectory')"
@ -78,6 +86,7 @@
</v-card-text> </v-card-text>
<v-divider class="mb-3"></v-divider> <v-divider class="mb-3"></v-divider>
<v-data-table <v-data-table
v-model="selectedFiles"
:items="displayFiles" :items="displayFiles"
class="files-table" class="files-table"
:headers="headers" :headers="headers"
@ -90,10 +99,11 @@
itemsPerPageAllText: $t('Timelapse.AllFiles'), itemsPerPageAllText: $t('Timelapse.AllFiles'),
itemsPerPageOptions: [10, 25, 50, 100, -1], itemsPerPageOptions: [10, 25, 50, 100, -1],
}" }"
item-key="name" item-key="filename"
:search="search" :search="search"
:custom-filter="advancedSearch" :custom-filter="advancedSearch"
mobile-breakpoint="0"> mobile-breakpoint="0"
show-select>
<template slot="items"> <template slot="items">
<td v-for="header in headers" :key="header.value">{{ header.text }}</td> <td v-for="header in headers" :key="header.value">{{ header.text }}</td>
</template> </template>
@ -111,19 +121,26 @@
</tr> </tr>
</template> </template>
<template #item="{ index, item }"> <template #item="{ index, item, isSelected, select }">
<tr <tr
:key="`${index} ${item.filename}`" :key="`${index} ${item.filename}`"
v-longpress:600="(e) => showContextMenu(e, item)" v-longpress:600="(e) => showContextMenu(e, item)"
class="file-list-cursor user-select-none" class="file-list-cursor user-select-none"
@contextmenu="showContextMenu($event, item)" @contextmenu="showContextMenu($event, item)"
@click="clickRow(item)"> @click="clickRow(item)">
<td class="pr-0 text-center" style="width: 32px"> <td class="file-list__select-td pr-0">
<v-simple-checkbox
v-ripple
:value="isSelected"
class="pa-0 mr-0"
@click.stop="select(!isSelected)"></v-simple-checkbox>
</td>
<td class="px-0 text-center" style="width: 32px">
<template v-if="item.isDirectory"> <template v-if="item.isDirectory">
<v-icon>{{ mdiFolder }}</v-icon> <v-icon width="32">{{ mdiFolder }}</v-icon>
</template> </template>
<template v-else-if="item.filename.endsWith('zip')"> <template v-else-if="item.filename.endsWith('zip')">
<v-icon>{{ mdiFolderZipOutline }}</v-icon> <v-icon width="32">{{ mdiFolderZipOutline }}</v-icon>
</template> </template>
<template v-else-if="getThumbnail(item)"> <template v-else-if="getThumbnail(item)">
<v-tooltip <v-tooltip
@ -139,11 +156,15 @@
width="32" width="32"
v-bind="attrs" v-bind="attrs"
v-on="on" /> v-on="on" />
<v-progress-circular <div slot="preloader">
slot="preloader" <v-progress-circular
indeterminate slot="preloader"
color="primary"></v-progress-circular> indeterminate
<v-icon slot="error">{{ mdiFile }}</v-icon> color="primary"></v-progress-circular>
</div>
<div slot="error">
<v-icon>{{ mdiFile }}</v-icon>
</div>
</vue-load-image> </vue-load-image>
</template> </template>
<span><img :src="getThumbnail(item)" :alt="item.filename" width="250" /></span> <span><img :src="getThumbnail(item)" :alt="item.filename" width="250" /></span>
@ -339,13 +360,33 @@
</v-card-text> </v-card-text>
</panel> </panel>
</v-dialog> </v-dialog>
<v-dialog v-model="deleteSelectedDialog" max-width="400">
<panel
:title="$t('Timelapse.Delete').toString()"
card-class="timelapse-delete-selected-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="deleteSelectedDialog = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<p class="mb-0">{{ $t('Timelapse.DeleteSelectedQuestion', { count: selectedFiles.length }) }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="" text @click="deleteSelectedDialog = false">{{ $t('Timelapse.Cancel') }}</v-btn>
<v-btn color="error" text @click="deleteSelectedFiles">{{ $t('Timelapse.Delete') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins } from 'vue-property-decorator' import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base' import BaseMixin from '@/components/mixins/base'
import { formatFilesize, formatDate, sortFiles } from '@/plugins/helpers' import { formatFilesize, formatDate, sortFiles } from '@/plugins/helpers'
import { FileStateFile } from '@/store/files/types' import { FileStateFile, FileStateGcodefile } from '@/store/files/types'
import Panel from '@/components/ui/Panel.vue' import Panel from '@/components/ui/Panel.vue'
import { import {
mdiFolderPlus, mdiFolderPlus,
@ -453,11 +494,13 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
}, },
} }
private deleteSelectedDialog = false
private input_rules = [(value: string) => value.indexOf(' ') === -1 || 'Name contains spaces!'] private input_rules = [(value: string) => value.indexOf(' ') === -1 || 'Name contains spaces!']
get headers() { get headers() {
return [ return [
{ text: '', value: '', align: 'left', configable: false, visible: true, filterable: false }, { text: '', value: '', align: 'left', configable: false, visible: true, sortable: false },
{ text: this.$t('Timelapse.Name'), value: 'filename', align: 'left', configable: false, visible: true }, { text: this.$t('Timelapse.Name'), value: 'filename', align: 'left', configable: false, visible: true },
{ text: this.$t('Timelapse.Filesize'), value: 'size', align: 'right', configable: true, visible: true }, { text: this.$t('Timelapse.Filesize'), value: 'size', align: 'right', configable: true, visible: true },
{ {
@ -532,6 +575,14 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.timelapse.currentPath', value: newVal }) this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.timelapse.currentPath', value: newVal })
} }
get selectedFiles() {
return this.$store.state.gui.view.timelapse.selectedFiles ?? []
}
set selectedFiles(newVal) {
this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.timelapse.selectedFiles', value: newVal })
}
createDirectory() { createDirectory() {
this.dialogCreateDirectory.name = '' this.dialogCreateDirectory.name = ''
this.dialogCreateDirectory.show = true this.dialogCreateDirectory.show = true
@ -733,5 +784,44 @@ export default class TimelapseFilesPanel extends Mixins(BaseMixin) {
{ action: 'files/getDeleteDir' } { action: 'files/getDeleteDir' }
) )
} }
deleteSelectedFiles() {
this.selectedFiles.forEach((item: FileStateGcodefile) => {
if (item.isDirectory) {
this.$socket.emit(
'server.files.delete_directory',
{ path: this.currentPath + '/' + item.filename, force: true },
{ action: 'files/getDeleteDir' }
)
} else {
const filename = item.filename.slice(0, item.filename.lastIndexOf('.'))
const fileExtension = item.filename.split('.').pop()
this.$socket.emit(
'server.files.delete_file',
{ path: this.currentPath + '/' + item.filename },
{ action: 'files/getDeleteFile' }
)
if (fileExtension !== 'mp4') return
/**
* if file-extension is mp4, also delete its corresponding thumbnail jpg
*/
const previewFilename = filename + '.jpg'
const previewExists = this.files.findIndex((file) => file.filename === previewFilename) !== -1
if (previewExists)
this.$socket.emit(
'server.files.delete_file',
{ path: this.currentPath + '/' + previewFilename },
{ action: 'files/getDeleteFile' }
)
}
})
this.selectedFiles = []
this.deleteSelectedDialog = false
}
} }
</script> </script>

View File

@ -990,6 +990,7 @@
"Delete": "Löschen", "Delete": "Löschen",
"DeleteDirectory": "Verzeichnis löschen", "DeleteDirectory": "Verzeichnis löschen",
"DeleteDirectoryQuestion": "Willst du wirklich das Verzeichnis \"{name}\" mit seinem gesamten Inhalt löschen?", "DeleteDirectoryQuestion": "Willst du wirklich das Verzeichnis \"{name}\" mit seinem gesamten Inhalt löschen?",
"DeleteSelectedQuestion": "Sollen wirklich {count} ausgewählte Dateien gelöscht werden?",
"Download": "Download", "Download": "Download",
"DuplicateLastframe": "Letztes Bild duplizieren", "DuplicateLastframe": "Letztes Bild duplizieren",
"Empty": "Keine fertigen Zeitraffer gefunden.", "Empty": "Keine fertigen Zeitraffer gefunden.",

View File

@ -990,6 +990,7 @@
"Delete": "Delete", "Delete": "Delete",
"DeleteDirectory": "Delete Directory", "DeleteDirectory": "Delete Directory",
"DeleteDirectoryQuestion": "Do you really want to delete the \"{name}\" directory with all its contents?", "DeleteDirectoryQuestion": "Do you really want to delete the \"{name}\" directory with all its contents?",
"DeleteSelectedQuestion": "Do you really want to delete {count} selected files?",
"Download": "Download", "Download": "Download",
"DuplicateLastframe": "Duplicate last frame", "DuplicateLastframe": "Duplicate last frame",
"Empty": "No finished timelapse found.", "Empty": "No finished timelapse found.",

View File

@ -228,6 +228,7 @@ export const getDefaultState = (): GuiState => {
sortDesc: true, sortDesc: true,
showHiddenFiles: false, showHiddenFiles: false,
currentPath: 'timelapse', currentPath: 'timelapse',
selectedFiles: [],
}, },
webcam: { webcam: {
currentCam: { currentCam: {

View File

@ -157,6 +157,7 @@ export interface GuiState {
sortDesc: boolean sortDesc: boolean
showHiddenFiles: boolean showHiddenFiles: boolean
currentPath: string currentPath: string
selectedFiles: FileStateFile[]
} }
webcam: { webcam: {
currentCam: { currentCam: {