Compare commits

...

1 Commits

Author SHA1 Message Date
Henrique Dias
83492a4dfb feat(frontend): migrate Vue to Composition API
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:23:20 +01:00
23 changed files with 1178 additions and 1181 deletions

View File

@@ -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,75 +41,54 @@ 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,
}, const pct = computed(() => {
text: { const pct = (props.val / props.max) * 100;
type: String, const pctFixed = pct.toFixed(2);
default: "", return Math.min(parseFloat(pctFixed), props.max);
}, });
"text-align": {
type: String, const size_px = computed(() => {
default: "center", // 'left', 'right' switch (props.size) {
},
"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": case "tiny":
return 2; return 2;
case "small": case "small":
@@ -129,10 +105,11 @@ export default {
return 64; return 64;
} }
return isNumber(this.size) ? this.size : 32; return isNumber(props.size) ? (props.size as number) : 32;
}, });
text_padding() {
switch (this.size) { const text_padding = computed(() => {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -140,13 +117,14 @@ export default {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12); return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
} }
return isNumber(this.spacing) ? this.spacing : 4; return isNumber(props.spacing) ? props.spacing : 4;
}, });
text_font_size() {
switch (this.size) { const text_font_size = computed(() => {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -154,72 +132,73 @@ export default {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32); return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
} }
return isNumber(this.fontSize) ? this.fontSize : 13; return isNumber(props.fontSize) ? props.fontSize : 13;
}, });
progress_style() {
const style = { const progress_style = computed(() => {
background: this.bgColor, const style: Record<string, string> = {
background: props.bgColor,
}; };
if (this.textPosition == "middle" || this.textPosition == "inside") { if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "relative"; style["position"] = "relative";
style["min-height"] = this.size_px + "px"; style["min-height"] = size_px.value + "px";
style["z-index"] = "-2"; style["z-index"] = "-2";
} }
if (this.barBorderRadius > 0) { if (props.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px"; style["border-radius"] = props.barBorderRadius + "px";
} }
return style; return style;
}, });
bar_style() {
const style = { const bar_style = computed(() => {
background: this.barColor, const style: Record<string, string> = {
width: this.pct + "%", background: props.barColor,
height: this.size_px + "px", width: pct.value + "%",
transition: this.barTransition, height: size_px.value + "px",
transition: props.barTransition,
}; };
if (this.barBorderRadius > 0) { if (props.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px"; style["border-radius"] = props.barBorderRadius + "px";
} }
if (this.textPosition == "middle" || this.textPosition == "inside") { if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute"; style["position"] = "absolute";
style["top"] = "0"; style["top"] = "0";
style["height"] = "100%"; style["height"] = "100%";
((style["min-height"] = this.size_px + "px"), style["min-height"] = size_px.value + "px";
(style["z-index"] = "-1")); style["z-index"] = "-1";
} }
return style; return style;
}, });
text_style() {
const style = { const text_style = computed(() => {
color: this.textFgColor, const style: Record<string, string> = {
"font-size": this.text_font_size + "px", color: props.textFgColor,
"text-align": this.textAlign, "font-size": text_font_size.value + "px",
"text-align": props.textAlign,
}; };
if ( if (
this.textPosition == "top" || props.textPosition == "top" ||
this.textPosition == "middle" || props.textPosition == "middle" ||
this.textPosition == "inside" props.textPosition == "inside"
) )
style["padding-bottom"] = this.text_padding + "px"; style["padding-bottom"] = text_padding.value + "px";
if ( if (
this.textPosition == "bottom" || props.textPosition == "bottom" ||
this.textPosition == "middle" || props.textPosition == "middle" ||
this.textPosition == "inside" props.textPosition == "inside"
) )
style["padding-top"] = this.text_padding + "px"; style["padding-top"] = text_padding.value + "px";
return style; return style;
}, });
},
};
</script> </script>

View File

