filebrowser/frontend/src/views/Share.vue

361 lines
10 KiB
Vue

<template>
<div>
<header-bar showMenu showLogo>
<title />
<action
v-if="selectedCount"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<button
v-if="isSingleFile()"
class="action copy-clipboard"
:data-clipboard-text="linkSelected()"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
<action
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
</header-bar>
<breadcrumbs :base="'/share/' + hash" />
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<span>{{ $t("files.loading") }}</span>
</h2>
</div>
<div v-else-if="error">
<div v-if="error.status === 401">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">
{{ $t("login.wrongCredentials") }}
</div>
<div class="card-title">
<h2>{{ $t("login.password") }}</h2>
</div>
<div class="card-content">
<input
v-focus
type="password"
:placeholder="$t('login.password')"
v-model="password"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
</div>
<errors v-else :errorCode="error.status" />
</div>
<div v-else>
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{
req.isDir
? $t("download.downloadFolder")
: $t("download.downloadFile")
}}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div>
<div class="share__box__element" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">
<div>
<i class="material-icons">file_download</i
>{{ $t("buttons.download") }}
</div>
</a>
<a
target="_blank"
:href="inlineLink"
class="button button--flat"
v-if="!req.isDir"
>
<div>
<i class="material-icons">open_in_new</i
>{{ $t("buttons.openFile") }}
</div>
</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="link" size="200" level="M"></qrcode-vue>
</div>
</div>
<div
v-if="req.isDir && req.items.length > 0"
class="share__box share__box__items"
>
<div class="share__box__header" v-if="req.isDir">
{{ $t("files.files") }}
</div>
<div id="listing" class="list file-icons">
<item
v-for="item in req.items.slice(0, this.showLimit)"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
readOnly
>
</item>
<div
v-if="req.items.length > showLimit"
class="item"
@click="showLimit += 100"
>
<div>
<p class="name">+ {{ req.items.length - showLimit }}</p>
</div>
</div>
<div
:class="{ active: $store.state.multiple }"
id="multiple-selection"
>
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
<div
@click="$store.commit('multiple', false)"
tabindex="0"
role="button"
:title="$t('files.clear')"
:aria-label="$t('files.clear')"
class="action"
>
<i class="material-icons">clear</i>
</div>
</div>
</div>
</div>
<div
v-else-if="req.isDir && req.items.length === 0"
class="share__box share__box__items"
>
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span>
</h2>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapGetters } from "vuex";
import { pub as api } from "@/api";
import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard";
export default {
name: "share",
components: {
HeaderBar,
Action,
Breadcrumbs,
Item,
QrcodeVue,
Errors,
},
data: () => ({
error: null,
showLimit: 100,
password: "",
attemptedPasswordLogin: false,
hash: null,
token: null,
clip: null,
}),
watch: {
$route: function () {
this.showLimit = 100;
this.fetchData();
},
},
created: async function () {
const hash = this.$route.params.pathMatch.split("/")[0];
this.hash = hash;
await this.fetchData();
},
mounted() {
window.addEventListener("keydown", this.keyEvent);
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
this.clip.destroy();
},
computed: {
...mapState(["req", "loading", "multiple", "selected"]),
...mapGetters(["selectedCount"]),
icon: function () {
if (this.req.isDir) return "folder";
if (this.req.type === "image") return "insert_photo";
if (this.req.type === "audio") return "volume_up";
if (this.req.type === "video") return "movie";
return "insert_drive_file";
},
link: function () {
return api.getDownloadURL(this.req);
},
inlineLink: function () {
return api.getDownloadURL(this.req, true);
},
humanSize: function () {
if (this.req.isDir) {
return this.req.items.length;
}
return filesize(this.req.size);
},
humanTime: function () {
return moment(this.req.modified).fromNow();
},
modTime: function () {
return new Date(Date.parse(this.req.modified)).toLocaleString();
},
},
methods: {
...mapMutations(["resetSelected", "updateRequest", "setLoading"]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)));
},
fetchData: async function () {
// Reset view information.
this.$store.commit("setReload", false);
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
this.$store.commit("closeHovers");
// Set loading to true and reset the error.
this.setLoading(true);
this.error = null;
if (this.password !== "") {
this.attemptedPasswordLogin = true;
}
let url = this.$route.path;
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
try {
let file = await api.fetch(url, this.password);
file.hash = this.hash;
this.token = file.token || "";
this.updateRequest(file);
document.title = `${file.name} - ${document.title}`;
} catch (e) {
this.error = e;
} finally {
this.setLoading(false);
}
},
keyEvent(event) {
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all
// files and folders.
if (this.selectedCount > 0) {
this.resetSelected();
}
}
},
toggleMultipleSelection() {
this.$store.commit("multiple", !this.multiple);
},
isSingleFile: function () {
return (
this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir
);
},
download() {
if (this.isSingleFile()) {
api.download(
null,
this.hash,
this.token,
this.req.items[this.selected[0]].path
);
return;
}
this.$store.commit("showHover", {
prompt: "download",
confirm: (format) => {
this.$store.commit("closeHovers");
let files = [];
for (let i of this.selected) {
files.push(this.req.items[i].path);
}
api.download(format, this.hash, this.token, ...files);
},
});
},
linkSelected: function () {
return this.isSingleFile()
? api.getDownloadURL({
hash: this.hash,
path: this.req.items[this.selected[0]].path,
})
: "";
},
},
};
</script>