feat(frontend): migrate Vue to Composition API
Signed-off-by: Henrique Dias <mail@hacdias.com>
This commit is contained in:
@@ -1,6 +1,3 @@
|
|||||||
<!-- 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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -44,182 +41,164 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
// We're leaving this untouched as you can read in the beginning
|
import { computed } from "vue";
|
||||||
const isNumber = function (n) {
|
|
||||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
const isNumber = (n: number | string): boolean => {
|
||||||
|
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const props = withDefaults(
|
||||||
name: "progress-bar",
|
defineProps<{
|
||||||
props: {
|
val?: number;
|
||||||
val: {
|
max?: number;
|
||||||
default: 0,
|
size?: number | string;
|
||||||
},
|
bgColor?: string;
|
||||||
max: {
|
barColor?: string;
|
||||||
default: 100,
|
barTransition?: string;
|
||||||
},
|
barBorderRadius?: number;
|
||||||
size: {
|
spacing?: number;
|
||||||
// either a number (pixel width/height) or 'tiny', 'small',
|
text?: string;
|
||||||
// 'medium', 'large', 'huge', 'massive' for common sizes
|
textAlign?: string;
|
||||||
default: 3,
|
textPosition?: string;
|
||||||
},
|
fontSize?: number;
|
||||||
"bg-color": {
|
textFgColor?: string;
|
||||||
type: String,
|
}>(),
|
||||||
default: "#eee",
|
{
|
||||||
},
|
val: 0,
|
||||||
"bar-color": {
|
max: 100,
|
||||||
type: String,
|
size: 3,
|
||||||
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
bgColor: "#eee",
|
||||||
},
|
barColor: "#2196f3",
|
||||||
"bar-transition": {
|
barTransition: "all 0.5s ease",
|
||||||
type: String,
|
barBorderRadius: 0,
|
||||||
default: "all 0.5s ease",
|
spacing: 4,
|
||||||
},
|
text: "",
|
||||||
"bar-border-radius": {
|
textAlign: "center",
|
||||||
type: Number,
|
textPosition: "bottom",
|
||||||
default: 0,
|
fontSize: 13,
|
||||||
},
|
textFgColor: "#222",
|
||||||
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() {
|
|
||||||
let 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;
|
const pct = computed(() => {
|
||||||
},
|
const pct = (props.val / props.max) * 100;
|
||||||
text_padding() {
|
const pctFixed = pct.toFixed(2);
|
||||||
switch (this.size) {
|
return Math.min(parseFloat(pctFixed), props.max);
|
||||||
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;
|
const size_px = computed(() => {
|
||||||
},
|
switch (props.size) {
|
||||||
text_font_size() {
|
case "tiny":
|
||||||
switch (this.size) {
|
return 2;
|
||||||
case "tiny":
|
case "small":
|
||||||
case "small":
|
return 4;
|
||||||
case "medium":
|
case "medium":
|
||||||
case "large":
|
return 8;
|
||||||
case "big":
|
case "large":
|
||||||
case "huge":
|
return 12;
|
||||||
case "massive":
|
case "big":
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
return 16;
|
||||||
}
|
case "huge":
|
||||||
|
return 32;
|
||||||
|
case "massive":
|
||||||
|
return 64;
|
||||||
|
}
|
||||||
|
|
||||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
return isNumber(props.size) ? (props.size as number) : 32;
|
||||||
},
|
});
|
||||||
progress_style() {
|
|
||||||
const style = {
|
|
||||||
background: this.bgColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
const text_padding = computed(() => {
|
||||||
style["position"] = "relative";
|
switch (props.size) {
|
||||||
style["min-height"] = this.size_px + "px";
|
case "tiny":
|
||||||
style["z-index"] = "-2";
|
case "small":
|
||||||
}
|
case "medium":
|
||||||
|
case "large":
|
||||||
|
case "big":
|
||||||
|
case "huge":
|
||||||
|
case "massive":
|
||||||
|
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
return isNumber(props.spacing) ? props.spacing : 4;
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
const text_font_size = computed(() => {
|
||||||
},
|
switch (props.size) {
|
||||||
bar_style() {
|
case "tiny":
|
||||||
const style = {
|
case "small":
|
||||||
background: this.barColor,
|
case "medium":
|
||||||
width: this.pct + "%",
|
case "large":
|
||||||
height: this.size_px + "px",
|
case "big":
|
||||||
transition: this.barTransition,
|
case "huge":
|
||||||
};
|
case "massive":
|
||||||
|
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
return isNumber(props.fontSize) ? props.fontSize : 13;
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
const progress_style = computed(() => {
|
||||||
style["position"] = "absolute";
|
const style: Record<string, string> = {
|
||||||
style["top"] = "0";
|
background: props.bgColor,
|
||||||
style["height"] = "100%";
|
};
|
||||||
((style["min-height"] = this.size_px + "px"),
|
|
||||||
(style["z-index"] = "-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
},
|
style["position"] = "relative";
|
||||||
text_style() {
|
style["min-height"] = size_px.value + "px";
|
||||||
const style = {
|
style["z-index"] = "-2";
|
||||||
color: this.textFgColor,
|
}
|
||||||
"font-size": this.text_font_size + "px",
|
|
||||||
"text-align": this.textAlign,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
if (props.barBorderRadius > 0) {
|
||||||
this.textPosition == "top" ||
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
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;
|
return style;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
const bar_style = computed(() => {
|
||||||
|
const style: Record<string, string> = {
|
||||||
|
background: props.barColor,
|
||||||
|
width: pct.value + "%",
|
||||||
|
height: size_px.value + "px",
|
||||||
|
transition: props.barTransition,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.barBorderRadius > 0) {
|
||||||
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
|
style["position"] = "absolute";
|
||||||
|
style["top"] = "0";
|
||||||
|
style["height"] = "100%";
|
||||||
|
style["min-height"] = size_px.value + "px";
|
||||||
|
style["z-index"] = "-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
const text_style = computed(() => {
|
||||||
|
const style: Record<string, string> = {
|
||||||
|
color: props.textFgColor,
|
||||||
|
"font-size": text_font_size.value + "px",
|
||||||
|
"text-align": props.textAlign,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.textPosition == "top" ||
|
||||||
|
props.textPosition == "middle" ||
|
||||||
|
props.textPosition == "inside"
|
||||||
|
)
|
||||||
|
style["padding-bottom"] = text_padding.value + "px";
|
||||||
|
if (
|
||||||
|
props.textPosition == "bottom" ||
|
||||||
|
props.textPosition == "middle" ||
|
||||||
|
props.textPosition == "inside"
|
||||||
|
)
|
||||||
|
style["padding-top"] = text_padding.value + "px";
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div
|
<div
|
||||||
class="shell"
|
class="shell"
|
||||||
:class="{ ['shell--hidden']: !showShell }"
|
:class="{ ['shell--hidden']: !showShell }"
|
||||||
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
|
:style="{ height: `${shellHeight}em`, direction: 'ltr' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@pointerdown="startDrag()"
|
@pointerdown="startDrag()"
|
||||||
@pointerup="stopDrag()"
|
@pointerup="stopDrag()"
|
||||||
class="shell__divider"
|
class="shell__divider"
|
||||||
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
|
:style="shellDrag ? { background: `${checkTheme()}` } : ''"
|
||||||
></div>
|
></div>
|
||||||
<div @click="focus" class="shell__content" ref="scrollable">
|
<div @click="focus" class="shell__content" ref="scrollable">
|
||||||
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||||
@@ -39,13 +39,15 @@
|
|||||||
<div
|
<div
|
||||||
@pointerup="stopDrag()"
|
@pointerup="stopDrag()"
|
||||||
class="shell__overlay"
|
class="shell__overlay"
|
||||||
v-show="this.shellDrag"
|
v-show="shellDrag"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapActions } from "pinia";
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
@@ -53,142 +55,164 @@ import { commands } from "@/api";
|
|||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash-es";
|
||||||
import { theme } from "@/utils/constants";
|
import { theme } from "@/utils/constants";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "shell",
|
|
||||||
computed: {
|
|
||||||
...mapState(useLayoutStore, ["showShell"]),
|
|
||||||
...mapState(useFileStore, ["isFiles"]),
|
|
||||||
path: function () {
|
|
||||||
if (this.isFiles) {
|
|
||||||
return this.$route.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
const fileStore = useFileStore();
|
||||||
},
|
const layoutStore = useLayoutStore();
|
||||||
},
|
|
||||||
data: () => ({
|
|
||||||
content: [],
|
|
||||||
history: [],
|
|
||||||
historyPos: 0,
|
|
||||||
canInput: true,
|
|
||||||
shellDrag: false,
|
|
||||||
shellHeight: 25,
|
|
||||||
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
|
||||||
}),
|
|
||||||
mounted() {
|
|
||||||
window.addEventListener("resize", this.resize);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
window.removeEventListener("resize", this.resize);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
|
||||||
checkTheme() {
|
|
||||||
if (theme == "dark") {
|
|
||||||
return "rgba(255, 255, 255, 0.4)";
|
|
||||||
}
|
|
||||||
return "rgba(127, 127, 127, 0.4)";
|
|
||||||
},
|
|
||||||
startDrag() {
|
|
||||||
document.addEventListener("pointermove", this.handleDrag);
|
|
||||||
this.shellDrag = true;
|
|
||||||
},
|
|
||||||
stopDrag() {
|
|
||||||
document.removeEventListener("pointermove", this.handleDrag);
|
|
||||||
this.shellDrag = false;
|
|
||||||
},
|
|
||||||
handleDrag: throttle(function (event) {
|
|
||||||
const top = window.innerHeight / this.fontsize - 4;
|
|
||||||
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
|
|
||||||
const bottom =
|
|
||||||
2.25 +
|
|
||||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
|
||||||
|
|
||||||
if (userPos <= top && userPos >= bottom) {
|
const { showShell } = storeToRefs(layoutStore);
|
||||||
this.shellHeight = userPos.toFixed(2);
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
}
|
const { toggleShell } = layoutStore;
|
||||||
}, 32),
|
|
||||||
resize: throttle(function () {
|
|
||||||
const top = window.innerHeight / this.fontsize - 4;
|
|
||||||
const bottom =
|
|
||||||
2.25 +
|
|
||||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
|
||||||
|
|
||||||
if (this.shellHeight > top) {
|
const scrollable = ref<HTMLElement | null>(null);
|
||||||
this.shellHeight = top;
|
const input = ref<HTMLElement | null>(null);
|
||||||
} else if (this.shellHeight < bottom) {
|
|
||||||
this.shellHeight = bottom;
|
|
||||||
}
|
|
||||||
}, 32),
|
|
||||||
scroll: function () {
|
|
||||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
|
||||||
},
|
|
||||||
focus: function () {
|
|
||||||
this.$refs.input.focus();
|
|
||||||
},
|
|
||||||
historyUp() {
|
|
||||||
if (this.historyPos > 0) {
|
|
||||||
this.$refs.input.innerText = this.history[--this.historyPos];
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
historyDown() {
|
|
||||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
|
||||||
this.$refs.input.innerText = this.history[++this.historyPos];
|
|
||||||
this.focus();
|
|
||||||
} else {
|
|
||||||
this.historyPos = this.history.length;
|
|
||||||
this.$refs.input.innerText = "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submit: function (event) {
|
|
||||||
const cmd = event.target.innerText.trim();
|
|
||||||
|
|
||||||
if (cmd === "") {
|
const content = ref<Array<{ text: string }>>([]);
|
||||||
return;
|
const history = ref<string[]>([]);
|
||||||
}
|
const historyPos = ref(0);
|
||||||
|
const canInput = ref(true);
|
||||||
|
const shellDrag = ref(false);
|
||||||
|
const shellHeight = ref(25);
|
||||||
|
const fontsize = ref(
|
||||||
|
parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
);
|
||||||
|
|
||||||
if (cmd === "clear") {
|
const path = computed(() => {
|
||||||
this.content = [];
|
if (isFiles.value) {
|
||||||
event.target.innerHTML = "";
|
return route.path;
|
||||||
return;
|
}
|
||||||
}
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
if (cmd === "exit") {
|
const checkTheme = () => {
|
||||||
event.target.innerHTML = "";
|
if (theme == "dark") {
|
||||||
this.toggleShell();
|
return "rgba(255, 255, 255, 0.4)";
|
||||||
return;
|
}
|
||||||
}
|
return "rgba(127, 127, 127, 0.4)";
|
||||||
|
|
||||||
this.canInput = false;
|
|
||||||
event.target.innerHTML = "";
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
text: `${cmd}\n\n`,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.history.push(cmd);
|
|
||||||
this.historyPos = this.history.length;
|
|
||||||
this.content.push(results);
|
|
||||||
|
|
||||||
commands(
|
|
||||||
this.path,
|
|
||||||
cmd,
|
|
||||||
(event) => {
|
|
||||||
results.text += `${event.data}\n`;
|
|
||||||
this.scroll();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
results.text = results.text
|
|
||||||
|
|
||||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
|
||||||
.trimEnd();
|
|
||||||
this.canInput = true;
|
|
||||||
this.$refs.input.focus();
|
|
||||||
this.scroll();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scroll = () => {
|
||||||
|
if (scrollable.value) {
|
||||||
|
scrollable.value.scrollTop = scrollable.value.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
input.value?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = throttle((event: PointerEvent) => {
|
||||||
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
|
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
|
||||||
|
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||||
|
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||||
|
|
||||||
|
if (userPos <= top && userPos >= bottom) {
|
||||||
|
shellHeight.value = parseFloat(userPos.toFixed(2));
|
||||||
|
}
|
||||||
|
}, 32);
|
||||||
|
|
||||||
|
const resize = throttle(() => {
|
||||||
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
|
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||||
|
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||||
|
|
||||||
|
if (shellHeight.value > top) {
|
||||||
|
shellHeight.value = top;
|
||||||
|
} else if (shellHeight.value < bottom) {
|
||||||
|
shellHeight.value = bottom;
|
||||||
|
}
|
||||||
|
}, 32);
|
||||||
|
|
||||||
|
const startDrag = () => {
|
||||||
|
document.addEventListener("pointermove", handleDrag as any);
|
||||||
|
shellDrag.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
document.removeEventListener("pointermove", handleDrag as any);
|
||||||
|
shellDrag.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const historyUp = () => {
|
||||||
|
if (historyPos.value > 0 && input.value) {
|
||||||
|
historyPos.value--;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const historyDown = () => {
|
||||||
|
if (
|
||||||
|
historyPos.value >= 0 &&
|
||||||
|
historyPos.value < history.value.length - 1 &&
|
||||||
|
input.value
|
||||||
|
) {
|
||||||
|
historyPos.value++;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
|
} else {
|
||||||
|
historyPos.value = history.value.length;
|
||||||
|
if (input.value) {
|
||||||
|
input.value.innerText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const cmd = target.innerText.trim();
|
||||||
|
|
||||||
|
if (cmd === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "clear") {
|
||||||
|
content.value = [];
|
||||||
|
target.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "exit") {
|
||||||
|
target.innerHTML = "";
|
||||||
|
toggleShell();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canInput.value = false;
|
||||||
|
target.innerHTML = "";
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
text: `${cmd}\n\n`,
|
||||||
|
};
|
||||||
|
|
||||||
|
history.value.push(cmd);
|
||||||
|
historyPos.value = history.value.length;
|
||||||
|
content.value.push(results);
|
||||||
|
|
||||||
|
commands(
|
||||||
|
path.value,
|
||||||
|
cmd,
|
||||||
|
(event: MessageEvent) => {
|
||||||
|
results.text += `${event.data}\n`;
|
||||||
|
scroll();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
results.text = results.text
|
||||||
|
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||||
|
.trimEnd();
|
||||||
|
canInput.value = true;
|
||||||
|
input.value?.focus();
|
||||||
|
scroll();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<button @click="toAccountSettings" class="action">
|
<button @click="toAccountSettings" class="action">
|
||||||
<i class="material-icons">person</i>
|
<i class="material-icons">person</i>
|
||||||
<span>{{ user.username }}</span>
|
<span>{{ user?.username }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="user.perm.create">
|
<div v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
@click="showHover('newDir')"
|
@click="showHover('newDir')"
|
||||||
class="action"
|
class="action"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="user.perm.admin">
|
<div v-if="user?.perm.admin">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toGlobalSettings"
|
@click="toGlobalSettings"
|
||||||
@@ -113,9 +113,10 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { reactive } from "vue";
|
import { reactive, ref, computed, watch, onUnmounted } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
@@ -135,84 +136,85 @@ import prettyBytes from "pretty-bytes";
|
|||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "sidebar",
|
const router = useRouter();
|
||||||
setup() {
|
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
const authStore = useAuthStore();
|
||||||
return { usage, usageAbortController: new AbortController() };
|
const fileStore = useFileStore();
|
||||||
},
|
const layoutStore = useLayoutStore();
|
||||||
components: {
|
|
||||||
ProgressBar,
|
const { user, isLoggedIn } = storeToRefs(authStore);
|
||||||
},
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
inject: ["$showError"],
|
const { currentPromptName } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers, showHover } = layoutStore;
|
||||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
|
||||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
const usageAbortController = ref(new AbortController());
|
||||||
active() {
|
|
||||||
return this.currentPromptName === "sidebar";
|
const active = computed(() => {
|
||||||
},
|
return currentPromptName.value === "sidebar";
|
||||||
signup: () => signup,
|
});
|
||||||
version: () => version,
|
|
||||||
disableExternal: () => disableExternal,
|
const canLogout = !noAuth && loginPage;
|
||||||
disableUsedPercentage: () => disableUsedPercentage,
|
|
||||||
canLogout: () => !noAuth && loginPage,
|
const abortOngoingFetchUsage = () => {
|
||||||
},
|
usageAbortController.value.abort();
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
|
||||||
abortOngoingFetchUsage() {
|
|
||||||
this.usageAbortController.abort();
|
|
||||||
},
|
|
||||||
async fetchUsage() {
|
|
||||||
const 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 {
|
|
||||||
this.abortOngoingFetchUsage();
|
|
||||||
this.usageAbortController = new AbortController();
|
|
||||||
const usage = await api.usage(path, this.usageAbortController.signal);
|
|
||||||
usageStats = {
|
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
return Object.assign(this.usage, usageStats);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toRoot() {
|
|
||||||
this.$router.push({ path: "/files" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
toAccountSettings() {
|
|
||||||
this.$router.push({ path: "/settings/profile" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
toGlobalSettings() {
|
|
||||||
this.$router.push({ path: "/settings/global" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
help() {
|
|
||||||
this.showHover("help");
|
|
||||||
},
|
|
||||||
logout: auth.logout,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route: {
|
|
||||||
handler(to) {
|
|
||||||
if (to.path.includes("/files")) {
|
|
||||||
this.fetchUsage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.abortOngoingFetchUsage();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchUsage = async () => {
|
||||||
|
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||||
|
let usageStats = USAGE_DEFAULT;
|
||||||
|
if (disableUsedPercentage) {
|
||||||
|
return Object.assign(usage, usageStats);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
abortOngoingFetchUsage();
|
||||||
|
usageAbortController.value = new AbortController();
|
||||||
|
const usageData = await api.usage(path, usageAbortController.value.signal);
|
||||||
|
usageStats = {
|
||||||
|
used: prettyBytes(usageData.used, { binary: true }),
|
||||||
|
total: prettyBytes(usageData.total, { binary: true }),
|
||||||
|
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
return Object.assign(usage, usageStats);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toRoot = () => {
|
||||||
|
router.push({ path: "/files" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAccountSettings = () => {
|
||||||
|
router.push({ path: "/settings/profile" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toGlobalSettings = () => {
|
||||||
|
router.push({ path: "/settings/global" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const help = () => {
|
||||||
|
showHover("help");
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
auth.logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
|
if (newPath.includes("/files")) {
|
||||||
|
fetchUsage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortOngoingFetchUsage();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||||
<file-list
|
<file-list
|
||||||
ref="fileList"
|
ref="fileList"
|
||||||
@update:selected="(val) => (dest = val)"
|
@update:selected="(val: string) => (dest = val)"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
class="card-action"
|
class="card-action"
|
||||||
style="display: flex; align-items: center; justify-content: space-between"
|
style="display: flex; align-items: center; justify-content: space-between"
|
||||||
>
|
>
|
||||||
<template v-if="user.perm.create">
|
<template v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="$refs.fileList.createDir()"
|
@click="fileList?.createDir()"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
style="justify-self: left"
|
style="justify-self: left"
|
||||||
@@ -53,8 +53,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { ref, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
@@ -64,90 +66,84 @@ import buttons from "@/utils/buttons";
|
|||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "copy",
|
const router = useRouter();
|
||||||
components: { FileList },
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
current: window.location.pathname,
|
|
||||||
dest: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
copy: async function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
// Create a new promise for each file.
|
const fileStore = useFileStore();
|
||||||
for (const item of this.selected) {
|
const layoutStore = useLayoutStore();
|
||||||
items.push({
|
const authStore = useAuthStore();
|
||||||
from: this.req.items[item].url,
|
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
|
||||||
name: this.req.items[item].name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
buttons.loading("copy");
|
const { user } = storeToRefs(authStore);
|
||||||
|
const { showHover, closeHovers } = layoutStore;
|
||||||
|
|
||||||
await api
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
.copy(items, overwrite, rename)
|
const dest = ref<string | null>(null);
|
||||||
.then(() => {
|
|
||||||
buttons.success("copy");
|
|
||||||
this.preselect = removePrefix(items[0].to);
|
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
const copy = async (event: Event) => {
|
||||||
this.reload = true;
|
event.preventDefault();
|
||||||
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
return;
|
// Create a new promise for each file.
|
||||||
}
|
for (const item of selected.value) {
|
||||||
|
items.push({
|
||||||
|
from: req.value!.items[item].url,
|
||||||
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
|
name: req.value!.items[item].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.$router.push({ path: this.dest });
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
})
|
buttons.loading("copy");
|
||||||
.catch((e) => {
|
|
||||||
buttons.done("copy");
|
|
||||||
this.$showError(e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
await api
|
||||||
this.closeHovers();
|
.copy(items, overwrite, rename)
|
||||||
action(false, true);
|
.then(() => {
|
||||||
|
buttons.success("copy");
|
||||||
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
|
|
||||||
return;
|
if (route.path === dest.value) {
|
||||||
}
|
fileStore.reload = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
router.push({ path: dest.value! });
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
buttons.done("copy");
|
||||||
|
$showError?.(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let overwrite = false;
|
if (route.path === dest.value) {
|
||||||
let rename = false;
|
closeHovers();
|
||||||
|
action(false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (conflict) {
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
this.showHover({
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
prompt: "replace-rename",
|
|
||||||
confirm: (event, option) => {
|
|
||||||
overwrite = option == "overwrite";
|
|
||||||
rename = option == "rename";
|
|
||||||
|
|
||||||
event.preventDefault();
|
let overwrite = false;
|
||||||
this.closeHovers();
|
let rename = false;
|
||||||
action(overwrite, rename);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
if (conflict) {
|
||||||
}
|
showHover({
|
||||||
|
prompt: "replace-rename",
|
||||||
|
confirm: (event: Event, option: string) => {
|
||||||
|
overwrite = option == "overwrite";
|
||||||
|
rename = option == "rename";
|
||||||
|
|
||||||
action(overwrite, rename);
|
event.preventDefault();
|
||||||
},
|
closeHovers();
|
||||||
},
|
action(overwrite, rename);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p v-if="!this.isListing || selectedCount === 1">
|
<p v-if="!isListing || selectedCount === 1">
|
||||||
{{ $t("prompts.deleteMessageSingle") }}
|
{{ $t("prompts.deleteMessageSingle") }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
@@ -32,67 +32,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "delete",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, [
|
|
||||||
"isListing",
|
|
||||||
"selectedCount",
|
|
||||||
"req",
|
|
||||||
"selected",
|
|
||||||
]),
|
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
submit: async function () {
|
|
||||||
buttons.loading("delete");
|
|
||||||
|
|
||||||
try {
|
const fileStore = useFileStore();
|
||||||
if (!this.isListing) {
|
const layoutStore = useLayoutStore();
|
||||||
await api.remove(this.$route.path);
|
|
||||||
buttons.success("delete");
|
|
||||||
|
|
||||||
this.currentPrompt?.confirm();
|
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
|
||||||
this.closeHovers();
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
return;
|
const { closeHovers } = layoutStore;
|
||||||
}
|
|
||||||
|
|
||||||
this.closeHovers();
|
const submit = async () => {
|
||||||
|
buttons.loading("delete");
|
||||||
|
|
||||||
if (this.selectedCount === 0) {
|
try {
|
||||||
return;
|
if (!isListing.value) {
|
||||||
}
|
await api.remove(route.path);
|
||||||
|
buttons.success("delete");
|
||||||
|
|
||||||
const promises = [];
|
currentPrompt.value?.confirm();
|
||||||
for (const index of this.selected) {
|
closeHovers();
|
||||||
promises.push(api.remove(this.req.items[index].url));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
closeHovers();
|
||||||
buttons.success("delete");
|
|
||||||
|
|
||||||
const nearbyItem =
|
if (selectedCount.value === 0) {
|
||||||
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.preselect = nearbyItem?.path;
|
const promises = [];
|
||||||
|
for (const index of selected.value) {
|
||||||
|
promises.push(api.remove(req.value!.items[index].url));
|
||||||
|
}
|
||||||
|
|
||||||
this.reload = true;
|
await Promise.all(promises);
|
||||||
} catch (e) {
|
buttons.success("delete");
|
||||||
buttons.done("delete");
|
|
||||||
this.$showError(e);
|
const nearbyItem =
|
||||||
if (this.isListing) this.reload = true;
|
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
|
||||||
}
|
|
||||||
},
|
fileStore.preselect = nearbyItem?.path;
|
||||||
},
|
|
||||||
|
fileStore.reload = true;
|
||||||
|
} catch (e) {
|
||||||
|
buttons.done("delete");
|
||||||
|
$showError?.(e);
|
||||||
|
if (isListing.value) fileStore.reload = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
|
|||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// const emit = defineEmits<{
|
|
||||||
// (e: "confirm"): void;
|
|
||||||
// }>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="currentPrompt.saveAction"
|
@click="currentPrompt?.saveAction"
|
||||||
:aria-label="$t('buttons.saveChanges')"
|
:aria-label="$t('buttons.saveChanges')"
|
||||||
:title="$t('buttons.saveChanges')"
|
:title="$t('buttons.saveChanges')"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
@click="currentPrompt.confirm"
|
@click="currentPrompt?.confirm"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.discardChanges')"
|
:aria-label="$t('buttons.discardChanges')"
|
||||||
:title="$t('buttons.discardChanges')"
|
:title="$t('buttons.discardChanges')"
|
||||||
@@ -38,17 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { mapActions, mapState } from "pinia";
|
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "discardEditorChanges",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,8 +24,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapActions } from "pinia";
|
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
@@ -34,147 +36,162 @@ import url from "@/utils/url";
|
|||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
import { StatusError } from "@/api/utils.js";
|
import { StatusError } from "@/api/utils.js";
|
||||||
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
name: "file-list",
|
exclude?: string[];
|
||||||
props: {
|
}>();
|
||||||
exclude: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
items: [],
|
|
||||||
touches: {
|
|
||||||
id: "",
|
|
||||||
count: 0,
|
|
||||||
},
|
|
||||||
selected: null,
|
|
||||||
current: window.location.pathname,
|
|
||||||
nextAbortController: new AbortController(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
...mapState(useFileStore, ["req"]),
|
|
||||||
nav() {
|
|
||||||
return decodeURIComponent(this.current);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fillOptions(this.req);
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.abortOngoingNext();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["showHover"]),
|
|
||||||
abortOngoingNext() {
|
|
||||||
this.nextAbortController.abort();
|
|
||||||
},
|
|
||||||
fillOptions(req) {
|
|
||||||
// Sets the current path and resets
|
|
||||||
// the current items.
|
|
||||||
this.current = req.url;
|
|
||||||
this.items = [];
|
|
||||||
|
|
||||||
this.$emit("update:selected", this.current);
|
const emit = defineEmits<{
|
||||||
|
"update:selected": [value: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
// If the path isn't the root path,
|
const route = useRoute();
|
||||||
// show a button to navigate to the previous
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
// directory.
|
|
||||||
if (req.url !== "/files/") {
|
|
||||||
this.items.push({
|
|
||||||
name: "..",
|
|
||||||
url: url.removeLastDir(req.url) + "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this folder is empty, finish here.
|
const authStore = useAuthStore();
|
||||||
if (req.items === null) return;
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
// Otherwise we add every directory to the
|
const { user } = storeToRefs(authStore);
|
||||||
// move options.
|
const { req } = storeToRefs(fileStore);
|
||||||
for (const item of req.items) {
|
const { showHover } = layoutStore;
|
||||||
if (!item.isDir) continue;
|
|
||||||
if (this.exclude?.includes(item.url)) continue;
|
|
||||||
|
|
||||||
this.items.push({
|
const items = ref<Array<{ name: string; url: string }>>([]);
|
||||||
name: item.name,
|
const touches = ref({
|
||||||
url: item.url,
|
id: "",
|
||||||
});
|
count: 0,
|
||||||
}
|
});
|
||||||
},
|
const selected = ref<string | null>(null);
|
||||||
next: function (event) {
|
const current = ref(window.location.pathname);
|
||||||
// Retrieves the URL of the directory the user
|
const nextAbortController = ref(new AbortController());
|
||||||
// just clicked in and fill the options with its
|
|
||||||
// content.
|
|
||||||
const uri = event.currentTarget.dataset.url;
|
|
||||||
this.abortOngoingNext();
|
|
||||||
this.nextAbortController = new AbortController();
|
|
||||||
files
|
|
||||||
.fetch(uri, this.nextAbortController.signal)
|
|
||||||
.then(this.fillOptions)
|
|
||||||
.catch((e) => {
|
|
||||||
if (e instanceof StatusError && e.is_canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$showError(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
touchstart(event) {
|
|
||||||
const url = event.currentTarget.dataset.url;
|
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
const nav = computed(() => {
|
||||||
setTimeout(() => {
|
return decodeURIComponent(current.value);
|
||||||
this.touches.count = 0;
|
});
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// If the element the user is touching
|
const abortOngoingNext = () => {
|
||||||
// is different from the last one he touched,
|
nextAbortController.value.abort();
|
||||||
// reset the count.
|
|
||||||
if (this.touches.id !== url) {
|
|
||||||
this.touches.id = url;
|
|
||||||
this.touches.count = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.touches.count++;
|
|
||||||
|
|
||||||
// If there is more than one touch already,
|
|
||||||
// open the next screen.
|
|
||||||
if (this.touches.count > 1) {
|
|
||||||
this.next(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemClick: function (event) {
|
|
||||||
if (this.user.singleClick) this.next(event);
|
|
||||||
else this.select(event);
|
|
||||||
},
|
|
||||||
select: function (event) {
|
|
||||||
// If the element is already selected, unselect it.
|
|
||||||
if (this.selected === event.currentTarget.dataset.url) {
|
|
||||||
this.selected = null;
|
|
||||||
this.$emit("update:selected", this.current);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise select the element.
|
|
||||||
this.selected = event.currentTarget.dataset.url;
|
|
||||||
this.$emit("update:selected", this.selected);
|
|
||||||
},
|
|
||||||
createDir: async function () {
|
|
||||||
this.showHover({
|
|
||||||
prompt: "newDir",
|
|
||||||
action: null,
|
|
||||||
confirm: null,
|
|
||||||
props: {
|
|
||||||
redirect: false,
|
|
||||||
base: this.current === this.$route.path ? null : this.current,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fillOptions = (reqData: any) => {
|
||||||
|
// Sets the current path and resets
|
||||||
|
// the current items.
|
||||||
|
current.value = reqData.url;
|
||||||
|
items.value = [];
|
||||||
|
|
||||||
|
emit("update:selected", current.value);
|
||||||
|
|
||||||
|
// If the path isn't the root path,
|
||||||
|
// show a button to navigate to the previous
|
||||||
|
// directory.
|
||||||
|
if (reqData.url !== "/files/") {
|
||||||
|
items.value.push({
|
||||||
|
name: "..",
|
||||||
|
url: url.removeLastDir(reqData.url) + "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this folder is empty, finish here.
|
||||||
|
if (reqData.items === null) return;
|
||||||
|
|
||||||
|
// Otherwise we add every directory to the
|
||||||
|
// move options.
|
||||||
|
for (const item of reqData.items) {
|
||||||
|
if (!item.isDir) continue;
|
||||||
|
if (props.exclude?.includes(item.url)) continue;
|
||||||
|
|
||||||
|
items.value.push({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = (event: Event) => {
|
||||||
|
// Retrieves the URL of the directory the user
|
||||||
|
// just clicked in and fill the options with its
|
||||||
|
// content.
|
||||||
|
const uri = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
|
abortOngoingNext();
|
||||||
|
nextAbortController.value = new AbortController();
|
||||||
|
files
|
||||||
|
.fetch(uri, nextAbortController.value.signal)
|
||||||
|
.then(fillOptions)
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof StatusError && e.is_canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$showError?.(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const touchstart = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
|
|
||||||
|
// In 300 milliseconds, we shall reset the count.
|
||||||
|
setTimeout(() => {
|
||||||
|
touches.value.count = 0;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// If the element the user is touching
|
||||||
|
// is different from the last one he touched,
|
||||||
|
// reset the count.
|
||||||
|
if (touches.value.id !== urlValue) {
|
||||||
|
touches.value.id = urlValue;
|
||||||
|
touches.value.count = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
touches.value.count++;
|
||||||
|
|
||||||
|
// If there is more than one touch already,
|
||||||
|
// open the next screen.
|
||||||
|
if (touches.value.count > 1) {
|
||||||
|
next(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemClick = (event: Event) => {
|
||||||
|
if (user.value?.singleClick) next(event);
|
||||||
|
else select(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const select = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
|
// If the element is already selected, unselect it.
|
||||||
|
if (selected.value === urlValue) {
|
||||||
|
selected.value = null;
|
||||||
|
emit("update:selected", current.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise select the element.
|
||||||
|
selected.value = urlValue;
|
||||||
|
emit("update:selected", selected.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDir = async () => {
|
||||||
|
showHover({
|
||||||
|
prompt: "newDir",
|
||||||
|
action: undefined,
|
||||||
|
confirm: undefined,
|
||||||
|
props: {
|
||||||
|
redirect: false,
|
||||||
|
base: current.value === route.path ? null : current.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (req.value) {
|
||||||
|
fillOptions(req.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortOngoingNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
createDir,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,14 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const { closeHovers } = useLayoutStore();
|
||||||
name: "help",
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
<template v-if="dir && selected.length === 0">
|
<template v-if="dir && selected.length === 0">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
|
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
|
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -99,98 +99,100 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { computed, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { filesize } from "@/utils";
|
import { filesize } from "@/utils";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "info",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, [
|
|
||||||
"req",
|
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
humanSize: function () {
|
|
||||||
if (this.selectedCount === 0 || !this.isListing) {
|
|
||||||
return filesize(this.req.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sum = 0;
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
for (const selected of this.selected) {
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
sum += this.req.items[selected].size;
|
const { closeHovers } = layoutStore;
|
||||||
}
|
|
||||||
|
|
||||||
return filesize(sum);
|
const humanSize = computed(() => {
|
||||||
},
|
if (selectedCount.value === 0 || !isListing.value) {
|
||||||
humanTime: function () {
|
return filesize(req.value?.size ?? 0);
|
||||||
if (this.selectedCount === 0) {
|
}
|
||||||
return dayjs(this.req.modified).fromNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
let sum = 0;
|
||||||
},
|
|
||||||
modTime: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
|
||||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(
|
for (const selectedIdx of selected.value) {
|
||||||
Date.parse(this.req.items[this.selected[0]].modified)
|
sum += req.value?.items[selectedIdx]?.size ?? 0;
|
||||||
).toLocaleString();
|
}
|
||||||
},
|
|
||||||
name: function () {
|
|
||||||
return this.selectedCount === 0
|
|
||||||
? this.req.name
|
|
||||||
: this.req.items[this.selected[0]].name;
|
|
||||||
},
|
|
||||||
dir: function () {
|
|
||||||
return (
|
|
||||||
this.selectedCount > 1 ||
|
|
||||||
(this.selectedCount === 0
|
|
||||||
? this.req.isDir
|
|
||||||
: this.req.items[this.selected[0]].isDir)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
resolution: function () {
|
|
||||||
if (this.selectedCount === 1) {
|
|
||||||
const selectedItem = this.req.items[this.selected[0]];
|
|
||||||
if (selectedItem && selectedItem.type === "image") {
|
|
||||||
return selectedItem.resolution;
|
|
||||||
}
|
|
||||||
} else if (this.req && this.req.type === "image") {
|
|
||||||
return this.req.resolution;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
checksum: async function (event, algo) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let link;
|
return filesize(sum);
|
||||||
|
});
|
||||||
|
|
||||||
if (this.selectedCount) {
|
const humanTime = computed(() => {
|
||||||
link = this.req.items[this.selected[0]].url;
|
if (selectedCount.value === 0) {
|
||||||
} else {
|
return dayjs(req.value?.modified).fromNow();
|
||||||
link = this.$route.path;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
|
||||||
const hash = await api.checksum(link, algo);
|
});
|
||||||
event.target.textContent = hash;
|
|
||||||
} catch (e) {
|
const modTime = computed(() => {
|
||||||
this.$showError(e);
|
if (selectedCount.value === 0) {
|
||||||
}
|
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
|
||||||
},
|
}
|
||||||
},
|
|
||||||
|
return new Date(
|
||||||
|
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
|
||||||
|
).toLocaleString();
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = computed(() => {
|
||||||
|
return selectedCount.value === 0
|
||||||
|
? (req.value?.name ?? "")
|
||||||
|
: (req.value?.items[selected.value[0]]?.name ?? "");
|
||||||
|
});
|
||||||
|
|
||||||
|
const dir = computed(() => {
|
||||||
|
return (
|
||||||
|
selectedCount.value > 1 ||
|
||||||
|
(selectedCount.value === 0
|
||||||
|
? (req.value?.isDir ?? false)
|
||||||
|
: (req.value?.items[selected.value[0]]?.isDir ?? false))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolution = computed(() => {
|
||||||
|
if (selectedCount.value === 1) {
|
||||||
|
const selectedItem = req.value?.items[selected.value[0]];
|
||||||
|
if (selectedItem && selectedItem.type === "image") {
|
||||||
|
return (selectedItem as any).resolution;
|
||||||
|
}
|
||||||
|
} else if (req.value && req.value.type === "image") {
|
||||||
|
return (req.value as any).resolution;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const checksum = async (event: Event, algo: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let link;
|
||||||
|
|
||||||
|
if (selectedCount.value) {
|
||||||
|
link = req.value?.items[selected.value[0]]?.url ?? "";
|
||||||
|
} else {
|
||||||
|
link = route.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hash = await api.checksum(link, algo as any);
|
||||||
|
(event.target as HTMLElement).textContent = hash;
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<file-list
|
<file-list
|
||||||
ref="fileList"
|
ref="fileList"
|
||||||
@update:selected="(val) => (dest = val)"
|
@update:selected="(val: string) => (dest = val)"
|
||||||
:exclude="excludedFolders"
|
:exclude="excludedFolders"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
class="card-action"
|
class="card-action"
|
||||||
style="display: flex; align-items: center; justify-content: space-between"
|
style="display: flex; align-items: center; justify-content: space-between"
|
||||||
>
|
>
|
||||||
<template v-if="user.perm.create">
|
<template v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="$refs.fileList.createDir()"
|
@click="fileList?.createDir()"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
style="justify-self: left"
|
style="justify-self: left"
|
||||||
@@ -54,8 +54,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { ref, computed, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
@@ -65,80 +67,76 @@ import buttons from "@/utils/buttons";
|
|||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const router = useRouter();
|
||||||
name: "move",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
components: { FileList },
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
current: window.location.pathname,
|
|
||||||
dest: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
...mapWritableState(useFileStore, ["preselect"]),
|
|
||||||
excludedFolders() {
|
|
||||||
return this.selected
|
|
||||||
.filter((idx) => this.req.items[idx].isDir)
|
|
||||||
.map((idx) => this.req.items[idx].url);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
move: async function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
for (const item of this.selected) {
|
const fileStore = useFileStore();
|
||||||
items.push({
|
const layoutStore = useLayoutStore();
|
||||||
from: this.req.items[item].url,
|
const authStore = useAuthStore();
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
|
||||||
name: this.req.items[item].name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
buttons.loading("move");
|
const { user } = storeToRefs(authStore);
|
||||||
|
const { showHover, closeHovers } = layoutStore;
|
||||||
|
|
||||||
await api
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
.move(items, overwrite, rename)
|
const dest = ref<string | null>(null);
|
||||||
.then(() => {
|
|
||||||
buttons.success("move");
|
|
||||||
this.preselect = removePrefix(items[0].to);
|
|
||||||
this.$router.push({ path: this.dest });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
buttons.done("move");
|
|
||||||
this.$showError(e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const excludedFolders = computed(() => {
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
return selected.value
|
||||||
|
.filter((idx) => req.value!.items[idx].isDir)
|
||||||
|
.map((idx) => req.value!.items[idx].url);
|
||||||
|
});
|
||||||
|
|
||||||
let overwrite = false;
|
const move = async (event: Event) => {
|
||||||
let rename = false;
|
event.preventDefault();
|
||||||
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
if (conflict) {
|
for (const item of selected.value) {
|
||||||
this.showHover({
|
items.push({
|
||||||
prompt: "replace-rename",
|
from: req.value!.items[item].url,
|
||||||
confirm: (event, option) => {
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
overwrite = option == "overwrite";
|
name: req.value!.items[item].name,
|
||||||
rename = option == "rename";
|
});
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
this.closeHovers();
|
buttons.loading("move");
|
||||||
action(overwrite, rename);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
await api
|
||||||
}
|
.move(items, overwrite, rename)
|
||||||
|
.then(() => {
|
||||||
|
buttons.success("move");
|
||||||
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
|
router.push({ path: dest.value! });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
buttons.done("move");
|
||||||
|
$showError?.(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
action(overwrite, rename);
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
},
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
},
|
|
||||||
|
let overwrite = false;
|
||||||
|
let rename = false;
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
showHover({
|
||||||
|
prompt: "replace-rename",
|
||||||
|
confirm: (event: Event, option: string) => {
|
||||||
|
overwrite = option == "overwrite";
|
||||||
|
rename = option == "rename";
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
closeHovers();
|
||||||
|
action(overwrite, rename);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,80 +40,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { ref, onMounted, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const router = useRouter();
|
||||||
name: "rename",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
name: "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.name = this.oldName();
|
|
||||||
},
|
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, [
|
|
||||||
"req",
|
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
cancel: function () {
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
oldName: function () {
|
|
||||||
if (!this.isListing) {
|
|
||||||
return this.req.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
const fileStore = useFileStore();
|
||||||
// This shouldn't happen.
|
const layoutStore = useLayoutStore();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].name;
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
},
|
const { closeHovers } = layoutStore;
|
||||||
submit: async function () {
|
|
||||||
let oldLink = "";
|
|
||||||
let newLink = "";
|
|
||||||
|
|
||||||
if (!this.isListing) {
|
const name = ref("");
|
||||||
oldLink = this.req.url;
|
|
||||||
} else {
|
|
||||||
oldLink = this.req.items[this.selected[0]].url;
|
|
||||||
}
|
|
||||||
|
|
||||||
newLink =
|
const oldName = (): string => {
|
||||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
if (!isListing.value) {
|
||||||
|
return req.value?.name ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
await api.move([{ from: oldLink, to: newLink }]);
|
// This shouldn't happen.
|
||||||
if (!this.isListing) {
|
return "";
|
||||||
this.$router.push({ path: newLink });
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preselect = removePrefix(newLink);
|
return req.value?.items[selected.value[0]].name ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
this.reload = true;
|
onMounted(() => {
|
||||||
} catch (e) {
|
name.value = oldName();
|
||||||
this.$showError(e);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.closeHovers();
|
const submit = async () => {
|
||||||
},
|
let oldLink = "";
|
||||||
},
|
let newLink = "";
|
||||||
|
|
||||||
|
if (!req.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isListing.value) {
|
||||||
|
oldLink = req.value.url;
|
||||||
|
} else {
|
||||||
|
oldLink = req.value.items[selected.value[0]].url;
|
||||||
|
}
|
||||||
|
|
||||||
|
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.move([{ from: oldLink, to: newLink }]);
|
||||||
|
if (!isListing.value) {
|
||||||
|
router.push({ path: newLink });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStore.preselect = removePrefix(newLink);
|
||||||
|
|
||||||
|
fileStore.reload = true;
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHovers();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="currentPrompt.action"
|
@click="currentPrompt?.action"
|
||||||
:aria-label="$t('buttons.continue')"
|
:aria-label="$t('buttons.continue')"
|
||||||
:title="$t('buttons.continue')"
|
:title="$t('buttons.continue')"
|
||||||
tabindex="2"
|
tabindex="2"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="currentPrompt.confirm"
|
@click="currentPrompt?.confirm"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
@@ -41,17 +41,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "replace",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
@click="(event) => currentPrompt?.confirm(event, 'rename')"
|
||||||
:aria-label="$t('buttons.rename')"
|
:aria-label="$t('buttons.rename')"
|
||||||
:title="$t('buttons.rename')"
|
:title="$t('buttons.rename')"
|
||||||
tabindex="2"
|
tabindex="2"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
@click="(event) => currentPrompt?.confirm(event, 'overwrite')"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
@@ -41,17 +41,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "replace-rename",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -129,138 +129,146 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { ref, computed, inject, onBeforeMount } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { share as api } from "@/api";
|
import { share as api } from "@/api";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { copy } from "@/utils/clipboard";
|
import { copy } from "@/utils/clipboard";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "share",
|
const { t } = useI18n();
|
||||||
data: function () {
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
return {
|
const $showSuccess = inject<(message: string) => void>("$showSuccess");
|
||||||
time: 0,
|
|
||||||
unit: "hours",
|
|
||||||
links: [],
|
|
||||||
clip: null,
|
|
||||||
password: "",
|
|
||||||
listing: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
inject: ["$showError", "$showSuccess"],
|
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, [
|
|
||||||
"req",
|
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
url() {
|
|
||||||
if (!this.isListing) {
|
|
||||||
return this.$route.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
const fileStore = useFileStore();
|
||||||
// This shouldn't happen.
|
const layoutStore = useLayoutStore();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].url;
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
|
const { closeHovers } = layoutStore;
|
||||||
|
|
||||||
|
const time = ref(0);
|
||||||
|
const unit = ref("hours");
|
||||||
|
const links = ref<any[]>([]);
|
||||||
|
const password = ref("");
|
||||||
|
const listing = ref(true);
|
||||||
|
|
||||||
|
const url = computed(() => {
|
||||||
|
if (!isListing.value) {
|
||||||
|
return route.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
|
// This shouldn't happen.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.value?.items[selected.value[0]].url ?? "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
copy({ text }).then(
|
||||||
|
() => {
|
||||||
|
// clipboard successfully set
|
||||||
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
},
|
() => {
|
||||||
async beforeMount() {
|
// clipboard write failed
|
||||||
try {
|
copy({ text }, { permission: true }).then(
|
||||||
const links = await api.get(this.url);
|
|
||||||
this.links = links;
|
|
||||||
this.sort();
|
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
|
||||||
this.listing = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
copyToClipboard: function (text) {
|
|
||||||
copy({ text }).then(
|
|
||||||
() => {
|
() => {
|
||||||
// clipboard successfully set
|
// clipboard successfully set
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
() => {
|
(e) => {
|
||||||
// clipboard write failed
|
// clipboard write failed
|
||||||
copy({ text }, { permission: true }).then(
|
$showError?.(e);
|
||||||
() => {
|
|
||||||
// clipboard successfully set
|
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
// clipboard write failed
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
submit: async function () {
|
);
|
||||||
try {
|
|
||||||
let res = null;
|
|
||||||
|
|
||||||
if (!this.time) {
|
|
||||||
res = await api.create(this.url, this.password);
|
|
||||||
} else {
|
|
||||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.links.push(res);
|
|
||||||
this.sort();
|
|
||||||
|
|
||||||
this.time = 0;
|
|
||||||
this.unit = "hours";
|
|
||||||
this.password = "";
|
|
||||||
|
|
||||||
this.listing = true;
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteLink: async function (event, link) {
|
|
||||||
event.preventDefault();
|
|
||||||
try {
|
|
||||||
await api.remove(link.hash);
|
|
||||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
|
||||||
this.listing = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
humanTime(time) {
|
|
||||||
return dayjs(time * 1000).fromNow();
|
|
||||||
},
|
|
||||||
buildLink(share) {
|
|
||||||
return api.getShareURL(share);
|
|
||||||
},
|
|
||||||
sort() {
|
|
||||||
this.links = this.links.sort((a, b) => {
|
|
||||||
if (a.expire === 0) return -1;
|
|
||||||
if (b.expire === 0) return 1;
|
|
||||||
return new Date(a.expire) - new Date(b.expire);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
switchListing() {
|
|
||||||
if (this.links.length == 0 && !this.listing) {
|
|
||||||
this.closeHovers();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listing = !this.listing;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
let res = null;
|
||||||
|
|
||||||
|
if (!time.value) {
|
||||||
|
res = await api.create(url.value, password.value);
|
||||||
|
} else {
|
||||||
|
res = await api.create(
|
||||||
|
url.value,
|
||||||
|
password.value,
|
||||||
|
String(time.value),
|
||||||
|
unit.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
links.value.push(res);
|
||||||
|
sort();
|
||||||
|
|
||||||
|
time.value = 0;
|
||||||
|
unit.value = "hours";
|
||||||
|
password.value = "";
|
||||||
|
|
||||||
|
listing.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLink = async (event: Event, link: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
await api.remove(link.hash);
|
||||||
|
links.value = links.value.filter((item) => item.hash !== link.hash);
|
||||||
|
|
||||||
|
if (links.value.length == 0) {
|
||||||
|
listing.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const humanTime = (time: number) => {
|
||||||
|
return dayjs(time * 1000).fromNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLink = (share: any) => {
|
||||||
|
return api.getShareURL(share);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sort = () => {
|
||||||
|
links.value = links.value.sort((a, b) => {
|
||||||
|
if (a.expire === 0) return -1;
|
||||||
|
if (b.expire === 0) return 1;
|
||||||
|
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchListing = () => {
|
||||||
|
if (links.value.length == 0 && !listing.value) {
|
||||||
|
closeHovers();
|
||||||
|
}
|
||||||
|
|
||||||
|
listing.value = !listing.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
try {
|
||||||
|
const fetchedLinks = await api.get(url.value);
|
||||||
|
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
|
||||||
|
sort();
|
||||||
|
|
||||||
|
if (links.value.length == 0) {
|
||||||
|
listing.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,20 +27,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "share-delete",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
const submit = () => {
|
||||||
methods: {
|
currentPrompt.value?.confirm();
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
submit: function () {
|
|
||||||
this.currentPrompt?.confirm();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,23 +8,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
name: "permissions",
|
|
||||||
props: ["commands"],
|
const props = defineProps<{
|
||||||
computed: {
|
commands: string[];
|
||||||
raw: {
|
}>();
|
||||||
get() {
|
|
||||||
return this.commands.join(" ");
|
const emit = defineEmits<{
|
||||||
},
|
"update:commands": [commands: string[]];
|
||||||
set(value) {
|
}>();
|
||||||
if (value !== "") {
|
|
||||||
this.$emit("update:commands", value.split(" "));
|
const raw = computed({
|
||||||
} else {
|
get() {
|
||||||
this.$emit("update:commands", []);
|
return props.commands.join(" ");
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
set(value: string) {
|
||||||
|
if (value !== "") {
|
||||||
|
emit("update:commands", value.split(" "));
|
||||||
|
} else {
|
||||||
|
emit("update:commands", []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,61 +6,50 @@
|
|||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default {
|
defineProps<{
|
||||||
name: "languages",
|
locale: string;
|
||||||
props: ["locale"],
|
}>();
|
||||||
data() {
|
|
||||||
const dataObj = {};
|
|
||||||
const locales = {
|
|
||||||
he: "עברית",
|
|
||||||
hr: "Hrvatski",
|
|
||||||
hu: "Magyar",
|
|
||||||
ar: "العربية",
|
|
||||||
ca: "Català",
|
|
||||||
cs: "Čeština",
|
|
||||||
de: "Deutsch",
|
|
||||||
el: "Ελληνικά",
|
|
||||||
en: "English",
|
|
||||||
es: "Español",
|
|
||||||
fr: "Français",
|
|
||||||
is: "Icelandic",
|
|
||||||
it: "Italiano",
|
|
||||||
ja: "日本語",
|
|
||||||
ko: "한국어",
|
|
||||||
"nl-be": "Dutch (Belgium)",
|
|
||||||
no: "Norsk",
|
|
||||||
pl: "Polski",
|
|
||||||
"pt-br": "Português",
|
|
||||||
pt: "Português (Brasil)",
|
|
||||||
ro: "Romanian",
|
|
||||||
ru: "Русский",
|
|
||||||
sk: "Slovenčina",
|
|
||||||
"sv-se": "Swedish (Sweden)",
|
|
||||||
tr: "Türkçe",
|
|
||||||
uk: "Українська",
|
|
||||||
vi: "Tiếng Việt",
|
|
||||||
"zh-cn": "中文 (简体)",
|
|
||||||
"zh-tw": "中文 (繁體)",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Vue3 reactivity breaks with this configuration
|
const emit = defineEmits<{
|
||||||
// so we need to use markRaw as a workaround
|
"update:locale": [locale: string];
|
||||||
// https://github.com/vuejs/core/issues/3024
|
}>();
|
||||||
Object.defineProperty(dataObj, "locales", {
|
|
||||||
value: markRaw(locales),
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return dataObj;
|
const locales = markRaw({
|
||||||
},
|
he: "עברית",
|
||||||
methods: {
|
hr: "Hrvatski",
|
||||||
change(event) {
|
hu: "Magyar",
|
||||||
this.$emit("update:locale", event.target.value);
|
ar: "العربية",
|
||||||
},
|
ca: "Català",
|
||||||
},
|
cs: "Čeština",
|
||||||
|
de: "Deutsch",
|
||||||
|
el: "Ελληνικά",
|
||||||
|
en: "English",
|
||||||
|
es: "Español",
|
||||||
|
fr: "Français",
|
||||||
|
is: "Icelandic",
|
||||||
|
it: "Italiano",
|
||||||
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
|
"nl-be": "Dutch (Belgium)",
|
||||||
|
no: "Norsk",
|
||||||
|
pl: "Polski",
|
||||||
|
"pt-br": "Português",
|
||||||
|
pt: "Português (Brasil)",
|
||||||
|
ro: "Romanian",
|
||||||
|
ru: "Русский",
|
||||||
|
sk: "Slovenčina",
|
||||||
|
"sv-se": "Swedish (Sweden)",
|
||||||
|
tr: "Türkçe",
|
||||||
|
uk: "Українська",
|
||||||
|
vi: "Tiếng Việt",
|
||||||
|
"zh-cn": "中文 (简体)",
|
||||||
|
"zh-tw": "中文 (繁體)",
|
||||||
|
});
|
||||||
|
|
||||||
|
const change = (event: Event) => {
|
||||||
|
emit("update:locale", (event.target as HTMLSelectElement).value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,27 +39,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
export default {
|
|
||||||
name: "permissions",
|
|
||||||
props: ["perm"],
|
|
||||||
computed: {
|
|
||||||
admin: {
|
|
||||||
get() {
|
|
||||||
return this.perm.admin;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
if (value) {
|
|
||||||
for (const key in this.perm) {
|
|
||||||
this.perm[key] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.perm.admin = value;
|
const props = defineProps<{
|
||||||
},
|
perm: UserPermissions;
|
||||||
},
|
}>();
|
||||||
isExecEnabled: () => enableExec,
|
|
||||||
|
const admin = computed({
|
||||||
|
get() {
|
||||||
|
return props.perm.admin;
|
||||||
},
|
},
|
||||||
};
|
set(value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
for (const key in props.perm) {
|
||||||
|
props.perm[key as keyof UserPermissions] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.perm.admin = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isExecEnabled = enableExec;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -32,32 +32,44 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
interface Rule {
|
||||||
name: "rules-textarea",
|
allow: boolean;
|
||||||
props: ["rules"],
|
path: string;
|
||||||
methods: {
|
regex: boolean;
|
||||||
remove(event, index) {
|
regexp: {
|
||||||
event.preventDefault();
|
raw: string;
|
||||||
const rules = [...this.rules];
|
};
|
||||||
rules.splice(index, 1);
|
}
|
||||||
this.$emit("update:rules", [...rules]);
|
|
||||||
},
|
|
||||||
create(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
this.$emit("update:rules", [
|
const props = defineProps<{
|
||||||
...this.rules,
|
rules: Rule[];
|
||||||
{
|
}>();
|
||||||
allow: true,
|
|
||||||
path: "",
|
const emit = defineEmits<{
|
||||||
regex: false,
|
"update:rules": [rules: Rule[]];
|
||||||
regexp: {
|
}>();
|
||||||
raw: "",
|
|
||||||
},
|
const remove = (event: Event, index: number) => {
|
||||||
},
|
event.preventDefault();
|
||||||
]);
|
const rules = [...props.rules];
|
||||||
|
rules.splice(index, 1);
|
||||||
|
emit("update:rules", [...rules]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
emit("update:rules", [
|
||||||
|
...props.rules,
|
||||||
|
{
|
||||||
|
allow: true,
|
||||||
|
path: "",
|
||||||
|
regex: false,
|
||||||
|
regexp: {
|
||||||
|
raw: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -80,12 +80,20 @@ const { t } = useI18n();
|
|||||||
const createUserDirData = ref<boolean | null>(null);
|
const createUserDirData = ref<boolean | null>(null);
|
||||||
const originalUserScope = ref<string | null>(null);
|
const originalUserScope = ref<string | null>(null);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<
|
||||||
user: IUserForm;
|
| {
|
||||||
isNew: boolean;
|
user: IUserForm;
|
||||||
isDefault: boolean;
|
isNew: boolean;
|
||||||
createUserDir?: boolean;
|
isDefault: false;
|
||||||
}>();
|
createUserDir?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
user: SettingsDefaults;
|
||||||
|
isNew: boolean;
|
||||||
|
isDefault: true;
|
||||||
|
createUserDir?: boolean;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.user.scope) {
|
if (props.user.scope) {
|
||||||
@@ -108,6 +116,7 @@ watch(
|
|||||||
() => props.user,
|
() => props.user,
|
||||||
() => {
|
() => {
|
||||||
if (!props.user?.perm?.admin) return;
|
if (!props.user?.perm?.admin) return;
|
||||||
|
if (props.isDefault) return;
|
||||||
props.user.lockPassword = false;
|
props.user.lockPassword = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
2
frontend/src/types/settings.d.ts
vendored
2
frontend/src/types/settings.d.ts
vendored
@@ -17,7 +17,7 @@ interface SettingsDefaults {
|
|||||||
viewMode: ViewModeType;
|
viewMode: ViewModeType;
|
||||||
singleClick: boolean;
|
singleClick: boolean;
|
||||||
sorting: Sorting;
|
sorting: Sorting;
|
||||||
perm: Permissions;
|
perm: UserPermissions;
|
||||||
commands: any[];
|
commands: any[];
|
||||||
hideDotfiles: boolean;
|
hideDotfiles: boolean;
|
||||||
dateFormat: boolean;
|
dateFormat: boolean;
|
||||||
|
|||||||
26
frontend/src/types/user.d.ts
vendored
26
frontend/src/types/user.d.ts
vendored
@@ -4,7 +4,7 @@ interface IUser {
|
|||||||
password: string;
|
password: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
perm: Permissions;
|
perm: UserPermissions;
|
||||||
commands: string[];
|
commands: string[];
|
||||||
rules: IRule[];
|
rules: IRule[];
|
||||||
lockPassword: boolean;
|
lockPassword: boolean;
|
||||||
@@ -20,20 +20,20 @@ type ViewModeType = "list" | "mosaic" | "mosaic gallery";
|
|||||||
|
|
||||||
interface IUserForm {
|
interface IUserForm {
|
||||||
id?: number;
|
id?: number;
|
||||||
username?: string;
|
username: string;
|
||||||
password?: string;
|
password: string;
|
||||||
scope?: string;
|
scope: string;
|
||||||
locale?: string;
|
locale: string;
|
||||||
perm?: Permissions;
|
perm: UserPermissions;
|
||||||
commands?: string[];
|
commands: string[];
|
||||||
rules?: IRule[];
|
rules: IRule[];
|
||||||
lockPassword?: boolean;
|
lockPassword: boolean;
|
||||||
hideDotfiles?: boolean;
|
hideDotfiles: boolean;
|
||||||
singleClick?: boolean;
|
singleClick: boolean;
|
||||||
dateFormat?: boolean;
|
dateFormat: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Permissions {
|
interface UserPermissions {
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
copy: boolean;
|
copy: boolean;
|
||||||
create: boolean;
|
create: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user