feat: migrate to vue 3 (#2689)

---------

Co-authored-by: Joep <jcbuhre@gmail.com>
Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com>
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
kloon15
2024-04-01 17:18:22 +02:00
committed by GitHub
parent 0e3b35b30e
commit 5100e587d7
164 changed files with 12202 additions and 8047 deletions

View File

@@ -3,8 +3,8 @@
<component
:is="element"
:to="base || ''"
:aria-label="$t('files.home')"
:title="$t('files.home')"
:aria-label="t('files.home')"
:title="t('files.home')"
>
<i class="material-icons">home</i>
</component>
@@ -18,58 +18,66 @@
</div>
</template>
<script>
export default {
name: "breadcrumbs",
props: ["base", "noLink"],
computed: {
items() {
const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split("/");
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
if (parts[0] === "") {
parts.shift();
}
const { t } = useI18n();
if (parts[parts.length - 1] === "") {
parts.pop();
}
const route = useRoute();
let breadcrumbs = [];
const props = defineProps<{
base: string;
noLink?: boolean;
}>();
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
let parts = relativePath.split("/");
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
if (parts[0] === "") {
parts.shift();
}
breadcrumbs[0].name = "...";
}
if (parts[parts.length - 1] === "") {
parts.pop();
}
return breadcrumbs;
},
element() {
if (this.noLink !== undefined) {
return "span";
}
let breadcrumbs: BreadCrumb[] = [];
return "router-link";
},
},
};
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: props.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
breadcrumbs[0].name = "...";
}
return breadcrumbs;
});
const element = computed(() => {
if (props.noLink) {
return "span";
}
return "router-link";
});
</script>
<style></style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="t-container">
<span>{{ message }}</span>
<button v-if="isReport" class="action" @click.stop="clicked">
{{ reportText }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
message: string;
reportText?: string;
isReport?: boolean;
}>();
const clicked = () => {
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
};
</script>
<style scoped>
.t-container {
width: 100%;
padding: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.action {
text-align: center;
height: 40px;
padding: 0 10px;
margin-left: 20px;
border-radius: 5px;
color: white;
cursor: pointer;
border: thin solid currentColor;
}
html[dir="rtl"] .action {
margin-left: initial;
margin-right: 20px;
}
</style>

View File

@@ -0,0 +1,224 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template>
<div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'top'"
>
{{ text }}
</div>
<div class="vue-simple-progress" :style="progress_style">
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'middle'"
>
{{ text }}
</div>
<div
style="position: relative; left: -9999px"
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style">
<div
:style="text_style"
v-if="text.length > 0 && textPosition == 'inside'"
>
{{ text }}
</div>
</div>
</div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'bottom'"
>
{{ text }}
</div>
</div>
</template>
<script>
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
export default {
name: "progress-bar",
props: {
val: {
default: 0,
},
max: {
default: 100,
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
},
"bg-color": {
type: String,
default: "#eee",
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
default: "all 0.5s ease",
},
"bar-border-radius": {
type: Number,
default: 0,
},
spacing: {
type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
var pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32;
},
text_padding() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4;
},
text_font_size() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
}
return isNumber(this.fontSize) ? this.fontSize : 13;
},
progress_style() {
var style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = this.size_px + "px";
style["z-index"] = "-2";
}
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
return style;
},
bar_style() {
var style = {
background: this.barColor,
width: this.pct + "%",
height: this.size_px + "px",
transition: this.barTransition,
};
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
}
return style;
},
text_style() {
var style = {
color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style;
},
},
};
</script>

View File

@@ -17,7 +17,7 @@
@keyup.enter="submit"
ref="input"
:autofocus="active"
v-model.trim="value"
v-model.trim="prompt"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
/>
@@ -28,7 +28,7 @@
<template v-if="isEmpty">
<p>{{ text }}</p>
<template v-if="value.length === 0">
<template v-if="prompt.length === 0">
<div class="boxes">
<h3>{{ $t("search.types") }}</h3>
<div>
@@ -49,7 +49,7 @@
</template>
<ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url">
<router-link v-on:click="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span>
@@ -64,138 +64,155 @@
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations } from "vuex";
<script setup lang="ts">
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
var boxes = {
const boxes = {
image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" },
};
export default {
name: "search",
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
currentPrompt(val, old) {
this.active = val?.prompt === "search";
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
if (old?.prompt === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
const { currentPromptName } = storeToRefs(layoutStore);
document.body.style.overflow = "auto";
this.reset();
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value() {
if (this.results.length) {
this.reset();
}
},
},
computed: {
...mapState(["user"]),
...mapGetters(["isListing", "currentPrompt"]),
boxes() {
return boxes;
},
isEmpty() {
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return "";
}
const prompt = ref<string>("");
const active = ref<boolean>(false);
const ongoing = ref<boolean>(false);
const results = ref<any[]>([]);
const reload = ref<boolean>(false);
const resultsCount = ref<number>(50);
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
filteredResults() {
return this.results.slice(0, this.resultsCount);
},
},
mounted() {
this.$refs.result.addEventListener("scroll", (event) => {
if (
event.target.offsetHeight + event.target.scrollTop >=
event.target.scrollHeight - 100
) {
this.resultsCount += 50;
}
});
},
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search");
},
close(event) {
event.stopPropagation();
event.preventDefault();
this.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event);
return;
}
const $showError = inject<IToastError>("$showError")!;
this.results.length = 0;
},
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault();
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
if (this.value === "") {
return;
}
const { t } = useI18n();
let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/";
}
const route = useRoute();
this.ongoing = true;
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
try {
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error);
}
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
}
this.ongoing = false;
},
},
document.body.style.overflow = "auto";
reset();
prompt.value = "";
active.value = false;
input.value?.blur();
} else if (active.value) {
reload.value = false;
input.value?.focus();
document.body.style.overflow = "hidden";
}
});
watch(prompt, () => {
if (results.value.length) {
reset();
}
});
// ...mapState(useFileStore, ["isListing"]),
// ...mapState(useLayoutStore, ["show"]),
// ...mapWritableState(useFileStore, { sReload: "reload" }),
const isEmpty = computed(() => {
return results.value.length === 0;
});
const text = computed(() => {
if (ongoing.value) {
return "";
}
return prompt.value === ""
? t("search.typeToSearch")
: t("search.pressToSearch");
});
const filteredResults = computed(() => {
return results.value.slice(0, resultsCount.value);
});
onMounted(() => {
if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if (
(event.target as HTMLElement).offsetHeight +
(event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) {
resultsCount.value += 50;
}
});
});
const open = () => {
!active.value && layoutStore.showHover("search");
};
const close = (event: Event) => {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
};
const keyup = (event: KeyboardEvent) => {
if (event.key === "Escape") {
close(event);
return;
}
results.value.length = 0;
};
const init = (string: string) => {
prompt.value = `${string} `;
input.value !== null ? input.value.focus() : "";
};
const reset = () => {
ongoing.value = false;
resultsCount.value = 50;
results.value = [];
};
const submit = async (event: Event) => {
event.preventDefault();
if (prompt.value === "") {
return;
}
let path = route.path;
if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/";
}
ongoing.value = true;
try {
results.value = await search(path, prompt.value);
} catch (error: any) {
$showError(error);
}
ongoing.value = false;
};
</script>

