Merge branch 'vue3-wip' into vue3

This commit is contained in:
Kloon ImKloon 2023-10-07 17:07:32 +02:00
commit f91c937626
No known key found for this signature in database
GPG Key ID: CCF1C86A995C5B6A
10 changed files with 735 additions and 669 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: any[] = [];
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

@ -7,17 +7,15 @@
</div>
</template>
<script>
export default {
name: "error-toast",
props: ["message", "reportText", "isReport"],
methods: {
clicked() {
window.open(
"https://github.com/filebrowser/filebrowser/issues/new/choose"
);
},
},
<script setup lang="ts">
defineProps<{
message: string;
reportText: string;
isReport: boolean;
}>();
const clicked = () => {
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
};
</script>

View File

@ -45,6 +45,7 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
</template>
<script>
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};

View File

@ -64,143 +64,153 @@
</div>
</template>
<script>
import { mapActions, mapState, mapWritableState } from "pinia";
<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";
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,
};
},
inject: ["$showError"],
watch: {
show(val, old) {
this.active = val === "search";
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
if (old === "search" && !this.active) {
if (this.reload) {
this.sReload = true;
}
const value = 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);
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(useFileStore, ["isListing"]),
...mapState(useLayoutStore, ["show"]),
...mapWritableState(useFileStore, { sReload: "reload" }),
boxes() {
return boxes;
},
isEmpty() {
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return "";
}
const $showError = inject<IToastError>("$showError")!;
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: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
open() {
this.showHover("search");
},
close(event) {
event.stopPropagation();
event.preventDefault();
this.closeHovers();
},
keyup(event) {
if (event.key === "Escape") {
this.close(event);
return;
}
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
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 { t } = useI18n();
if (this.value === "") {
return;
}
const route = useRoute();
let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/";
}
// @ts-ignore
watch(layoutStore.show, (newVal: string, oldVal: string) => {
active.value = newVal === "search";
this.ongoing = true;
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
}
try {
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error);
}
document.body.style.overflow = "auto";
reset();
value.value = "";
active.value = false;
input.value?.blur();
} else if (active.value) {
reload.value = false;
input.value?.focus();
document.body.style.overflow = "hidden";
}
});
this.ongoing = false;
},
},
watch(value, () => {
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 value.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 = () => {
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) => {
value.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 (value.value === "") {
return;
}
let path = route.path;
if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/";
}
ongoing.value = true;
try {
results.value = await search(path, value.value);
} catch (error: any) {
$showError(error);
}
ongoing.value = false;
};
</script>

View File

@ -13,261 +13,288 @@
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
</div>
</template>
<script>
<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;
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);
});
// @ts-ignore
watch(props.src, () => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
}
scale.value = 1;
setZoom();
setCenter();
});
// 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() ?? "";
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;
};
const onLoad = () => {
imageLoaded.value = true;
if (imgex.value === null) {
return;
}
imgex.value.classList.remove("image-ex-img-center");
setCenter();
imgex.value.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", onMouseUp);
let realSize = imgex.value.naturalWidth;
let displaySize = imgex.value.offsetWidth;
// Image is in portrait orientation
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
realSize = imgex.value.naturalHeight;
displaySize = imgex.value.offsetHeight;
}
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// Full size plus additional zoom
maxScale.value = fullScale + 4;
};
const onMouseUp = () => {
inDrag.value = false;
};
const onResize = throttle(function () {
if (imageLoaded.value) {
setCenter();
doMove(position.value.relative.x, position.value.relative.y);
}
}, 100);
const setCenter = () => {
if (container.value === null || imgex.value === null) {
return;
}
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
);
imgex.value.style.left = position.value.center.x + "px";
imgex.value.style.top = position.value.center.y + "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);
}
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%";
}
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);
}
};
window.addEventListener("resize", this.onResize);
},
beforeUnmount() {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
watch: {
src: function () {
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
const doMove = (x: number, y: number) => {
if (imgex.value === null) {
return;
}
const style = imgex.value.style;
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;
let posX = pxStringToNumber(style.left) + x;
let posY = pxStringToNumber(style.top) + y;
this.imageLoaded = true;
style.left = posX + "px";
style.top = posY + "px";
if (img === undefined) {
return;
}
position.value.relative.x = Math.abs(position.value.center.x - posX);
position.value.relative.y = Math.abs(position.value.center.y - posY);
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
if (posX < position.value.center.x) {
position.value.relative.x = position.value.relative.x * -1;
}
document.addEventListener("mouseup", this.onMouseUp);
let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
// Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
// Scale needed to display the image on full size
const fullScale = realSize / displaySize;
// 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;
this.position.center.x = Math.floor(
(container.clientWidth - img.clientWidth) / 2
);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
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
);
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;
style.left = posX + "px";
style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1;
}
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", "");
},
},
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,8 +34,7 @@
</div>
</template>
<script>
import { mapState, mapActions, mapWritableState } from "pinia";
<script setup lang="ts">
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@ -45,213 +44,233 @@ import { filesize } from "@/utils";
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,
};
},
inject: ["$showError"],
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
"path",
],
computed: {
...mapState(useAuthStore, ["user", "jwt"]),
...mapState(useFileStore, ["req", "selectedCount", "multiple"]),
...mapWritableState(useFileStore, ["reload", "selected"]),
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;
});
// ...mapActions(useFileStore, ["removeSelected"]),
// ...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
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: {
...mapActions(useFileStore, ["removeSelected"]),
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
humanSize: function () {
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
},
humanTime: function () {
if (this.readOnly == undefined && this.user.dateFormat) {
return dayjs(this.modified).format("L LT");
}
return dayjs(this.modified).fromNow();
},
dragStart: function () {
if (this.selectedCount === 0) {
this.selected.push(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) => {
if (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 instanceof KeyboardEvent &&
event.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.selected = [];
this.selected.push(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return;
return;
}
event.preventDefault();
let el = event.target;
if (
event instanceof KeyboardEvent &&
!singleClick.value &&
!event.ctrlKey &&
!event.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.reload = true;
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false;
let rename = false;
if (conflict) {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
},
itemClick: function (event) {
if (this.singleClick && !this.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.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.selected.indexOf(fi) == -1) {
this.selected.push(fi);
}
}
return;
}
if (
!this.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!this.multiple
) {
this.selected = [];
}
this.selected.push(this.index);
},
open: function () {
this.$router.push({ path: this.url });
},
},
const open = () => {
router.push({ path: props.url });
};
</script>

View File

@ -6,24 +6,28 @@
</button>
</template>
<script>
import { mapActions } from "pinia";
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
export default {
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
...mapActions(useLayoutStore, ["showHover"]),
action: function () {
if (this.show) {
this.showHover(this.show);
}
defineProps<{
icon?: string;
label?: any;
counter?: any;
show?: any;
}>();
this.$emit("action");
},
},
const emit = defineEmits<{
(e: "action"): any;
}>();
const layoutStore = useLayoutStore();
const action = () => {
if (layoutStore.show) {
// TODO: is not very pretty
layoutStore.showHover(layoutStore.show as string);
}
emit("action");
};
</script>
<style></style>

View File

@ -1,58 +1,56 @@
<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="showHover('sidebar')"
:label="t('buttons.toggleSidebar')"
@action="layoutStore.showHover('sidebar')"
/>
<slot />
<div id="dropdown" :class="{ active: this.show === 'more' }">
<div id="dropdown" :class="{ active: layoutStore.show === '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="showHover('more')"
:label="t('buttons.more')"
@action="layoutStore.showHover('more')"
/>
<div class="overlay" v-show="this.show == 'more'" @click="closeHovers" />
<div
class="overlay"
v-show="layoutStore.show == 'more'"
@click="layoutStore.closeHovers"
/>
</header>
</template>
<script>
import { mapActions, mapState } from "pinia";
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue";
import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default {
name: "header-bar",
props: ["showLogo", "showMenu"],
components: {
Action,
},
data: function () {
return {
logoURL,
};
},
computed: {
...mapState(useLayoutStore, ["show"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
},
};
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

@ -18,13 +18,13 @@ interface Resource extends ResourceBase {
sorting: Sorting;
hash?: string;
token?: string;
index?: number;
index: number;
subtitles?: string[];
content?: string;
}
interface ResourceItem extends ResourceBase {
index?: number;
index: number;
subtitles?: string[];
}

1
frontend/src/types/utif.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "utif";