feat: add multiselect to timelapse file manager (#1039)
This commit is contained in:
parent
99075fb259
commit
cbb70759f5
@ -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>
|
||||||
|
@ -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.",
|
||||||
|
@ -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.",
|
||||||
|
@ -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: {
|
||||||
|
@ -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: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user