@@ -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;
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { showShell } = storeToRefs(layoutStore);
const { isFiles } = storeToRefs(fileStore);
const { toggleShell } = layoutStore;
const scrollable = ref<HTMLElement | null>(null);
const input = ref<HTMLElement | null>(null);
const content = ref<Array<{ text: string }>>([]);
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)
);
const path = computed(() => {
if (isFiles.value) {
return route.path;
}
return ""; return "";
}, });
},
data: () => ({ const checkTheme = () => {
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") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";
} }
return "rgba(127, 127, 127, 0.4)"; return "rgba(127, 127, 127, 0.4)";
}, };
startDrag() {
document.addEventListener("pointermove", this.handleDrag); const scroll = () => {
this.shellDrag = true; if (scrollable.value) {
}, scrollable.value.scrollTop = scrollable.value.scrollHeight;
stopDrag() { }
document.removeEventListener("pointermove", this.handleDrag); };
this.shellDrag = false;
}, const focus = () => {
handleDrag: throttle(function (event) { input.value?.focus();
const top = window.innerHeight / this.fontsize - 4; };
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom = const handleDrag = throttle((event: PointerEvent) => {
2.25 + const top = window.innerHeight / fontsize.value - 4;
document.querySelector(".shell__divider").offsetHeight / this.fontsize; 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) { if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2); shellHeight.value = parseFloat(userPos.toFixed(2));
} }
}, 32), }, 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 resize = throttle(() => {
this.shellHeight = top; const top = window.innerHeight / fontsize.value - 4;
} else if (this.shellHeight < bottom) { const divider = document.querySelector(".shell__divider") as HTMLElement;
this.shellHeight = bottom; 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), }, 32);
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight; const startDrag = () => {
}, document.addEventListener("pointermove", handleDrag as any);
focus: function () { shellDrag.value = true;
this.$refs.input.focus(); };
},
historyUp() { const stopDrag = () => {
if (this.historyPos > 0) { document.removeEventListener("pointermove", handleDrag as any);
this.$refs.input.innerText = this.history[--this.historyPos]; shellDrag.value = false;
this.focus(); };
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
} }
}, };
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) { const historyDown = () => {
this.$refs.input.innerText = this.history[++this.historyPos]; if (
this.focus(); historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else { } else {
this.historyPos = this.history.length; historyPos.value = history.value.length;
this.$refs.input.innerText = ""; if (input.value) {
input.value.innerText = "";
} }
}, }
submit: function (event) { };
const cmd = event.target.innerText.trim();
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") { if (cmd === "") {
return; return;
} }
if (cmd === "clear") { if (cmd === "clear") {
this.content = []; content.value = [];
event.target.innerHTML = ""; target.innerHTML = "";
return; return;
} }
if (cmd === "exit") { if (cmd === "exit") {
event.target.innerHTML = ""; target.innerHTML = "";
this.toggleShell(); toggleShell();
return; return;
} }
this.canInput = false; canInput.value = false;
event.target.innerHTML = ""; target.innerHTML = "";
const results = { const results = {
text: `${cmd}\n\n`, text: `${cmd}\n\n`,
}; };
this.history.push(cmd); history.value.push(cmd);
this.historyPos = this.history.length; historyPos.value = history.value.length;
this.content.push(results); content.value.push(results);
commands( commands(
this.path, path.value,
cmd, cmd,
(event) => { (event: MessageEvent) => {
results.text += `${event.data}\n`; results.text += `${event.data}\n`;
this.scroll(); scroll();
}, },
() => { () => {
results.text = results.text results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now .replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd(); .trimEnd();
this.canInput = true; canInput.value = true;
this.$refs.input.focus(); input.value?.focus();
this.scroll(); scroll();
} }
); );
},
},
}; };
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script> </script>

View File

