feat: listing context menu (#9)

* feat: listing context menu

* fix: extract archive check method
pull/3756/head
Laurynas Gadliauskas 2021-06-07 11:38:20 +03:00 committed by GitHub
parent cfb4f8662e
commit cff060225e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 264 additions and 19 deletions

View File

@ -0,0 +1,160 @@
<template>
<div
id="context-menu"
ref="contextMenu"
class="card"
:style="menuPosition"
@click="close"
>
<p>
<action icon="info" :label="$t('buttons.info')" show="info" />
</p>
<p v-if="options.share">
<action icon="share" :label="$t('buttons.share')" show="share" />
</p>
<p v-if="options.rename">
<action icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
</p>
<p v-if="options.copy">
<action
id="copy-button"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
</p>
<p v-if="options.move">
<action
id="move-button"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
</p>
<p v-if="options.permissions">
<action
id="permissions-button"
icon="lock"
:label="$t('buttons.permissions')"
show="permissions"
/>
</p>
<p v-if="options.archive">
<action
id="archive-button"
icon="archive"
:label="$t('buttons.archive')"
show="archive"
/>
</p>
<p v-if="options.unarchive">
<action
id="unarchive-button"
icon="unarchive"
:label="$t('buttons.unarchive')"
show="unarchive"
/>
</p>
<p v-if="options.download">
<action
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
</p>
<p v-if="options.delete">
<action
id="delete-button"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</p>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import { files as api } from "@/api";
import Action from "@/components/header/Action";
export default {
name: "context-menu",
components: { Action },
computed: {
...mapState(["req", "selected", "user", "selected", "contextMenu"]),
...mapGetters(["selectedCount", "onlyArchivesSelected"]),
menuPosition() {
if (this.contextMenu === null) {
return { left: "0px", right: "0px" };
}
let style = {
left: this.contextMenu.x + "px",
top: this.contextMenu.y + "px",
};
if (window.innerWidth - this.contextMenu.x < 150) {
style.transform = "translateX(calc(-100% - 3px))";
}
return style;
},
options() {
return {
download: this.user.perm.download,
delete: this.selectedCount > 0 && this.user.perm.delete,
rename: this.selectedCount === 1 && this.user.perm.rename,
share: this.selectedCount === 1 && this.user.perm.share,
move: this.selectedCount > 0 && this.user.perm.rename,
copy: this.selectedCount > 0 && this.user.perm.create,
permissions: this.selectedCount === 1 && this.user.perm.modify,
archive: this.selectedCount > 0 && this.user.perm.create,
unarchive: this.selectedCount === 1 && this.onlyArchivesSelected,
};
},
},
mounted() {
window.addEventListener("mousedown", this.windowClick);
},
beforeDestroy() {
window.removeEventListener("mousedown", this.windowClick);
},
methods: {
windowClick(event) {
if (!this.$refs.contextMenu.contains(event.target)) {
this.close();
}
},
close() {
this.$store.commit("hideContextMenu");
},
download() {
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url);
return;
}
this.$store.commit("showHover", {
prompt: "download",
confirm: (format) => {
this.$store.commit("closeHovers");
let files = [];
if (this.selectedCount > 0) {
for (let i of this.selected) {
files.push(this.req.items[i].url);
}
} else {
files.push(this.$route.path);
}
api.download(format, ...files);
},
});
},
},
};
</script>

View File

@ -11,6 +11,7 @@
:data-dir="isDir" :data-dir="isDir"
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
ref="item"
> >
<div> <div>
<img <img
@ -54,6 +55,7 @@ import filesize from "filesize";
import moment from "moment"; import moment from "moment";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { eventPosition } from "@/utils/event";
export default { export default {
name: "item", name: "item",
@ -117,8 +119,20 @@ export default {
return enableThumbs; return enableThumbs;
}, },
}, },
mounted() {
this.$refs.item.addEventListener("contextmenu", this.contextMenu);
},
beforeDestroy() {
this.$refs.item.removeEventListener("contextmenu", this.contextMenu);
},
methods: { methods: {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]), ...mapMutations([
"addSelected",
"removeSelected",
"resetSelected",
"showContextMenu",
"hideContextMenu",
]),
permissions() { permissions() {
let s = ""; let s = "";
if (this.isSymlink) { if (this.isSymlink) {
@ -283,6 +297,17 @@ export default {
open: function () { open: function () {
this.$router.push({ path: this.url }); this.$router.push({ path: this.url });
}, },
contextMenu(event) {
event.preventDefault();
this.hideContextMenu();
if (this.$store.state.selected.indexOf(this.index) === -1) {
this.resetSelected();
this.addSelected(this.index);
}
let pos = eventPosition(event);
pos.x += 2;
this.showContextMenu(pos);
},
}, },
}; };
</script> </script>

View File

@ -286,4 +286,24 @@
font-size: 1em; font-size: 1em;
left: .9em; left: .9em;
bottom: .4em; bottom: .4em;
}
#context-menu {
position: absolute;
z-index: 10;
}
#context-menu >p {
margin: 0;
}
#context-menu .action {
border-radius: 0;
width: 100%;
min-width: max-content;
padding-right: .8em;
}
#context-menu .action > * {
vertical-align: middle;
} }

