mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-07-13 19:40:31 +00:00

macOS saves the download URL in the metadata of the downloaded file. This means that the downloaded file contains a metadata item with the JWT token of the user. If the user were to share this file with someone else, they would have access to their account using the JWT in the metadata during the validity of the JWT. The JWT has been removed from the URLs. Since the user is logged in, there is an authentication cookie set. A JWT in the URL is not necessary.
431 lines
12 KiB
Vue
431 lines
12 KiB
Vue
<template>
|
|
<div
|
|
id="previewer"
|
|
@touchmove.prevent.stop
|
|
@wheel.prevent.stop
|
|
@mousemove="toggleNavigation"
|
|
@touchstart="toggleNavigation"
|
|
>
|
|
<header-bar v-if="isPdf || isEpub || showNav">
|
|
<action icon="close" :label="$t('buttons.close')" @action="close()" />
|
|
<title>{{ name }}</title>
|
|
<action
|
|
:disabled="layoutStore.loading"
|
|
v-if="isResizeEnabled && fileStore.req?.type === 'image'"
|
|
:icon="fullSize ? 'photo_size_select_large' : 'hd'"
|
|
@action="toggleSize"
|
|
/>
|
|
|
|
<template #actions>
|
|
<action
|
|
:disabled="layoutStore.loading"
|
|
v-if="authStore.user?.perm.rename"
|
|
icon="mode_edit"
|
|
:label="$t('buttons.rename')"
|
|
show="rename"
|
|
/>
|
|
<action
|
|
:disabled="layoutStore.loading"
|
|
v-if="authStore.user?.perm.delete"
|
|
icon="delete"
|
|
:label="$t('buttons.delete')"
|
|
@action="deleteFile"
|
|
id="delete-button"
|
|
/>
|
|
<action
|
|
:disabled="layoutStore.loading"
|
|
v-if="authStore.user?.perm.download"
|
|
icon="file_download"
|
|
:label="$t('buttons.download')"
|
|
@action="download"
|
|
/>
|
|
<action
|
|
:disabled="layoutStore.loading"
|
|
icon="info"
|
|
:label="$t('buttons.info')"
|
|
show="info"
|
|
/>
|
|
</template>
|
|
</header-bar>
|
|
|
|
<div class="loading delayed" v-if="layoutStore.loading">
|
|
<div class="spinner">
|
|
<div class="bounce1"></div>
|
|
<div class="bounce2"></div>
|
|
<div class="bounce3"></div>
|
|
</div>
|
|
</div>
|
|
<template v-else>
|
|
<div class="preview">
|
|
<div v-if="isEpub" class="epub-reader">
|
|
<vue-reader
|
|
:location="location"
|
|
:url="raw"
|
|
:get-rendition="getRendition"
|
|
:epubInitOptions="{
|
|
requestCredentials: true,
|
|
}"
|
|
:epubOptions="{
|
|
allowPopups: true,
|
|
allowScriptedContent: true,
|
|
}"
|
|
@update:location="locationChange"
|
|
/>
|
|
<div class="size">
|
|
<button
|
|
@click="changeSize(Math.max(100, size - 10))"
|
|
class="reader-button"
|
|
>
|
|
<i class="material-icons">remove</i>
|
|
</button>
|
|
<button
|
|
@click="changeSize(Math.min(150, size + 10))"
|
|
class="reader-button"
|
|
>
|
|
<i class="material-icons">add</i>
|
|
</button>
|
|
<span>{{ size }}%</span>
|
|
</div>
|
|
</div>
|
|
<ExtendedImage v-else-if="fileStore.req?.type == 'image'" :src="raw" />
|
|
<audio
|
|
v-else-if="fileStore.req?.type == 'audio'"
|
|
ref="player"
|
|
:src="raw"
|
|
controls
|
|
:autoplay="autoPlay"
|
|
@play="autoPlay = true"
|
|
></audio>
|
|
<VideoPlayer
|
|
v-else-if="fileStore.req?.type == 'video'"
|
|
ref="player"
|
|
:source="raw"
|
|
:subtitles="subtitles"
|
|
:options="videoOptions"
|
|
>
|
|
</VideoPlayer>
|
|
<object v-else-if="isPdf" class="pdf" :data="raw"></object>
|
|
<div v-else-if="fileStore.req?.type == 'blob'" class="info">
|
|
<div class="title">
|
|
<i class="material-icons">feedback</i>
|
|
{{ $t("files.noPreview") }}
|
|
</div>
|
|
<div>
|
|
<a target="_blank" :href="downloadUrl" class="button button--flat">
|
|
<div>
|
|
<i class="material-icons">file_download</i
|
|
>{{ $t("buttons.download") }}
|
|
</div>
|
|
</a>
|
|
<a
|
|
target="_blank"
|
|
:href="raw"
|
|
class="button button--flat"
|
|
v-if="!fileStore.req?.isDir"
|
|
>
|
|
<div>
|
|
<i class="material-icons">open_in_new</i
|
|
>{{ $t("buttons.openFile") }}
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<button
|
|
@click="prev"
|
|
@mouseover="hoverNav = true"
|
|
@mouseleave="hoverNav = false"
|
|
:class="{ hidden: !hasPrevious || !showNav }"
|
|
:aria-label="$t('buttons.previous')"
|
|
:title="$t('buttons.previous')"
|
|
>
|
|
<i class="material-icons">chevron_left</i>
|
|
</button>
|
|
<button
|
|
@click="next"
|
|
@mouseover="hoverNav = true"
|
|
@mouseleave="hoverNav = false"
|
|
:class="{ hidden: !hasNext || !showNav }"
|
|
:aria-label="$t('buttons.next')"
|
|
:title="$t('buttons.next')"
|
|
>
|
|
<i class="material-icons">chevron_right</i>
|
|
</button>
|
|
<link rel="prefetch" :href="previousRaw" />
|
|
<link rel="prefetch" :href="nextRaw" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useStorage } from "@vueuse/core";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useFileStore } from "@/stores/file";
|
|
import { useLayoutStore } from "@/stores/layout";
|
|
|
|
import { files as api } from "@/api";
|
|
import { createURL } from "@/api/utils";
|
|
import { resizePreview } from "@/utils/constants";
|
|
import url from "@/utils/url";
|
|
import { throttle } from "lodash-es";
|
|
import HeaderBar from "@/components/header/HeaderBar.vue";
|
|
import Action from "@/components/header/Action.vue";
|
|
import ExtendedImage from "@/components/files/ExtendedImage.vue";
|
|
import VideoPlayer from "@/components/files/VideoPlayer.vue";
|
|
import { VueReader } from "vue-reader";
|
|
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import type { Rendition } from "epubjs";
|
|
import { getTheme } from "@/utils/theme";
|
|
|
|
const location = useStorage("book-progress", 0, undefined, {
|
|
serializer: {
|
|
read: (v) => JSON.parse(v),
|
|
write: (v) => JSON.stringify(v),
|
|
},
|
|
});
|
|
const size = useStorage("book-size", 120, undefined, {
|
|
serializer: {
|
|
read: (v) => JSON.parse(v),
|
|
write: (v) => JSON.stringify(v),
|
|
},
|
|
});
|
|
|
|
const locationChange = (epubcifi: number) => {
|
|
location.value = epubcifi;
|
|
};
|
|
let rendition: Rendition | null = null;
|
|
const changeSize = (val: number) => {
|
|
size.value = val;
|
|
rendition?.themes.fontSize(`${val}%`);
|
|
};
|
|
|
|
const getRendition = (_rendition: Rendition) => {
|
|
rendition = _rendition;
|
|
switch (getTheme()) {
|
|
case "dark": {
|
|
rendition.themes.override("color", "rgba(255, 255, 255, 0.6)");
|
|
break;
|
|
}
|
|
case "light": {
|
|
rendition.themes.override("color", "rgb(111, 111, 111)");
|
|
break;
|
|
}
|
|
}
|
|
rendition.themes.registerRules("h2Transparent", {
|
|
"h1,h2,h3,h4": {
|
|
"background-color": "transparent !important",
|
|
},
|
|
});
|
|
rendition?.themes.fontSize(`${size.value}%`);
|
|
rendition.themes.select("h2Transparent");
|
|
rendition.themes.override("background-color", "transparent", true);
|
|
};
|
|
|
|
const mediaTypes: ResourceType[] = ["image", "video", "audio", "blob"];
|
|
|
|
const previousLink = ref<string>("");
|
|
const nextLink = ref<string>("");
|
|
const listing = ref<ResourceItem[] | null>(null);
|
|
const name = ref<string>("");
|
|
const fullSize = ref<boolean>(false);
|
|
const showNav = ref<boolean>(true);
|
|
const navTimeout = ref<null | number>(null);
|
|
const hoverNav = ref<boolean>(false);
|
|
const autoPlay = ref<boolean>(false);
|
|
const previousRaw = ref<string>("");
|
|
const nextRaw = ref<string>("");
|
|
|
|
const player = ref<HTMLVideoElement | HTMLAudioElement | null>(null);
|
|
|
|
const $showError = inject<IToastError>("$showError")!;
|
|
|
|
const authStore = useAuthStore();
|
|
const fileStore = useFileStore();
|
|
const layoutStore = useLayoutStore();
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const hasPrevious = computed(() => previousLink.value !== "");
|
|
|
|
const hasNext = computed(() => nextLink.value !== "");
|
|
|
|
const downloadUrl = computed(() =>
|
|
fileStore.req ? api.getDownloadURL(fileStore.req, false) : ""
|
|
);
|
|
|
|
const raw = computed(() => {
|
|
if (fileStore.req?.type === "image" && !fullSize.value) {
|
|
return api.getPreviewURL(fileStore.req, "big");
|
|
}
|
|
|
|
if (isEpub.value) {
|
|
return createURL("api/raw" + fileStore.req?.path, {});
|
|
}
|
|
|
|
return downloadUrl.value;
|
|
});
|
|
|
|
const isPdf = computed(() => fileStore.req?.extension.toLowerCase() == ".pdf");
|
|
const isEpub = computed(
|
|
() => fileStore.req?.extension.toLowerCase() == ".epub"
|
|
);
|
|
|
|
const isResizeEnabled = computed(() => resizePreview);
|
|
|
|
const subtitles = computed(() => {
|
|
if (fileStore.req?.subtitles) {
|
|
return api.getSubtitlesURL(fileStore.req);
|
|
}
|
|
return [];
|
|
});
|
|
|
|
const videoOptions = computed(() => {
|
|
return { autoplay: autoPlay.value };
|
|
});
|
|
|
|
watch(route, () => {
|
|
updatePreview();
|
|
toggleNavigation();
|
|
});
|
|
|
|
// Specify hooks
|
|
onMounted(async () => {
|
|
window.addEventListener("keydown", key);
|
|
if (fileStore.oldReq) {
|
|
listing.value = fileStore.oldReq.items;
|
|
updatePreview();
|
|
}
|
|
});
|
|
|
|
onBeforeUnmount(() => window.removeEventListener("keydown", key));
|
|
|
|
// Specify methods
|
|
const deleteFile = () => {
|
|
layoutStore.showHover({
|
|
prompt: "delete",
|
|
confirm: () => {
|
|
if (listing.value === null) {
|
|
return;
|
|
}
|
|
listing.value = listing.value.filter((item) => item.name !== name.value);
|
|
|
|
if (hasNext.value) {
|
|
next();
|
|
} else if (!hasPrevious.value && !hasNext.value) {
|
|
close();
|
|
} else {
|
|
prev();
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const prev = () => {
|
|
hoverNav.value = false;
|
|
router.replace({ path: previousLink.value });
|
|
};
|
|
|
|
const next = () => {
|
|
hoverNav.value = false;
|
|
router.replace({ path: nextLink.value });
|
|
};
|
|
|
|
const key = (event: KeyboardEvent) => {
|
|
if (layoutStore.currentPrompt !== null) {
|
|
return;
|
|
}
|
|
if (event.which === 13 || event.which === 39) {
|
|
// right arrow
|
|
if (hasNext.value) next();
|
|
} else if (event.which === 37) {
|
|
// left arrow
|
|
if (hasPrevious.value) prev();
|
|
} else if (event.which === 27) {
|
|
// esc
|
|
close();
|
|
}
|
|
};
|
|
const updatePreview = async () => {
|
|
if (player.value && player.value.paused && !player.value.ended) {
|
|
autoPlay.value = false;
|
|
}
|
|
|
|
const dirs = route.fullPath.split("/");
|
|
name.value = decodeURIComponent(dirs[dirs.length - 1]);
|
|
|
|
if (!listing.value) {
|
|
try {
|
|
const path = url.removeLastDir(route.path);
|
|
const res = await api.fetch(path);
|
|
listing.value = res.items;
|
|
} catch (e: any) {
|
|
$showError(e);
|
|
}
|
|
}
|
|
|
|
previousLink.value = "";
|
|
nextLink.value = "";
|
|
if (listing.value) {
|
|
for (let i = 0; i < listing.value.length; i++) {
|
|
if (listing.value[i].name !== name.value) {
|
|
continue;
|
|
}
|
|
|
|
for (let j = i - 1; j >= 0; j--) {
|
|
if (mediaTypes.includes(listing.value[j].type)) {
|
|
previousLink.value = listing.value[j].url;
|
|
previousRaw.value = prefetchUrl(listing.value[j]);
|
|
break;
|
|
}
|
|
}
|
|
for (let j = i + 1; j < listing.value.length; j++) {
|
|
if (mediaTypes.includes(listing.value[j].type)) {
|
|
nextLink.value = listing.value[j].url;
|
|
nextRaw.value = prefetchUrl(listing.value[j]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
const prefetchUrl = (item: ResourceItem) => {
|
|
if (item.type !== "image") {
|
|
return "";
|
|
}
|
|
|
|
return fullSize.value
|
|
? api.getDownloadURL(item, true)
|
|
: api.getPreviewURL(item, "big");
|
|
};
|
|
|
|
const toggleSize = () => (fullSize.value = !fullSize.value);
|
|
|
|
const toggleNavigation = throttle(function () {
|
|
showNav.value = true;
|
|
|
|
if (navTimeout.value) {
|
|
clearTimeout(navTimeout.value);
|
|
}
|
|
|
|
navTimeout.value = window.setTimeout(() => {
|
|
showNav.value = false || hoverNav.value;
|
|
navTimeout.value = null;
|
|
}, 1500);
|
|
}, 500);
|
|
|
|
const close = () => {
|
|
fileStore.updateRequest(null);
|
|
|
|
const uri = url.removeLastDir(route.path) + "/";
|
|
router.push({ path: uri });
|
|
};
|
|
|
|
const download = () => window.open(downloadUrl.value);
|
|
</script>
|