@@ -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 authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user, isLoggedIn } = storeToRefs(authStore);
const { isFiles } = storeToRefs(fileStore);
const { currentPromptName } = storeToRefs(layoutStore);
const { closeHovers, showHover } = layoutStore;
const usage = reactive(USAGE_DEFAULT); const usage = reactive(USAGE_DEFAULT);
return { usage, usageAbortController: new AbortController() }; const usageAbortController = ref(new AbortController());
},
components: { const active = computed(() => {
ProgressBar, return currentPromptName.value === "sidebar";
}, });
inject: ["$showError"],
computed: { const canLogout = !noAuth && loginPage;
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]), const abortOngoingFetchUsage = () => {
...mapState(useLayoutStore, ["currentPromptName"]), usageAbortController.value.abort();
active() { };
return this.currentPromptName === "sidebar";
}, const fetchUsage = async () => {
signup: () => signup, const path = route.path.endsWith("/") ? route.path : route.path + "/";
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
},
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; let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) { if (disableUsedPercentage) {
return Object.assign(this.usage, usageStats); return Object.assign(usage, usageStats);
} }
try { try {
this.abortOngoingFetchUsage(); abortOngoingFetchUsage();
this.usageAbortController = new AbortController(); usageAbortController.value = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal); const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = { usageStats = {
used: prettyBytes(usage.used, { binary: true }), used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }), total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100), usedPercentage: Math.round((usageData.used / usageData.total) * 100),
}; };
} finally { } finally {
return Object.assign(this.usage, usageStats); return Object.assign(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 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>

View File

@@ -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,81 +66,77 @@ 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 { const fileStore = useFileStore();
current: window.location.pathname, const layoutStore = useLayoutStore();
dest: null, const authStore = useAuthStore();
};
}, const { req, selected } = storeToRefs(fileStore);
inject: ["$showError"], const { user } = storeToRefs(authStore);
computed: { const { showHover, closeHovers } = layoutStore;
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]), const fileList = ref<InstanceType<typeof FileList> | null>(null);
...mapWritableState(useFileStore, ["reload", "preselect"]), const dest = ref<string | null>(null);
},
methods: { const copy = async (event: Event) => {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault(); event.preventDefault();
const items = []; const items: Array<{ from: string; to: string; name: string }> = [];
// Create a new promise for each file. // Create a new promise for each file.
for (const item of this.selected) { for (const item of selected.value) {
items.push({ items.push({
from: this.req.items[item].url, from: req.value!.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: this.req.items[item].name, name: req.value!.items[item].name,
}); });
} }
const action = async (overwrite, rename) => { const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("copy"); buttons.loading("copy");
await api await api
.copy(items, overwrite, rename) .copy(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("copy"); buttons.success("copy");
this.preselect = removePrefix(items[0].to); fileStore.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) {
this.reload = true;
if (route.path === dest.value) {
fileStore.reload = true;
return; return;
} }
this.$router.push({ path: this.dest }); router.push({ path: dest.value! });
}) })
.catch((e) => { .catch((e) => {
buttons.done("copy"); buttons.done("copy");
this.$showError(e); $showError?.(e);
}); });
}; };
if (this.$route.path === this.dest) { if (route.path === dest.value) {
this.closeHovers(); closeHovers();
action(false, true); action(false, true);
return; return;
} }
const dstItems = (await api.fetch(this.dest)).items; const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items, dstItems); const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: string) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -147,7 +145,5 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
},
},
}; };
</script> </script>

View File

