From 8b6b6b622ef37526c81297ee69b97e62e41c1438 Mon Sep 17 00:00:00 2001 From: "kissudad@outlook.com" Date: Sat, 6 Apr 2024 16:35:47 +0800 Subject: [PATCH] feat: Optimize shared file viewing experience - Add and update APIs for thumbnail display and file sorting. - Refresh frontend views and components to enhance shared file presentation. --- frontend/src/api/pub.ts | 16 +++- frontend/src/components/files/ListingItem.vue | 19 ++-- frontend/src/stores/auth.ts | 95 ++++++++++++------- frontend/src/views/Share.vue | 93 +++++++----------- frontend/src/views/files/FileListing.vue | 9 +- http/http.go | 2 + http/preview.go | 3 +- http/public.go | 35 ++++++- 8 files changed, 168 insertions(+), 104 deletions(-) diff --git a/frontend/src/api/pub.ts b/frontend/src/api/pub.ts index 4328f64c..dadf2a68 100644 --- a/frontend/src/api/pub.ts +++ b/frontend/src/api/pub.ts @@ -1,11 +1,12 @@ import { fetchURL, removePrefix, createURL } from "./utils"; import { baseURL } from "@/utils/constants"; - +import { useAuthStore } from "@/stores/auth"; export async function fetch(url: string, password: string = "") { url = removePrefix(url); + const authStore = useAuthStore(); const res = await fetchURL( - `/api/public/share${url}`, + `/api/public/share${url}?s=${authStore.shareConfig.sortBy}&a=${Number(authStore.shareConfig.asc)}`, { headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) }, }, @@ -73,3 +74,14 @@ export function getDownloadURL(res: Resource, inline = false) { return createURL("api/public/dl/" + res.hash + res.path, params, false); } + +export function getPreviewURL(file: ResourceItem, size: string) { + const authStore = useAuthStore(); + const params = { + inline: "true", + token: authStore.guestJwt, + key: Date.parse(file.modified), + }; + + return createURL("api/public/preview/" + size + file.path, params, false); +} diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 51093ce8..11eb1b0f 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -14,10 +14,7 @@ :aria-selected="isSelected" >
- +
@@ -42,15 +39,16 @@ import { useLayoutStore } from "@/stores/layout"; import { enableThumbs } from "@/utils/constants"; import { filesize } from "@/utils"; import dayjs from "dayjs"; -import { files as api } from "@/api"; +import { files as api, pub as pub_api } from "@/api"; import * as upload from "@/utils/upload"; import { computed, inject, ref } from "vue"; -import { useRouter } from "vue-router"; +import { useRouter, useRoute } from "vue-router"; const touches = ref(0); const $showError = inject("$showError")!; const router = useRouter(); +const route = useRoute(); const props = defineProps<{ name: string; @@ -95,8 +93,13 @@ const thumbnailUrl = computed(() => { path: props.path, modified: props.modified, }; - - return api.getPreviewURL(file as Resource, "thumb"); + if (route.name === "Share") { + const hash = props.url.split("/")[2]; + file.path = `/${hash}${props.path}`; + return pub_api.getPreviewURL(file as Resource, "thumb"); + } else { + return api.getPreviewURL(file as Resource, "thumb"); + } }); const isThumbsEnabled = computed(() => { diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index 459141ad..2f5f653a 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -1,41 +1,70 @@ import { defineStore } from "pinia"; import { detectLocale, setLocale } from "@/i18n"; import { cloneDeep } from "lodash-es"; +import { useStorage } from "@vueuse/core"; +import { computed, ref } from "vue"; -export const useAuthStore = defineStore("auth", { - // convert to a function - state: (): { - user: IUser | null; - jwt: string; - } => ({ - user: null, - jwt: "", - }), - getters: { - // user and jwt getter removed, no longer needed - isLoggedIn: (state) => state.user !== null, - }, - actions: { - // no context as first argument, use `this` instead - setUser(user: IUser) { - if (user === null) { - this.user = null; - return; +export const useAuthStore = defineStore("auth", () => { + const registeredUser = ref(null); + const jwt = ref(""); + + const guestJwt = ref(""); + const guestUser = useStorage("guest", { + locale: "zh-cn", + viewMode: "list", + singleClick: false, + perm: { create: false }, + }); + + const shareConfig = ref({ sortBy: "name", asc: false }); + const isLoggedIn = computed(() => registeredUser.value !== null); + const user = computed({ + get: () => + isLoggedIn.value ? registeredUser.value : (guestUser.value as IUser), + set: (val) => { + if (isLoggedIn.value) { + registeredUser.value = val; + } else { + guestUser.value = val; } + }, + }); - setLocale(user.locale || detectLocale()); - this.user = user; - }, - updateUser(user: Partial) { - if (user.locale) { - setLocale(user.locale); - } + function setUser(_user: IUser) { + if (_user === null) { + registeredUser.value = null; + return; + } - this.user = { ...this.user, ...cloneDeep(user) } as IUser; - }, - // easily reset state using `$reset` - clearUser() { - this.$reset(); - }, - }, + setLocale(_user.locale || detectLocale()); + registeredUser.value = _user; + } + + function updateUser(_user: Partial) { + if (_user.locale) { + setLocale(_user.locale); + } + + user.value = { + ...user.value, + ...cloneDeep(_user), + } as IUser; + } + // easily reset state using `$reset` + function clearUser() { + registeredUser.value = null; + } + + return { + jwt, + guestJwt, + shareConfig, + + isLoggedIn, + user, + + setUser, + updateUser, + clearUser, + }; }); diff --git a/frontend/src/views/Share.vue b/frontend/src/views/Share.vue index 53f0cfb1..1beb6606 100644 --- a/frontend/src/views/Share.vue +++ b/frontend/src/views/Share.vue @@ -95,7 +95,8 @@ v-if="!req.isDir" class="share__box__element share__box__center share__box__icon" > - {{ icon }} + + {{ icon }}
(null); const showLimit = ref(100); @@ -331,7 +293,9 @@ const { t } = useI18n({}); const route = useRoute(); const fileStore = useFileStore(); +const { reload } = storeToRefs(fileStore); const layoutStore = useLayoutStore(); +const authStore = useAuthStore(); watch(route, () => { showLimit.value = 100; @@ -352,16 +316,27 @@ const icon = computed(() => { }); const link = computed(() => (req.value ? api.getDownloadURL(req.value) : "")); + +// 用于转换 URL 的辅助函数 +function transformUrl(url: string) { + return url.replace(/share/, "api/public/dl") + "?token=" + token.value; +} const raw = computed(() => { - return req.value - ? req.value.items[fileStore.selected[0]].url.replace( - /share/, - "api/public/dl" - ) + - "?token=" + - token.value - : ""; + // 如果 req.value 不存在,则直接返回空字符串 + if (!req.value) { + return ""; + } + + // 如果 req.value 是目录 + if (req.value.isDir) { + const selectedItemUrl = req.value.items[fileStore.selected[0]].url; + return transformUrl(selectedItemUrl); + } + + // 如果 req.value 不是目录 + return transformUrl(req.value.url); }); + const inlineLink = computed(() => req.value ? api.getDownloadURL(req.value, true) : "" ); @@ -382,7 +357,6 @@ const modTime = computed(() => ); // Functions -const base64 = (name: any) => Base64.encodeURI(name); const play = () => { if (tag.value) { audio.value?.pause(); @@ -392,6 +366,10 @@ const play = () => { tag.value = true; } }; + +watch(reload, (newValue) => { + newValue && fetchData(); +}); const fetchData = async () => { fileStore.reload = false; fileStore.selected = []; @@ -408,12 +386,12 @@ const fetchData = async () => { let url = route.path; if (url === "") url = "/"; if (url[0] !== "/") url = "/" + url; - try { const file = await api.fetch(url, password.value); file.hash = hash.value; token.value = file.token || ""; + authStore.guestJwt = token.value; fileStore.updateRequest(file); document.title = `${file.name} - ${document.title}`; @@ -504,6 +482,7 @@ onMounted(async () => { hash.value = route.params.path[0]; window.addEventListener("keydown", keyEvent); await fetchData(); + fileStore.selected[0] = 0; }); onBeforeUnmount(() => { diff --git a/frontend/src/views/files/FileListing.vue b/frontend/src/views/files/FileListing.vue index a26ac67e..4d6f41eb 100644 --- a/frontend/src/views/files/FileListing.vue +++ b/frontend/src/views/files/FileListing.vue @@ -74,6 +74,7 @@ /> { "sorting", ]); } + authStore.shareConfig.sortBy = by; + authStore.shareConfig.asc = asc; } catch (e: any) { $showError(e); } @@ -904,8 +907,10 @@ const switchView = async () => { viewMode: modes[authStore.user?.viewMode ?? "list"] || "list", }; - // @ts-ignore - users.update(data, ["viewMode"]).catch($showError); + if (authStore.isLoggedIn) { + // @ts-ignore + users.update(data, ["viewMode"]).catch($showError); + } // @ts-ignore authStore.updateUser(data); diff --git a/http/http.go b/http/http.go index f91ec426..9ed79b48 100644 --- a/http/http.go +++ b/http/http.go @@ -90,6 +90,8 @@ func NewHandler( public := api.PathPrefix("/public").Subrouter() public.PathPrefix("/dl").Handler(monkey(publicDlHandler, "/api/public/dl/")).Methods("GET") public.PathPrefix("/share").Handler(monkey(publicShareHandler, "/api/public/share/")).Methods("GET") + public.PathPrefix("/preview/{size}/{path:.*}"). + Handler(monkey(publicPreviewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/public/preview")).Methods("GET") return stripPrefix(server.BaseURL, r), nil } diff --git a/http/preview.go b/http/preview.go index 1abc6019..2fbefa9b 100644 --- a/http/preview.go +++ b/http/preview.go @@ -111,7 +111,8 @@ func handleImagePreview( } func createPreview(imgSvc ImgService, fileCache FileCache, - file *files.FileInfo, previewSize PreviewSize) ([]byte, error) { + file *files.FileInfo, previewSize PreviewSize, +) ([]byte, error) { fd, err := file.Fs.Open(file.Path) if err != nil { return nil, err diff --git a/http/public.go b/http/public.go index 5e9e01ba..964a9c03 100644 --- a/http/public.go +++ b/http/public.go @@ -2,12 +2,14 @@ package http import ( "errors" + "fmt" "net/http" "net/url" "path" "path/filepath" "strings" + "github.com/gorilla/mux" "github.com/spf13/afero" "golang.org/x/crypto/bcrypt" @@ -98,7 +100,16 @@ var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Reques file := d.raw.(*files.FileInfo) if file.IsDir { - file.Listing.Sorting = files.Sorting{By: "name", Asc: false} + sortBy := r.URL.Query().Get("s") + ascStr := r.URL.Query().Get("a") + asc := false + if sortBy == "" { + sortBy = "name" + } + if ascStr == "1" { + asc = true + } + file.Listing.Sorting = files.Sorting{By: sortBy, Asc: asc} file.Listing.ApplySort() return renderJSON(w, r, file) } @@ -115,6 +126,28 @@ var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, return rawDirHandler(w, r, d, file) }) +func publicPreviewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc { + return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + vars := mux.Vars(r) + previewSize, err := ParsePreviewSize(vars["size"]) + if err != nil { + return http.StatusBadRequest, err + } + r.URL.Path = vars["path"] + return withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + file := d.raw.(*files.FileInfo) + setContentDisposition(w, r, file) + + switch file.Type { + case "image": + return handleImagePreview(w, r, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview) + default: + return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type) + } + })(w, r, d) + } +} + func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) { if l.PasswordHash == "" { return 0, nil