View File

@@ -29,9 +29,9 @@
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
:contenteditable="true"
@keydown.prevent.arrow-up="historyUp"
@keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
@@ -45,7 +45,10 @@
</template>
<script>
import { mapMutations, mapState, mapGetters } from "vuex";
import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
@@ -53,8 +56,8 @@ import { theme } from "@/utils/constants";
export default {
name: "shell",
computed: {
...mapState(["user", "showShell"]),
...mapGetters(["isFiles", "isLogged"]),
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
@@ -75,11 +78,11 @@ export default {
mounted() {
window.addEventListener("resize", this.resize);
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapMutations(["toggleShell"]),
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";

View File

@@ -1,6 +1,6 @@
<template>
<nav :class="{ active }">
<template v-if="isLogged">
<template v-if="isLoggedIn">
<button
class="action"
@click="toRoot"
@@ -13,7 +13,7 @@
<div v-if="user.perm.create">
<button
@click="$store.commit('showHover', 'newDir')"
@click="showHover('newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
@@ -23,7 +23,7 @@
</button>
<button
@click="$store.commit('showHover', 'newFile')"
@click="showHover('newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
@@ -82,9 +82,7 @@
<div
class="credits"
v-if="
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
"
v-if="isFiles && !disableUsedPercentage"
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
>
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
@@ -112,7 +110,12 @@
</template>
<script>
import { mapState, mapGetters } from "vuex";
import { reactive } from "vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as auth from "@/utils/auth";
import {
version,
@@ -123,19 +126,27 @@ import {
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
import ProgressBar from "vue-simple-progress";
import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default {
name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
},
components: {
ProgressBar,
},
inject: ["$showError"],
computed: {
...mapState(["user"]),
...mapGetters(["isLogged", "currentPrompt"]),
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() {
return this.currentPrompt?.prompt === "sidebar";
return this.currentPromptName === "sidebar";
},
signup: () => signup,
version: () => version,
@@ -143,47 +154,45 @@ export default {
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
},
asyncComputed: {
usage: {
async get() {
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
if (this.disableUsedPercentage) {
return usageStats;
}
try {
let usage = await api.usage(path);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
this.$showError(error);
}
return usageStats;
},
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
async fetchUsage() {
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
let usage = await api.usage(path);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
this.$showError(error);
}
return Object.assign(this.usage, usageStats);
},
toRoot() {
this.$router.push({ path: "/files/" }, () => {});
this.$store.commit("closeHovers");
this.$router.push({ path: "/files" });
this.closeHovers();
},
toSettings() {
this.$router.push({ path: "/settings" }, () => {});
this.$store.commit("closeHovers");
this.$router.push({ path: "/settings" });
this.closeHovers();
},
help() {
this.$store.commit("showHover", "help");
this.showHover("help");
},
logout: auth.logout,
},
watch: {
isFiles(newValue) {
newValue && this.fetchUsage();
},
},
};
</script>

View File

@@ -13,261 +13,290 @@
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
</div>
</template>
<script>
import throttle from "lodash.throttle";
<script setup lang="ts">
import throttle from "lodash/throttle";
import UTIF from "utif";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
export default {
props: {
src: String,
moveDisabledTime: {
type: Number,
default: () => 200,
},
classList: {
type: Array,
default: () => [],
},
zoomStep: {
type: Number,
default: () => 0.25,
},
},
data() {
return {
scale: 1,
lastX: null,
lastY: null,
inDrag: false,
touches: 0,
lastTouchDistance: 0,
moveDisabled: false,
disabledTimer: null,
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 },
},
maxScale: 4,
minScale: 0.25,
};
},
mounted() {
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className));
// set width and height if they are zero
if (getComputedStyle(container).width === "0px") {
container.style.width = "100%";
}
if (getComputedStyle(container).height === "0px") {
container.style.height = "100%";
interface IProps {
src: string;
moveDisabledTime: number;
classList: any[];
zoomStep: number;
}
const props = withDefaults(defineProps<IProps>(), {
moveDisabledTime: () => 200,
classList: () => [],
zoomStep: () => 0.25,
});
const scale = ref<number>(1);
const lastX = ref<number | null>(null);
const lastY = ref<number | null>(null);
const inDrag = ref<boolean>(false);
const touches = ref<number>(0);
const lastTouchDistance = ref<number | null>(0);
const moveDisabled = ref<boolean>(false);
const disabledTimer = ref<number | null>(null);
const imageLoaded = ref<boolean>(false);
const position = ref<{
center: { x: number; y: number };
relative: { x: number; y: number };
}>({
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 },
});
const maxScale = ref<number>(4);
const minScale = ref<number>(0.25);
// Refs
const imgex = ref<HTMLImageElement | null>(null);
const container = ref<HTMLDivElement | null>(null);
onMounted(() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
}
props.classList.forEach((className) =>
container.value !== null ? container.value.classList.add(className) : ""
);
if (container.value === null) {
return;
}
// set width and height if they are zero
if (getComputedStyle(container.value).width === "0px") {
container.value.style.width = "100%";
}
if (getComputedStyle(container.value).height === "0px") {
container.value.style.height = "100%";
}
window.addEventListener("resize", onResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onResize);
document.removeEventListener("mouseup", onMouseUp);
});
watch(
() => props.src,
() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
}
window.addEventListener("resize", this.onResize);
},
beforeDestroy() {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
watch: {
src: function () {
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
scale.value = 1;
setZoom();
setCenter();
}
);
this.scale = 1;
this.setZoom();
this.setCenter();
},
},
methods: {
// Modified from UTIF.replaceIMG
decodeUTIF() {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase();
if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex);
xhr.open("GET", this.src);
xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded;
xhr.send();
return true;
},
onLoad() {
let img = this.$refs.imgex;
// Modified from UTIF.replaceIMG
const decodeUTIF = () => {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
if (document?.location?.pathname === undefined) {
return;
}
let suff = document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
this.imageLoaded = true;
if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr);
UTIF._imgs.push(imgex.value);
xhr.open("GET", props.src);
xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded;
xhr.send();
return true;
};
if (img === undefined) {
return;
}
const onLoad = () => {
imageLoaded.value = true;
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
if (imgex.value === null) {
return;
}
document.addEventListener("mouseup", this.onMouseUp);
imgex.value.classList.remove("image-ex-img-center");
setCenter();
imgex.value.classList.add("image-ex-img-ready");
let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
document.addEventListener("mouseup", onMouseUp);
// Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
let realSize = imgex.value.naturalWidth;
let displaySize = imgex.value.offsetWidth;
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// Image is in portrait orientation
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
realSize = imgex.value.naturalHeight;
displaySize = imgex.value.offsetHeight;
}
// Full size plus additional zoom
this.maxScale = fullScale + 4;
},
onMouseUp() {
this.inDrag = false;
},
onResize: throttle(function () {
if (this.imageLoaded) {
this.setCenter();
this.doMove(this.position.relative.x, this.position.relative.y);
}
}, 100),
setCenter() {
let container = this.$refs.container;
let img = this.$refs.imgex;
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
this.position.center.x = Math.floor(
(container.clientWidth - img.clientWidth) / 2
);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
// Full size plus additional zoom
maxScale.value = fullScale + 4;
};
img.style.left = this.position.center.x + "px";
img.style.top = this.position.center.y + "px";
},
mousedownStart(event) {
this.lastX = null;
this.lastY = null;
this.inDrag = true;
event.preventDefault();
},
mouseMove(event) {
if (!this.inDrag) return;
this.doMove(event.movementX, event.movementY);
event.preventDefault();
},
mouseUp(event) {
this.inDrag = false;
event.preventDefault();
},
touchStart(event) {
this.lastX = null;
this.lastY = null;
this.lastTouchDistance = null;
if (event.targetTouches.length < 2) {
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.zoomAuto(event);
}
}
event.preventDefault();
},
zoomAuto(event) {
switch (this.scale) {
case 1:
this.scale = 2;
break;
case 2:
this.scale = 4;
break;
default:
case 4:
this.scale = 1;
this.setCenter();
break;
}
this.setZoom();
event.preventDefault();
},
touchMove(event) {
event.preventDefault();
if (this.lastX === null) {
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
return;
}
let step = this.$refs.imgex.width / 5;
if (event.targetTouches.length === 2) {
this.moveDisabled = true;
clearTimeout(this.disabledTimer);
this.disabledTimer = setTimeout(
() => (this.moveDisabled = false),
this.moveDisabledTime
);
const onMouseUp = () => {
inDrag.value = false;
};
let p1 = event.targetTouches[0];
let p2 = event.targetTouches[1];
let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
);
if (!this.lastTouchDistance) {
this.lastTouchDistance = touchDistance;
return;
}
this.scale += (touchDistance - this.lastTouchDistance) / step;
this.lastTouchDistance = touchDistance;
this.setZoom();
} else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return;
let x = event.targetTouches[0].pageX - this.lastX;
let y = event.targetTouches[0].pageY - this.lastY;
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
this.doMove(x, y);
}
},
doMove(x, y) {
let style = this.$refs.imgex.style;
let posX = this.pxStringToNumber(style.left) + x;
let posY = this.pxStringToNumber(style.top) + y;
const onResize = throttle(function () {
if (imageLoaded.value) {
setCenter();
doMove(position.value.relative.x, position.value.relative.y);
}
}, 100);
style.left = posX + "px";
style.top = posY + "px";
const setCenter = () => {
if (container.value === null || imgex.value === null) {
return;
}
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
position.value.center.x = Math.floor(
(container.value.clientWidth - imgex.value.clientWidth) / 2
);
position.value.center.y = Math.floor(
(container.value.clientHeight - imgex.value.clientHeight) / 2
);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1;
}
imgex.value.style.left = position.value.center.x + "px";
imgex.value.style.top = position.value.center.y + "px";
};
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1;
}
},
wheelMove(event) {
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
this.$refs.imgex.style.transform = `scale(${this.scale})`;
},
pxStringToNumber(style) {
return +style.replace("px", "");
},
},
const mousedownStart = (event: Event) => {
lastX.value = null;
lastY.value = null;
inDrag.value = true;
event.preventDefault();
};
const mouseMove = (event: MouseEvent) => {
if (!inDrag.value) return;
doMove(event.movementX, event.movementY);
event.preventDefault();
};
const mouseUp = (event: Event) => {
inDrag.value = false;
event.preventDefault();
};
const touchStart = (event: TouchEvent) => {
lastX.value = null;
lastY.value = null;
lastTouchDistance.value = null;
if (event.targetTouches.length < 2) {
setTimeout(() => {
touches.value = 0;
}, 300);
touches.value++;
if (touches.value > 1) {
zoomAuto(event);
}
}
event.preventDefault();
};
const zoomAuto = (event: Event) => {
switch (scale.value) {
case 1:
scale.value = 2;
break;
case 2:
scale.value = 4;
break;
default:
case 4:
scale.value = 1;
setCenter();
break;
}
setZoom();
event.preventDefault();
};
const touchMove = (event: TouchEvent) => {
event.preventDefault();
if (lastX.value === null) {
lastX.value = event.targetTouches[0].pageX;
lastY.value = event.targetTouches[0].pageY;
return;
}
if (imgex.value === null) {
return;
}
let step = imgex.value.width / 5;
if (event.targetTouches.length === 2) {
moveDisabled.value = true;
if (disabledTimer.value) clearTimeout(disabledTimer.value);
disabledTimer.value = window.setTimeout(
() => (moveDisabled.value = false),
props.moveDisabledTime
);
let p1 = event.targetTouches[0];
let p2 = event.targetTouches[1];
let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
);
if (!lastTouchDistance.value) {
lastTouchDistance.value = touchDistance;
return;
}
scale.value += (touchDistance - lastTouchDistance.value) / step;
lastTouchDistance.value = touchDistance;
setZoom();
} else if (event.targetTouches.length === 1) {
if (moveDisabled.value) return;
let x = event.targetTouches[0].pageX - (lastX.value ?? 0);
let y = event.targetTouches[0].pageY - (lastY.value ?? 0);
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
lastX.value = event.targetTouches[0].pageX;
lastY.value = event.targetTouches[0].pageY;
doMove(x, y);
}
};
const doMove = (x: number, y: number) => {
if (imgex.value === null) {
return;
}
const style = imgex.value.style;
let posX = pxStringToNumber(style.left) + x;
let posY = pxStringToNumber(style.top) + y;
style.left = posX + "px";
style.top = posY + "px";
position.value.relative.x = Math.abs(position.value.center.x - posX);
position.value.relative.y = Math.abs(position.value.center.y - posY);
if (posX < position.value.center.x) {
position.value.relative.x = position.value.relative.x * -1;
}
if (posY < position.value.center.y) {
position.value.relative.y = position.value.relative.y * -1;
}
};
const wheelMove = (event: WheelEvent) => {
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
setZoom();
};
const setZoom = () => {
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
if (imgex.value !== null)
imgex.value.style.transform = `scale(${scale.value})`;
};
const pxStringToNumber = (style: string) => {
return +style.replace("px", "");
};
</script>
<style>