@@ -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: { const fileStore = useFileStore();
...mapState(useFileStore, [ const layoutStore = useLayoutStore();
"isListing",
"selectedCount", const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
"req", const { currentPrompt } = storeToRefs(layoutStore);
"selected", const { closeHovers } = layoutStore;
]),
...mapState(useLayoutStore, ["currentPrompt"]), const submit = async () => {
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
try { try {
if (!this.isListing) { if (!isListing.value) {
await api.remove(this.$route.path); await api.remove(route.path);
buttons.success("delete"); buttons.success("delete");
this.currentPrompt?.confirm(); currentPrompt.value?.confirm();
this.closeHovers(); closeHovers();
return; return;
} }
this.closeHovers(); closeHovers();
if (this.selectedCount === 0) { if (selectedCount.value === 0) {
return; return;
} }
const promises = []; const promises = [];
for (const index of this.selected) { for (const index of selected.value) {
promises.push(api.remove(this.req.items[index].url)); promises.push(api.remove(req.value!.items[index].url));
} }
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
const nearbyItem = const nearbyItem =
this.req.items[Math.max(0, Math.min(this.selected) - 1)]; req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
this.preselect = nearbyItem?.path; fileStore.preselect = nearbyItem?.path;
this.reload = true; fileStore.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); $showError?.(e);
if (this.isListing) this.reload = true; if (isListing.value) fileStore.reload = true;
} }
},
},
}; };
</script> </script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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, const emit = defineEmits<{
default: () => [], "update:selected": [value: string];
}, }>();
},
data: function () { const route = useRoute();
return { const $showError = inject<(error: unknown) => void>("$showError");
items: [],
touches: { const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user } = storeToRefs(authStore);
const { req } = storeToRefs(fileStore);
const { showHover } = layoutStore;
const items = ref<Array<{ name: string; url: string }>>([]);
const touches = ref({
id: "", id: "",
count: 0, count: 0,
}, });
selected: null, const selected = ref<string | null>(null);
current: window.location.pathname, const current = ref(window.location.pathname);
nextAbortController: new AbortController(), const nextAbortController = ref(new AbortController());
const nav = computed(() => {
return decodeURIComponent(current.value);
});
const abortOngoingNext = () => {
nextAbortController.value.abort();
}; };
},
inject: ["$showError"], const fillOptions = (reqData: any) => {
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 // Sets the current path and resets
// the current items. // the current items.
this.current = req.url; current.value = reqData.url;
this.items = []; items.value = [];
this.$emit("update:selected", this.current); emit("update:selected", current.value);
// If the path isn't the root path, // If the path isn't the root path,
// show a button to navigate to the previous // show a button to navigate to the previous
// directory. // directory.
if (req.url !== "/files/") { if (reqData.url !== "/files/") {
this.items.push({ items.value.push({
name: "..", name: "..",
url: url.removeLastDir(req.url) + "/", url: url.removeLastDir(reqData.url) + "/",
}); });
} }
// If this folder is empty, finish here. // If this folder is empty, finish here.
if (req.items === null) return; if (reqData.items === null) return;
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (const item of req.items) { for (const item of reqData.items) {
if (!item.isDir) continue; if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue; if (props.exclude?.includes(item.url)) continue;
this.items.push({ items.value.push({
name: item.name, name: item.name,
url: item.url, url: item.url,
}); });
} }
}, };
next: function (event) {
const next = (event: Event) => {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
const uri = event.currentTarget.dataset.url; const uri = (event.currentTarget as HTMLElement).dataset.url!;
this.abortOngoingNext(); abortOngoingNext();
this.nextAbortController = new AbortController(); nextAbortController.value = new AbortController();
files files
.fetch(uri, this.nextAbortController.signal) .fetch(uri, nextAbortController.value.signal)
.then(this.fillOptions) .then(fillOptions)
.catch((e) => { .catch((e) => {
if (e instanceof StatusError && e.is_canceled) { if (e instanceof StatusError && e.is_canceled) {
return; return;
} }
this.$showError(e); $showError?.(e);
}); });
}, };
touchstart(event) {
const url = event.currentTarget.dataset.url; const touchstart = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {
this.touches.count = 0; touches.value.count = 0;
}, 300); }, 300);
// If the element the user is touching // If the element the user is touching
// is different from the last one he touched, // is different from the last one he touched,
// reset the count. // reset the count.
if (this.touches.id !== url) { if (touches.value.id !== urlValue) {
this.touches.id = url; touches.value.id = urlValue;
this.touches.count = 1; touches.value.count = 1;
return; return;
} }
this.touches.count++; touches.value.count++;
// If there is more than one touch already, // If there is more than one touch already,
// open the next screen. // open the next screen.
if (this.touches.count > 1) { if (touches.value.count > 1) {
this.next(event); next(event);
} }
}, };
itemClick: function (event) {
if (this.user.singleClick) this.next(event); const itemClick = (event: Event) => {
else this.select(event); if (user.value?.singleClick) next(event);
}, else select(event);
select: function (event) { };
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it. // If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) { if (selected.value === urlValue) {
this.selected = null; selected.value = null;
this.$emit("update:selected", this.current); emit("update:selected", current.value);
return; return;
} }
// Otherwise select the element. // Otherwise select the element.
this.selected = event.currentTarget.dataset.url; selected.value = urlValue;
this.$emit("update:selected", this.selected); emit("update:selected", selected.value);
}, };
createDir: async function () {
this.showHover({ const createDir = async () => {
showHover({
prompt: "newDir", prompt: "newDir",
action: null, action: undefined,
confirm: null, confirm: undefined,
props: { props: {
redirect: false, redirect: false,
base: this.current === this.$route.path ? null : this.current, base: current.value === route.path ? null : current.value,
}, },
}); });
},
},
}; };
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script> </script>

