mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
feat(filetransfer): implement multi-file selection and bulk deletion (#687)
* feat(filetransfer): implement multi-file selection and bulk deletion * feat(filetransfer): enhance file selection with Ctrl/Meta key support. * feat(locale): add bulk delete and selection messages in multiple languages --------- Co-authored-by: Miroslav Šedivý <sedivy.miro@gmail.com>
This commit is contained in:
committed by
GitHub
parent
baf21c1d97
commit
44ccf64257
@@ -4,14 +4,57 @@
|
||||
<p>{{ cwd }}</p>
|
||||
<i class="fas fa-rotate-right refresh" @click="refresh" />
|
||||
</div>
|
||||
<div class="files-actions" v-if="files.filter((f) => f.type !== 'dir').length > 2">
|
||||
<div class="left-controls">
|
||||
<span v-if="!selectionMode" class="action-btn select-toggle" @click="toggleSelectionMode">
|
||||
{{ tWithFallback('files.select', 'Select') }}
|
||||
</span>
|
||||
<span v-else class="action-btn select-all" @click="toggleSelectAll">
|
||||
{{
|
||||
isAllSelected
|
||||
? tWithFallback('files.unselect_all', 'Unselect All')
|
||||
: tWithFallback('files.select_all', 'Select All')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectionMode" class="right-controls">
|
||||
<span class="action-btn delete-selected" v-if="selectedFiles.length > 0" @click="deleteSelectedFiles">
|
||||
<i class="fas fa-trash"></i> {{ tWithFallback('files.delete', 'Delete') }} ({{ selectedFiles.length }})
|
||||
</span>
|
||||
<span class="action-btn select-toggle cancel-btn" @click="toggleSelectionMode">
|
||||
{{ tWithFallback('files.cancel', 'Cancel') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="files-list">
|
||||
<div v-for="item in files" :key="item.name" class="files-list-item">
|
||||
<div
|
||||
v-for="item in files"
|
||||
:key="item.name"
|
||||
class="files-list-item"
|
||||
:class="{
|
||||
'selectable-item': selectionMode && item.type !== 'dir',
|
||||
'selected-item': selectionMode && isSelected(item.name),
|
||||
}"
|
||||
@click="onItemClick(item, $event)"
|
||||
>
|
||||
<input
|
||||
v-if="selectionMode && item.type !== 'dir'"
|
||||
type="checkbox"
|
||||
class="file-checkbox"
|
||||
:checked="isSelected(item.name)"
|
||||
@click.stop
|
||||
@change.stop="toggleSelectFile(item.name)"
|
||||
/>
|
||||
<i :class="fileIcon(item)" />
|
||||
<p class="file-name" :title="item.name">{{ item.name }}</p>
|
||||
<p class="file-size">{{ fileSize(item.size) }}</p>
|
||||
<i v-if="item.type !== 'dir' && canDownload" class="fas fa-download download" @click="download(item)" />
|
||||
<i
|
||||
v-if="item.type !== 'dir' && canDelete"
|
||||
v-if="!selectionMode && item.type !== 'dir' && canDownload"
|
||||
class="fas fa-download download"
|
||||
@click="download(item)"
|
||||
/>
|
||||
<i
|
||||
v-if="!selectionMode && item.type !== 'dir' && canDelete"
|
||||
class="fas fa-trash delete"
|
||||
:title="$t('files.delete')"
|
||||
@click="deleteFile(item)"
|
||||
@@ -111,6 +154,48 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.files-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px 10px 0px 10px;
|
||||
padding: 0.5em;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 5px;
|
||||
font-size: 0.9em;
|
||||
|
||||
.left-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
display: flex;
|
||||
gap: 1.2em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: rgba($color: #fff, $alpha: 0.6);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.delete-selected {
|
||||
color: $style-error;
|
||||
|
||||
&:hover {
|
||||
color: lighten($style-error, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.files-list {
|
||||
margin: 10px 10px 10px 10px;
|
||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||
@@ -146,6 +231,25 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.file-checkbox {
|
||||
margin-right: 0.8em;
|
||||
cursor: pointer;
|
||||
accent-color: $style-primary;
|
||||
}
|
||||
|
||||
.selectable-item {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color: #fff, $alpha: 0.08);
|
||||
}
|
||||
|
||||
&.selected-item {
|
||||
background-color: rgba($color: #fff, $alpha: 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.transfers-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -292,6 +396,111 @@
|
||||
})
|
||||
export default class extends Vue {
|
||||
public uploadAreaDrag: boolean = false
|
||||
public selectionMode: boolean = false
|
||||
public selectedFiles: string[] = []
|
||||
|
||||
toggleSelectionMode() {
|
||||
this.selectionMode = !this.selectionMode
|
||||
if (!this.selectionMode) {
|
||||
this.selectedFiles = []
|
||||
}
|
||||
}
|
||||
|
||||
isSelected(name: string) {
|
||||
return this.selectedFiles.includes(name)
|
||||
}
|
||||
|
||||
toggleSelectFile(name: string) {
|
||||
const index = this.selectedFiles.indexOf(name)
|
||||
if (index > -1) {
|
||||
this.selectedFiles.splice(index, 1)
|
||||
} else {
|
||||
this.selectedFiles.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
onItemClick(item: FileListItem, event: MouseEvent) {
|
||||
if (item.type === 'dir') {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (!this.selectionMode) {
|
||||
this.selectionMode = true
|
||||
}
|
||||
this.toggleSelectFile(item.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectionMode) {
|
||||
this.toggleSelectFile(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
get isAllSelected() {
|
||||
const nonDirFiles = this.files.filter((f) => f.type !== 'dir')
|
||||
if (nonDirFiles.length === 0) return false
|
||||
return nonDirFiles.every((f) => this.isSelected(f.name))
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const nonDirFiles = this.files.filter((f) => f.type !== 'dir')
|
||||
if (this.isAllSelected) {
|
||||
this.selectedFiles = []
|
||||
} else {
|
||||
this.selectedFiles = nonDirFiles.map((f) => f.name)
|
||||
}
|
||||
}
|
||||
|
||||
tWithFallback(key: string, fallback: string, formatData?: any) {
|
||||
const translated = this.$t(key, formatData)
|
||||
if (translated === key) {
|
||||
if (formatData) {
|
||||
let str = fallback
|
||||
for (const k of Object.keys(formatData)) {
|
||||
str = str.replace(`{${k}}`, formatData[k])
|
||||
}
|
||||
return str
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
return translated as string
|
||||
}
|
||||
|
||||
async deleteSelectedFiles() {
|
||||
if (this.selectedFiles.length === 0) return
|
||||
|
||||
const count = this.selectedFiles.length
|
||||
const { isConfirmed } = await this.$swal({
|
||||
title: this.tWithFallback('files.delete_selected_title', 'Delete selected files?'),
|
||||
text: this.tWithFallback(
|
||||
'files.delete_selected_confirm',
|
||||
'Do you really want to remove the {count} selected files?',
|
||||
{ count },
|
||||
),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: this.$t('context.confirm.button_yes') as string,
|
||||
cancelButtonText: this.$t('context.confirm.button_cancel') as string,
|
||||
})
|
||||
if (!isConfirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
const filesToRemove = [...this.selectedFiles]
|
||||
this.selectionMode = false
|
||||
this.selectedFiles = []
|
||||
|
||||
for (const name of filesToRemove) {
|
||||
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(name)
|
||||
try {
|
||||
await this.$http.delete(url)
|
||||
} catch (error: any) {
|
||||
this.$log.error(new Error(`Failed to delete ${name}: ${error.message || error}`))
|
||||
}
|
||||
}
|
||||
this.$accessor.files.refresh()
|
||||
}
|
||||
|
||||
get canDownload() {
|
||||
return this.$accessor.user.admin || this.$accessor.files.userDownload
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Löschen',
|
||||
delete_title: '"{name}" löschen?',
|
||||
delete_confirm: 'Möchten Sie diese Datei wirklich entfernen?',
|
||||
select: 'Auswählen',
|
||||
select_all: 'Alle auswählen',
|
||||
unselect_all: 'Alle abwählen',
|
||||
cancel: 'Abbrechen',
|
||||
delete_selected_title: 'Ausgewählte Dateien löschen?',
|
||||
delete_selected_confirm: 'Möchten Sie die {count} ausgewählten Dateien wirklich entfernen?',
|
||||
}
|
||||
|
||||
@@ -130,4 +130,10 @@ export const files = {
|
||||
delete: 'Delete',
|
||||
delete_title: 'Delete "{name}"?',
|
||||
delete_confirm: 'Do you really want to remove this file?',
|
||||
select: 'Select',
|
||||
select_all: 'Select All',
|
||||
unselect_all: 'Unselect All',
|
||||
cancel: 'Cancel',
|
||||
delete_selected_title: 'Delete selected files?',
|
||||
delete_selected_confirm: 'Do you really want to remove the {count} selected files?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Eliminar',
|
||||
delete_title: '¿Eliminar "{name}"?',
|
||||
delete_confirm: '¿Realmente deseas eliminar este archivo?',
|
||||
select: 'Seleccionar',
|
||||
select_all: 'Seleccionar todo',
|
||||
unselect_all: 'Deseleccionar todo',
|
||||
cancel: 'Cancelar',
|
||||
delete_selected_title: '¿Eliminar los archivos seleccionados?',
|
||||
delete_selected_confirm: '¿Realmente deseas eliminar los {count} archivos seleccionados?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Poista',
|
||||
delete_title: 'Poistetaanko "{name}"?',
|
||||
delete_confirm: 'Haluatko varmasti poistaa tämän tiedoston?',
|
||||
select: 'Valitse',
|
||||
select_all: 'Valitse kaikki',
|
||||
unselect_all: 'Poista kaikki valinnat',
|
||||
cancel: 'Peruuta',
|
||||
delete_selected_title: 'Poistetaanko valitut tiedostot?',
|
||||
delete_selected_confirm: 'Haluatko varmasti poistaa {count} valittua tiedostoa?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Supprimer',
|
||||
delete_title: 'Supprimer "{name}" ?',
|
||||
delete_confirm: 'Voulez-vous vraiment supprimer ce fichier ?',
|
||||
select: 'Sélectionner',
|
||||
select_all: 'Tout sélectionner',
|
||||
unselect_all: 'Tout désélectionner',
|
||||
cancel: 'Annuler',
|
||||
delete_selected_title: 'Supprimer les fichiers sélectionnés ?',
|
||||
delete_selected_confirm: 'Voulez-vous vraiment supprimer les {count} fichiers sélectionnés ?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Hapus',
|
||||
delete_title: 'Hapus "{name}"?',
|
||||
delete_confirm: 'Apakah Anda benar-benar ingin menghapus berkas ini?',
|
||||
select: 'Pilih',
|
||||
select_all: 'Pilih Semua',
|
||||
unselect_all: 'Batalkan Semua Pilihan',
|
||||
cancel: 'Batal',
|
||||
delete_selected_title: 'Hapus berkas yang dipilih?',
|
||||
delete_selected_confirm: 'Apakah Anda benar-benar ingin menghapus {count} berkas yang dipilih?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: '削除',
|
||||
delete_title: '"{name}" を削除しますか?',
|
||||
delete_confirm: '本当にこのファイルを削除しますか?',
|
||||
select: '選択',
|
||||
select_all: 'すべて選択',
|
||||
unselect_all: 'すべて選択解除',
|
||||
cancel: 'キャンセル',
|
||||
delete_selected_title: '選択したファイルを削除しますか?',
|
||||
delete_selected_confirm: '選択した{count}個のファイルを本当に削除しますか?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: '삭제',
|
||||
delete_title: '"{name}" 을(를) 삭제하시겠습니까?',
|
||||
delete_confirm: '정말로 이 파일을 삭제하시겠습니까?',
|
||||
select: '선택',
|
||||
select_all: '모두 선택',
|
||||
unselect_all: '모두 선택 해제',
|
||||
cancel: '취소',
|
||||
delete_selected_title: '선택한 파일을 삭제하시겠습니까?',
|
||||
delete_selected_confirm: '선택한 {count}개의 파일을 정말로 삭제하시겠습니까?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Slett',
|
||||
delete_title: 'Slette "{name}"?',
|
||||
delete_confirm: 'Er du sikker på at du vil slette denne filen?',
|
||||
select: 'Velg',
|
||||
select_all: 'Velg alle',
|
||||
unselect_all: 'Fjern alle valg',
|
||||
cancel: 'Avbryt',
|
||||
delete_selected_title: 'Slette valgte filer?',
|
||||
delete_selected_confirm: 'Er du sikker på at du vil slette de {count} valgte filene?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Usuń',
|
||||
delete_title: 'Usunąć "{name}"?',
|
||||
delete_confirm: 'Czy na pewno chcesz usunąć ten plik?',
|
||||
select: 'Zaznacz',
|
||||
select_all: 'Zaznacz wszystko',
|
||||
unselect_all: 'Odznacz wszystko',
|
||||
cancel: 'Anuluj',
|
||||
delete_selected_title: 'Usunąć zaznaczone pliki?',
|
||||
delete_selected_confirm: 'Czy na pewno chcesz usunąć {count} zaznaczonych plików?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Удалить',
|
||||
delete_title: 'Удалить "{name}"?',
|
||||
delete_confirm: 'Вы действительно хотите удалить этот файл?',
|
||||
select: 'Выбрать',
|
||||
select_all: 'Выбрать все',
|
||||
unselect_all: 'Снять выделение',
|
||||
cancel: 'Отмена',
|
||||
delete_selected_title: 'Удалить выбранные файлы?',
|
||||
delete_selected_confirm: 'Вы действительно хотите удалить {count} выбранных файлов?',
|
||||
}
|
||||
|
||||
@@ -133,4 +133,10 @@ export const files = {
|
||||
delete: 'Odstrániť',
|
||||
delete_title: 'Odstrániť "{name}"?',
|
||||
delete_confirm: 'Naozaj chcete odstrániť tento súbor?',
|
||||
select: 'Vybrať',
|
||||
select_all: 'Vybrať všetko',
|
||||
unselect_all: 'Zrušiť výber',
|
||||
cancel: 'Zrušiť',
|
||||
delete_selected_title: 'Odstrániť vybrané súbory?',
|
||||
delete_selected_confirm: 'Naozaj chcete odstrániť {count} vybraných súborov?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: 'Ta bort',
|
||||
delete_title: 'Ta bort "{name}"?',
|
||||
delete_confirm: 'Vill du verkligen ta bort den här filen?',
|
||||
select: 'Markera',
|
||||
select_all: 'Markera alla',
|
||||
unselect_all: 'Avmarkera alla',
|
||||
cancel: 'Avbryt',
|
||||
delete_selected_title: 'Ta bort markerade filer?',
|
||||
delete_selected_confirm: 'Vill du verkligen ta bort de {count} markerade filerna?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: '删除',
|
||||
delete_title: '删除"{name}"?',
|
||||
delete_confirm: '确定要删除此文件吗?',
|
||||
select: '选择',
|
||||
select_all: '全选',
|
||||
unselect_all: '取消全选',
|
||||
cancel: '取消',
|
||||
delete_selected_title: '删除选中的文件?',
|
||||
delete_selected_confirm: '确定要删除选中的 {count} 个文件吗?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,10 @@ export const files = {
|
||||
delete: '刪除',
|
||||
delete_title: '刪除「{name}」?',
|
||||
delete_confirm: '確定要刪除此檔案嗎?',
|
||||
select: '選擇',
|
||||
select_all: '全選',
|
||||
unselect_all: '取消全選',
|
||||
cancel: '取消',
|
||||
delete_selected_title: '刪除選取的檔案?',
|
||||
delete_selected_confirm: '確定要刪除選取的 {count} 個檔案嗎?',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user