added media filter, improved search, ui changes

This commit is contained in:
subnub
2024-10-16 00:31:23 -04:00
parent b8febb710c
commit af7b3bd61c
21 changed files with 285 additions and 107 deletions
+2
View File
@@ -413,6 +413,7 @@ class FileController {
const startAtName = (query.startAtName as string) || undefined;
const trashMode = query.trashMode === "true";
const mediaMode = query.mediaMode === "true";
const mediaFilter = (query.mediaFilter as string) || "all";
const queryData: FileListQueryType = {
userID,
@@ -423,6 +424,7 @@ class FileController {
trashMode,
mediaMode,
sortBy,
mediaFilter,
};
const fileList = await fileService.getList(queryData, sortBy, limit);
@@ -40,6 +40,10 @@ export const getListValidationRules = [
.optional()
.isBoolean()
.withMessage("Trash Mode must be a boolean"),
query("mediaFilter")
.optional()
.isString()
.withMessage("Media Filter must be a string"),
query("mediaMode")
.optional()
.isBoolean()
@@ -9,11 +9,17 @@ type GenericParmasType = {
export const createGenericParams = ({ filePath, Key }: GenericParmasType) => {
// TODO: Remove file split after migration
if (env.dbType === "fs") {
const filePathSplit = filePath!.split("/");
const fileName = filePathSplit[filePathSplit.length - 1];
return {
filePath: env.fsDirectory + fileName,
};
if (filePath?.includes("/")) {
const filePathSplit = filePath!.split("/");
const fileName = filePathSplit[filePathSplit.length - 1];
return {
filePath: env.fsDirectory + fileName,
};
} else {
return {
filePath: env.fsDirectory + Key!,
};
}
} else {
return {
Key,
+1
View File
@@ -7,4 +7,5 @@ export interface FileListQueryType {
trashMode: boolean;
mediaMode: boolean;
sortBy: string;
mediaFilter: string;
}
+8
View File
@@ -19,6 +19,7 @@ export interface FileQueryInterface {
"metadata.personalFile"?: boolean | null;
"metadata.trashed"?: boolean | null;
"metadata.hasThumbnail"?: boolean | null;
"metadata.isVideo"?: boolean | null;
}
export const createFileQuery = ({
@@ -30,6 +31,7 @@ export const createFileQuery = ({
trashMode,
mediaMode,
sortBy,
mediaFilter,
}: FileListQueryType) => {
const query: FileQueryInterface = { "metadata.owner": userID };
@@ -57,6 +59,12 @@ export const createFileQuery = ({
if (mediaMode) {
query["metadata.hasThumbnail"] = true;
if (mediaFilter === "photos") {
query["metadata.isVideo"] = false;
} else if (mediaFilter === "videos") {
query["metadata.isVideo"] = true;
}
}
return query;
-1
View File
@@ -39,7 +39,6 @@ const updateDocs = async () => {
/\.(mp4|mov|avi|mkv|webm|wmv|flv|mpg|mpeg|3gp|3g2|mxf|ogv|ogg|m4v)$/i,
},
"metadata.thumbnailID": "",
"metadata.owner": "670c8c321ebf0d4211617eb9",
});
console.log("Found", files.length, "files");
+3
View File
@@ -12,6 +12,7 @@ interface QueryKeyParams {
startAt?: boolean;
trashMode?: boolean;
mediaMode?: boolean;
mediaFilter?: string;
}
// GET
@@ -29,6 +30,7 @@ export const getFilesListAPI = async ({
limit = 50,
trashMode,
mediaMode,
mediaFilter,
},
] = queryKey;
@@ -39,6 +41,7 @@ export const getFilesListAPI = async ({
limit,
trashMode,
mediaMode,
mediaFilter,
};
if (pageParam?.startAtDate && pageParam?.startAtName) {
+2 -1
View File
@@ -78,6 +78,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
useEffect(() => {
if (!wrapperRef.current) return;
const modalWidth = wrapperRef.current.clientWidth;
const modalHeight = wrapperRef.current.clientHeight;
@@ -87,7 +88,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
let Y = contextSelected.Y;
if (X + modalWidth > windowWidth) {
X = windowWidth - windowWidth;
X = windowWidth - modalWidth - 10;
}
if (Y + modalHeight > windowHeight) {
+12 -2
View File
@@ -1,6 +1,6 @@
import { useFiles } from "../../hooks/files";
import { useUtils } from "../../hooks/utils";
import React, { memo } from "react";
import React, { memo, useMemo } from "react";
import FileItem from "../FileItem/FileItem";
import ParentBar from "../ParentBar/ParentBar";
import classNames from "classnames";
@@ -17,8 +17,18 @@ const Files = memo(() => {
const changeListViewMode = () => {
dispatch(toggleListView());
};
const title = useMemo(() => {
const hasFiles = !!files?.pages.find((file) => file);
if (hasFiles) {
return "Files";
} else {
return "No Files";
}
}, [files?.pages]);
return (
<div className="mt-8">
<div className="mt-8 select-none">
<div>
<div className="flex justify-between items-center mb-5">
{isHome && <h2 className="m-0 text-xl font-medium">Home Files</h2>}
+16 -6
View File
@@ -1,6 +1,6 @@
import { useFolders } from "../../hooks/folders";
import FolderItem from "../FolderItem/FolderItem";
import { memo, useCallback } from "react";
import { memo, useCallback, useMemo } from "react";
import classNames from "classnames";
import { useUtils } from "../../hooks/utils";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
@@ -8,7 +8,7 @@ import { setSortBy } from "../../reducers/filter";
const Folders = memo(() => {
const { data: folders } = useFolders(false);
const { isHome } = useUtils();
const { isHome, isTrash, isSearch } = useUtils();
const sortBy = useAppSelector((state) => state.filter.sortBy);
const dispatch = useAppDispatch();
@@ -65,16 +65,26 @@ const Folders = memo(() => {
[sortBy]
);
const title = useMemo(() => {
if (isTrash) {
return "Trash";
} else if (isSearch) {
return "Search";
} else {
return folders?.length === 0 ? "No Folders" : "Folders";
}
}, [isHome, isTrash, isSearch, folders?.length]);
return (
<div className="mt-8">
<div className="mt-8 select-none">
<div className="flex flex-row mb-5 justify-between items-center">
<h2
className={classNames(
"m-0 text-xl font-medium",
isHome ? "block" : "invisible"
"m-0 text-xl font-medium"
// isHome || isTrash ? "block" : "invisible"
)}
>
{folders?.length === 0 ? "No Folders" : "Folders"}
{title}
</h2>
<div className="flex flex-row items-center">
<a className="mr-2" onClick={switchOrderSortBy}>
+1 -1
View File
@@ -31,7 +31,7 @@ const Header = () => {
);
return (
<header id="header">
<header id="header" className="select-none">
<div className="px-6 flex justify-between min-h-16 items-center py-3.5">
<div className="items-center w-[260px] hidden desktopMode:flex">
<a
+1 -1
View File
@@ -81,7 +81,7 @@ const LeftSection = () => {
}
)}
>
<div className="flex flex-col h-full">
<div className="flex flex-col h-full select-none">
<div>
<div className="relative mb-7">
<a
+88 -2
View File
@@ -1,9 +1,11 @@
import classNames from "classnames";
import React, { memo, useEffect, useMemo, useState } from "react";
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
import MediaItem from "../MediaItem/MediaItem";
import { useFiles } from "../../hooks/files";
import MultiSelectBar from "../MultiSelectBar/MultiSelectBar";
import { useInfiniteScroll } from "../../hooks/infiniteScroll";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { setMediaFilter, setSortBy } from "../../reducers/filter";
const Medias = memo(() => {
const {
@@ -13,6 +15,9 @@ const Medias = memo(() => {
} = useFiles();
const [initialLoad, setInitialLoad] = useState(true);
const { sentinelRef, reachedIntersect } = useInfiniteScroll();
const sortBy = useAppSelector((state) => state.filter.sortBy);
const mediaFilter = useAppSelector((state) => state.filter.mediaFilter);
const dispatch = useAppDispatch();
useEffect(() => {
if (initialLoad) {
@@ -26,8 +31,89 @@ const Medias = memo(() => {
}
}, [reachedIntersect, initialLoad, isFetchingNextPage]);
const switchOrderSortBy = useCallback(() => {
let newSortBy = "";
switch (sortBy) {
case "date_asc": {
newSortBy = "date_desc";
break;
}
case "date_desc": {
newSortBy = "date_asc";
break;
}
case "alp_asc": {
newSortBy = "alp_desc";
break;
}
case "alp_desc": {
newSortBy = "alp_asc";
break;
}
default: {
newSortBy = "date_desc";
break;
}
}
dispatch(setSortBy(newSortBy));
}, [sortBy]);
const mediaFilterOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
dispatch(setMediaFilter(value));
};
const title = useMemo(() => {
if (mediaFilter === "all") {
return "Photos and Videos";
} else if (mediaFilter === "photos") {
return "Photos";
} else if (mediaFilter === "videos") {
return "Videos";
}
}, [mediaFilter]);
return (
<div className="w-full p-[17px_15px] desktopMode:p-[17px_40px] overflow-y-scroll">
<div className="w-full p-[17px_15px] desktopMode:p-[17px_40px] overflow-y-scroll select-none">
<div className="flex flex-row mb-5 justify-between items-center mt-2">
<h2 className={classNames("m-0 text-xl font-medium")}>{title}</h2>
<div className="flex flex-row items-center">
<a className="mr-2" onClick={switchOrderSortBy}>
<svg
className="h-3 w-3 cursor-pointer animate"
width="6"
height="10"
viewBox="0 0 6 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={
sortBy === "date_desc" || sortBy === "alp_desc"
? { transform: "scaleY(-1)" }
: {}
}
>
<g id="upload">
<path
id="Path"
d="M5.58035 2.51616L3.20339 0.139199C3.01776 -0.0463997 2.71681 -0.0463997 2.53119 0.139199L0.154195 2.51616C-0.0282007 2.70502 -0.0229639 3.00597 0.165894 3.18836C0.350128 3.3663 0.642189 3.3663 0.826423 3.18836L2.39191 1.62288V9.50781C2.39191 9.77037 2.60475 9.98321 2.86731 9.98321C3.12988 9.98321 3.34272 9.77037 3.34272 9.50781V1.6229L4.90821 3.18839C5.09706 3.37079 5.39801 3.36555 5.58041 3.17669C5.75829 2.99246 5.75829 2.7004 5.58035 2.51616Z"
fill="currentColor"
/>
</g>
</svg>
</a>
<select
className="text-sm font-medium"
onChange={mediaFilterOnChange}
value={mediaFilter}
>
<option value="all">All</option>
<option value="photos">Photos</option>
<option value="videos">Videos</option>
</select>
</div>
</div>
<div
className={classNames(
"grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-[10px]"
@@ -216,6 +216,11 @@ const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
dispatch(resetPopupSelect());
}, [resetPopupSelect]);
const outterWrapperClick = (e: any) => {
if (e.target.id !== "outer-wrapper") return;
closePhotoViewer();
};
useEffect(() => {
if (file.metadata.isVideo) {
getVideo();
@@ -227,10 +232,7 @@ const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
}, [file.metadata.isVideo, getVideo, cleanUpVideo]);
return (
<div
className="w-screen h-screen bg-black bg-opacity-80 absolute top-0 left-0 right-0 bottom-0 z-50 flex justify-center items-center flex-col"
id="outer-wrapper"
>
<div className="w-screen h-screen bg-black bg-opacity-80 absolute top-0 left-0 right-0 bottom-0 z-50 flex justify-center items-center flex-col">
{contextMenuState.selected && (
<div onClick={clickStopPropagation}>
<ContextMenu
+1 -1
View File
@@ -14,7 +14,7 @@ const QuickAccess = memo(() => {
return (
<div
className="overflow-hidden"
className="overflow-hidden select-none"
style={isHome ? { display: "block" } : { display: "none" }}
>
<div className="flex flex-row items-center justify-between mb-5">
+102 -68
View File
@@ -4,12 +4,14 @@ import { memo, useMemo } from "react";
import ContextMenu from "../ContextMenu/ContextMenu";
import classNames from "classnames";
import { useDispatch } from "react-redux";
import { getFileExtension } from "../../utils/files";
import { getFileColor, getFileExtension } from "../../utils/files";
import { useContextMenu } from "../../hooks/contextMenu";
import { useNavigate } from "react-router-dom";
import { useAppSelector } from "../../hooks/store";
import { resetSelected, setPopupSelect } from "../../reducers/selected";
import { useUtils } from "../../hooks/utils";
import { useThumbnail } from "../../hooks/files";
import CloseIcon from "../../icons/CloseIcon";
const RightSection = memo(() => {
const selectedItem = useAppSelector((state) => state.selected.mainSection);
@@ -77,10 +79,31 @@ const RightSection = memo(() => {
navigate(`/folder-trash/${selectedItem.id}`);
}
};
const bannerBackgroundColor = useMemo(() => {
if (selectedItem.file) {
return getFileColor(selectedItem.file.filename);
} else if (selectedItem.folder) {
return "#3c85ee";
} else {
return "#3c85ee";
}
}, [selectedItem.file?.filename, selectedItem.folder?.name]);
const bannerText = useMemo(() => {
if (selectedItem.file) {
return getFileExtension(selectedItem.file.filename);
} else if (selectedItem.folder) {
return "Folder";
} else {
return "";
}
}, [selectedItem.file?.filename, selectedItem.folder?.name]);
return (
<div
className={classNames(
"!hidden desktopMode:!flex min-w-[260px] max-w-[260px] border-l border-gray-secondary p-6 bg-white right-0 justify-center relative mt-1.5",
"!hidden desktopMode:!flex min-w-[260px] max-w-[260px] border-l border-gray-secondary bg-white right-0 justify-center relative mt-1.5",
selectedItem.id === "" ? "flex justify-center items-center" : ""
)}
>
@@ -94,86 +117,97 @@ const RightSection = memo(() => {
</p>
</div>
) : (
<div className="w-[210px]">
<div className="w-full">
<div className="flex flex-row">
<div>
{/* <div>
<img
className="flex w-auto max-w-full"
src="/assets/typedetailed1.svg"
alt="typedetailed1"
/>
</div> */}
<div>
{/* <img className="object-cover w-full" src={thumbnail} /> */}
</div>
<img
className="w-[30px] h-[30px] ml-8 cursor-pointer absolute right-3"
src="/images/close_icon.png"
<div className="w-full h-16 flex items-center relative">
<div
className="opacity-40 w-full h-full absolute"
style={{ background: bannerBackgroundColor }}
></div>
<p className="text-sm ml-6 z-10">{bannerText}</p>
</div>
<CloseIcon
className="w-5 h-5 p-1 cursor-pointer absolute right-3 top-5 bg-white rounded-full shadow-lg z-10"
onClick={reset}
/>
</div>
<div className="m-[20px_0]">
<p className="m-0 text-[#212b36] text-[16px] font-bold max-h-[90px] overflow-hidden text-ellipsis block break-all">
{formattedName}
</p>
</div>
<div>
<div className="flex mb-[7px] justify-start">
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Type
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{selectedItem.file ? fileExtension : "Folder"}
</span>
<div className="p-6">
<div className="mb-5">
<p className="m-0 text-[#212b36] text-[16px] font-bold max-h-[90px] overflow-hidden text-ellipsis block break-all">
{formattedName}
</p>
</div>
<div
className="flex mb-[7px] justify-start"
style={
!selectedItem.file ? { display: "none" } : { display: "flex" }
}
>
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Size
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{fileSize}
</span>
</div>
<div className="flex mb-[7px] justify-start">
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Created
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{formattedDate}
</span>
</div>
<div
className="flex mb-[7px] justify-start"
style={
!selectedItem.file ? { display: "none" } : { display: "flex" }
}
>
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Access
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{selectedItem.file?.metadata.link ? "Public" : "Private"}
</span>
</div>
</div>
<div className="mt-[15px] flex items-center">
<a
className="w-[80px] h-[40px] inline-flex items-center justify-center border border-[#3c85ee] rounded-[4px] text-[#3c85ee] text-[15px] font-medium no-underline animate cursor-pointer hover:bg-[#f6f5fd]"
onClick={openItem}
>
Open
</a>
<div className="ml-[15px] px-[20px]">
<a
className="w-[40px] h-[40px] rounded-[4px] inline-flex items-center justify-center border border-[#919eab] text-[#919eab] no-underline animate cursor-pointer"
// @ts-ignore
onClick={onContextMenu}
<div>
<div className="flex mb-[7px] justify-start">
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Type
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{selectedItem.file ? fileExtension : "Folder"}
</span>
</div>
<div
className="flex mb-[7px] justify-start"
style={
!selectedItem.file ? { display: "none" } : { display: "flex" }
}
>
<i className="fas fa-ellipsis-h" aria-hidden="true"></i>
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Size
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{fileSize}
</span>
</div>
<div className="flex mb-[7px] justify-start">
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Created
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{formattedDate}
</span>
</div>
<div
className="flex mb-[7px] justify-start"
style={
!selectedItem.file ? { display: "none" } : { display: "flex" }
}
>
<span className="text-[#637381] text-[13px] font-normal mr-[35px] leading-[20px] min-w-[50px]">
Access
</span>
<span className="text-[#212b36] text-[13px] font-normal leading-[20px]">
{selectedItem.file?.metadata.link ? "Public" : "Private"}
</span>
</div>
</div>
<div className="mt-[15px] flex items-center">
<a
className="w-[80px] h-[40px] inline-flex items-center justify-center border border-[#3c85ee] rounded-[4px] text-[#3c85ee] text-[15px] font-medium no-underline animate cursor-pointer hover:bg-[#f6f5fd]"
onClick={openItem}
>
Open
</a>
<div className="ml-[15px] px-[20px]">
<a
className="w-[40px] h-[40px] rounded-[4px] inline-flex items-center justify-center border border-[#919eab] text-[#919eab] no-underline animate cursor-pointer"
// @ts-ignore
onClick={onContextMenu}
>
<i className="fas fa-ellipsis-h" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
{contextMenuState.selected && (
+5 -4
View File
@@ -37,13 +37,14 @@ const SearchBar = memo(() => {
setDebouncedSearchText("");
}, []);
const { wrapperRef } = useClickOutOfBounds(resetState);
const outOfContainerClick = useCallback(() => {
closeDrawer();
}, []);
const { wrapperRef } = useClickOutOfBounds(outOfContainerClick);
// TODO: Fix any
const onSearch = (e: any) => {
e.preventDefault();
setSearchText("");
setDebouncedSearchText("");
if (isMedia) {
if (searchText.length) {
navigate(`/search-media/${searchText}`);
@@ -10,10 +10,10 @@ const SettingsPageGeneral = () => {
const fileListStyleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setListViewStyle(value);
if (value === "grid") {
window.localStorage.setItem("grid-mode", "true");
if (value === "list") {
window.localStorage.setItem("list-mode", "true");
} else {
window.localStorage.removeItem("grid-mode");
window.localStorage.removeItem("list-mode");
}
};
@@ -63,8 +63,8 @@ const SettingsPageGeneral = () => {
};
useEffect(() => {
const gridModeLocalStorage = window.localStorage.getItem("grid-mode");
const gridModeEnabled = gridModeLocalStorage === "true";
const listModeLocalStorage = window.localStorage.getItem("list-mode");
const listModeEnabled = listModeLocalStorage === "true";
const sortByLocalStorage = window.localStorage.getItem("sort-name");
const sortByNameEnabled = sortByLocalStorage === "true";
@@ -82,7 +82,7 @@ const SettingsPageGeneral = () => {
);
const loadThumbnailsDisabled = loadThumbnailsLocalStorage === "true";
setListViewStyle(gridModeEnabled ? "grid" : "list");
setListViewStyle(listModeEnabled ? "list" : "grid");
setSortBy(sortByNameEnabled ? "name" : "date");
setOrderBy(orderByAscendingEnabled ? "ascending" : "descending");
setDoubleClickFolders(doubleClickFoldersEnabled ? "enabled" : "disabled");
@@ -102,8 +102,8 @@ const SettingsPageGeneral = () => {
onChange={fileListStyleChange}
className="text-primary"
>
<option value="list">List</option>
<option value="grid">Grid</option>
<option value="list">List</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
+2
View File
@@ -33,6 +33,7 @@ export const useFiles = (enabled = true) => {
const params = useParams();
// TODO: Remove any
const sortBy = useAppSelector((state) => state.filter.sortBy);
const mediaFilter = useAppSelector((state) => state.filter.mediaFilter);
const { isTrash, isMedia } = useUtils();
const limit = isMedia ? 100 : 50;
const filesReactQuery: UseInfiniteQueryResult<FileInterface[]> =
@@ -46,6 +47,7 @@ export const useFiles = (enabled = true) => {
limit,
trashMode: isTrash,
mediaMode: isMedia,
mediaFilter: mediaFilter,
},
],
getFilesListAPI,
+5 -1
View File
@@ -31,7 +31,11 @@ export const useUtils = () => {
return location.pathname.includes("/folder/");
}, [location.pathname]);
return { isHome, isTrash, isMedia, isSettings, isHomeFolder };
const isSearch = useMemo(() => {
return location.pathname.includes("/search/");
}, [location.pathname]);
return { isHome, isTrash, isMedia, isSettings, isHomeFolder, isSearch };
};
export const useClickOutOfBounds = (
+8 -3
View File
@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const gridView = window.localStorage.getItem("grid-mode");
const listView = window.localStorage.getItem("list-mode");
const sortBy = window.localStorage.getItem("name-mode")
? window.localStorage.getItem("asc-mode")
@@ -14,7 +14,8 @@ const initialState = {
sortBy: sortBy,
limit: 50,
search: "",
listView: !gridView,
listView: listView === "true",
mediaFilter: "all",
};
const leftSectionSlice = createSlice({
@@ -27,9 +28,13 @@ const leftSectionSlice = createSlice({
setSortBy: (state, action: PayloadAction<string>) => {
state.sortBy = action.payload;
},
setMediaFilter: (state, action: PayloadAction<string>) => {
state.mediaFilter = action.payload;
},
},
});
export const { toggleListView, setSortBy } = leftSectionSlice.actions;
export const { toggleListView, setSortBy, setMediaFilter } =
leftSectionSlice.actions;
export default leftSectionSlice.reducer;