View File

@@ -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>

View File

@@ -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: { const fileStore = useFileStore();
...mapState(useFileStore, [ const layoutStore = useLayoutStore();
"req",
"selected", const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
"selectedCount", const { closeHovers } = layoutStore;
"isListing",
]), const humanSize = computed(() => {
humanSize: function () { if (selectedCount.value === 0 || !isListing.value) {
if (this.selectedCount === 0 || !this.isListing) { return filesize(req.value?.size ?? 0);
return filesize(this.req.size);
} }
let sum = 0; let sum = 0;
for (const selected of this.selected) { for (const selectedIdx of selected.value) {
sum += this.req.items[selected].size; sum += req.value?.items[selectedIdx]?.size ?? 0;
} }
return filesize(sum); return filesize(sum);
}, });
humanTime: function () {
if (this.selectedCount === 0) { const humanTime = computed(() => {
return dayjs(this.req.modified).fromNow(); if (selectedCount.value === 0) {
return dayjs(req.value?.modified).fromNow();
} }
return dayjs(this.req.items[this.selected[0]].modified).fromNow(); return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
}, });
modTime: function () {
if (this.selectedCount === 0) { const modTime = computed(() => {
return new Date(Date.parse(this.req.modified)).toLocaleString(); if (selectedCount.value === 0) {
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
} }
return new Date( return new Date(
Date.parse(this.req.items[this.selected[0]].modified) Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString(); ).toLocaleString();
}, });
name: function () {
return this.selectedCount === 0 const name = computed(() => {
? this.req.name return selectedCount.value === 0
: this.req.items[this.selected[0]].name; ? (req.value?.name ?? "")
}, : (req.value?.items[selected.value[0]]?.name ?? "");
dir: function () { });
const dir = computed(() => {
return ( return (
this.selectedCount > 1 || selectedCount.value > 1 ||
(this.selectedCount === 0 (selectedCount.value === 0
? this.req.isDir ? (req.value?.isDir ?? false)
: this.req.items[this.selected[0]].isDir) : (req.value?.items[selected.value[0]]?.isDir ?? false))
); );
}, });
resolution: function () {
if (this.selectedCount === 1) { const resolution = computed(() => {
const selectedItem = this.req.items[this.selected[0]]; if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") { if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution; return (selectedItem as any).resolution;
} }
} else if (this.req && this.req.type === "image") { } else if (req.value && req.value.type === "image") {
return this.req.resolution; return (req.value as any).resolution;
} }
return null; return null;
}, });
},
methods: { const checksum = async (event: Event, algo: string) => {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
let link; let link;
if (this.selectedCount) { if (selectedCount.value) {
link = this.req.items[this.selected[0]].url; link = req.value?.items[selected.value[0]]?.url ?? "";
} else { } else {
link = this.$route.path; link = route.path;
} }
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo as any);
event.target.textContent = hash; (event.target as HTMLElement).textContent = hash;
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
},
},
}; };
</script> </script>

View File