View File

@@ -15,7 +15,7 @@
>
<div>
<img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-if="!readOnly && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons"></i>
@@ -34,221 +34,240 @@
</div>
</template>
<script>
<script setup lang="ts">
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales";
import dayjs from "dayjs";
import { files as api } from "@/api";
import * as upload from "@/utils/upload";
import { computed, inject, ref } from "vue";
import { useRouter } from "vue-router";
export default {
name: "item",
data: function () {
return {
touches: 0,
};
},
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
"path",
],
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
const touches = ref<number>(0);
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
return false;
}
const $showError = inject<IToastError>("$showError")!;
const router = useRouter();
const props = defineProps<{
name: string;
isDir: boolean;
url: string;
type: string;
size: number;
modified: string;
index: number;
readOnly?: boolean;
path?: string;
}>();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const singleClick = computed(
() => !props.readOnly && authStore.user?.singleClick
);
const isSelected = computed(
() => fileStore.selected.indexOf(props.index) !== -1
);
const isDraggable = computed(
() => !props.readOnly && authStore.user?.perm.rename
);
const canDrop = computed(() => {
if (!props.isDir || props.readOnly) return false;
for (let i of fileStore.selected) {
if (fileStore.req?.items[i].url === props.url) {
return false;
}
}
return true;
});
const thumbnailUrl = computed(() => {
const file = {
path: props.path,
modified: props.modified,
};
return api.getPreviewURL(file as Resource, "thumb");
});
const isThumbsEnabled = computed(() => {
return enableThumbs;
});
const humanSize = () => {
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
};
const humanTime = () => {
if (!props.readOnly && authStore.user?.dateFormat) {
return dayjs(props.modified).format("L LT");
}
return dayjs(props.modified).fromNow();
};
const dragStart = () => {
if (fileStore.selectedCount === 0) {
fileStore.selected.push(props.index);
return;
}
if (!isSelected.value) {
fileStore.selected = [];
fileStore.selected.push(props.index);
}
};
const dragOver = (event: Event) => {
if (!canDrop.value) return;
event.preventDefault();
let el = event.target as HTMLElement | null;
if (el !== null) {
for (let i = 0; i < 5; i++) {
if (!el?.classList.contains("item")) {
el = el?.parentElement ?? null;
}
}
return true;
},
thumbnailUrl() {
const file = {
path: this.path,
modified: this.modified,
};
if (el !== null) el.style.opacity = "1";
}
};
return api.getPreviewURL(file, "thumb");
},
isThumbsEnabled() {
return enableThumbs;
},
},
methods: {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () {
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
},
humanTime: function () {
if (this.readOnly == undefined && this.user.dateFormat) {
return moment(this.modified).format("L LT");
}
return moment(this.modified).fromNow();
},
dragStart: function () {
if (this.selectedCount === 0) {
this.addSelected(this.index);
return;
const drop = async (event: Event) => {
if (!canDrop.value) return;
event.preventDefault();
if (fileStore.selectedCount === 0) return;
let el = event.target as HTMLElement | null;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let items: any[] = [];
for (let i of fileStore.selected) {
if (fileStore.req) {
items.push({
from: fileStore.req?.items[i].url,
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: fileStore.req?.items[i].name,
});
}
}
// Get url from ListingItem instance
if (el === null) {
return;
}
let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items;
let action = (overwrite: boolean, rename: boolean) => {
api
.move(items, overwrite, rename)
.then(() => {
fileStore.reload = true;
})
.catch($showError);
};
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false;
let rename = false;
if (conflict) {
layoutStore.showHover({
prompt: "replace-rename",
confirm: (event: Event, option: any) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
layoutStore.closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
};
const itemClick = (event: Event | KeyboardEvent) => {
if (
!((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey) &&
singleClick.value &&
!fileStore.multiple
)
open();
else click(event);
};
const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
setTimeout(() => {
touches.value = 0;
}, 300);
touches.value++;
if (touches.value > 1) {
open();
}
if (fileStore.selected.indexOf(props.index) !== -1) {
fileStore.removeSelected(props.index);
return;
}
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
let fi = 0;
let la = 0;
if (props.index > fileStore.selected[0]) {
fi = fileStore.selected[0] + 1;
la = props.index;
} else {
fi = props.index;
la = fileStore.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (fileStore.selected.indexOf(fi) == -1) {
fileStore.selected.push(fi);
}
}
if (!this.isSelected) {
this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return;
return;
}
event.preventDefault();
let el = event.target;
if (
!singleClick.value &&
!(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey &&
!fileStore.multiple
) {
fileStore.selected = [];
}
fileStore.selected.push(props.index);
};
for (let i = 0; i < 5; i++) {
if (!el.classList.contains("item")) {
el = el.parentElement;
}
}
el.style.opacity = 1;
},
drop: async function (event) {
if (!this.canDrop) return;
event.preventDefault();
if (this.selectedCount === 0) return;
let el = event.target;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + encodeURIComponent(this.req.items[i].name),
name: this.req.items[i].name,
});
}
// Get url from ListingItem instance
let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => {
api
.move(items, overwrite, rename)
.then(() => {
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
},
itemClick: function (event) {
if (
!(event.ctrlKey || event.metaKey) &&
this.singleClick &&
!this.$store.state.multiple
)
this.open();
else this.click(event);
},
click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.open();
}
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index);
return;
}
if (event.shiftKey && this.selected.length > 0) {
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1;
la = this.index;
} else {
fi = this.index;
la = this.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi);
}
}
return;
}
if (
!this.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!this.$store.state.multiple
)
this.resetSelected();
this.addSelected(this.index);
},
open: function () {
this.$router.push({ path: this.url });
},
},
const open = () => {
router.push({ path: props.url });
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<video ref="videoPlayer" class="video-max video-js" controls>
<source :src="source" />
<track
kind="subtitles"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="subLabel(sub)"
:default="index === 0"
/>
<p class="vjs-no-js">
Sorry, your browser doesn't support embedded videos, but don't worry, you
can <a :href="source">download it</a>
and watch it with your favorite video player!
</p>
</video>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "videojs-mobile-ui";
import "videojs-hotkeys";
import "video.js/dist/video-js.min.css";
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
const videoPlayer = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
const props = withDefaults(
defineProps<{
source: string;
subtitles?: string[];
options?: any;
}>(),
{
options: {},
}
);
onMounted(() => {
player.value = videojs(
videoPlayer.value!,
{
html5: {
// needed for customizable subtitles
// TODO: add to user settings
nativeTextTracks: false,
},
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 10,
enableModifiersForNumbers: false,
},
},
...props.options,
},
// onReady callback
async () => {
// player.value!.log("onPlayerReady", this);
}
);
// TODO: need to test on mobile
// @ts-ignore
player.value!.mobileUi();
});
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose();
player.value = null;
}
});
const subLabel = (subUrl: string) => {
let url: URL;
try {
url = new URL(subUrl);
} catch (_) {
// treat it as a relative url
// we only need this for filename
url = new URL(subUrl, window.location.origin);
}
const label = decodeURIComponent(
url.pathname
.split("/")
.pop()!
.replace(/\.[^/.]+$/, "")
);
return label;
};
</script>
<style scoped>
.video-max {
width: 100%;
height: 100%;
}
</style>

