feat: migrate to vue 3 (#2689)
--------- Co-authored-by: Joep <jcbuhre@gmail.com> Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com> Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
<component
|
||||
:is="element"
|
||||
:to="base || ''"
|
||||
:aria-label="$t('files.home')"
|
||||
:title="$t('files.home')"
|
||||
:aria-label="t('files.home')"
|
||||
:title="t('files.home')"
|
||||
>
|
||||
<i class="material-icons">home</i>
|
||||
</component>
|
||||
@@ -18,58 +18,66 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "breadcrumbs",
|
||||
props: ["base", "noLink"],
|
||||
computed: {
|
||||
items() {
|
||||
const relativePath = this.$route.path.replace(this.base, "");
|
||||
let parts = relativePath.split("/");
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
let breadcrumbs = [];
|
||||
const props = defineProps<{
|
||||
base: string;
|
||||
noLink?: boolean;
|
||||
}>();
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: this.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
const items = computed(() => {
|
||||
const relativePath = route.path.replace(props.base, "");
|
||||
let parts = relativePath.split("/");
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
},
|
||||
element() {
|
||||
if (this.noLink !== undefined) {
|
||||
return "span";
|
||||
}
|
||||
let breadcrumbs: BreadCrumb[] = [];
|
||||
|
||||
return "router-link";
|
||||
},
|
||||
},
|
||||
};
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: props.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
});
|
||||
|
||||
const element = computed(() => {
|
||||
if (props.noLink) {
|
||||
return "span";
|
||||
}
|
||||
|
||||
return "router-link";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
45
frontend/src/components/CustomToast.vue
Normal file
45
frontend/src/components/CustomToast.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="t-container">
|
||||
<span>{{ message }}</span>
|
||||
<button v-if="isReport" class="action" @click.stop="clicked">
|
||||
{{ reportText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
message: string;
|
||||
reportText?: string;
|
||||
isReport?: boolean;
|
||||
}>();
|
||||
|
||||
const clicked = () => {
|
||||
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.t-container {
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.action {
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
margin-left: 20px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
border: thin solid currentColor;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .action {
|
||||
margin-left: initial;
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
224
frontend/src/components/ProgressBar.vue
Normal file
224
frontend/src/components/ProgressBar.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<!-- This component taken directly from vue-simple-progress
|
||||
since it didnt support Vue 3 but the component itself does
|
||||
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'top'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
<div class="vue-simple-progress" :style="progress_style">
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'middle'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
<div
|
||||
style="position: relative; left: -9999px"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'inside'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
<div class="vue-simple-progress-bar" :style="bar_style">
|
||||
<div
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'inside'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'bottom'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// We're leaving this untouched as you can read in the beginning
|
||||
var isNumber = function (n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "progress-bar",
|
||||
props: {
|
||||
val: {
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
default: 100,
|
||||
},
|
||||
size: {
|
||||
// either a number (pixel width/height) or 'tiny', 'small',
|
||||
// 'medium', 'large', 'huge', 'massive' for common sizes
|
||||
default: 3,
|
||||
},
|
||||
"bg-color": {
|
||||
type: String,
|
||||
default: "#eee",
|
||||
},
|
||||
"bar-color": {
|
||||
type: String,
|
||||
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
||||
},
|
||||
"bar-transition": {
|
||||
type: String,
|
||||
default: "all 0.5s ease",
|
||||
},
|
||||
"bar-border-radius": {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
spacing: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
"text-align": {
|
||||
type: String,
|
||||
default: "center", // 'left', 'right'
|
||||
},
|
||||
"text-position": {
|
||||
type: String,
|
||||
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
|
||||
},
|
||||
"font-size": {
|
||||
type: Number,
|
||||
default: 13,
|
||||
},
|
||||
"text-fg-color": {
|
||||
type: String,
|
||||
default: "#222",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pct() {
|
||||
var pct = (this.val / this.max) * 100;
|
||||
pct = pct.toFixed(2);
|
||||
return Math.min(pct, this.max);
|
||||
},
|
||||
size_px() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
return 2;
|
||||
case "small":
|
||||
return 4;
|
||||
case "medium":
|
||||
return 8;
|
||||
case "large":
|
||||
return 12;
|
||||
case "big":
|
||||
return 16;
|
||||
case "huge":
|
||||
return 32;
|
||||
case "massive":
|
||||
return 64;
|
||||
}
|
||||
|
||||
return isNumber(this.size) ? this.size : 32;
|
||||
},
|
||||
text_padding() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
||||
}
|
||||
|
||||
return isNumber(this.spacing) ? this.spacing : 4;
|
||||
},
|
||||
text_font_size() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
||||
}
|
||||
|
||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
||||
},
|
||||
progress_style() {
|
||||
var style = {
|
||||
background: this.bgColor,
|
||||
};
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "relative";
|
||||
style["min-height"] = this.size_px + "px";
|
||||
style["z-index"] = "-2";
|
||||
}
|
||||
|
||||
if (this.barBorderRadius > 0) {
|
||||
style["border-radius"] = this.barBorderRadius + "px";
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
bar_style() {
|
||||
var style = {
|
||||
background: this.barColor,
|
||||
width: this.pct + "%",
|
||||
height: this.size_px + "px",
|
||||
transition: this.barTransition,
|
||||
};
|
||||
|
||||
if (this.barBorderRadius > 0) {
|
||||
style["border-radius"] = this.barBorderRadius + "px";
|
||||
}
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "absolute";
|
||||
style["top"] = "0";
|
||||
style["height"] = "100%";
|
||||
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
text_style() {
|
||||
var style = {
|
||||
color: this.textFgColor,
|
||||
"font-size": this.text_font_size + "px",
|
||||
"text-align": this.textAlign,
|
||||
};
|
||||
|
||||
if (
|
||||
this.textPosition == "top" ||
|
||||
this.textPosition == "middle" ||
|
||||
this.textPosition == "inside"
|
||||
)
|
||||
style["padding-bottom"] = this.text_padding + "px";
|
||||
if (
|
||||
this.textPosition == "bottom" ||
|
||||
this.textPosition == "middle" ||
|
||||
this.textPosition == "inside"
|
||||
)
|
||||
style["padding-top"] = this.text_padding + "px";
|
||||
|
||||
return style;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -17,7 +17,7 @@
|
||||
@keyup.enter="submit"
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
v-model.trim="prompt"
|
||||
:aria-label="$t('search.search')"
|
||||
:placeholder="$t('search.search')"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@
|
||||
<template v-if="isEmpty">
|
||||
<p>{{ text }}</p>
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<template v-if="prompt.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t("search.types") }}</h3>
|
||||
<div>
|
||||
@@ -49,7 +49,7 @@
|
||||
</template>
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in filteredResults" :key="k">
|
||||
<router-link @click.native="close" :to="s.url">
|
||||
<router-link v-on:click="close" :to="s.url">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
@@ -64,138 +64,155 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import url from "@/utils/url";
|
||||
import { search } from "@/api";
|
||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
var boxes = {
|
||||
const boxes = {
|
||||
image: { label: "images", icon: "insert_photo" },
|
||||
audio: { label: "music", icon: "volume_up" },
|
||||
video: { label: "video", icon: "movie" },
|
||||
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "search",
|
||||
data: function () {
|
||||
return {
|
||||
value: "",
|
||||
active: false,
|
||||
ongoing: false,
|
||||
results: [],
|
||||
reload: false,
|
||||
resultsCount: 50,
|
||||
scrollable: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currentPrompt(val, old) {
|
||||
this.active = val?.prompt === "search";
|
||||
const layoutStore = useLayoutStore();
|
||||
const fileStore = useFileStore();
|
||||
|
||||
if (old?.prompt === "search" && !this.active) {
|
||||
if (this.reload) {
|
||||
this.setReload(true);
|
||||
}
|
||||
const { currentPromptName } = storeToRefs(layoutStore);
|
||||
|
||||
document.body.style.overflow = "auto";
|
||||
this.reset();
|
||||
this.value = "";
|
||||
this.active = false;
|
||||
this.$refs.input.blur();
|
||||
} else if (this.active) {
|
||||
this.reload = false;
|
||||
this.$refs.input.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
},
|
||||
value() {
|
||||
if (this.results.length) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["isListing", "currentPrompt"]),
|
||||
boxes() {
|
||||
return boxes;
|
||||
},
|
||||
isEmpty() {
|
||||
return this.results.length === 0;
|
||||
},
|
||||
text() {
|
||||
if (this.ongoing) {
|
||||
return "";
|
||||
}
|
||||
const prompt = ref<string>("");
|
||||
const active = ref<boolean>(false);
|
||||
const ongoing = ref<boolean>(false);
|
||||
const results = ref<any[]>([]);
|
||||
const reload = ref<boolean>(false);
|
||||
const resultsCount = ref<number>(50);
|
||||
|
||||
return this.value === ""
|
||||
? this.$t("search.typeToSearch")
|
||||
: this.$t("search.pressToSearch");
|
||||
},
|
||||
filteredResults() {
|
||||
return this.results.slice(0, this.resultsCount);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.result.addEventListener("scroll", (event) => {
|
||||
if (
|
||||
event.target.offsetHeight + event.target.scrollTop >=
|
||||
event.target.scrollHeight - 100
|
||||
) {
|
||||
this.resultsCount += 50;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
||||
open() {
|
||||
this.showHover("search");
|
||||
},
|
||||
close(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.closeHovers();
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event);
|
||||
return;
|
||||
}
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
this.results.length = 0;
|
||||
},
|
||||
init(string) {
|
||||
this.value = `${string} `;
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
reset() {
|
||||
this.ongoing = false;
|
||||
this.resultsCount = 50;
|
||||
this.results = [];
|
||||
},
|
||||
async submit(event) {
|
||||
event.preventDefault();
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
const result = ref<HTMLElement | null>(null);
|
||||
|
||||
if (this.value === "") {
|
||||
return;
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
let path = this.$route.path;
|
||||
if (!this.isListing) {
|
||||
path = url.removeLastDir(path) + "/";
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
this.ongoing = true;
|
||||
watch(currentPromptName, (newVal, oldVal) => {
|
||||
active.value = newVal === "search";
|
||||
|
||||
try {
|
||||
this.results = await search(path, this.value);
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
if (oldVal === "search" && !active.value) {
|
||||
if (reload.value) {
|
||||
fileStore.reload = true;
|
||||
}
|
||||
|
||||
this.ongoing = false;
|
||||
},
|
||||
},
|
||||
document.body.style.overflow = "auto";
|
||||
reset();
|
||||
prompt.value = "";
|
||||
active.value = false;
|
||||
input.value?.blur();
|
||||
} else if (active.value) {
|
||||
reload.value = false;
|
||||
input.value?.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
});
|
||||
|
||||
watch(prompt, () => {
|
||||
if (results.value.length) {
|
||||
reset();
|
||||
}
|
||||
});
|
||||
|
||||
// ...mapState(useFileStore, ["isListing"]),
|
||||
// ...mapState(useLayoutStore, ["show"]),
|
||||
// ...mapWritableState(useFileStore, { sReload: "reload" }),
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
return results.value.length === 0;
|
||||
});
|
||||
const text = computed(() => {
|
||||
if (ongoing.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return prompt.value === ""
|
||||
? t("search.typeToSearch")
|
||||
: t("search.pressToSearch");
|
||||
});
|
||||
const filteredResults = computed(() => {
|
||||
return results.value.slice(0, resultsCount.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (result.value === null) {
|
||||
return;
|
||||
}
|
||||
result.value.addEventListener("scroll", (event: Event) => {
|
||||
if (
|
||||
(event.target as HTMLElement).offsetHeight +
|
||||
(event.target as HTMLElement).scrollTop >=
|
||||
(event.target as HTMLElement).scrollHeight - 100
|
||||
) {
|
||||
resultsCount.value += 50;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const open = () => {
|
||||
!active.value && layoutStore.showHover("search");
|
||||
};
|
||||
|
||||
const close = (event: Event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
};
|
||||
|
||||
const keyup = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
close(event);
|
||||
return;
|
||||
}
|
||||
results.value.length = 0;
|
||||
};
|
||||
|
||||
const init = (string: string) => {
|
||||
prompt.value = `${string} `;
|
||||
input.value !== null ? input.value.focus() : "";
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
ongoing.value = false;
|
||||
resultsCount.value = 50;
|
||||
results.value = [];
|
||||
};
|
||||
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (prompt.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = route.path;
|
||||
if (!fileStore.isListing) {
|
||||
path = url.removeLastDir(path) + "/";
|
||||
}
|
||||
|
||||
ongoing.value = true;
|
||||
|
||||
try {
|
||||
results.value = await search(path, prompt.value);
|
||||
} catch (error: any) {
|
||||
$showError(error);
|
||||
}
|
||||
|
||||
ongoing.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
tabindex="0"
|
||||
ref="input"
|
||||
class="shell__text"
|
||||
contenteditable="true"
|
||||
@keydown.prevent.38="historyUp"
|
||||
@keydown.prevent.40="historyDown"
|
||||
:contenteditable="true"
|
||||
@keydown.prevent.arrow-up="historyUp"
|
||||
@keydown.prevent.arrow-down="historyDown"
|
||||
@keypress.prevent.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
@@ -45,7 +45,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapState, mapGetters } from "vuex";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { commands } from "@/api";
|
||||
import { throttle } from "lodash";
|
||||
import { theme } from "@/utils/constants";
|
||||
@@ -53,8 +56,8 @@ import { theme } from "@/utils/constants";
|
||||
export default {
|
||||
name: "shell",
|
||||
computed: {
|
||||
...mapState(["user", "showShell"]),
|
||||
...mapGetters(["isFiles", "isLogged"]),
|
||||
...mapState(useLayoutStore, ["showShell"]),
|
||||
...mapState(useFileStore, ["isFiles"]),
|
||||
path: function () {
|
||||
if (this.isFiles) {
|
||||
return this.$route.path;
|
||||
@@ -75,11 +78,11 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.resize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["toggleShell"]),
|
||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
||||
checkTheme() {
|
||||
if (theme == "dark") {
|
||||
return "rgba(255, 255, 255, 0.4)";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<nav :class="{ active }">
|
||||
<template v-if="isLogged">
|
||||
<template v-if="isLoggedIn">
|
||||
<button
|
||||
class="action"
|
||||
@click="toRoot"
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div v-if="user.perm.create">
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newDir')"
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
@@ -23,7 +23,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newFile')"
|
||||
@click="showHover('newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
@@ -82,9 +82,7 @@
|
||||
|
||||
<div
|
||||
class="credits"
|
||||
v-if="
|
||||
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
|
||||
"
|
||||
v-if="isFiles && !disableUsedPercentage"
|
||||
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
||||
>
|
||||
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
||||
@@ -112,7 +110,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { reactive } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import * as auth from "@/utils/auth";
|
||||
import {
|
||||
version,
|
||||
@@ -123,19 +126,27 @@ import {
|
||||
loginPage,
|
||||
} from "@/utils/constants";
|
||||
import { files as api } from "@/api";
|
||||
import ProgressBar from "vue-simple-progress";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
setup() {
|
||||
const usage = reactive(USAGE_DEFAULT);
|
||||
return { usage };
|
||||
},
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["isLogged", "currentPrompt"]),
|
||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
||||
active() {
|
||||
return this.currentPrompt?.prompt === "sidebar";
|
||||
return this.currentPromptName === "sidebar";
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
@@ -143,47 +154,45 @@ export default {
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
},
|
||||
asyncComputed: {
|
||||
usage: {
|
||||
async get() {
|
||||
let path = this.$route.path.endsWith("/")
|
||||
? this.$route.path
|
||||
: this.$route.path + "/";
|
||||
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
try {
|
||||
let usage = await api.usage(path);
|
||||
usageStats = {
|
||||
used: prettyBytes(usage.used, { binary: true }),
|
||||
total: prettyBytes(usage.total, { binary: true }),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
return usageStats;
|
||||
},
|
||||
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
shouldUpdate() {
|
||||
return this.$router.currentRoute.path.includes("/files/");
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||
async fetchUsage() {
|
||||
let path = this.$route.path.endsWith("/")
|
||||
? this.$route.path
|
||||
: this.$route.path + "/";
|
||||
let usageStats = USAGE_DEFAULT;
|
||||
if (this.disableUsedPercentage) {
|
||||
return Object.assign(this.usage, usageStats);
|
||||
}
|
||||
try {
|
||||
let usage = await api.usage(path);
|
||||
usageStats = {
|
||||
used: prettyBytes(usage.used, { binary: true }),
|
||||
total: prettyBytes(usage.total, { binary: true }),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
return Object.assign(this.usage, usageStats);
|
||||
},
|
||||
toRoot() {
|
||||
this.$router.push({ path: "/files/" }, () => {});
|
||||
this.$store.commit("closeHovers");
|
||||
this.$router.push({ path: "/files" });
|
||||
this.closeHovers();
|
||||
},
|
||||
toSettings() {
|
||||
this.$router.push({ path: "/settings" }, () => {});
|
||||
this.$store.commit("closeHovers");
|
||||
this.$router.push({ path: "/settings" });
|
||||
this.closeHovers();
|
||||
},
|
||||
help() {
|
||||
this.$store.commit("showHover", "help");
|
||||
this.showHover("help");
|
||||
},
|
||||
logout: auth.logout,
|
||||
},
|
||||
watch: {
|
||||
isFiles(newValue) {
|
||||
newValue && this.fetchUsage();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,261 +13,290 @@
|
||||
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from "lodash.throttle";
|
||||
<script setup lang="ts">
|
||||
import throttle from "lodash/throttle";
|
||||
import UTIF from "utif";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
moveDisabledTime: {
|
||||
type: Number,
|
||||
default: () => 200,
|
||||
},
|
||||
classList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
zoomStep: {
|
||||
type: Number,
|
||||
default: () => 0.25,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scale: 1,
|
||||
lastX: null,
|
||||
lastY: null,
|
||||
inDrag: false,
|
||||
touches: 0,
|
||||
lastTouchDistance: 0,
|
||||
moveDisabled: false,
|
||||
disabledTimer: null,
|
||||
imageLoaded: false,
|
||||
position: {
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 },
|
||||
},
|
||||
maxScale: 4,
|
||||
minScale: 0.25,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.decodeUTIF()) {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
let container = this.$refs.container;
|
||||
this.classList.forEach((className) => container.classList.add(className));
|
||||
// set width and height if they are zero
|
||||
if (getComputedStyle(container).width === "0px") {
|
||||
container.style.width = "100%";
|
||||
}
|
||||
if (getComputedStyle(container).height === "0px") {
|
||||
container.style.height = "100%";
|
||||
interface IProps {
|
||||
src: string;
|
||||
moveDisabledTime: number;
|
||||
classList: any[];
|
||||
zoomStep: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
moveDisabledTime: () => 200,
|
||||
classList: () => [],
|
||||
zoomStep: () => 0.25,
|
||||
});
|
||||
|
||||
const scale = ref<number>(1);
|
||||
const lastX = ref<number | null>(null);
|
||||
const lastY = ref<number | null>(null);
|
||||
const inDrag = ref<boolean>(false);
|
||||
const touches = ref<number>(0);
|
||||
const lastTouchDistance = ref<number | null>(0);
|
||||
const moveDisabled = ref<boolean>(false);
|
||||
const disabledTimer = ref<number | null>(null);
|
||||
const imageLoaded = ref<boolean>(false);
|
||||
const position = ref<{
|
||||
center: { x: number; y: number };
|
||||
relative: { x: number; y: number };
|
||||
}>({
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 },
|
||||
});
|
||||
const maxScale = ref<number>(4);
|
||||
const minScale = ref<number>(0.25);
|
||||
|
||||
// Refs
|
||||
const imgex = ref<HTMLImageElement | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (!decodeUTIF() && imgex.value !== null) {
|
||||
imgex.value.src = props.src;
|
||||
}
|
||||
|
||||
props.classList.forEach((className) =>
|
||||
container.value !== null ? container.value.classList.add(className) : ""
|
||||
);
|
||||
|
||||
if (container.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set width and height if they are zero
|
||||
if (getComputedStyle(container.value).width === "0px") {
|
||||
container.value.style.width = "100%";
|
||||
}
|
||||
if (getComputedStyle(container.value).height === "0px") {
|
||||
container.value.style.height = "100%";
|
||||
}
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
if (!decodeUTIF() && imgex.value !== null) {
|
||||
imgex.value.src = props.src;
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
document.removeEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
if (!this.decodeUTIF()) {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
scale.value = 1;
|
||||
setZoom();
|
||||
setCenter();
|
||||
}
|
||||
);
|
||||
|
||||
this.scale = 1;
|
||||
this.setZoom();
|
||||
this.setCenter();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Modified from UTIF.replaceIMG
|
||||
decodeUTIF() {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
||||
if (sufs.indexOf(suff) == -1) return false;
|
||||
let xhr = new XMLHttpRequest();
|
||||
UTIF._xhrs.push(xhr);
|
||||
UTIF._imgs.push(this.$refs.imgex);
|
||||
xhr.open("GET", this.src);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = UTIF._imgLoaded;
|
||||
xhr.send();
|
||||
return true;
|
||||
},
|
||||
onLoad() {
|
||||
let img = this.$refs.imgex;
|
||||
// Modified from UTIF.replaceIMG
|
||||
const decodeUTIF = () => {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
if (document?.location?.pathname === undefined) {
|
||||
return;
|
||||
}
|
||||
let suff = document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
|
||||
|
||||
this.imageLoaded = true;
|
||||
if (sufs.indexOf(suff) == -1) return false;
|
||||
let xhr = new XMLHttpRequest();
|
||||
UTIF._xhrs.push(xhr);
|
||||
UTIF._imgs.push(imgex.value);
|
||||
xhr.open("GET", props.src);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = UTIF._imgLoaded;
|
||||
xhr.send();
|
||||
return true;
|
||||
};
|
||||
|
||||
if (img === undefined) {
|
||||
return;
|
||||
}
|
||||
const onLoad = () => {
|
||||
imageLoaded.value = true;
|
||||
|
||||
img.classList.remove("image-ex-img-center");
|
||||
this.setCenter();
|
||||
img.classList.add("image-ex-img-ready");
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener("mouseup", this.onMouseUp);
|
||||
imgex.value.classList.remove("image-ex-img-center");
|
||||
setCenter();
|
||||
imgex.value.classList.add("image-ex-img-ready");
|
||||
|
||||
let realSize = img.naturalWidth;
|
||||
let displaySize = img.offsetWidth;
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
// Image is in portrait orientation
|
||||
if (img.naturalHeight > img.naturalWidth) {
|
||||
realSize = img.naturalHeight;
|
||||
displaySize = img.offsetHeight;
|
||||
}
|
||||
let realSize = imgex.value.naturalWidth;
|
||||
let displaySize = imgex.value.offsetWidth;
|
||||
|
||||
// Scale needed to display the image on full size
|
||||
const fullScale = realSize / displaySize;
|
||||
// Image is in portrait orientation
|
||||
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
|
||||
realSize = imgex.value.naturalHeight;
|
||||
displaySize = imgex.value.offsetHeight;
|
||||
}
|
||||
|
||||
// Full size plus additional zoom
|
||||
this.maxScale = fullScale + 4;
|
||||
},
|
||||
onMouseUp() {
|
||||
this.inDrag = false;
|
||||
},
|
||||
onResize: throttle(function () {
|
||||
if (this.imageLoaded) {
|
||||
this.setCenter();
|
||||
this.doMove(this.position.relative.x, this.position.relative.y);
|
||||
}
|
||||
}, 100),
|
||||
setCenter() {
|
||||
let container = this.$refs.container;
|
||||
let img = this.$refs.imgex;
|
||||
// Scale needed to display the image on full size
|
||||
const fullScale = realSize / displaySize;
|
||||
|
||||
this.position.center.x = Math.floor(
|
||||
(container.clientWidth - img.clientWidth) / 2
|
||||
);
|
||||
this.position.center.y = Math.floor(
|
||||
(container.clientHeight - img.clientHeight) / 2
|
||||
);
|
||||
// Full size plus additional zoom
|
||||
maxScale.value = fullScale + 4;
|
||||
};
|
||||
|
||||
img.style.left = this.position.center.x + "px";
|
||||
img.style.top = this.position.center.y + "px";
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null;
|
||||
this.lastY = null;
|
||||
this.inDrag = true;
|
||||
event.preventDefault();
|
||||
},
|
||||
mouseMove(event) {
|
||||
if (!this.inDrag) return;
|
||||
this.doMove(event.movementX, event.movementY);
|
||||
event.preventDefault();
|
||||
},
|
||||
mouseUp(event) {
|
||||
this.inDrag = false;
|
||||
event.preventDefault();
|
||||
},
|
||||
touchStart(event) {
|
||||
this.lastX = null;
|
||||
this.lastY = null;
|
||||
this.lastTouchDistance = null;
|
||||
if (event.targetTouches.length < 2) {
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.zoomAuto(event);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
zoomAuto(event) {
|
||||
switch (this.scale) {
|
||||
case 1:
|
||||
this.scale = 2;
|
||||
break;
|
||||
case 2:
|
||||
this.scale = 4;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
this.scale = 1;
|
||||
this.setCenter();
|
||||
break;
|
||||
}
|
||||
this.setZoom();
|
||||
event.preventDefault();
|
||||
},
|
||||
touchMove(event) {
|
||||
event.preventDefault();
|
||||
if (this.lastX === null) {
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
this.lastY = event.targetTouches[0].pageY;
|
||||
return;
|
||||
}
|
||||
let step = this.$refs.imgex.width / 5;
|
||||
if (event.targetTouches.length === 2) {
|
||||
this.moveDisabled = true;
|
||||
clearTimeout(this.disabledTimer);
|
||||
this.disabledTimer = setTimeout(
|
||||
() => (this.moveDisabled = false),
|
||||
this.moveDisabledTime
|
||||
);
|
||||
const onMouseUp = () => {
|
||||
inDrag.value = false;
|
||||
};
|
||||
|
||||
let p1 = event.targetTouches[0];
|
||||
let p2 = event.targetTouches[1];
|
||||
let touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
);
|
||||
if (!this.lastTouchDistance) {
|
||||
this.lastTouchDistance = touchDistance;
|
||||
return;
|
||||
}
|
||||
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
||||
this.lastTouchDistance = touchDistance;
|
||||
this.setZoom();
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
if (this.moveDisabled) return;
|
||||
let x = event.targetTouches[0].pageX - this.lastX;
|
||||
let y = event.targetTouches[0].pageY - this.lastY;
|
||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
this.lastY = event.targetTouches[0].pageY;
|
||||
this.doMove(x, y);
|
||||
}
|
||||
},
|
||||
doMove(x, y) {
|
||||
let style = this.$refs.imgex.style;
|
||||
let posX = this.pxStringToNumber(style.left) + x;
|
||||
let posY = this.pxStringToNumber(style.top) + y;
|
||||
const onResize = throttle(function () {
|
||||
if (imageLoaded.value) {
|
||||
setCenter();
|
||||
doMove(position.value.relative.x, position.value.relative.y);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
const setCenter = () => {
|
||||
if (container.value === null || imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||
position.value.center.x = Math.floor(
|
||||
(container.value.clientWidth - imgex.value.clientWidth) / 2
|
||||
);
|
||||
position.value.center.y = Math.floor(
|
||||
(container.value.clientHeight - imgex.value.clientHeight) / 2
|
||||
);
|
||||
|
||||
if (posX < this.position.center.x) {
|
||||
this.position.relative.x = this.position.relative.x * -1;
|
||||
}
|
||||
imgex.value.style.left = position.value.center.x + "px";
|
||||
imgex.value.style.top = position.value.center.y + "px";
|
||||
};
|
||||
|
||||
if (posY < this.position.center.y) {
|
||||
this.position.relative.y = this.position.relative.y * -1;
|
||||
}
|
||||
},
|
||||
wheelMove(event) {
|
||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||
this.setZoom();
|
||||
},
|
||||
setZoom() {
|
||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||
},
|
||||
pxStringToNumber(style) {
|
||||
return +style.replace("px", "");
|
||||
},
|
||||
},
|
||||
const mousedownStart = (event: Event) => {
|
||||
lastX.value = null;
|
||||
lastY.value = null;
|
||||
inDrag.value = true;
|
||||
event.preventDefault();
|
||||
};
|
||||
const mouseMove = (event: MouseEvent) => {
|
||||
if (!inDrag.value) return;
|
||||
doMove(event.movementX, event.movementY);
|
||||
event.preventDefault();
|
||||
};
|
||||
const mouseUp = (event: Event) => {
|
||||
inDrag.value = false;
|
||||
event.preventDefault();
|
||||
};
|
||||
const touchStart = (event: TouchEvent) => {
|
||||
lastX.value = null;
|
||||
lastY.value = null;
|
||||
lastTouchDistance.value = null;
|
||||
if (event.targetTouches.length < 2) {
|
||||
setTimeout(() => {
|
||||
touches.value = 0;
|
||||
}, 300);
|
||||
touches.value++;
|
||||
if (touches.value > 1) {
|
||||
zoomAuto(event);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const zoomAuto = (event: Event) => {
|
||||
switch (scale.value) {
|
||||
case 1:
|
||||
scale.value = 2;
|
||||
break;
|
||||
case 2:
|
||||
scale.value = 4;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
scale.value = 1;
|
||||
setCenter();
|
||||
break;
|
||||
}
|
||||
setZoom();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const touchMove = (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
if (lastX.value === null) {
|
||||
lastX.value = event.targetTouches[0].pageX;
|
||||
lastY.value = event.targetTouches[0].pageY;
|
||||
return;
|
||||
}
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
let step = imgex.value.width / 5;
|
||||
if (event.targetTouches.length === 2) {
|
||||
moveDisabled.value = true;
|
||||
if (disabledTimer.value) clearTimeout(disabledTimer.value);
|
||||
disabledTimer.value = window.setTimeout(
|
||||
() => (moveDisabled.value = false),
|
||||
props.moveDisabledTime
|
||||
);
|
||||
|
||||
let p1 = event.targetTouches[0];
|
||||
let p2 = event.targetTouches[1];
|
||||
let touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
);
|
||||
if (!lastTouchDistance.value) {
|
||||
lastTouchDistance.value = touchDistance;
|
||||
return;
|
||||
}
|
||||
scale.value += (touchDistance - lastTouchDistance.value) / step;
|
||||
lastTouchDistance.value = touchDistance;
|
||||
setZoom();
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
if (moveDisabled.value) return;
|
||||
let x = event.targetTouches[0].pageX - (lastX.value ?? 0);
|
||||
let y = event.targetTouches[0].pageY - (lastY.value ?? 0);
|
||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||
lastX.value = event.targetTouches[0].pageX;
|
||||
lastY.value = event.targetTouches[0].pageY;
|
||||
doMove(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
const doMove = (x: number, y: number) => {
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
const style = imgex.value.style;
|
||||
|
||||
let posX = pxStringToNumber(style.left) + x;
|
||||
let posY = pxStringToNumber(style.top) + y;
|
||||
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
|
||||
position.value.relative.x = Math.abs(position.value.center.x - posX);
|
||||
position.value.relative.y = Math.abs(position.value.center.y - posY);
|
||||
|
||||
if (posX < position.value.center.x) {
|
||||
position.value.relative.x = position.value.relative.x * -1;
|
||||
}
|
||||
|
||||
if (posY < position.value.center.y) {
|
||||
position.value.relative.y = position.value.relative.y * -1;
|
||||
}
|
||||
};
|
||||
const wheelMove = (event: WheelEvent) => {
|
||||
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
|
||||
setZoom();
|
||||
};
|
||||
const setZoom = () => {
|
||||
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
|
||||
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
|
||||
if (imgex.value !== null)
|
||||
imgex.value.style.transform = `scale(${scale.value})`;
|
||||
};
|
||||
const pxStringToNumber = (style: string) => {
|
||||
return +style.replace("px", "");
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
||||
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
||||
v-lazy="thumbnailUrl"
|
||||
/>
|
||||
<i v-else class="material-icons"></i>
|
||||
@@ -34,221 +34,240 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { enableThumbs } from "@/utils/constants";
|
||||
import { mapMutations, mapGetters, mapState } from "vuex";
|
||||
import { filesize } from "@/utils";
|
||||
import moment from "moment/min/moment-with-locales";
|
||||
import dayjs from "dayjs";
|
||||
import { files as api } from "@/api";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
data: function () {
|
||||
return {
|
||||
touches: 0,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
"name",
|
||||
"isDir",
|
||||
"url",
|
||||
"type",
|
||||
"size",
|
||||
"modified",
|
||||
"index",
|
||||
"readOnly",
|
||||
"path",
|
||||
],
|
||||
computed: {
|
||||
...mapState(["user", "selected", "req", "jwt"]),
|
||||
...mapGetters(["selectedCount"]),
|
||||
singleClick() {
|
||||
return this.readOnly == undefined && this.user.singleClick;
|
||||
},
|
||||
isSelected() {
|
||||
return this.selected.indexOf(this.index) !== -1;
|
||||
},
|
||||
isDraggable() {
|
||||
return this.readOnly == undefined && this.user.perm.rename;
|
||||
},
|
||||
canDrop() {
|
||||
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||
const touches = ref<number>(0);
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
return false;
|
||||
}
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
isDir: boolean;
|
||||
url: string;
|
||||
type: string;
|
||||
size: number;
|
||||
modified: string;
|
||||
index: number;
|
||||
readOnly?: boolean;
|
||||
path?: string;
|
||||
}>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const singleClick = computed(
|
||||
() => !props.readOnly && authStore.user?.singleClick
|
||||
);
|
||||
const isSelected = computed(
|
||||
() => fileStore.selected.indexOf(props.index) !== -1
|
||||
);
|
||||
const isDraggable = computed(
|
||||
() => !props.readOnly && authStore.user?.perm.rename
|
||||
);
|
||||
|
||||
const canDrop = computed(() => {
|
||||
if (!props.isDir || props.readOnly) return false;
|
||||
|
||||
for (let i of fileStore.selected) {
|
||||
if (fileStore.req?.items[i].url === props.url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const thumbnailUrl = computed(() => {
|
||||
const file = {
|
||||
path: props.path,
|
||||
modified: props.modified,
|
||||
};
|
||||
|
||||
return api.getPreviewURL(file as Resource, "thumb");
|
||||
});
|
||||
|
||||
const isThumbsEnabled = computed(() => {
|
||||
return enableThumbs;
|
||||
});
|
||||
|
||||
const humanSize = () => {
|
||||
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
|
||||
};
|
||||
|
||||
const humanTime = () => {
|
||||
if (!props.readOnly && authStore.user?.dateFormat) {
|
||||
return dayjs(props.modified).format("L LT");
|
||||
}
|
||||
return dayjs(props.modified).fromNow();
|
||||
};
|
||||
|
||||
const dragStart = () => {
|
||||
if (fileStore.selectedCount === 0) {
|
||||
fileStore.selected.push(props.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSelected.value) {
|
||||
fileStore.selected = [];
|
||||
fileStore.selected.push(props.index);
|
||||
}
|
||||
};
|
||||
|
||||
const dragOver = (event: Event) => {
|
||||
if (!canDrop.value) return;
|
||||
|
||||
event.preventDefault();
|
||||
let el = event.target as HTMLElement | null;
|
||||
if (el !== null) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el?.classList.contains("item")) {
|
||||
el = el?.parentElement ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl() {
|
||||
const file = {
|
||||
path: this.path,
|
||||
modified: this.modified,
|
||||
};
|
||||
if (el !== null) el.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
return api.getPreviewURL(file, "thumb");
|
||||
},
|
||||
isThumbsEnabled() {
|
||||
return enableThumbs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||
humanSize: function () {
|
||||
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.readOnly == undefined && this.user.dateFormat) {
|
||||
return moment(this.modified).format("L LT");
|
||||
}
|
||||
return moment(this.modified).fromNow();
|
||||
},
|
||||
dragStart: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index);
|
||||
return;
|
||||
const drop = async (event: Event) => {
|
||||
if (!canDrop.value) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (fileStore.selectedCount === 0) return;
|
||||
|
||||
let el = event.target as HTMLElement | null;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
let items: any[] = [];
|
||||
|
||||
for (let i of fileStore.selected) {
|
||||
if (fileStore.req) {
|
||||
items.push({
|
||||
from: fileStore.req?.items[i].url,
|
||||
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||
name: fileStore.req?.items[i].name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get url from ListingItem instance
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
let path = el.__vue__.url;
|
||||
let baseItems = (await api.fetch(path)).items;
|
||||
|
||||
let action = (overwrite: boolean, rename: boolean) => {
|
||||
api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
fileStore.reload = true;
|
||||
})
|
||||
.catch($showError);
|
||||
};
|
||||
|
||||
let conflict = upload.checkConflict(items, baseItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
layoutStore.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event: Event, option: any) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
};
|
||||
|
||||
const itemClick = (event: Event | KeyboardEvent) => {
|
||||
if (
|
||||
!((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey) &&
|
||||
singleClick.value &&
|
||||
!fileStore.multiple
|
||||
)
|
||||
open();
|
||||
else click(event);
|
||||
};
|
||||
|
||||
const click = (event: Event | KeyboardEvent) => {
|
||||
if (!singleClick.value && fileStore.selectedCount !== 0)
|
||||
event.preventDefault();
|
||||
|
||||
setTimeout(() => {
|
||||
touches.value = 0;
|
||||
}, 300);
|
||||
|
||||
touches.value++;
|
||||
if (touches.value > 1) {
|
||||
open();
|
||||
}
|
||||
|
||||
if (fileStore.selected.indexOf(props.index) !== -1) {
|
||||
fileStore.removeSelected(props.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
|
||||
let fi = 0;
|
||||
let la = 0;
|
||||
|
||||
if (props.index > fileStore.selected[0]) {
|
||||
fi = fileStore.selected[0] + 1;
|
||||
la = props.index;
|
||||
} else {
|
||||
fi = props.index;
|
||||
la = fileStore.selected[0] - 1;
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
if (fileStore.selected.indexOf(fi) == -1) {
|
||||
fileStore.selected.push(fi);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isSelected) {
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.canDrop) return;
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
let el = event.target;
|
||||
if (
|
||||
!singleClick.value &&
|
||||
!(event as KeyboardEvent).ctrlKey &&
|
||||
!(event as KeyboardEvent).metaKey &&
|
||||
!fileStore.multiple
|
||||
) {
|
||||
fileStore.selected = [];
|
||||
}
|
||||
fileStore.selected.push(props.index);
|
||||
};
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
el.style.opacity = 1;
|
||||
},
|
||||
drop: async function (event) {
|
||||
if (!this.canDrop) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (this.selectedCount === 0) return;
|
||||
|
||||
let el = event.target;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
to: this.url + encodeURIComponent(this.req.items[i].name),
|
||||
name: this.req.items[i].name,
|
||||
});
|
||||
}
|
||||
|
||||
// Get url from ListingItem instance
|
||||
let path = el.__vue__.url;
|
||||
let baseItems = (await api.fetch(path)).items;
|
||||
|
||||
let action = (overwrite, rename) => {
|
||||
api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
this.$store.commit("setReload", true);
|
||||
})
|
||||
.catch(this.$showError);
|
||||
};
|
||||
|
||||
let conflict = upload.checkConflict(items, baseItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
itemClick: function (event) {
|
||||
if (
|
||||
!(event.ctrlKey || event.metaKey) &&
|
||||
this.singleClick &&
|
||||
!this.$store.state.multiple
|
||||
)
|
||||
this.open();
|
||||
else this.click(event);
|
||||
},
|
||||
click: function (event) {
|
||||
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
||||
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.open();
|
||||
}
|
||||
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length > 0) {
|
||||
let fi = 0;
|
||||
let la = 0;
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1;
|
||||
la = this.index;
|
||||
} else {
|
||||
fi = this.index;
|
||||
la = this.selected[0] - 1;
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||
this.addSelected(fi);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!this.$store.state.multiple
|
||||
)
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
},
|
||||
open: function () {
|
||||
this.$router.push({ path: this.url });
|
||||
},
|
||||
},
|
||||
const open = () => {
|
||||
router.push({ path: props.url });
|
||||
};
|
||||
</script>
|
||||
|
||||
104
frontend/src/components/files/VideoPlayer.vue
Normal file
104
frontend/src/components/files/VideoPlayer.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<video ref="videoPlayer" class="video-max video-js" controls>
|
||||
<source :src="source" />
|
||||
<track
|
||||
kind="subtitles"
|
||||
v-for="(sub, index) in subtitles"
|
||||
:key="index"
|
||||
:src="sub"
|
||||
:label="subLabel(sub)"
|
||||
:default="index === 0"
|
||||
/>
|
||||
<p class="vjs-no-js">
|
||||
Sorry, your browser doesn't support embedded videos, but don't worry, you
|
||||
can <a :href="source">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
</p>
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import videojs from "video.js";
|
||||
import type Player from "video.js/dist/types/player";
|
||||
import "videojs-mobile-ui";
|
||||
import "videojs-hotkeys";
|
||||
|
||||
import "video.js/dist/video-js.min.css";
|
||||
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
|
||||
|
||||
const videoPlayer = ref<HTMLElement | null>(null);
|
||||
const player = ref<Player | null>(null);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
source: string;
|
||||
subtitles?: string[];
|
||||
options?: any;
|
||||
}>(),
|
||||
{
|
||||
options: {},
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
player.value = videojs(
|
||||
videoPlayer.value!,
|
||||
{
|
||||
html5: {
|
||||
// needed for customizable subtitles
|
||||
// TODO: add to user settings
|
||||
nativeTextTracks: false,
|
||||
},
|
||||
plugins: {
|
||||
hotkeys: {
|
||||
volumeStep: 0.1,
|
||||
seekStep: 10,
|
||||
enableModifiersForNumbers: false,
|
||||
},
|
||||
},
|
||||
...props.options,
|
||||
},
|
||||
// onReady callback
|
||||
async () => {
|
||||
// player.value!.log("onPlayerReady", this);
|
||||
}
|
||||
);
|
||||
// TODO: need to test on mobile
|
||||
// @ts-ignore
|
||||
player.value!.mobileUi();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player.value) {
|
||||
player.value.dispose();
|
||||
player.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const subLabel = (subUrl: string) => {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(subUrl);
|
||||
} catch (_) {
|
||||
// treat it as a relative url
|
||||
// we only need this for filename
|
||||
url = new URL(subUrl, window.location.origin);
|
||||
}
|
||||
|
||||
const label = decodeURIComponent(
|
||||
url.pathname
|
||||
.split("/")
|
||||
.pop()!
|
||||
.replace(/\.[^/.]+$/, "")
|
||||
);
|
||||
|
||||
return label;
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.video-max {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -2,24 +2,31 @@
|
||||
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "action",
|
||||
props: ["icon", "label", "counter", "show"],
|
||||
methods: {
|
||||
action: function () {
|
||||
if (this.show) {
|
||||
this.$store.commit("showHover", this.show);
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
this.$emit("action");
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
icon?: string;
|
||||
label?: string;
|
||||
counter?: number;
|
||||
show?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action"): any;
|
||||
}>();
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const action = () => {
|
||||
if (props.show) {
|
||||
layoutStore.showHover(props.show);
|
||||
}
|
||||
|
||||
emit("action");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -1,62 +1,59 @@
|
||||
<template>
|
||||
<header>
|
||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||
<action
|
||||
v-if="showMenu !== undefined"
|
||||
<img v-if="showLogo" :src="logoURL" />
|
||||
<Action
|
||||
v-if="showMenu"
|
||||
class="menu-button"
|
||||
icon="menu"
|
||||
:label="$t('buttons.toggleSidebar')"
|
||||
@action="openSidebar()"
|
||||
:label="t('buttons.toggleSidebar')"
|
||||
@action="layoutStore.showHover('sidebar')"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
|
||||
<div
|
||||
id="dropdown"
|
||||
:class="{ active: layoutStore.currentPromptName === 'more' }"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="this.$slots.actions"
|
||||
<Action
|
||||
v-if="ifActionsSlot"
|
||||
id="more"
|
||||
icon="more_vert"
|
||||
:label="$t('buttons.more')"
|
||||
@action="$store.commit('showHover', 'more')"
|
||||
:label="t('buttons.more')"
|
||||
@action="layoutStore.showHover('more')"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="overlay"
|
||||
v-show="this.currentPromptName == 'more'"
|
||||
@click="$store.commit('closeHovers')"
|
||||
v-show="layoutStore.currentPromptName == 'more'"
|
||||
@click="layoutStore.closeHovers"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { logoURL } from "@/utils/constants";
|
||||
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import { mapGetters } from "vuex";
|
||||
import { computed, useSlots } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "header-bar",
|
||||
props: ["showLogo", "showMenu"],
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
logoURL,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openSidebar() {
|
||||
this.$store.commit("showHover", "sidebar");
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["currentPromptName"]),
|
||||
},
|
||||
};
|
||||
defineProps<{
|
||||
showLogo?: boolean;
|
||||
showMenu?: boolean;
|
||||
}>();
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
const slots = useSlots();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ifActionsSlot = computed(() => (slots.actions ? true : false));
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
21
frontend/src/components/prompts/BaseModal.vue
Normal file
21
frontend/src/components/prompts/BaseModal.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<VueFinalModal
|
||||
overlay-transition="vfm-fade"
|
||||
content-transition="vfm-fade"
|
||||
@closed="layoutStore.closeHovers"
|
||||
:focus-trap="{
|
||||
initialFocus: '#focus-prompt',
|
||||
fallbackFocus: 'div.vfm__content',
|
||||
}"
|
||||
style="z-index: 9999999"
|
||||
>
|
||||
<slot />
|
||||
</VueFinalModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VueFinalModal } from "vue-final-modal";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
</script>
|
||||
@@ -6,8 +6,11 @@
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||
</file-list>
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -28,17 +31,20 @@
|
||||
<div>
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.copy") }}
|
||||
</button>
|
||||
@@ -48,7 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import FileList from "./FileList.vue";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
@@ -63,8 +72,14 @@ export default {
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(["req", "selected", "user"]),
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
@@ -87,7 +102,7 @@ export default {
|
||||
buttons.success("copy");
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit("setReload", true);
|
||||
this.reload = true;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +116,7 @@ export default {
|
||||
};
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(false, true);
|
||||
|
||||
return;
|
||||
@@ -114,14 +129,14 @@ export default {
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
@click="submit"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
@@ -30,18 +33,27 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations, mapState } from "vuex";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
|
||||
...mapState(["req", "selected"]),
|
||||
...mapState(useFileStore, [
|
||||
"isListing",
|
||||
"selectedCount",
|
||||
"req",
|
||||
"selected",
|
||||
"currentPrompt",
|
||||
]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["closeHovers"]),
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
submit: async function () {
|
||||
buttons.loading("delete");
|
||||
|
||||
@@ -69,11 +81,11 @@ export default {
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
this.$store.commit("setReload", true);
|
||||
this.reload = true;
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
this.$showError(e);
|
||||
if (this.isListing) this.$store.commit("setReload", true);
|
||||
if (this.isListing) this.reload = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
40
frontend/src/components/prompts/DeleteUser.vue
Normal file
40
frontend/src/components/prompts/DeleteUser.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p>{{ t("prompts.deleteUser") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--grey"
|
||||
@click="layoutStore.closeHovers"
|
||||
:aria-label="t('buttons.cancel')"
|
||||
:title="t('buttons.cancel')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="layoutStore.currentPrompt?.confirm()"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ t("buttons.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// const emit = defineEmits<{
|
||||
// (e: "confirm"): void;
|
||||
// }>();
|
||||
</script>
|
||||
@@ -7,18 +7,21 @@
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="button button--flat button--grey"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
@click="submit"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.discardChanges')"
|
||||
:title="$t('buttons.discardChanges')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.discardChanges") }}
|
||||
</button>
|
||||
@@ -27,15 +30,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from "vuex";
|
||||
import { mapActions } from "pinia";
|
||||
import url from "@/utils/url";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
|
||||
export default {
|
||||
name: "discardEditorChanges",
|
||||
methods: {
|
||||
...mapMutations(["closeHovers"]),
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
...mapActions(useFileStore, ["updateRequest"]),
|
||||
submit: async function () {
|
||||
this.$store.commit("updateRequest", {});
|
||||
this.updateRequest(null);
|
||||
|
||||
let uri = url.removeLastDir(this.$route.path) + "/";
|
||||
this.$router.push({ path: uri });
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.download") }}</h2>
|
||||
<h2>{{ t("prompts.download") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.downloadMessage") }}</p>
|
||||
<p>{{ t("prompts.downloadMessage") }}</p>
|
||||
|
||||
<button
|
||||
id="focus-prompt"
|
||||
v-for="(ext, format) in formats"
|
||||
:key="format"
|
||||
class="button button--block"
|
||||
@click="currentPrompt.confirm(format)"
|
||||
v-focus
|
||||
@click="layoutStore.currentPrompt?.confirm(format)"
|
||||
>
|
||||
{{ ext }}
|
||||
</button>
|
||||
@@ -20,26 +20,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "download",
|
||||
data: function () {
|
||||
return {
|
||||
formats: {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["currentPrompt"]),
|
||||
},
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formats = {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
|
||||
import url from "@/utils/url";
|
||||
import { files } from "@/api";
|
||||
|
||||
@@ -42,8 +45,10 @@ export default {
|
||||
current: window.location.pathname,
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["req", "user"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapState(useFileStore, ["req"]),
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
},
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
id="focus-prompt"
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
@@ -33,5 +35,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { name: "help" };
|
||||
import { mapActions } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "help",
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -40,33 +40,45 @@
|
||||
<p>
|
||||
<strong>MD5: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'md5')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'md5')"
|
||||
@keypress.enter="checksum($event, 'md5')"
|
||||
tabindex="2"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA1: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha1')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha1')"
|
||||
@keypress.enter="checksum($event, 'sha1')"
|
||||
tabindex="3"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA256: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha256')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha256')"
|
||||
@keypress.enter="checksum($event, 'sha256')"
|
||||
tabindex="4"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA512: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha512')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha512')"
|
||||
@keypress.enter="checksum($event, 'sha512')"
|
||||
tabindex="5"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
@@ -74,8 +86,9 @@
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
id="focus-prompt"
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
@@ -87,16 +100,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { filesize } from "@/utils";
|
||||
import moment from "moment/min/moment-with-locales";
|
||||
import dayjs from "dayjs";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "info",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["req", "selected"]),
|
||||
...mapGetters(["selectedCount", "isListing"]),
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || !this.isListing) {
|
||||
return filesize(this.req.size);
|
||||
@@ -112,13 +132,19 @@ export default {
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow();
|
||||
return dayjs(this.req.modified).fromNow();
|
||||
}
|
||||
|
||||
return moment(this.req.items[this.selected[0]].modified).fromNow();
|
||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
||||
},
|
||||
modTime: function () {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
if (this.selectedCount === 0) {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
}
|
||||
|
||||
return new Date(
|
||||
Date.parse(this.req.items[this.selected[0]].modified)
|
||||
).toLocaleString();
|
||||
},
|
||||
name: function () {
|
||||
return this.selectedCount === 0
|
||||
@@ -146,6 +172,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
checksum: async function (event, algo) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -159,8 +186,7 @@ export default {
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo);
|
||||
// eslint-disable-next-line
|
||||
event.target.innerHTML = hash;
|
||||
event.target.textContent = hash;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||
</file-list>
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -27,18 +30,21 @@
|
||||
<div>
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.move") }}
|
||||
</button>
|
||||
@@ -48,7 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import FileList from "./FileList.vue";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
@@ -63,8 +72,13 @@ export default {
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(["req", "selected", "user"]),
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
move: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
@@ -99,14 +113,14 @@ export default {
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,98 +1,104 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.newDir") }}</h2>
|
||||
<h2>{{ t("prompts.newDir") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.newDirMessage") }}</p>
|
||||
<p>{{ t("prompts.newDirMessage") }}</p>
|
||||
<input
|
||||
id="focus-prompt"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
v-focus
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
@click="layoutStore.closeHovers"
|
||||
:aria-label="t('buttons.cancel')"
|
||||
:title="t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
{{ t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
:title="t('buttons.create')"
|
||||
@click="submit"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.create") }}
|
||||
{{ t("buttons.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "new-dir",
|
||||
props: {
|
||||
redirect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
base: {
|
||||
type: [String, null],
|
||||
default: null,
|
||||
},
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
const props = defineProps({
|
||||
base: String,
|
||||
redirect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["isFiles", "isListing"]),
|
||||
},
|
||||
methods: {
|
||||
submit: async function (event) {
|
||||
event.preventDefault();
|
||||
if (this.new === "") return;
|
||||
});
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri;
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
if (this.base) uri = this.base;
|
||||
else if (this.isFiles) uri = this.$route.path + "/";
|
||||
else uri = "/";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
const name = ref<string>("");
|
||||
|
||||
uri += encodeURIComponent(this.name) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
try {
|
||||
await api.post(uri);
|
||||
if (this.redirect) {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
this.$store.commit("updateRequest", res);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (name.value === "") return;
|
||||
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
// Build the path of the new directory.
|
||||
let uri: string;
|
||||
if (props.base) uri = props.base;
|
||||
else if (fileStore.isFiles) uri = route.path + "/";
|
||||
else uri = "/";
|
||||
|
||||
if (!fileStore.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(name.value) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
if (props.redirect) {
|
||||
router.push({ path: uri });
|
||||
} else if (!props.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
fileStore.updateRequest(res);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
$showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
layoutStore.closeHovers();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.newFile") }}</h2>
|
||||
<h2>{{ t("prompts.newFile") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.newFileMessage") }}</p>
|
||||
<p>{{ t("prompts.newFileMessage") }}</p>
|
||||
<input
|
||||
id="focus-prompt"
|
||||
class="input input--block"
|
||||
v-focus
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
@@ -18,63 +18,68 @@
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
@click="layoutStore.closeHovers"
|
||||
:aria-label="t('buttons.cancel')"
|
||||
:title="t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
{{ t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
:aria-label="t('buttons.create')"
|
||||
:title="t('buttons.create')"
|
||||
>
|
||||
{{ $t("buttons.create") }}
|
||||
{{ t("buttons.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "new-file",
|
||||
data: function () {
|
||||
return {
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["isFiles", "isListing"]),
|
||||
},
|
||||
methods: {
|
||||
submit: async function (event) {
|
||||
event.preventDefault();
|
||||
if (this.new === "") return;
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri = this.isFiles ? this.$route.path + "/" : "/";
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
uri += encodeURIComponent(this.name);
|
||||
uri = uri.replace("//", "/");
|
||||
const name = ref<string>("");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
this.$router.push({ path: uri });
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (name.value === "") return;
|
||||
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
// Build the path of the new directory.
|
||||
let uri = fileStore.isFiles ? route.path + "/" : "/";
|
||||
|
||||
if (!fileStore.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(name.value);
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
router.push({ path: uri });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
$showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
layoutStore.closeHovers();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<component
|
||||
v-if="showOverlay"
|
||||
:ref="currentPromptName"
|
||||
:is="currentPromptName"
|
||||
v-bind="currentPrompt.props"
|
||||
>
|
||||
</component>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
<ModalsContainer />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { ModalsContainer, useModal } from "vue-final-modal";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import BaseModal from "./BaseModal.vue";
|
||||
import Help from "./Help.vue";
|
||||
import Info from "./Info.vue";
|
||||
import Delete from "./Delete.vue";
|
||||
import Rename from "./Rename.vue";
|
||||
import DeleteUser from "./DeleteUser.vue";
|
||||
import Download from "./Download.vue";
|
||||
import Rename from "./Rename.vue";
|
||||
import Move from "./Move.vue";
|
||||
import Copy from "./Copy.vue";
|
||||
import NewFile from "./NewFile.vue";
|
||||
@@ -24,87 +22,61 @@ import NewDir from "./NewDir.vue";
|
||||
import Replace from "./Replace.vue";
|
||||
import ReplaceRename from "./ReplaceRename.vue";
|
||||
import Share from "./Share.vue";
|
||||
import Upload from "./Upload.vue";
|
||||
import ShareDelete from "./ShareDelete.vue";
|
||||
import Sidebar from "../Sidebar.vue";
|
||||
import Upload from "./Upload.vue";
|
||||
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import buttons from "@/utils/buttons";
|
||||
|
||||
export default {
|
||||
name: "prompts",
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
Rename,
|
||||
Download,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help,
|
||||
Replace,
|
||||
ReplaceRename,
|
||||
Upload,
|
||||
ShareDelete,
|
||||
Sidebar,
|
||||
DiscardEditorChanges,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
pluginData: {
|
||||
buttons,
|
||||
store: this.$store,
|
||||
router: this.$router,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (this.currentPrompt == null) return;
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const promptName = this.currentPrompt.prompt;
|
||||
const prompt = this.$refs[promptName];
|
||||
const { currentPromptName } = storeToRefs(layoutStore);
|
||||
|
||||
if (event.code === "Escape") {
|
||||
event.stopImmediatePropagation();
|
||||
this.$store.commit("closeHovers");
|
||||
}
|
||||
const closeModal = ref<() => Promise<string>>();
|
||||
|
||||
if (event.code === "Enter") {
|
||||
switch (promptName) {
|
||||
case "delete":
|
||||
prompt.submit();
|
||||
break;
|
||||
case "copy":
|
||||
prompt.copy(event);
|
||||
break;
|
||||
case "move":
|
||||
prompt.move(event);
|
||||
break;
|
||||
case "replace":
|
||||
prompt.showConfirm(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapState(["plugins"]),
|
||||
...mapGetters(["currentPrompt", "currentPromptName"]),
|
||||
showOverlay: function () {
|
||||
return (
|
||||
this.currentPrompt !== null &&
|
||||
this.currentPrompt.prompt !== "search" &&
|
||||
this.currentPrompt.prompt !== "more"
|
||||
);
|
||||
const components = new Map<string, any>([
|
||||
["info", Info],
|
||||
["help", Help],
|
||||
["delete", Delete],
|
||||
["rename", Rename],
|
||||
["move", Move],
|
||||
["copy", Copy],
|
||||
["newFile", NewFile],
|
||||
["newDir", NewDir],
|
||||
["download", Download],
|
||||
["replace", Replace],
|
||||
["replace-rename", ReplaceRename],
|
||||
["share", Share],
|
||||
["upload", Upload],
|
||||
["share-delete", ShareDelete],
|
||||
["deleteUser", DeleteUser],
|
||||
["discardEditorChanges", DiscardEditorChanges],
|
||||
]);
|
||||
|
||||
watch(currentPromptName, (newValue) => {
|
||||
if (closeModal.value) {
|
||||
closeModal.value();
|
||||
closeModal.value = undefined;
|
||||
}
|
||||
|
||||
const modal = components.get(newValue!);
|
||||
if (!modal) return;
|
||||
|
||||
const { open, close } = useModal({
|
||||
component: BaseModal,
|
||||
slots: {
|
||||
default: modal,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetPrompts() {
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
closeModal.value = close;
|
||||
open();
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (!layoutStore.currentPrompt) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.stopImmediatePropagation();
|
||||
layoutStore.closeHovers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
>:
|
||||
</p>
|
||||
<input
|
||||
id="focus-prompt"
|
||||
class="input input--block"
|
||||
v-focus
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
>
|
||||
@@ -41,7 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import url from "@/utils/url";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
@@ -55,13 +57,20 @@ export default {
|
||||
created() {
|
||||
this.name = this.oldName();
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["req", "selected", "selectedCount"]),
|
||||
...mapGetters(["isListing"]),
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
cancel: function () {
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
},
|
||||
oldName: function () {
|
||||
if (!this.isListing) {
|
||||
@@ -96,12 +105,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit("setReload", true);
|
||||
this.reload = true;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
@@ -22,14 +23,17 @@
|
||||
@click="currentPrompt.action"
|
||||
:aria-label="$t('buttons.continue')"
|
||||
:title="$t('buttons.continue')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.continue") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--red"
|
||||
@click="currentPrompt.confirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.replace") }}
|
||||
</button>
|
||||
@@ -38,10 +42,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "replace",
|
||||
computed: mapGetters(["currentPrompt"]),
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
@@ -22,14 +23,17 @@
|
||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.rename") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--red"
|
||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.replace") }}
|
||||
</button>
|
||||
@@ -38,10 +42,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "replace-rename",
|
||||
computed: mapGetters(["currentPrompt"]),
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="card floating share__promt__card" id="share">
|
||||
<div class="card floating" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
@@ -25,9 +25,9 @@
|
||||
<td class="small">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"
|
||||
@click="copyToClipboard(buildLink(link))"
|
||||
>
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
@@ -35,9 +35,9 @@
|
||||
<td class="small" v-if="hasDownloadLink()">
|
||||
<button
|
||||
class="action copy-clipboard"
|
||||
:data-clipboard-text="buildDownloadLink(link)"
|
||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
@click="copyToClipboard(buildDownloadLink(link))"
|
||||
>
|
||||
<i class="material-icons">content_paste_go</i>
|
||||
</button>
|
||||
@@ -59,17 +59,20 @@
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--blue"
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.new')"
|
||||
:title="$t('buttons.new')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.new") }}
|
||||
</button>
|
||||
@@ -80,15 +83,22 @@
|
||||
<div class="card-content">
|
||||
<p>{{ $t("settings.shareDuration") }}</p>
|
||||
<div class="input-group input">
|
||||
<input
|
||||
v-focus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="1"
|
||||
<vue-number-input
|
||||
center
|
||||
controls
|
||||
size="small"
|
||||
:max="2147483647"
|
||||
:min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time"
|
||||
v-model="time"
|
||||
tabindex="1"
|
||||
/>
|
||||
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
|
||||
<select
|
||||
class="right"
|
||||
v-model="unit"
|
||||
:aria-label="$t('time.unit')"
|
||||
tabindex="2"
|
||||
>
|
||||
<option value="seconds">{{ $t("time.seconds") }}</option>
|
||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||
<option value="hours">{{ $t("time.hours") }}</option>
|
||||
@@ -100,6 +110,7 @@
|
||||
class="input input--block"
|
||||
type="password"
|
||||
v-model.trim="password"
|
||||
tabindex="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -109,14 +120,17 @@
|
||||
@click="() => switchListing()"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="5"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--blue"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.share')"
|
||||
:title="$t('buttons.share')"
|
||||
tabindex="4"
|
||||
>
|
||||
{{ $t("buttons.share") }}
|
||||
</button>
|
||||
@@ -126,16 +140,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { share as api, pub as pub_api } from "@/api";
|
||||
import moment from "moment/min/moment-with-locales";
|
||||
import Clipboard from "clipboard";
|
||||
import dayjs from "dayjs";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { copy } from "@/utils/clipboard";
|
||||
|
||||
export default {
|
||||
name: "share",
|
||||
data: function () {
|
||||
return {
|
||||
time: "",
|
||||
time: 0,
|
||||
unit: "hours",
|
||||
links: [],
|
||||
clip: null,
|
||||
@@ -143,9 +159,14 @@ export default {
|
||||
listing: true,
|
||||
};
|
||||
},
|
||||
inject: ["$showError", "$showSuccess"],
|
||||
computed: {
|
||||
...mapState(["req", "selected", "selectedCount"]),
|
||||
...mapGetters(["isListing"]),
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
url() {
|
||||
if (!this.isListing) {
|
||||
return this.$route.path;
|
||||
@@ -172,23 +193,24 @@ export default {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clip.destroy();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
copyToClipboard: function (text) {
|
||||
copy(text).then(
|
||||
() => {
|
||||
// clipboard successfully set
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
},
|
||||
() => {
|
||||
// clipboard write failed
|
||||
}
|
||||
);
|
||||
},
|
||||
submit: async function () {
|
||||
let isPermanent = !this.time || this.time == 0;
|
||||
|
||||
try {
|
||||
let res = null;
|
||||
|
||||
if (isPermanent) {
|
||||
if (!this.time) {
|
||||
res = await api.create(this.url, this.password);
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||
@@ -197,7 +219,7 @@ export default {
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
|
||||
this.time = "";
|
||||
this.time = 0;
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
|
||||
@@ -220,7 +242,7 @@ export default {
|
||||
}
|
||||
},
|
||||
humanTime(time) {
|
||||
return moment(time * 1000).fromNow();
|
||||
return dayjs(time * 1000).fromNow();
|
||||
},
|
||||
buildLink(share) {
|
||||
return api.getShareURL(share);
|
||||
@@ -242,7 +264,7 @@ export default {
|
||||
},
|
||||
switchListing() {
|
||||
if (this.links.length == 0 && !this.listing) {
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
}
|
||||
|
||||
this.listing = !this.listing;
|
||||
|
||||
@@ -5,18 +5,21 @@
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
@click="submit"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
@@ -25,14 +28,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "share-delete",
|
||||
computed: {
|
||||
...mapGetters(["currentPrompt"]),
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
submit: function () {
|
||||
this.currentPrompt?.confirm();
|
||||
},
|
||||
|
||||
@@ -1,38 +1,111 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.upload") }}</h2>
|
||||
<h2>{{ t("prompts.upload") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.uploadMessage") }}</p>
|
||||
<p>{{ t("prompts.uploadMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action full">
|
||||
<div @click="uploadFile" class="action">
|
||||
<div
|
||||
@click="uploadFile"
|
||||
@keypress.enter="uploadFile"
|
||||
class="action"
|
||||
id="focus-prompt"
|
||||
tabindex="1"
|
||||
>
|
||||
<i class="material-icons">insert_drive_file</i>
|
||||
<div class="title">{{ $t("buttons.file") }}</div>
|
||||
<div class="title">{{ t("buttons.file") }}</div>
|
||||
</div>
|
||||
<div @click="uploadFolder" class="action">
|
||||
<div
|
||||
@click="uploadFolder"
|
||||
@keypress.enter="uploadFolder"
|
||||
class="action"
|
||||
tabindex="2"
|
||||
>
|
||||
<i class="material-icons">folder</i>
|
||||
<div class="title">{{ $t("buttons.folder") }}</div>
|
||||
<div class="title">{{ t("buttons.folder") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "upload",
|
||||
methods: {
|
||||
uploadFile: function () {
|
||||
document.getElementById("upload-input").value = "";
|
||||
document.getElementById("upload-input").click();
|
||||
},
|
||||
uploadFolder: function () {
|
||||
document.getElementById("upload-folder-input").value = "";
|
||||
document.getElementById("upload-folder-input").click();
|
||||
},
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import * as upload from "@/utils/upload";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
// TODO: this is a copy of the same function in FileListing.vue
|
||||
const uploadInput = (event: Event) => {
|
||||
layoutStore.closeHovers();
|
||||
|
||||
let files = (event.currentTarget as HTMLInputElement)?.files;
|
||||
if (files === null) return;
|
||||
|
||||
let folder_upload = !!files[0].webkitRelativePath;
|
||||
|
||||
const uploadFiles: UploadList = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fullPath = folder_upload ? file.webkitRelativePath : undefined;
|
||||
uploadFiles.push({
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
isDir: false,
|
||||
fullPath,
|
||||
});
|
||||
}
|
||||
|
||||
let path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
let conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
||||
|
||||
if (conflict) {
|
||||
layoutStore.showHover({
|
||||
prompt: "replace",
|
||||
action: (event: Event) => {
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, false);
|
||||
},
|
||||
confirm: (event: Event) => {
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
upload.handleFiles(uploadFiles, path, true);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
upload.handleFiles(uploadFiles, path);
|
||||
};
|
||||
|
||||
const openUpload = (isFolder: boolean) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.webkitdirectory = isFolder;
|
||||
// TODO: call the function in FileListing.vue instead
|
||||
input.onchange = uploadInput;
|
||||
input.click();
|
||||
};
|
||||
|
||||
const uploadFile = () => {
|
||||
openUpload(false);
|
||||
};
|
||||
const uploadFolder = () => {
|
||||
openUpload(true);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -53,7 +53,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from "vuex";
|
||||
import { mapState, mapWritableState, mapActions } from "pinia";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { abortAllUploads } from "@/api/tus";
|
||||
import buttons from "@/utils/buttons";
|
||||
|
||||
@@ -65,19 +67,20 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
...mapState(useUploadStore, [
|
||||
"filesInUpload",
|
||||
"filesInUploadCount",
|
||||
"uploadSpeed",
|
||||
"eta",
|
||||
"getETA",
|
||||
]),
|
||||
...mapMutations(["resetUpload"]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
...mapActions(useUploadStore, ["reset"]),
|
||||
formattedETA() {
|
||||
if (!this.eta || this.eta === Infinity) {
|
||||
if (!this.getETA || this.getETA === Infinity) {
|
||||
return "--:--:--";
|
||||
}
|
||||
|
||||
let totalSeconds = this.eta;
|
||||
let totalSeconds = this.getETA;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
totalSeconds %= 3600;
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
@@ -97,8 +100,8 @@ export default {
|
||||
abortAllUploads();
|
||||
buttons.done("upload");
|
||||
this.open = false;
|
||||
this.$store.commit("resetUpload");
|
||||
this.$store.commit("setReload", true);
|
||||
this.reset();
|
||||
this.reload = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="locale">
|
||||
<select name="selectLanguage" v-on:change="change" :value="locale">
|
||||
<option v-for="(language, value) in locales" :key="value" :value="value">
|
||||
{{ $t("languages." + language) }}
|
||||
</option>
|
||||
@@ -7,40 +7,45 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default {
|
||||
name: "languages",
|
||||
props: ["locale"],
|
||||
data() {
|
||||
let dataObj = {
|
||||
locales: {
|
||||
he: "he",
|
||||
hu: "hu",
|
||||
ar: "ar",
|
||||
de: "de",
|
||||
el: "el",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
is: "is",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
"nl-be": "nlBE",
|
||||
pl: "pl",
|
||||
"pt-br": "ptBR",
|
||||
pt: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
sk: "sk",
|
||||
"sv-se": "svSE",
|
||||
tr: "tr",
|
||||
ua: "ua",
|
||||
"zh-cn": "zhCN",
|
||||
"zh-tw": "zhTW",
|
||||
},
|
||||
let dataObj = {};
|
||||
const locales = {
|
||||
he: "he",
|
||||
hu: "hu",
|
||||
ar: "ar",
|
||||
de: "de",
|
||||
el: "el",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
is: "is",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
"nl-be": "nlBE",
|
||||
pl: "pl",
|
||||
"pt-br": "ptBR",
|
||||
pt: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
sk: "sk",
|
||||
"sv-se": "svSE",
|
||||
tr: "tr",
|
||||
uk: "uk",
|
||||
"zh-cn": "zhCN",
|
||||
"zh-tw": "zhTW",
|
||||
};
|
||||
|
||||
// Vue3 reactivity breaks with this configuration
|
||||
// so we need to use markRaw as a workaround
|
||||
// https://github.com/vuejs/core/issues/3024
|
||||
Object.defineProperty(dataObj, "locales", {
|
||||
value: markRaw(locales),
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="theme">
|
||||
<option value="">{{ $t("settings.themes.light") }}</option>
|
||||
<option value="dark">{{ $t("settings.themes.dark") }}</option>
|
||||
<option value="">{{ t("settings.themes.default") }}</option>
|
||||
<option value="light">{{ t("settings.themes.light") }}</option>
|
||||
<option value="dark">{{ t("settings.themes.dark") }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "themes",
|
||||
props: ["theme"],
|
||||
methods: {
|
||||
change(event) {
|
||||
this.$emit("update:theme", event.target.value);
|
||||
},
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { SelectHTMLAttributes } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
theme: UserTheme;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(e: "update:theme", val: string | null): void;
|
||||
}>();
|
||||
|
||||
const change = (event: Event) => {
|
||||
emit("update:theme", (event.target as SelectHTMLAttributes)?.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="!isDefault">
|
||||
<label for="username">{{ $t("settings.username") }}</label>
|
||||
<p v-if="!isDefault && props.user !== null">
|
||||
<label for="username">{{ t("settings.username") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@@ -11,7 +11,7 @@
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<label for="password">{{ t("settings.password") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
type="password"
|
||||
@@ -22,9 +22,9 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="scope">{{ $t("settings.scope") }}</label>
|
||||
<label for="scope">{{ t("settings.scope") }}</label>
|
||||
<input
|
||||
:disabled="createUserDirData"
|
||||
:disabled="createUserDirData ?? false"
|
||||
:placeholder="scopePlaceholder"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@@ -34,86 +34,89 @@
|
||||
</p>
|
||||
<p class="small" v-if="displayHomeDirectoryCheckbox">
|
||||
<input type="checkbox" v-model="createUserDirData" />
|
||||
{{ $t("settings.createUserHomeDirectory") }}
|
||||
{{ t("settings.createUserHomeDirectory") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="locale">{{ $t("settings.language") }}</label>
|
||||
<label for="locale">{{ t("settings.language") }}</label>
|
||||
<languages
|
||||
class="input input--block"
|
||||
id="locale"
|
||||
:locale.sync="user.locale"
|
||||
v-model:locale="user.locale"
|
||||
></languages>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<p v-if="!isDefault && user.perm">
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="user.perm.admin"
|
||||
v-model="user.lockPassword"
|
||||
/>
|
||||
{{ $t("settings.lockPassword") }}
|
||||
{{ t("settings.lockPassword") }}
|
||||
</p>
|
||||
|
||||
<permissions :perm.sync="user.perm" />
|
||||
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
|
||||
<permissions v-model:perm="user.perm" />
|
||||
<commands v-if="enableExec" v-model:commands="user.commands" />
|
||||
|
||||
<div v-if="!isDefault">
|
||||
<h3>{{ $t("settings.rules") }}</h3>
|
||||
<p class="small">{{ $t("settings.rulesHelp") }}</p>
|
||||
<rules :rules.sync="user.rules" />
|
||||
<h3>{{ t("settings.rules") }}</h3>
|
||||
<p class="small">{{ t("settings.rulesHelp") }}</p>
|
||||
<rules v-model:rules="user.rules" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import Languages from "./Languages.vue";
|
||||
import Rules from "./Rules.vue";
|
||||
import Permissions from "./Permissions.vue";
|
||||
import Commands from "./Commands.vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "user",
|
||||
data: () => {
|
||||
return {
|
||||
createUserDirData: false,
|
||||
originalUserScope: "/",
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Permissions,
|
||||
Languages,
|
||||
Rules,
|
||||
Commands,
|
||||
},
|
||||
props: ["user", "createUserDir", "isNew", "isDefault"],
|
||||
created() {
|
||||
this.originalUserScope = this.user.scope;
|
||||
this.createUserDirData = this.createUserDir;
|
||||
},
|
||||
computed: {
|
||||
passwordPlaceholder() {
|
||||
return this.isNew ? "" : this.$t("settings.avoidChanges");
|
||||
},
|
||||
scopePlaceholder() {
|
||||
return this.createUserDir
|
||||
? this.$t("settings.userScopeGenerationPlaceholder")
|
||||
: "";
|
||||
},
|
||||
displayHomeDirectoryCheckbox() {
|
||||
return this.isNew && this.createUserDir;
|
||||
},
|
||||
isExecEnabled: () => enableExec,
|
||||
},
|
||||
watch: {
|
||||
"user.perm.admin": function () {
|
||||
if (!this.user.perm.admin) return;
|
||||
this.user.lockPassword = false;
|
||||
},
|
||||
createUserDirData() {
|
||||
this.user.scope = this.createUserDirData ? "" : this.originalUserScope;
|
||||
},
|
||||
},
|
||||
};
|
||||
const { t } = useI18n();
|
||||
|
||||
const createUserDirData = ref<boolean | null>(null);
|
||||
const originalUserScope = ref<string | null>(null);
|
||||
|
||||
const props = defineProps<{
|
||||
user: IUserForm;
|
||||
isNew: boolean;
|
||||
isDefault: boolean;
|
||||
createUserDir?: boolean;
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
if (props.user.scope) {
|
||||
originalUserScope.value = props.user.scope;
|
||||
createUserDirData.value = props.createUserDir;
|
||||
}
|
||||
});
|
||||
|
||||
const passwordPlaceholder = computed(() =>
|
||||
props.isNew ? "" : t("settings.avoidChanges")
|
||||
);
|
||||
const scopePlaceholder = computed(() =>
|
||||
createUserDirData.value ? t("settings.userScopeGenerationPlaceholder") : ""
|
||||
);
|
||||
const displayHomeDirectoryCheckbox = computed(
|
||||
() => props.isNew && createUserDirData.value
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.user,
|
||||
() => {
|
||||
if (!props.user?.perm?.admin) return;
|
||||
props.user.lockPassword = false;
|
||||
}
|
||||
);
|
||||
|
||||
watch(createUserDirData, () => {
|
||||
if (props.user?.scope) {
|
||||
props.user.scope = createUserDirData.value
|
||||
? ""
|
||||
: originalUserScope.value ?? "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user