@@ -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,71 +67,69 @@ 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();
const layoutStore = useLayoutStore();
const authStore = useAuthStore();
const { req, selected } = storeToRefs(fileStore);
const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
const fileList = ref<InstanceType<typeof FileList> | null>(null);
const dest = ref<string | null>(null);
const excludedFolders = computed(() => {
return selected.value
.filter((idx) => req.value!.items[idx].isDir)
.map((idx) => req.value!.items[idx].url);
});
const move = async (event: Event) => {
event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
for (const item of selected.value) {
items.push({ items.push({
from: this.req.items[item].url, from: req.value!.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: this.req.items[item].name, name: req.value!.items[item].name,
}); });
} }
const action = async (overwrite, rename) => { const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("move"); buttons.loading("move");
await api await api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("move"); buttons.success("move");
this.preselect = removePrefix(items[0].to); fileStore.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest }); router.push({ path: dest.value! });
}) })
.catch((e) => { .catch((e) => {
buttons.done("move"); buttons.done("move");
this.$showError(e); $showError?.(e);
}); });
}; };
const dstItems = (await api.fetch(this.dest)).items; const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items, dstItems); const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: string) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -138,7 +138,5 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
},
},
}; };
</script> </script>

View File

@@ -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 { const fileStore = useFileStore();
name: "", const layoutStore = useLayoutStore();
};
}, const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
created() { const { closeHovers } = layoutStore;
this.name = this.oldName();
}, const name = ref("");
inject: ["$showError"],
computed: { const oldName = (): string => {
...mapState(useFileStore, [ if (!isListing.value) {
"req", return req.value?.name ?? "";
"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) { if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen. // This shouldn't happen.
return; return "";
} }
return this.req.items[this.selected[0]].name; return req.value?.items[selected.value[0]].name ?? "";
}, };
submit: async function () {
onMounted(() => {
name.value = oldName();
});
const submit = async () => {
let oldLink = ""; let oldLink = "";
let newLink = ""; let newLink = "";
if (!this.isListing) { if (!req.value) {
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
this.$router.push({ path: newLink });
return; return;
} }
this.preselect = removePrefix(newLink); if (!isListing.value) {
oldLink = req.value.url;
this.reload = true; } else {
} catch (e) { oldLink = req.value.items[selected.value[0]].url;
this.$showError(e);
} }
this.closeHovers(); 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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", const fileStore = useFileStore();
links: [], const layoutStore = useLayoutStore();
clip: null,
password: "", const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
listing: true, const { closeHovers } = layoutStore;
};
}, const time = ref(0);
inject: ["$showError", "$showSuccess"], const unit = ref("hours");
computed: { const links = ref<any[]>([]);
...mapState(useFileStore, [ const password = ref("");
"req", const listing = ref(true);
"selected",
"selectedCount", const url = computed(() => {
"isListing", if (!isListing.value) {
]), return route.path;
url() {
if (!this.isListing) {
return this.$route.path;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen. // This shouldn't happen.
return; return "";
} }
return this.req.items[this.selected[0]].url; return req.value?.items[selected.value[0]].url ?? "";
}, });
},
async beforeMount() {
try {
const links = await api.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) { const copyToClipboard = (text: string) => {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then( copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
() => { () => {
// clipboard write failed // clipboard write failed
copy({ text }, { permission: true }).then( copy({ text }, { permission: true }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
(e) => { (e) => {
// clipboard write failed // clipboard write failed
this.$showError(e); $showError?.(e);
} }
); );
} }
); );
}, };
submit: async function () {
const submit = async () => {
try { try {
let res = null; let res = null;
if (!this.time) { if (!time.value) {
res = await api.create(this.url, this.password); res = await api.create(url.value, password.value);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
);
} }
this.links.push(res); links.value.push(res);
this.sort(); sort();
this.time = 0; time.value = 0;
this.unit = "hours"; unit.value = "hours";
this.password = ""; password.value = "";
this.listing = true; listing.value = true;
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
}, };
deleteLink: async function (event, link) {
const deleteLink = async (event: Event, link: any) => {
event.preventDefault(); event.preventDefault();
try { try {
await api.remove(link.hash); await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash); links.value = links.value.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) { if (links.value.length == 0) {
this.listing = false; listing.value = false;
} }
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
}, };
humanTime(time) {
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow(); return dayjs(time * 1000).fromNow();
}, };
buildLink(share) {
const buildLink = (share: any) => {
return api.getShareURL(share); return api.getShareURL(share);
}, };
sort() {
this.links = this.links.sort((a, b) => { const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1; if (a.expire === 0) return -1;
if (b.expire === 0) return 1; if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire); return new Date(a.expire).getTime() - new Date(b.expire).getTime();
}); });
}, };
switchListing() {
if (this.links.length == 0 && !this.listing) { const switchListing = () => {
this.closeHovers(); if (links.value.length == 0 && !listing.value) {
closeHovers();
} }
this.listing = !this.listing; 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>

View File

@@ -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>

View File

@@ -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: { }>();
const emit = defineEmits<{
"update:commands": [commands: string[]];
}>();
const raw = computed({
get() { get() {
return this.commands.join(" "); return props.commands.join(" ");
}, },
set(value) { set(value: string) {
if (value !== "") { if (value !== "") {
this.$emit("update:commands", value.split(" ")); emit("update:commands", value.split(" "));
} else { } else {
this.$emit("update:commands", []); emit("update:commands", []);
} }
}, },
}, });
},
};
</script> </script>