View File

@@ -2,24 +2,31 @@
<button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span>
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
</button>
</template>
<script>
export default {
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
action: function () {
if (this.show) {
this.$store.commit("showHover", this.show);
}
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
this.$emit("action");
},
},
const props = defineProps<{
icon?: string;
label?: string;
counter?: number;
show?: string;
}>();
const emit = defineEmits<{
(e: "action"): any;
}>();
const layoutStore = useLayoutStore();
const action = () => {
if (props.show) {
layoutStore.showHover(props.show);
}
emit("action");
};
</script>
<style></style>

View File

@@ -1,62 +1,59 @@
<template>
<header>
<img v-if="showLogo !== undefined" :src="logoURL" />
<action
v-if="showMenu !== undefined"
<img v-if="showLogo" :src="logoURL" />
<Action
v-if="showMenu"
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="openSidebar()"
:label="t('buttons.toggleSidebar')"
@action="layoutStore.showHover('sidebar')"
/>
<slot />
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
<div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" />
</div>
<action
v-if="this.$slots.actions"
<Action
v-if="ifActionsSlot"
id="more"
icon="more_vert"
:label="$t('buttons.more')"
@action="$store.commit('showHover', 'more')"
:label="t('buttons.more')"
@action="layoutStore.showHover('more')"
/>
<div
class="overlay"
v-show="this.currentPromptName == 'more'"
@click="$store.commit('closeHovers')"
v-show="layoutStore.currentPromptName == 'more'"
@click="layoutStore.closeHovers"
/>
</header>
</template>
<script>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex";
import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default {
name: "header-bar",
props: ["showLogo", "showMenu"],
components: {
Action,
},
data: function () {
return {
logoURL,
};
},
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
defineProps<{
showLogo?: boolean;
showMenu?: boolean;
}>();
const layoutStore = useLayoutStore();
const slots = useSlots();
const { t } = useI18n();
const ifActionsSlot = computed(() => (slots.actions ? true : false));
</script>
<style></style>

View File

@@ -0,0 +1,21 @@
<template>
<VueFinalModal
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
style="z-index: 9999999"
>
<slot />
</VueFinalModal>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
</script>

View File

@@ -6,8 +6,11 @@
<div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
<file-list
ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div>
<div
@@ -28,17 +31,20 @@
<div>
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
tabindex="2"
>
{{ $t("buttons.copy") }}
</button>
@@ -48,7 +54,10 @@
</template>
<script>
import { mapState } from "vuex";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
@@ -63,8 +72,14 @@ export default {
dest: null,
};
},
computed: mapState(["req", "selected", "user"]),
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
let items = [];
@@ -87,7 +102,7 @@ export default {
buttons.success("copy");
if (this.$route.path === this.dest) {
this.$store.commit("setReload", true);
this.reload = true;
return;
}
@@ -101,7 +116,7 @@ export default {
};
if (this.$route.path === this.dest) {
this.$store.commit("closeHovers");
this.closeHovers();
action(false, true);
return;
@@ -114,14 +129,14 @@ export default {
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
this.closeHovers();
action(overwrite, rename);
},
});

