Merge branch 'vue3-wip' into vue3
This commit is contained in:
commit
f91c937626
@ -3,8 +3,8 @@
|
|||||||
<component
|
<component
|
||||||
:is="element"
|
:is="element"
|
||||||
:to="base || ''"
|
:to="base || ''"
|
||||||
:aria-label="$t('files.home')"
|
:aria-label="t('files.home')"
|
||||||
:title="$t('files.home')"
|
:title="t('files.home')"
|
||||||
>
|
>
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</component>
|
</component>
|
||||||
@ -18,58 +18,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
name: "breadcrumbs",
|
import { useI18n } from "vue-i18n";
|
||||||
props: ["base", "noLink"],
|
import { useRoute } from "vue-router";
|
||||||
computed: {
|
|
||||||
items() {
|
|
||||||
const relativePath = this.$route.path.replace(this.base, "");
|
|
||||||
let parts = relativePath.split("/");
|
|
||||||
|
|
||||||
if (parts[0] === "") {
|
const { t } = useI18n();
|
||||||
parts.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[parts.length - 1] === "") {
|
const route = useRoute();
|
||||||
parts.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let breadcrumbs = [];
|
const props = defineProps<{
|
||||||
|
base: string;
|
||||||
|
noLink?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
const items = computed(() => {
|
||||||
if (i === 0) {
|
const relativePath = route.path.replace(props.base, "");
|
||||||
breadcrumbs.push({
|
let parts = relativePath.split("/");
|
||||||
name: decodeURIComponent(parts[i]),
|
|
||||||
url: this.base + "/" + parts[i] + "/",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
breadcrumbs.push({
|
|
||||||
name: decodeURIComponent(parts[i]),
|
|
||||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (breadcrumbs.length > 3) {
|
if (parts[0] === "") {
|
||||||
while (breadcrumbs.length !== 4) {
|
parts.shift();
|
||||||
breadcrumbs.shift();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumbs[0].name = "...";
|
if (parts[parts.length - 1] === "") {
|
||||||
}
|
parts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
return breadcrumbs;
|
let breadcrumbs: any[] = [];
|
||||||
},
|
|
||||||
element() {
|
|
||||||
if (this.noLink !== undefined) {
|
|
||||||
return "span";
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@ -7,17 +7,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
defineProps<{
|
||||||
name: "error-toast",
|
message: string;
|
||||||
props: ["message", "reportText", "isReport"],
|
reportText: string;
|
||||||
methods: {
|
isReport: boolean;
|
||||||
clicked() {
|
}>();
|
||||||
window.open(
|
|
||||||
"https://github.com/filebrowser/filebrowser/issues/new/choose"
|
const clicked = () => {
|
||||||
);
|
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,7 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// We're leaving this untouched as you can read in the beginning
|
||||||
var isNumber = function (n) {
|
var isNumber = function (n) {
|
||||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -64,143 +64,153 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { search } from "@/api";
|
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" },
|
image: { label: "images", icon: "insert_photo" },
|
||||||
audio: { label: "music", icon: "volume_up" },
|
audio: { label: "music", icon: "volume_up" },
|
||||||
video: { label: "video", icon: "movie" },
|
video: { label: "video", icon: "movie" },
|
||||||
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "search",
|
const fileStore = useFileStore();
|
||||||
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";
|
|
||||||
|
|
||||||
if (old === "search" && !this.active) {
|
const value = ref<string>("");
|
||||||
if (this.reload) {
|
const active = ref<boolean>(false);
|
||||||
this.sReload = true;
|
const ongoing = ref<boolean>(false);
|
||||||
}
|
const results = ref<any[]>([]);
|
||||||
|
const reload = ref<boolean>(false);
|
||||||
|
const resultsCount = ref<number>(50);
|
||||||
|
|
||||||
document.body.style.overflow = "auto";
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
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 "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value === ""
|
const input = ref<HTMLInputElement | null>(null);
|
||||||
? this.$t("search.typeToSearch")
|
const result = ref<HTMLElement | null>(null);
|
||||||
: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.results.length = 0;
|
const { t } = useI18n();
|
||||||
},
|
|
||||||
init(string) {
|
|
||||||
this.value = `${string} `;
|
|
||||||
this.$refs.input.focus();
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.ongoing = false;
|
|
||||||
this.resultsCount = 50;
|
|
||||||
this.results = [];
|
|
||||||
},
|
|
||||||
async submit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (this.value === "") {
|
const route = useRoute();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = this.$route.path;
|
// @ts-ignore
|
||||||
if (!this.isListing) {
|
watch(layoutStore.show, (newVal: string, oldVal: string) => {
|
||||||
path = url.removeLastDir(path) + "/";
|
active.value = newVal === "search";
|
||||||
}
|
|
||||||
|
|
||||||
this.ongoing = true;
|
if (oldVal === "search" && !active.value) {
|
||||||
|
if (reload.value) {
|
||||||
|
fileStore.reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
document.body.style.overflow = "auto";
|
||||||
this.results = await search(path, this.value);
|
reset();
|
||||||
} catch (error) {
|
value.value = "";
|
||||||
this.$showError(error);
|
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>
|
</script>
|
||||||
|
|||||||
@ -13,261 +13,288 @@
|
|||||||
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
import UTIF from "utif";
|
import UTIF from "utif";
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
export default {
|
interface IProps {
|
||||||
props: {
|
src: string;
|
||||||
src: String,
|
moveDisabledTime: number;
|
||||||
moveDisabledTime: {
|
classList: any[];
|
||||||
type: Number,
|
zoomStep: number;
|
||||||
default: () => 200,
|
}
|
||||||
},
|
|
||||||
classList: {
|
const props = withDefaults(defineProps<IProps>(), {
|
||||||
type: Array,
|
moveDisabledTime: () => 200,
|
||||||
default: () => [],
|
classList: () => [],
|
||||||
},
|
zoomStep: () => 0.25,
|
||||||
zoomStep: {
|
});
|
||||||
type: Number,
|
|
||||||
default: () => 0.25,
|
const scale = ref<number>(1);
|
||||||
},
|
const lastX = ref<number | null>(null);
|
||||||
},
|
const lastY = ref<number | null>(null);
|
||||||
data() {
|
const inDrag = ref<boolean>(false);
|
||||||
return {
|
const touches = ref<number>(0);
|
||||||
scale: 1,
|
const lastTouchDistance = ref<number | null>(0);
|
||||||
lastX: null,
|
const moveDisabled = ref<boolean>(false);
|
||||||
lastY: null,
|
const disabledTimer = ref<number | null>(null);
|
||||||
inDrag: false,
|
const imageLoaded = ref<boolean>(false);
|
||||||
touches: 0,
|
const position = ref<{
|
||||||
lastTouchDistance: 0,
|
center: { x: number; y: number };
|
||||||
moveDisabled: false,
|
relative: { x: number; y: number };
|
||||||
disabledTimer: null,
|
}>({
|
||||||
imageLoaded: false,
|
center: { x: 0, y: 0 },
|
||||||
position: {
|
relative: { x: 0, y: 0 },
|
||||||
center: { x: 0, y: 0 },
|
});
|
||||||
relative: { x: 0, y: 0 },
|
const maxScale = ref<number>(4);
|
||||||
},
|
const minScale = ref<number>(0.25);
|
||||||
maxScale: 4,
|
|
||||||
minScale: 0.25,
|
// Refs
|
||||||
};
|
const imgex = ref<HTMLImageElement | null>(null);
|
||||||
},
|
const container = ref<HTMLDivElement | null>(null);
|
||||||
mounted() {
|
|
||||||
if (!this.decodeUTIF()) {
|
onMounted(() => {
|
||||||
this.$refs.imgex.src = this.src;
|
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));
|
event.preventDefault();
|
||||||
// set width and height if they are zero
|
};
|
||||||
if (getComputedStyle(container).width === "0px") {
|
|
||||||
container.style.width = "100%";
|
const zoomAuto = (event: Event) => {
|
||||||
}
|
switch (scale.value) {
|
||||||
if (getComputedStyle(container).height === "0px") {
|
case 1:
|
||||||
container.style.height = "100%";
|
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);
|
const doMove = (x: number, y: number) => {
|
||||||
},
|
if (imgex.value === null) {
|
||||||
beforeUnmount() {
|
return;
|
||||||
window.removeEventListener("resize", this.onResize);
|
}
|
||||||
document.removeEventListener("mouseup", this.onMouseUp);
|
const style = imgex.value.style;
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
src: function () {
|
|
||||||
if (!this.decodeUTIF()) {
|
|
||||||
this.$refs.imgex.src = this.src;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scale = 1;
|
let posX = pxStringToNumber(style.left) + x;
|
||||||
this.setZoom();
|
let posY = pxStringToNumber(style.top) + y;
|
||||||
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;
|
|
||||||
|
|
||||||
this.imageLoaded = true;
|
style.left = posX + "px";
|
||||||
|
style.top = posY + "px";
|
||||||
|
|
||||||
if (img === undefined) {
|
position.value.relative.x = Math.abs(position.value.center.x - posX);
|
||||||
return;
|
position.value.relative.y = Math.abs(position.value.center.y - posY);
|
||||||
}
|
|
||||||
|
|
||||||
img.classList.remove("image-ex-img-center");
|
if (posX < position.value.center.x) {
|
||||||
this.setCenter();
|
position.value.relative.x = position.value.relative.x * -1;
|
||||||
img.classList.add("image-ex-img-ready");
|
}
|
||||||
|
|
||||||
document.addEventListener("mouseup", this.onMouseUp);
|
if (posY < position.value.center.y) {
|
||||||
|
position.value.relative.y = position.value.relative.y * -1;
|
||||||
let realSize = img.naturalWidth;
|
}
|
||||||
let displaySize = img.offsetWidth;
|
};
|
||||||
|
const wheelMove = (event: WheelEvent) => {
|
||||||
// Image is in portrait orientation
|
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
|
||||||
if (img.naturalHeight > img.naturalWidth) {
|
setZoom();
|
||||||
realSize = img.naturalHeight;
|
};
|
||||||
displaySize = img.offsetHeight;
|
const setZoom = () => {
|
||||||
}
|
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
|
||||||
|
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
|
||||||
// Scale needed to display the image on full size
|
if (imgex.value !== null)
|
||||||
const fullScale = realSize / displaySize;
|
imgex.value.style.transform = `scale(${scale.value})`;
|
||||||
|
};
|
||||||
// Full size plus additional zoom
|
const pxStringToNumber = (style: string) => {
|
||||||
this.maxScale = fullScale + 4;
|
return +style.replace("px", "");
|
||||||
},
|
|
||||||
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", "");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
||||||
v-lazy="thumbnailUrl"
|
v-lazy="thumbnailUrl"
|
||||||
/>
|
/>
|
||||||
<i v-else class="material-icons"></i>
|
<i v-else class="material-icons"></i>
|
||||||
@ -34,8 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapActions, mapWritableState } from "pinia";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
@ -45,213 +44,233 @@ import { filesize } from "@/utils";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { computed, inject, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
export default {
|
const touches = ref<number>(0);
|
||||||
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;
|
|
||||||
|
|
||||||
for (let i of this.selected) {
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
if (this.req.items[i].url === this.url) {
|
const router = useRouter();
|
||||||
return false;
|
|
||||||
}
|
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;
|
if (el !== null) el.style.opacity = "1";
|
||||||
},
|
}
|
||||||
thumbnailUrl() {
|
};
|
||||||
const file = {
|
|
||||||
path: this.path,
|
|
||||||
modified: this.modified,
|
|
||||||
};
|
|
||||||
|
|
||||||
return api.getPreviewURL(file, "thumb");
|
const drop = async (event: Event) => {
|
||||||
},
|
if (!canDrop.value) return;
|
||||||
isThumbsEnabled() {
|
event.preventDefault();
|
||||||
return enableThumbs;
|
|
||||||
},
|
if (fileStore.selectedCount === 0) return;
|
||||||
},
|
|
||||||
methods: {
|
let el = event.target as HTMLElement | null;
|
||||||
...mapActions(useFileStore, ["removeSelected"]),
|
for (let i = 0; i < 5; i++) {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
if (el !== null && !el.classList.contains("item")) {
|
||||||
humanSize: function () {
|
el = el.parentElement;
|
||||||
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
}
|
||||||
},
|
}
|
||||||
humanTime: function () {
|
|
||||||
if (this.readOnly == undefined && this.user.dateFormat) {
|
let items: any[] = [];
|
||||||
return dayjs(this.modified).format("L LT");
|
|
||||||
}
|
for (let i of fileStore.selected) {
|
||||||
return dayjs(this.modified).fromNow();
|
if (fileStore.req) {
|
||||||
},
|
items.push({
|
||||||
dragStart: function () {
|
from: fileStore.req?.items[i].url,
|
||||||
if (this.selectedCount === 0) {
|
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||||
this.selected.push(this.index);
|
name: fileStore.req?.items[i].name,
|
||||||
return;
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
return;
|
||||||
this.selected = [];
|
}
|
||||||
this.selected.push(this.index);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dragOver: function (event) {
|
|
||||||
if (!this.canDrop) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
if (
|
||||||
let el = event.target;
|
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++) {
|
const open = () => {
|
||||||
if (!el.classList.contains("item")) {
|
router.push({ path: props.url });
|
||||||
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 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -6,24 +6,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
defineProps<{
|
||||||
name: "action",
|
icon?: string;
|
||||||
props: ["icon", "label", "counter", "show"],
|
label?: any;
|
||||||
methods: {
|
counter?: any;
|
||||||
...mapActions(useLayoutStore, ["showHover"]),
|
show?: any;
|
||||||
action: function () {
|
}>();
|
||||||
if (this.show) {
|
|
||||||
this.showHover(this.show);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|||||||
@ -1,58 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
<img v-if="showLogo" :src="logoURL" />
|
||||||
<action
|
<Action
|
||||||
v-if="showMenu !== undefined"
|
v-if="showMenu"
|
||||||
class="menu-button"
|
class="menu-button"
|
||||||
icon="menu"
|
icon="menu"
|
||||||
:label="$t('buttons.toggleSidebar')"
|
:label="t('buttons.toggleSidebar')"
|
||||||
@action="showHover('sidebar')"
|
@action="layoutStore.showHover('sidebar')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<div id="dropdown" :class="{ active: this.show === 'more' }">
|
<div id="dropdown" :class="{ active: layoutStore.show === 'more' }">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<action
|
<Action
|
||||||
v-if="this.$slots.actions"
|
v-if="ifActionsSlot"
|
||||||
id="more"
|
id="more"
|
||||||
icon="more_vert"
|
icon="more_vert"
|
||||||
:label="$t('buttons.more')"
|
:label="t('buttons.more')"
|
||||||
@action="showHover('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>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import { logoURL } from "@/utils/constants";
|
import { logoURL } from "@/utils/constants";
|
||||||
|
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
|
import { computed, useSlots } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
export default {
|
defineProps<{
|
||||||
name: "header-bar",
|
showLogo?: boolean;
|
||||||
props: ["showLogo", "showMenu"],
|
showMenu?: boolean;
|
||||||
components: {
|
}>();
|
||||||
Action,
|
|
||||||
},
|
const layoutStore = useLayoutStore();
|
||||||
data: function () {
|
const slots = useSlots();
|
||||||
return {
|
|
||||||
logoURL,
|
const { t } = useI18n();
|
||||||
};
|
|
||||||
},
|
const ifActionsSlot = computed(() => (slots.actions ? true : false));
|
||||||
computed: {
|
|
||||||
...mapState(useLayoutStore, ["show"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
4
frontend/src/types/file.d.ts
vendored
4
frontend/src/types/file.d.ts
vendored
@ -18,13 +18,13 @@ interface Resource extends ResourceBase {
|
|||||||
sorting: Sorting;
|
sorting: Sorting;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
index?: number;
|
index: number;
|
||||||
subtitles?: string[];
|
subtitles?: string[];
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceItem extends ResourceBase {
|
interface ResourceItem extends ResourceBase {
|
||||||
index?: number;
|
index: number;
|
||||||
subtitles?: string[];
|
subtitles?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
frontend/src/types/utif.d.ts
vendored
Normal file
1
frontend/src/types/utif.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module "utif";
|
||||||
Loading…
Reference in New Issue
Block a user