View File

@@ -6,15 +6,18 @@
</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 emit = defineEmits<{
const locales = { "update:locale": [locale: string];
}>();
const locales = markRaw({
he: "עברית", he: "עברית",
hr: "Hrvatski", hr: "Hrvatski",
hu: "Magyar", hu: "Magyar",
@@ -44,23 +47,9 @@ export default {
vi: "Tiếng Việt", vi: "Tiếng Việt",
"zh-cn": "中文 (简体)", "zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)", "zh-tw": "中文 (繁體)",
};
// 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,
}); });
return dataObj; const change = (event: Event) => {
}, emit("update:locale", (event.target as HTMLSelectElement).value);
methods: {
change(event) {
this.$emit("update:locale", event.target.value);
},
},
}; };
</script> </script>

View File

@@ -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", const props = defineProps<{
props: ["perm"], perm: UserPermissions;
computed: { }>();
admin: {
const admin = computed({
get() { get() {
return this.perm.admin; return props.perm.admin;
}, },
set(value) { set(value: boolean) {
if (value) { if (value) {
for (const key in this.perm) { for (const key in props.perm) {
this.perm[key] = true; props.perm[key as keyof UserPermissions] = true;
} }
} }
this.perm.admin = value; props.perm.admin = value;
}, },
}, });
isExecEnabled: () => enableExec,
}, const isExecEnabled = enableExec;
};
</script> </script>

View File

@@ -32,22 +32,36 @@
</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: {
raw: string;
};
}
const props = defineProps<{
rules: Rule[];
}>();
const emit = defineEmits<{
"update:rules": [rules: Rule[]];
}>();
const remove = (event: Event, index: number) => {
event.preventDefault(); event.preventDefault();
const rules = [...this.rules]; const rules = [...props.rules];
rules.splice(index, 1); rules.splice(index, 1);
this.$emit("update:rules", [...rules]); emit("update:rules", [...rules]);
}, };
create(event) {
const create = (event: Event) => {
event.preventDefault(); event.preventDefault();
this.$emit("update:rules", [ emit("update:rules", [
...this.rules, ...props.rules,
{ {
allow: true, allow: true,
path: "", path: "",
@@ -57,7 +71,5 @@ export default {
}, },
}, },
]); ]);
},
},
}; };
</script> </script>

View File

@@ -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; user: IUserForm;
isNew: boolean; isNew: boolean;
isDefault: boolean; isDefault: false;
createUserDir?: boolean; 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;
} }
); );

View File

@@ -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;

View File

@@ -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;