View File

@@ -10,18 +10,21 @@
</div>
<div class="card-action">
<button
@click="$store.commit('closeHovers')"
@click="closeHovers"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
tabindex="1"
>
{{ $t("buttons.delete") }}
</button>
@@ -30,18 +33,27 @@
</template>
<script>
import { mapGetters, mapMutations, mapState } from "vuex";
import { mapActions, mapState, mapWritableState } from "pinia";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "delete",
inject: ["$showError"],
computed: {
...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
...mapState(["req", "selected"]),
...mapState(useFileStore, [
"isListing",
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapMutations(["closeHovers"]),
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
@@ -69,11 +81,11 @@ export default {
await Promise.all(promises);
buttons.success("delete");
this.$store.commit("setReload", true);
this.reload = true;
} catch (e) {
buttons.done("delete");
this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true);
if (this.isListing) this.reload = true;
}
},
},

View File

@@ -0,0 +1,40 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ t("prompts.deleteUser") }}</p>
</div>
<div class="card-action">
<button
id="focus-prompt"
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="1"
>
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="layoutStore.currentPrompt?.confirm()"
tabindex="2"
>
{{ t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>

View File

@@ -7,18 +7,21 @@
</div>
<div class="card-action">
<button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')"
tabindex="1"
>
{{ $t("buttons.discardChanges") }}
</button>
@@ -27,15 +30,18 @@
</template>
<script>
import { mapMutations } from "vuex";
import { mapActions } from "pinia";
import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default {
name: "discardEditorChanges",
methods: {
...mapMutations(["closeHovers"]),
...mapActions(useLayoutStore, ["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () {
this.$store.commit("updateRequest", {});
this.updateRequest(null);
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });

View File

@@ -1,18 +1,18 @@
<template>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t("prompts.download") }}</h2>
<h2>{{ t("prompts.download") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.downloadMessage") }}</p>
<p>{{ t("prompts.downloadMessage") }}</p>
<button
id="focus-prompt"
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="currentPrompt.confirm(format)"
v-focus
@click="layoutStore.currentPrompt?.confirm(format)"
>
{{ ext }}
</button>
@@ -20,26 +20,21 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "download",
data: function () {
return {
formats: {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
},
};
},
computed: {
...mapGetters(["currentPrompt"]),
},
const layoutStore = useLayoutStore();
const { t } = useI18n();
const formats = {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
};
</script>

View File

@@ -25,7 +25,10 @@
</template>
<script>
import { mapState } from "vuex";
import { mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import url from "@/utils/url";
import { files } from "@/api";
@@ -42,8 +45,10 @@ export default {
current: window.location.pathname,
};
},
inject: ["$showError"],
computed: {
...mapState(["req", "user"]),
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},

View File

@@ -20,11 +20,13 @@
<div class="card-action">
<button
id="focus-prompt"
type="submit"
@click="$store.commit('closeHovers')"
@click="closeHovers"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
tabindex="1"
>
{{ $t("buttons.ok") }}
</button>
@@ -33,5 +35,13 @@
</template>
<script>
export default { name: "help" };
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>

View File

@@ -40,33 +40,45 @@
<p>
<strong>MD5: </strong
><code
><a @click="checksum($event, 'md5')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'md5')"
@keypress.enter="checksum($event, 'md5')"
tabindex="2"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA1: </strong
><code
><a @click="checksum($event, 'sha1')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha1')"
@keypress.enter="checksum($event, 'sha1')"
tabindex="3"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA256: </strong
><code
><a @click="checksum($event, 'sha256')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha256')"
@keypress.enter="checksum($event, 'sha256')"
tabindex="4"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA512: </strong
><code
><a @click="checksum($event, 'sha512')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha512')"
@keypress.enter="checksum($event, 'sha512')"
tabindex="5"
>{{ $t("prompts.show") }}</a
></code
>
</p>
</template>
@@ -74,8 +86,9 @@
<div class="card-action">
<button
id="focus-prompt"
type="submit"
@click="$store.commit('closeHovers')"
@click="closeHovers"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
@@ -87,16 +100,23 @@
</template>
<script>
import { mapState, mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils";
import moment from "moment/min/moment-with-locales";
import dayjs from "dayjs";
import { files as api } from "@/api";
export default {
name: "info",
inject: ["$showError"],
computed: {
...mapState(["req", "selected"]),
...mapGetters(["selectedCount", "isListing"]),
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
@@ -112,13 +132,19 @@ export default {
},
humanTime: function () {
if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow();
return dayjs(this.req.modified).fromNow();
}
return moment(this.req.items[this.selected[0]].modified).fromNow();
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
},
modTime: function () {
return new Date(Date.parse(this.req.modified)).toLocaleString();
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date(
Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString();
},
name: function () {
return this.selectedCount === 0
@@ -146,6 +172,7 @@ export default {
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault();
@@ -159,8 +186,7 @@ export default {
try {
const hash = await api.checksum(link, algo);
// eslint-disable-next-line
event.target.innerHTML = hash;
event.target.textContent = hash;
} catch (e) {
this.$showError(e);
}

View File

@@ -5,8 +5,11 @@
</div>
<div class="card-content">
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
<file-list
ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div>
<div
@@ -27,18 +30,21 @@
<div>
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
tabindex="2"
>
{{ $t("buttons.move") }}
</button>
@@ -48,7 +54,10 @@
</template>
<script>
import { mapState } from "vuex";
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
@@ -63,8 +72,13 @@ export default {
dest: null,
};
},
computed: mapState(["req", "selected", "user"]),
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault();
let items = [];
@@ -99,14 +113,14 @@ export default {
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
this.closeHovers();
action(overwrite, rename);
},
});

View File

@@ -1,98 +1,104 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.newDir") }}</h2>
<h2>{{ t("prompts.newDir") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.newDirMessage") }}</p>
<p>{{ t("prompts.newDirMessage") }}</p>
<input
id="focus-prompt"
class="input input--block"
type="text"
@keyup.enter="submit"
v-model.trim="name"
v-focus
tabindex="1"
/>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
:title="t('buttons.create')"
@click="submit"
tabindex="2"
>
{{ $t("buttons.create") }}
{{ t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script setup lang="ts">
import { inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api";
import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
export default {
name: "new-dir",
props: {
redirect: {
type: Boolean,
default: true,
},
base: {
type: [String, null],
default: null,
},
const $showError = inject<IToastError>("$showError")!;
const props = defineProps({
base: String,
redirect: {
type: Boolean,
default: true,
},
data: function () {
return {
name: "",
};
},
computed: {
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
});
// Build the path of the new directory.
let uri;
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (this.base) uri = this.base;
else if (this.isFiles) uri = this.$route.path + "/";
else uri = "/";
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
const name = ref<string>("");
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri);
if (this.redirect) {
this.$router.push({ path: uri });
} else if (!this.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
this.$store.commit("updateRequest", res);
}
} catch (e) {
this.$showError(e);
}
const submit = async (event: Event) => {
event.preventDefault();
if (name.value === "") return;
this.$store.commit("closeHovers");
},
},
// Build the path of the new directory.
let uri: string;
if (props.base) uri = props.base;
else if (fileStore.isFiles) uri = route.path + "/";
else uri = "/";
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(name.value) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri);
if (props.redirect) {
router.push({ path: uri });
} else if (!props.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
fileStore.updateRequest(res);
}
} catch (e) {
if (e instanceof Error) {
$showError(e);
}
}
layoutStore.closeHovers();
};
</script>

View File

@@ -1,14 +1,14 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.newFile") }}</h2>
<h2>{{ t("prompts.newFile") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.newFileMessage") }}</p>
<p>{{ t("prompts.newFileMessage") }}</p>
<input
id="focus-prompt"
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
@@ -18,63 +18,68 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
:aria-label="t('buttons.create')"
:title="t('buttons.create')"
>
{{ $t("buttons.create") }}
{{ t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script setup lang="ts">
import { inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: "new-file",
data: function () {
return {
name: "",
};
},
computed: {
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
const $showError = inject<IToastError>("$showError")!;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/";
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
const name = ref<string>("");
try {
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e);
}
const submit = async (event: Event) => {
event.preventDefault();
if (name.value === "") return;
this.$store.commit("closeHovers");
},
},
// Build the path of the new directory.
let uri = fileStore.isFiles ? route.path + "/" : "/";
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(name.value);
uri = uri.replace("//", "/");
try {
await api.post(uri);
router.push({ path: uri });
} catch (e) {
if (e instanceof Error) {
$showError(e);
}
}
layoutStore.closeHovers();
};
</script>

View File

@@ -1,22 +1,20 @@
<template>
<div>
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
<ModalsContainer />
</template>
<script>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import BaseModal from "./BaseModal.vue";
import Help from "./Help.vue";
import Info from "./Info.vue";
import Delete from "./Delete.vue";
import Rename from "./Rename.vue";
import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue";
import Rename from "./Rename.vue";
import Move from "./Move.vue";
import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue";
@@ -24,87 +22,61 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue";
import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue";
import Sidebar from "../Sidebar.vue";
import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
export default {
name: "prompts",
components: {
Info,
Delete,
Rename,
Download,
Move,
Copy,
Share,
NewFile,
NewDir,
Help,
Replace,
ReplaceRename,
Upload,
ShareDelete,
Sidebar,
DiscardEditorChanges,
},
data: function () {
return {
pluginData: {
buttons,
store: this.$store,
router: this.$router,
},
};
},
created() {
window.addEventListener("keydown", (event) => {
if (this.currentPrompt == null) return;
const layoutStore = useLayoutStore();
const promptName = this.currentPrompt.prompt;
const prompt = this.$refs[promptName];
const { currentPromptName } = storeToRefs(layoutStore);
if (event.code === "Escape") {
event.stopImmediatePropagation();
this.$store.commit("closeHovers");
}
const closeModal = ref<() => Promise<string>>();
if (event.code === "Enter") {
switch (promptName) {
case "delete":
prompt.submit();
break;
case "copy":
prompt.copy(event);
break;
case "move":
prompt.move(event);
break;
case "replace":
prompt.showConfirm(event);
break;
}
}
});
},
computed: {
...mapState(["plugins"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
showOverlay: function () {
return (
this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
);
const components = new Map<string, any>([
["info", Info],
["help", Help],
["delete", Delete],
["rename", Rename],
["move", Move],
["copy", Copy],
["newFile", NewFile],
["newDir", NewDir],
["download", Download],
["replace", Replace],
["replace-rename", ReplaceRename],
["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges],
]);
watch(currentPromptName, (newValue) => {
if (closeModal.value) {
closeModal.value();
closeModal.value = undefined;
}
const modal = components.get(newValue!);
if (!modal) return;
const { open, close } = useModal({
component: BaseModal,
slots: {
default: modal,
},
},
methods: {
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
});
closeModal.value = close;
open();
});
window.addEventListener("keydown", (event) => {
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
</script>

View File

@@ -10,8 +10,8 @@
>:
</p>
<input
id="focus-prompt"
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
@@ -21,7 +21,7 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
@@ -41,7 +41,9 @@
</template>
<script>
import { mapState, mapGetters } from "vuex";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
import { files as api } from "@/api";
@@ -55,13 +57,20 @@ export default {
created() {
this.name = this.oldName();
},
inject: ["$showError"],
computed: {
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.$store.commit("closeHovers");
this.closeHovers();
},
oldName: function () {
if (!this.isListing) {
@@ -96,12 +105,12 @@ export default {
return;
}
this.$store.commit("setReload", true);
this.reload = true;
} catch (e) {
this.$showError(e);
}
this.$store.commit("closeHovers");
this.closeHovers();
},
},
};

View File

@@ -11,9 +11,10 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
@@ -22,14 +23,17 @@
@click="currentPrompt.action"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
tabindex="2"
>
{{ $t("buttons.continue") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--red"
@click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
>
{{ $t("buttons.replace") }}
</button>
@@ -38,10 +42,16 @@
</template>
<script>
import { mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "replace",
computed: mapGetters(["currentPrompt"]),
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>

View File

@@ -11,9 +11,10 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
@@ -22,14 +23,17 @@
@click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')"
tabindex="2"
>
{{ $t("buttons.rename") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
>
{{ $t("buttons.replace") }}
</button>
@@ -38,10 +42,16 @@
</template>
<script>
import { mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "replace-rename",
computed: mapGetters(["currentPrompt"]),
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="card floating share__promt__card" id="share">
<div class="card floating" id="share">
<div class="card-title">
<h2>{{ $t("buttons.share") }}</h2>
</div>
@@ -25,9 +25,9 @@
<td class="small">
<button
class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
>
<i class="material-icons">content_paste</i>
</button>
@@ -35,9 +35,9 @@
<td class="small" v-if="hasDownloadLink()">
<button
class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
@@ -59,17 +59,20 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
@click="closeHovers"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')"
tabindex="2"
>
{{ $t("buttons.close") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--blue"
@click="() => switchListing()"
:aria-label="$t('buttons.new')"
:title="$t('buttons.new')"
tabindex="1"
>
{{ $t("buttons.new") }}
</button>
@@ -80,15 +83,22 @@
<div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input">
<input
v-focus
type="number"
max="2147483647"
min="1"
<vue-number-input
center
controls
size="small"
:max="2147483647"
:min="0"
@keyup.enter="submit"
v-model.trim="time"
v-model="time"
tabindex="1"
/>
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
<select
class="right"
v-model="unit"
:aria-label="$t('time.unit')"
tabindex="2"
>
<option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option>
@@ -100,6 +110,7 @@
class="input input--block"
type="password"
v-model.trim="password"
tabindex="3"
/>
</div>
@@ -109,14 +120,17 @@
@click="() => switchListing()"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="5"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--blue"
@click="submit"
:aria-label="$t('buttons.share')"
:title="$t('buttons.share')"
tabindex="4"
>
{{ $t("buttons.share") }}
</button>
@@ -126,16 +140,18 @@
</template>
<script>
import { mapState, mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api, pub as pub_api } from "@/api";
import moment from "moment/min/moment-with-locales";
import Clipboard from "clipboard";
import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
export default {
name: "share",
data: function () {
return {
time: "",
time: 0,
unit: "hours",
links: [],
clip: null,
@@ -143,9 +159,14 @@ export default {
listing: true,
};
},
inject: ["$showError", "$showSuccess"],
computed: {
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
url() {
if (!this.isListing) {
return this.$route.path;
@@ -172,23 +193,24 @@ export default {
this.$showError(e);
}
},
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy() {
this.clip.destroy();
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy(text).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
},
submit: async function () {
let isPermanent = !this.time || this.time == 0;
try {
let res = null;
if (isPermanent) {
if (!this.time) {
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
@@ -197,7 +219,7 @@ export default {
this.links.push(res);
this.sort();
this.time = "";
this.time = 0;
this.unit = "hours";
this.password = "";
@@ -220,7 +242,7 @@ export default {
}
},
humanTime(time) {
return moment(time * 1000).fromNow();
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.getShareURL(share);
@@ -242,7 +264,7 @@ export default {
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.$store.commit("closeHovers");
this.closeHovers();
}
this.listing = !this.listing;

View File

@@ -5,18 +5,21 @@
</div>
<div class="card-action">
<button
@click="$store.commit('closeHovers')"
@click="closeHovers"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
tabindex="1"
>
{{ $t("buttons.delete") }}
</button>
@@ -25,14 +28,16 @@
</template>
<script>
import { mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "share-delete",
computed: {
...mapGetters(["currentPrompt"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},

View File

@@ -1,38 +1,111 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2>
<h2>{{ t("prompts.upload") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p>
<p>{{ t("prompts.uploadMessage") }}</p>
</div>
<div class="card-action full">
<div @click="uploadFile" class="action">
<div
@click="uploadFile"
@keypress.enter="uploadFile"
class="action"
id="focus-prompt"
tabindex="1"
>
<i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div>
<div class="title">{{ t("buttons.file") }}</div>
</div>
<div @click="uploadFolder" class="action">
<div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div>
<div class="title">{{ t("buttons.folder") }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "upload",
methods: {
uploadFile: function () {
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
},
uploadFolder: function () {
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
},
},
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as upload from "@/utils/upload";
const { t } = useI18n();
const route = useRoute();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// TODO: this is a copy of the same function in FileListing.vue
const uploadInput = (event: Event) => {
layoutStore.closeHovers();
let files = (event.currentTarget as HTMLInputElement)?.files;
if (files === null) return;
let folder_upload = !!files[0].webkitRelativePath;
const uploadFiles: UploadList = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folder_upload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file,
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
let path = route.path.endsWith("/") ? route.path : route.path + "/";
let conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
if (conflict) {
layoutStore.showHover({
prompt: "replace",
action: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, false);
},
confirm: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, true);
},
});
return;
}
upload.handleFiles(uploadFiles, path);
};
const openUpload = (isFolder: boolean) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.webkitdirectory = isFolder;
// TODO: call the function in FileListing.vue instead
input.onchange = uploadInput;
input.click();
};
const uploadFile = () => {
openUpload(false);
};
const uploadFolder = () => {
openUpload(true);
};
</script>

View File

@@ -53,7 +53,9 @@
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
@@ -65,19 +67,20 @@ export default {
};
},
computed: {
...mapGetters([
...mapState(useUploadStore, [
"filesInUpload",
"filesInUploadCount",
"uploadSpeed",
"eta",
"getETA",
]),
...mapMutations(["resetUpload"]),
...mapWritableState(useFileStore, ["reload"]),
...mapActions(useUploadStore, ["reset"]),
formattedETA() {
if (!this.eta || this.eta === Infinity) {
if (!this.getETA || this.getETA === Infinity) {
return "--:--:--";
}
let totalSeconds = this.eta;
let totalSeconds = this.getETA;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
@@ -97,8 +100,8 @@ export default {
abortAllUploads();
buttons.done("upload");
this.open = false;
this.$store.commit("resetUpload");
this.$store.commit("setReload", true);
this.reset();
this.reload = true;
}
},
},

View File

@@ -1,5 +1,5 @@
<template>
<select v-on:change="change" :value="locale">
<select name="selectLanguage" v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }}
</option>
@@ -7,40 +7,45 @@
</template>
<script>
import { markRaw } from "vue";
export default {
name: "languages",
props: ["locale"],
data() {
let dataObj = {
locales: {
he: "he",
hu: "hu",
ar: "ar",
de: "de",
el: "el",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
sk: "sk",
"sv-se": "svSE",
tr: "tr",
ua: "ua",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
},
let dataObj = {};
const locales = {
he: "he",
hu: "hu",
ar: "ar",
de: "de",
el: "el",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
sk: "sk",
"sv-se": "svSE",
tr: "tr",
uk: "uk",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
};
// Vue3 reactivity breaks with this configuration
// so we need to use markRaw as a workaround
// https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
});

View File

@@ -1,18 +1,27 @@
<template>
<select v-on:change="change" :value="theme">
<option value="">{{ $t("settings.themes.light") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option>
<option value="">{{ t("settings.themes.default") }}</option>
<option value="light">{{ t("settings.themes.light") }}</option>
<option value="dark">{{ t("settings.themes.dark") }}</option>
</select>
</template>
<script>
export default {
name: "themes",
props: ["theme"],
methods: {
change(event) {
this.$emit("update:theme", event.target.value);
},
},
<script setup lang="ts">
import { SelectHTMLAttributes } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineProps<{
theme: UserTheme;
}>();
const emit = defineEmits<{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(e: "update:theme", val: string | null): void;
}>();
const change = (event: Event) => {
emit("update:theme", (event.target as SelectHTMLAttributes)?.value);
};
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<p v-if="!isDefault">
<label for="username">{{ $t("settings.username") }}</label>
<p v-if="!isDefault && props.user !== null">
<label for="username">{{ t("settings.username") }}</label>
<input
class="input input--block"
type="text"
@@ -11,7 +11,7 @@
</p>
<p v-if="!isDefault">
<label for="password">{{ $t("settings.password") }}</label>
<label for="password">{{ t("settings.password") }}</label>
<input
class="input input--block"
type="password"
@@ -22,9 +22,9 @@
</p>
<p>
<label for="scope">{{ $t("settings.scope") }}</label>
<label for="scope">{{ t("settings.scope") }}</label>
<input
:disabled="createUserDirData"
:disabled="createUserDirData ?? false"
:placeholder="scopePlaceholder"
class="input input--block"
type="text"
@@ -34,86 +34,89 @@
</p>
<p class="small" v-if="displayHomeDirectoryCheckbox">
<input type="checkbox" v-model="createUserDirData" />
{{ $t("settings.createUserHomeDirectory") }}
{{ t("settings.createUserHomeDirectory") }}
</p>
<p>
<label for="locale">{{ $t("settings.language") }}</label>
<label for="locale">{{ t("settings.language") }}</label>
<languages
class="input input--block"
id="locale"
:locale.sync="user.locale"
v-model:locale="user.locale"
></languages>
</p>
<p v-if="!isDefault">
<p v-if="!isDefault && user.perm">
<input
type="checkbox"
:disabled="user.perm.admin"
v-model="user.lockPassword"
/>
{{ $t("settings.lockPassword") }}
{{ t("settings.lockPassword") }}
</p>
<permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
<permissions v-model:perm="user.perm" />
<commands v-if="enableExec" v-model:commands="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" />
<h3>{{ t("settings.rules") }}</h3>
<p class="small">{{ t("settings.rulesHelp") }}</p>
<rules v-model:rules="user.rules" />
</div>
</div>
</template>
<script>
<script setup lang="ts">
import Languages from "./Languages.vue";
import Rules from "./Rules.vue";
import Permissions from "./Permissions.vue";
import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants";
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
export default {
name: "user",
data: () => {
return {
createUserDirData: false,
originalUserScope: "/",
};
},
components: {
Permissions,
Languages,
Rules,
Commands,
},
props: ["user", "createUserDir", "isNew", "isDefault"],
created() {
this.originalUserScope = this.user.scope;
this.createUserDirData = this.createUserDir;
},
computed: {
passwordPlaceholder() {
return this.isNew ? "" : this.$t("settings.avoidChanges");
},
scopePlaceholder() {
return this.createUserDir
? this.$t("settings.userScopeGenerationPlaceholder")
: "";
},
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir;
},
isExecEnabled: () => enableExec,
},
watch: {
"user.perm.admin": function () {
if (!this.user.perm.admin) return;
this.user.lockPassword = false;
},
createUserDirData() {
this.user.scope = this.createUserDirData ? "" : this.originalUserScope;
},
},
};
const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null);
const props = defineProps<{
user: IUserForm;
isNew: boolean;
isDefault: boolean;
createUserDir?: boolean;
}>();
onMounted(() => {
if (props.user.scope) {
originalUserScope.value = props.user.scope;
createUserDirData.value = props.createUserDir;
}
});
const passwordPlaceholder = computed(() =>
props.isNew ? "" : t("settings.avoidChanges")
);
const scopePlaceholder = computed(() =>
createUserDirData.value ? t("settings.userScopeGenerationPlaceholder") : ""
);
const displayHomeDirectoryCheckbox = computed(
() => props.isNew && createUserDirData.value
);
watch(
() => props.user,
() => {
if (!props.user?.perm?.admin) return;
props.user.lockPassword = false;
}
);
watch(createUserDirData, () => {
if (props.user?.scope) {
props.user.scope = createUserDirData.value
? ""
: originalUserScope.value ?? "";
}
});
</script>