View File

@ -2,6 +2,8 @@ const getters = {
isLogged: (state) => state.user !== null, isLogged: (state) => state.user !== null,
isFiles: (state) => !state.loading && state.route.name === "Files", isFiles: (state) => !state.loading && state.route.name === "Files",
isListing: (state, getters) => getters.isFiles && state.req.isDir, isListing: (state, getters) => getters.isFiles && state.req.isDir,
isVisibleContext: (state, getters) =>
getters.isListing && state.contextMenu !== null,
selectedCount: (state) => state.selected.length, selectedCount: (state) => state.selected.length,
progress: (state) => { progress: (state) => {
if (state.upload.progress.length == 0) { if (state.upload.progress.length == 0) {
@ -11,6 +13,20 @@ const getters = {
let sum = state.upload.progress.reduce((acc, val) => acc + val); let sum = state.upload.progress.reduce((acc, val) => acc + val);
return Math.ceil((sum / state.upload.size) * 100); return Math.ceil((sum / state.upload.size) * 100);
}, },
onlyArchivesSelected: (state, getters) => {
let extensions = [".zip", ".tar", ".gz", ".bz2", ".xz", ".lz4", ".sz"];
let items = state.req.items;
if (getters.selectedCount < 1) {
return false;
}
for (const i of state.selected) {
let item = items[i];
if (item.isDir || !extensions.includes(item.extension)) {
return false;
}
}
return true;
},
}; };
export default getters; export default getters;

View File

@ -23,6 +23,7 @@ const state = {
show: null, show: null,
showShell: false, showShell: false,
showConfirm: null, showConfirm: null,
contextMenu: null,
}; };
export default new Vuex.Store({ export default new Vuex.Store({

View File

@ -82,6 +82,15 @@ const mutations = {
state.clipboard.key = ""; state.clipboard.key = "";
state.clipboard.items = []; state.clipboard.items = [];
}, },
showContextMenu: (state, value) => {
state.contextMenu = {
x: value.x,
y: value.y,
};
},
hideContextMenu: (state) => {
state.contextMenu = null;
},
}; };
export default mutations; export default mutations;

View File

@ -0,0 +1,25 @@
function eventPosition(event) {
let posx = 0;
let posy = 0;
if (event.pageX || event.pageY) {
posx = event.pageX;
posy = event.pageY;
} else if (event.clientX || event.clientY) {
posx =
event.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
posy =
event.clientY +
document.body.scrollTop +
document.documentElement.scrollTop;
}
return {
x: posx,
y: posy,
};
}
export { eventPosition };

View File

@ -9,6 +9,7 @@
<shell v-if="isExecEnabled && isLogged && user.perm.execute" /> <shell v-if="isExecEnabled && isLogged && user.perm.execute" />
</main> </main>
<prompts></prompts> <prompts></prompts>
<context-menu v-if="isVisibleContext"></context-menu>
</div> </div>
</template> </template>
@ -16,6 +17,7 @@
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import Prompts from "@/components/prompts/Prompts"; import Prompts from "@/components/prompts/Prompts";
import ContextMenu from "@/components/files/ContextMenu";
import Shell from "@/components/Shell"; import Shell from "@/components/Shell";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
@ -24,15 +26,17 @@ export default {
components: { components: {
Sidebar, Sidebar,
Prompts, Prompts,
ContextMenu,
Shell, Shell,
}, },
computed: { computed: {
...mapGetters(["isLogged", "progress"]), ...mapGetters(["isLogged", "isVisibleContext", "progress"]),
...mapState(["user"]), ...mapState(["user"]),
isExecEnabled: () => enableExec, isExecEnabled: () => enableExec,
}, },
watch: { watch: {
$route: function () { $route: function () {
this.$store.commit("hideContextMenu");
this.$store.commit("resetSelected"); this.$store.commit("resetSelected");
this.$store.commit("multiple", false); this.$store.commit("multiple", false);
if (this.$store.state.show !== "success") if (this.$store.state.show !== "success")

View File

@ -325,7 +325,7 @@ export default {
"selected", "selected",
"loading", "loading",
]), ]),
...mapGetters(["selectedCount"]), ...mapGetters(["selectedCount", "onlyArchivesSelected"]),
nameSorted() { nameSorted() {
return this.req.sorting.by === "name"; return this.req.sorting.by === "name";
}, },
@ -401,22 +401,6 @@ export default {
isMobile() { isMobile() {
return this.width <= 736; return this.width <= 736;
}, },
onlyArchivesSelected() {
let extensions = [".zip", ".tar", ".gz", ".bz2", ".xz", ".lz4", ".sz"];
if (this.selectedCount < 1) {
return false;
}
for (const i of this.selected) {
let item = this.req.items[i];
if (item.isDir || !extensions.includes(item.extension)) {
return false;
}
}
return true;
},
}, },
watch: { watch: {
req: function () { req: function () {
@ -481,6 +465,7 @@ export default {
if (event.keyCode === 27) { if (event.keyCode === 27) {
// Reset files selection. // Reset files selection.
this.$store.commit("resetSelected"); this.$store.commit("resetSelected");
this.$store.commit("hideContextMenu");
} }
// Del! // Del!