From e7fbdb479c0a250353b5d3d165c63aaa0ceb2f84 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 4 Sep 2023 20:38:09 +0800 Subject: [PATCH] [ENG-999] Order aware pagination (#1283) * correct types * remove optional override * handle group_directories properly * throw errors if is_dir is null * disable size ordering * usePathsInfiniteQuery * implement for objects too * cleanup --- apps/mobile/src/screens/Location.tsx | 3 +- apps/mobile/src/screens/Search.tsx | 3 +- apps/mobile/src/screens/Tag.tsx | 3 +- apps/web/src/App.tsx | 22 +- core/src/api/search.rs | 348 ++++++++++++------ core/src/preferences/library.rs | 2 +- interface/app/$libraryId/Explorer/Context.tsx | 4 +- interface/app/$libraryId/Explorer/index.tsx | 1 - interface/app/$libraryId/Explorer/store.ts | 13 +- .../Explorer/useObjectsInfiniteQuery.ts | 77 ++++ .../Explorer/usePathsInfiniteQuery.ts | 154 ++++++++ interface/app/$libraryId/TopBar/SearchBar.tsx | 1 - interface/app/$libraryId/ephemeral.tsx | 4 +- interface/app/$libraryId/location/$id.tsx | 90 ++--- interface/app/$libraryId/overview/data.ts | 116 +++--- interface/app/$libraryId/overview/index.tsx | 35 +- interface/app/$libraryId/search.tsx | 4 +- interface/app/$libraryId/tag/$id.tsx | 20 +- interface/app/route-schemas.ts | 5 +- interface/hooks/useZodSearchParams.ts | 4 +- packages/client/src/core.ts | 34 +- 21 files changed, 610 insertions(+), 333 deletions(-) create mode 100644 interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts create mode 100644 interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts diff --git a/apps/mobile/src/screens/Location.tsx b/apps/mobile/src/screens/Location.tsx index 83b284e55..43b8df4b9 100644 --- a/apps/mobile/src/screens/Location.tsx +++ b/apps/mobile/src/screens/Location.tsx @@ -15,7 +15,8 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps< filter: { locationId: id, path: path ?? '' - } + }, + take: 100 } ]); diff --git a/apps/mobile/src/screens/Search.tsx b/apps/mobile/src/screens/Search.tsx index 17f761cbc..8526a8d55 100644 --- a/apps/mobile/src/screens/Search.tsx +++ b/apps/mobile/src/screens/Search.tsx @@ -25,7 +25,8 @@ const SearchScreen = ({ navigation }: RootStackScreenProps<'Search'>) => { // ...args, filter: { search: deferredSearch - } + }, + take: 100 } ], { diff --git a/apps/mobile/src/screens/Tag.tsx b/apps/mobile/src/screens/Tag.tsx index fbe60d79c..714e10f50 100644 --- a/apps/mobile/src/screens/Tag.tsx +++ b/apps/mobile/src/screens/Tag.tsx @@ -11,7 +11,8 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag' { filter: { tags: [id] - } + }, + take: 100 } ]); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 92132ea52..4a42f1447 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -46,15 +46,19 @@ const platform: Platform = { const queryClient = new QueryClient({ defaultOptions: { - queries: import.meta.env.VITE_SD_DEMO_MODE - ? { - refetchOnWindowFocus: false, - staleTime: Infinity, - cacheTime: Infinity, - networkMode: 'offlineFirst', - enabled: false - } - : undefined + queries: { + ...(import.meta.env.VITE_SD_DEMO_MODE && { + refetchOnWindowFocus: false, + staleTime: Infinity, + cacheTime: Infinity, + networkMode: 'offlineFirst', + enabled: false + }), + networkMode: 'always' + }, + mutations: { + networkMode: 'always' + } // TODO: Mutations can't be globally disable which is annoying! } }); diff --git a/core/src/api/search.rs b/core/src/api/search.rs index a812f6ee1..71d916333 100644 --- a/core/src/api/search.rs +++ b/core/src/api/search.rs @@ -15,13 +15,15 @@ use crate::{ use std::{collections::BTreeSet, path::PathBuf}; use chrono::{DateTime, FixedOffset, Utc}; -use prisma_client_rust::{operator, or}; +use prisma_client_rust::{operator, or, WhereQuery}; use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::{Deserialize, Serialize}; use specta::Type; use super::{Ctx, R}; +const MAX_TAKE: u8 = 100; + #[derive(Serialize, Type, Debug)] struct SearchData { cursor: Option>, @@ -53,16 +55,16 @@ impl From for prisma::SortOrder { #[derive(Serialize, Deserialize, Type, Debug, Clone)] #[serde(rename_all = "camelCase", tag = "field", content = "value")] -pub enum FilePathSearchOrdering { +pub enum FilePathOrder { Name(SortOrder), SizeInBytes(SortOrder), DateCreated(SortOrder), DateModified(SortOrder), DateIndexed(SortOrder), - Object(Box), + Object(Box), } -impl FilePathSearchOrdering { +impl FilePathOrder { fn get_sort_order(&self) -> prisma::SortOrder { (*match self { Self::Name(v) => v, @@ -184,14 +186,55 @@ impl FilePathFilterArgs { } } +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CursorOrderItem { + order: SortOrder, + data: T, +} + +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub enum FilePathObjectCursor { + DateAccessed(CursorOrderItem>), + Kind(CursorOrderItem), +} + +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub enum FilePathCursorVariant { + None(file_path::pub_id::Type), + Name(CursorOrderItem), + // SizeInBytes(CursorOrderItem>), + DateCreated(CursorOrderItem>), + DateModified(CursorOrderItem>), + DateIndexed(CursorOrderItem>), + Object(FilePathObjectCursor), +} + +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FilePathCursor { + is_dir: bool, + variant: FilePathCursorVariant, +} + +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub enum ObjectCursor { + None(object::pub_id::Type), + DateAccessed(CursorOrderItem>), + Kind(CursorOrderItem), +} + #[derive(Serialize, Deserialize, Type, Debug, Clone)] #[serde(rename_all = "camelCase", tag = "field", content = "value")] -pub enum ObjectSearchOrdering { +pub enum ObjectOrder { DateAccessed(SortOrder), Kind(SortOrder), } -impl ObjectSearchOrdering { +impl ObjectOrder { fn get_sort_order(&self) -> prisma::SortOrder { (*match self { Self::DateAccessed(v) => v, @@ -211,6 +254,14 @@ impl ObjectSearchOrdering { } } +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +pub enum OrderAndPagination { + OrderOnly(TOrder), + Offset { offset: i32, order: Option }, + Cursor(TCursor), +} + #[derive(Deserialize, Type, Debug, Default, Clone, Copy)] #[serde(rename_all = "camelCase")] enum ObjectHiddenFilter { @@ -277,7 +328,7 @@ pub fn mount() -> AlphaRouter { .procedure("ephemeralPaths", { #[derive(Serialize, Deserialize, Type, Debug, Clone)] #[serde(rename_all = "camelCase", tag = "field", content = "value")] - enum NonIndexedPathOrdering { + enum EphemeralPathOrder { Name(SortOrder), SizeInBytes(SortOrder), DateCreated(SortOrder), @@ -286,16 +337,16 @@ pub fn mount() -> AlphaRouter { #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] - struct NonIndexedPath { + struct EphemeralPathSearchArgs { path: PathBuf, with_hidden_files: bool, #[specta(optional)] - order: Option, + order: Option, } R.with2(library()).query( |(node, library), - NonIndexedPath { + EphemeralPathSearchArgs { path, with_hidden_files, order, @@ -303,53 +354,36 @@ pub fn mount() -> AlphaRouter { let mut paths = non_indexed::walk(path, with_hidden_files, node, library).await?; + macro_rules! order_match { + ($order:ident, [$(($variant:ident, |$i:ident| $func:expr)),+]) => {{ + match $order { + $(EphemeralPathOrder::$variant(order) => { + paths.entries.sort_unstable_by(|path1, path2| { + let func = |$i: &ExplorerItem| $func; + + let one = func(path1); + let two = func(path2); + + match order { + SortOrder::Desc => two.cmp(&one), + SortOrder::Asc => one.cmp(&two), + } + }); + })+ + } + }}; + } + if let Some(order) = order { - match order { - NonIndexedPathOrdering::Name(order) => { - paths.entries.sort_unstable_by(|path1, path2| { - let one = path1.name().to_lowercase(); - let two = path2.name().to_lowercase(); - - match order { - SortOrder::Desc => two.cmp(&one), - SortOrder::Asc => one.cmp(&two), - } - }); - } - NonIndexedPathOrdering::SizeInBytes(order) => { - paths.entries.sort_unstable_by(|path1, path2| { - let one = path1.size_in_bytes(); - let two = path2.size_in_bytes(); - - match order { - SortOrder::Desc => two.cmp(&one), - SortOrder::Asc => one.cmp(&two), - } - }); - } - NonIndexedPathOrdering::DateCreated(order) => { - paths.entries.sort_unstable_by(|path1, path2| { - let one = path1.date_created(); - let two = path2.date_created(); - - match order { - SortOrder::Desc => two.cmp(&one), - SortOrder::Asc => one.cmp(&two), - } - }); - } - NonIndexedPathOrdering::DateModified(order) => { - paths.entries.sort_unstable_by(|path1, path2| { - let one = path1.date_modified(); - let two = path2.date_modified(); - - match order { - SortOrder::Desc => two.cmp(&one), - SortOrder::Asc => one.cmp(&two), - } - }); - } - } + order_match!( + order, + [ + (Name, |p| p.name().to_lowercase()), + (SizeInBytes, |p| p.size_in_bytes()), + (DateCreated, |p| p.date_created()), + (DateModified, |p| p.date_modified()) + ] + ) } Ok(paths) @@ -357,22 +391,12 @@ pub fn mount() -> AlphaRouter { ) }) .procedure("paths", { - #[derive(Deserialize, Type, Debug)] - #[serde(rename_all = "camelCase")] - enum FilePathPagination { - Cursor { pub_id: file_path::pub_id::Type }, - Offset(i32), - } - #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] struct FilePathSearchArgs { + take: u8, #[specta(optional)] - take: Option, - #[specta(optional)] - order: Option, - #[specta(optional)] - pagination: Option, + order_and_pagination: Option>, #[serde(default)] filter: FilePathFilterArgs, #[serde(default = "default_group_directories")] @@ -387,19 +411,18 @@ pub fn mount() -> AlphaRouter { |(node, library), FilePathSearchArgs { take, - order, - pagination, + order_and_pagination, filter, group_directories, }| async move { let Library { db, .. } = library.as_ref(); - let take = take.unwrap_or(100); + let take = take.min(MAX_TAKE); let mut query = db .file_path() .find_many(filter.into_params(db).await?) - .take(take as i64 + 1); + .take(take as i64); // WARN: this order_by for grouping directories MUST always come before the other order_by if group_directories { @@ -407,32 +430,97 @@ pub fn mount() -> AlphaRouter { } // WARN: this order_by for sorting data MUST always come after the other order_by - if let Some(order) = order { - query = query.order_by(order.into_param()); - } - - if let Some(pagination) = pagination { - match pagination { - FilePathPagination::Cursor { pub_id } => { - query = query.cursor(file_path::pub_id::equals(pub_id)); + if let Some(order_and_pagination) = order_and_pagination { + match order_and_pagination { + OrderAndPagination::OrderOnly(order) => { + query = query.order_by(order.into_param()); + } + OrderAndPagination::Offset { offset, order } => { + query = query.skip(offset as i64); + + if let Some(order) = order { + query = query.order_by(order.into_param()) + } + } + OrderAndPagination::Cursor(cursor) => { + // This may seem dumb but it's vital! + // If we're grouping by directories + all directories have been fetched, + // we don't want to include them in the results. + // It's important to keep in mind that since the `order_by` for + // `group_directories` comes before all other orderings, + // all other orderings will be applied independently to directories and paths. + if group_directories && !cursor.is_dir { + query.add_where(file_path::is_dir::not(Some(true))) + } + + macro_rules! arm { + ($field:ident, $item:ident) => {{ + let item = $item; + + query.add_where(match item.order { + SortOrder::Asc => file_path::$field::gt(item.data), + SortOrder::Desc => file_path::$field::lt(item.data), + }); + + query = query + .order_by(file_path::$field::order(item.order.into())); + }}; + } + + match cursor.variant { + FilePathCursorVariant::None(item) => { + query = query.cursor(file_path::pub_id::equals(item)); + } + FilePathCursorVariant::Name(item) => arm!(name, item), + FilePathCursorVariant::DateCreated(item) => { + arm!(date_created, item) + } + FilePathCursorVariant::DateModified(item) => { + arm!(date_modified, item) + } + FilePathCursorVariant::DateIndexed(item) => { + arm!(date_indexed, item) + } + FilePathCursorVariant::Object(obj) => { + macro_rules! arm { + ($field:ident, $item:ident) => {{ + let item = $item; + + query.add_where(match item.order { + SortOrder::Asc => file_path::object::is(vec![ + object::$field::gt(item.data), + ]), + SortOrder::Desc => file_path::object::is(vec![ + object::$field::lt(item.data), + ]), + }); + + query = + query.order_by(file_path::object::order(vec![ + object::$field::order(item.order.into()), + ])); + }}; + } + + match obj { + FilePathObjectCursor::Kind(item) => arm!(kind, item), + FilePathObjectCursor::DateAccessed(item) => { + arm!(date_accessed, item) + } + }; + } + }; + + query = query + .order_by(file_path::pub_id::order(prisma::SortOrder::Asc)); } - FilePathPagination::Offset(offset) => query = query.skip(offset as i64), } } - let (file_paths, cursor) = { - let mut paths = query - .include(file_path_with_object::include()) - .exec() - .await?; - - let cursor = (paths.len() as i32 > take) - .then(|| paths.pop()) - .flatten() - .map(|r| r.pub_id); - - (paths, cursor) - }; + let file_paths = query + .include(file_path_with_object::include()) + .exec() + .await?; let mut items = Vec::with_capacity(file_paths.len()); @@ -453,7 +541,10 @@ pub fn mount() -> AlphaRouter { }) } - Ok(SearchData { items, cursor }) + Ok(SearchData { + items, + cursor: None, + }) }, ) }) @@ -478,22 +569,12 @@ pub fn mount() -> AlphaRouter { }) }) .procedure("objects", { - #[derive(Deserialize, Type, Debug)] - #[serde(rename_all = "camelCase")] - enum ObjectPagination { - Cursor { pub_id: object::pub_id::Type }, - Offset(i32), - } - #[derive(Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] struct ObjectSearchArgs { + take: u8, #[specta(optional)] - take: Option, - #[specta(optional)] - order: Option, - #[specta(optional)] - pagination: Option, + order_and_pagination: Option>, #[serde(default)] filter: ObjectFilterArgs, } @@ -502,30 +583,55 @@ pub fn mount() -> AlphaRouter { |(node, library), ObjectSearchArgs { take, - order, - pagination, + order_and_pagination, filter, }| async move { let Library { db, .. } = library.as_ref(); - let take = take.unwrap_or(100); + let take = take.max(MAX_TAKE); let mut query = db .object() .find_many(filter.into_params()) - .take(take as i64 + 1); + .take(take as i64); - if let Some(order) = order { - query = query.order_by(order.into_param()); - } - - if let Some(pagination) = pagination { - match pagination { - ObjectPagination::Cursor { pub_id } => { - query = query.cursor(object::pub_id::equals(pub_id)); + if let Some(order_and_pagination) = order_and_pagination { + match order_and_pagination { + OrderAndPagination::OrderOnly(order) => { + query = query.order_by(order.into_param()); } - ObjectPagination::Offset(offset) => { + OrderAndPagination::Offset { offset, order } => { query = query.skip(offset as i64); + + if let Some(order) = order { + query = query.order_by(order.into_param()) + } + } + OrderAndPagination::Cursor(cursor) => { + macro_rules! arm { + ($field:ident, $item:ident) => {{ + let item = $item; + + query.add_where(match item.order { + SortOrder::Asc => object::$field::gt(item.data), + SortOrder::Desc => object::$field::lt(item.data), + }); + + query = query + .order_by(object::$field::order(item.order.into())); + }}; + } + + match cursor { + ObjectCursor::None(item) => { + query = query.cursor(object::pub_id::equals(item)); + } + ObjectCursor::Kind(item) => arm!(kind, item), + ObjectCursor::DateAccessed(item) => arm!(date_accessed, item), + } + + query = + query.order_by(object::pub_id::order(prisma::SortOrder::Asc)) } } } @@ -536,7 +642,7 @@ pub fn mount() -> AlphaRouter { .exec() .await?; - let cursor = (objects.len() as i32 > take) + let cursor = (objects.len() as u8 > take) .then(|| objects.pop()) .flatten() .map(|r| r.pub_id); diff --git a/core/src/preferences/library.rs b/core/src/preferences/library.rs index 8d501315c..b4231ec67 100644 --- a/core/src/preferences/library.rs +++ b/core/src/preferences/library.rs @@ -52,7 +52,7 @@ impl LibraryPreferences { #[derive(Clone, Serialize, Deserialize, Type, Debug)] #[serde(rename_all = "camelCase")] pub struct LocationSettings { - explorer: ExplorerSettings, + explorer: ExplorerSettings, } #[derive(Clone, Serialize, Deserialize, Type, Debug)] diff --git a/interface/app/$libraryId/Explorer/Context.tsx b/interface/app/$libraryId/Explorer/Context.tsx index 3a8b8781e..8677a4e54 100644 --- a/interface/app/$libraryId/Explorer/Context.tsx +++ b/interface/app/$libraryId/Explorer/Context.tsx @@ -16,9 +16,9 @@ export const useExplorerContext = () => { return ctx; }; -export const ExplorerContextProvider = ({ +export const ExplorerContextProvider = >({ explorer, children }: PropsWithChildren<{ - explorer: UseExplorer; + explorer: TExplorer; }>) => {children}; diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 4c01e9861..c9519d95c 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -25,7 +25,6 @@ const INSPECTOR_WIDTH = 260; export default function Explorer(props: PropsWithChildren) { const explorerStore = useExplorerStore(); const explorer = useExplorerContext(); - const [{ path }] = useExplorerSearchParams(); // Can we put this somewhere else -_- useLibrarySubscription(['jobs.newThumbnail'], { diff --git a/interface/app/$libraryId/Explorer/store.ts b/interface/app/$libraryId/Explorer/store.ts index eb04fea73..62f844485 100644 --- a/interface/app/$libraryId/Explorer/store.ts +++ b/interface/app/$libraryId/Explorer/store.ts @@ -65,13 +65,11 @@ export function getOrderingDirection(ordering: Ordering): SortOrder { else return ordering.value; } -export const createDefaultExplorerSettings = ({ - order -}: { - order: TOrder | null; +export const createDefaultExplorerSettings = (args?: { + order?: TOrder | null; }) => ({ - order, + order: args?.order ?? null, layoutMode: 'grid' as ExplorerLayout, gridItemSize: 110 as number, showBytesInGridView: true as boolean, @@ -109,7 +107,6 @@ const state = { newThumbnails: proxySet() as Set, cutCopyState: { type: 'Idle' } as CutCopyState, quickViewObject: null as ExplorerItem | null, - groupBy: 'none', isDragging: false, gridGap: 8 }; @@ -148,7 +145,7 @@ export function isCut(item: ExplorerItem, cutCopyState: ReadonlyDeep; +} & Pick>, 'enabled'>) { + const ctx = useRspcLibraryContext(); + const explorerSettings = settings.useSettingsSnapshot(); + + if (explorerSettings.order) { + arg.orderAndPagination = { orderOnly: explorerSettings.order }; + } + + return useInfiniteQuery({ + queryKey: ['search.objects', { library_id: library.uuid, arg }] as const, + queryFn: ({ pageParam, queryKey: [_, { arg }] }) => { + const cItem: Extract = pageParam; + const { order } = explorerSettings; + + let orderAndPagination: OrderAndPagination | undefined; + + if (!cItem) { + if (order) orderAndPagination = { orderOnly: order }; + } else { + let cursor: ObjectCursor | undefined; + + if (!order) cursor = { none: [] }; + else if (cItem) { + const direction = order.value; + + switch (order.field) { + case 'kind': { + const data = cItem.item.kind; + if (data !== null) cursor = { kind: { order: direction, data } }; + break; + } + case 'dateAccessed': { + const data = cItem.item.date_accessed; + if (data !== null) + cursor = { dateAccessed: { order: direction, data } }; + break; + } + } + } + + if (cursor) orderAndPagination = { cursor }; + } + + arg.orderAndPagination = orderAndPagination; + + return ctx.client.query(['search.objects', arg]); + }, + getNextPageParam: (lastPage) => { + if (lastPage.items.length < arg.take) return undefined; + else return lastPage.items[arg.take]; + }, + ...args + }); +} diff --git a/interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts b/interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts new file mode 100644 index 000000000..b6e1b4130 --- /dev/null +++ b/interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts @@ -0,0 +1,154 @@ +import { UseInfiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'; +import { + ExplorerItem, + FilePathCursor, + FilePathCursorVariant, + FilePathObjectCursor, + FilePathOrder, + FilePathSearchArgs, + LibraryConfigWrapped, + OrderAndPagination, + SearchData, + useRspcLibraryContext +} from '@sd/client'; +import { getExplorerStore } from './store'; +import { UseExplorerSettings } from './useExplorer'; + +export function usePathsInfiniteQuery({ + library, + arg, + settings, + ...args +}: { + library: LibraryConfigWrapped; + arg: FilePathSearchArgs; + settings: UseExplorerSettings; +} & Pick>, 'enabled'>) { + const ctx = useRspcLibraryContext(); + const explorerSettings = settings.useSettingsSnapshot(); + + if (explorerSettings.order) { + arg.orderAndPagination = { orderOnly: explorerSettings.order }; + } + + return useInfiniteQuery({ + queryKey: ['search.paths', { library_id: library.uuid, arg }] as const, + queryFn: ({ pageParam, queryKey: [_, { arg }] }) => { + const cItem: Extract = pageParam; + const { order } = explorerSettings; + + let orderAndPagination: OrderAndPagination | undefined; + + if (!cItem) { + if (order) orderAndPagination = { orderOnly: order }; + } else { + let variant: FilePathCursorVariant | undefined; + + if (!order) variant = { none: [] }; + else if (cItem) { + switch (order.field) { + case 'name': { + const data = cItem.item.name; + if (data !== null) + variant = { + name: { + order: order.value, + data + } + }; + break; + } + case 'dateCreated': { + const data = cItem.item.date_created; + if (data !== null) + variant = { + dateCreated: { + order: order.value, + data + } + }; + break; + } + case 'dateModified': { + const data = cItem.item.date_modified; + if (data !== null) + variant = { + dateModified: { + order: order.value, + data + } + }; + break; + } + case 'dateIndexed': { + const data = cItem.item.date_indexed; + if (data !== null) + variant = { + dateIndexed: { + order: order.value, + data + } + }; + break; + } + case 'object': { + const object = cItem.item.object; + if (!object) break; + + let objectCursor: FilePathObjectCursor | undefined; + + switch (order.value.field) { + case 'dateAccessed': { + const data = object.date_accessed; + if (data !== null) + objectCursor = { + dateAccessed: { + order: order.value.value, + data + } + }; + break; + } + case 'kind': { + const data = object.kind; + if (data !== null) + objectCursor = { + kind: { + order: order.value.value, + data + } + }; + break; + } + } + + if (objectCursor) + variant = { + object: objectCursor + }; + + break; + } + } + } + + if (cItem.item.is_dir === null) throw new Error(); + + if (variant) + orderAndPagination = { + cursor: { variant, isDir: cItem.item.is_dir } + }; + } + + arg.orderAndPagination = orderAndPagination; + + return ctx.client.query(['search.paths', arg]); + }, + getNextPageParam: (lastPage) => { + if (lastPage.items.length < arg.take) return undefined; + else return lastPage.items[arg.take]; + }, + onSuccess: () => getExplorerStore().resetNewThumbnails(), + ...args + }); +} diff --git a/interface/app/$libraryId/TopBar/SearchBar.tsx b/interface/app/$libraryId/TopBar/SearchBar.tsx index c172c70ab..51923a9ee 100644 --- a/interface/app/$libraryId/TopBar/SearchBar.tsx +++ b/interface/app/$libraryId/TopBar/SearchBar.tsx @@ -15,7 +15,6 @@ export default () => { const navigate = useNavigate(); const location = useLocation(); - const platform = useOperatingSystem(false); const os = useOperatingSystem(true); const keybind = keybindForOs(os); diff --git a/interface/app/$libraryId/ephemeral.tsx b/interface/app/$libraryId/ephemeral.tsx index 613b8e3db..94121319c 100644 --- a/interface/app/$libraryId/ephemeral.tsx +++ b/interface/app/$libraryId/ephemeral.tsx @@ -1,5 +1,5 @@ import { Suspense, memo, useDeferredValue, useMemo } from 'react'; -import { type NonIndexedPathOrdering, getExplorerItemData, useLibraryQuery } from '@sd/client'; +import { type EphemeralPathOrder, getExplorerItemData, useLibraryQuery } from '@sd/client'; import { Tooltip } from '@sd/ui'; import { type PathParams, PathParamsSchema } from '~/app/route-schemas'; import { useOperatingSystem, useZodSearchParams } from '~/hooks'; @@ -22,7 +22,7 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => { const explorerSettings = useExplorerSettings({ settings: useMemo( () => - createDefaultExplorerSettings({ + createDefaultExplorerSettings({ order: { field: 'name', value: 'Asc' diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index 43d6c03e3..0a4d8f27c 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -1,16 +1,16 @@ -import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { stringify } from 'uuid'; import { ExplorerSettings, FilePathFilterArgs, - FilePathSearchOrdering, + FilePathOrder, + ObjectKindEnum, useLibraryContext, useLibraryMutation, useLibraryQuery, - useLibrarySubscription, - useRspcLibraryContext + useLibrarySubscription } from '@sd/client'; import { LocationIdParamsSchema } from '~/app/route-schemas'; import { Folder } from '~/components'; @@ -18,12 +18,9 @@ import { useKeyDeleteFile, useZodRouteParams } from '~/hooks'; import Explorer from '../Explorer'; import { ExplorerContextProvider } from '../Explorer/Context'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; -import { - createDefaultExplorerSettings, - filePathOrderingKeysSchema, - getExplorerStore -} from '../Explorer/store'; +import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store'; import { UseExplorerSettings, useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; +import { usePathsInfiniteQuery } from '../Explorer/usePathsInfiniteQuery'; import { useExplorerSearchParams } from '../Explorer/util'; import { TopBarPortal } from '../TopBar/Portal'; import LocationOptions from './LocationOptions'; @@ -38,11 +35,8 @@ export const Component = () => { const updatePreferences = useLibraryMutation('preferences.update'); const settings = useMemo(() => { - const defaults = createDefaultExplorerSettings({ - order: { - field: 'name', - value: 'Asc' - } + const defaults = createDefaultExplorerSettings({ + order: { field: 'name', value: 'Asc' } }); if (!location.data) return defaults; @@ -61,16 +55,12 @@ export const Component = () => { }, [location.data, preferences.data?.location]); const onSettingsChanged = useDebouncedCallback( - async (settings: ExplorerSettings) => { + async (settings: ExplorerSettings) => { if (!location.data) return; const pubId = stringify(location.data.pub_id); try { await updatePreferences.mutateAsync({ - location: { - [pubId]: { - explorer: settings - } - } + location: { [pubId]: { explorer: settings } } }); queryClient.invalidateQueries(['preferences.get']); } catch (e) { @@ -80,7 +70,7 @@ export const Component = () => { 500 ); - const explorerSettings = useExplorerSettings({ + const explorerSettings = useExplorerSettings({ settings, onSettingsChanged, orderingKeys: filePathOrderingKeysSchema @@ -92,23 +82,14 @@ export const Component = () => { items, count, loadMore, - parent: location.data - ? { - type: 'Location', - location: location.data - } - : undefined, - settings: explorerSettings + settings: explorerSettings, + ...(location.data && { + parent: { type: 'Location', location: location.data } + }) }); useLibrarySubscription( - [ - 'locations.quickRescan', - { - sub_path: path ?? '', - location_id: locationId - } - ], + ['locations.quickRescan', { sub_path: path ?? '', location_id: locationId }], { onData() {} } ); @@ -151,11 +132,10 @@ const useItems = ({ settings }: { locationId: number; - settings: UseExplorerSettings; + settings: UseExplorerSettings; }) => { const [{ path, take }] = useExplorerSearchParams(); - const ctx = useRspcLibraryContext(); const { library } = useLibraryContext(); const explorerSettings = settings.useSettingsSnapshot(); @@ -163,35 +143,16 @@ const useItems = ({ const filter: FilePathFilterArgs = { locationId, ...(explorerSettings.layoutMode === 'media' - ? { object: { kind: [5, 7] } } + ? { object: { kind: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } : { path: path ?? '' }) }; const count = useLibraryQuery(['search.pathsCount', { filter }]); - const query = useInfiniteQuery({ - queryKey: [ - 'search.paths', - { - library_id: library.uuid, - arg: { - order: explorerSettings.order, - filter, - take - } - } - ] as const, - queryFn: ({ pageParam: cursor, queryKey }) => - ctx.client.query([ - 'search.paths', - { - ...queryKey[1].arg, - pagination: cursor ? { cursor: { pub_id: cursor } } : undefined - } - ]), - getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, - keepPreviousData: true, - onSuccess: () => getExplorerStore().resetNewThumbnails() + const query = usePathsInfiniteQuery({ + arg: { filter, take }, + library, + settings }); const items = useMemo(() => query.data?.pages.flatMap((d) => d.items) || null, [query.data]); @@ -202,12 +163,7 @@ const useItems = ({ } }, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]); - return { - query, - items, - loadMore, - count: count.data - }; + return { query, items, loadMore, count: count.data }; }; function getLastSectionOfPath(path: string): string | undefined { diff --git a/interface/app/$libraryId/overview/data.ts b/interface/app/$libraryId/overview/data.ts index 695a95203..1953f6465 100644 --- a/interface/app/$libraryId/overview/data.ts +++ b/interface/app/$libraryId/overview/data.ts @@ -1,18 +1,25 @@ import { iconNames } from '@sd/assets/util'; -import { useInfiniteQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { Category, FilePathFilterArgs, + FilePathOrder, ObjectFilterArgs, - ObjectSearchOrdering, + ObjectKindEnum, + ObjectOrder, useLibraryContext, useLibraryQuery, useRspcLibraryContext } from '@sd/client'; -import { useExplorerContext } from '../Explorer/Context'; -import { getExplorerStore, useExplorerStore } from '../Explorer/store'; -import { UseExplorerSettings } from '../Explorer/useExplorer'; +import { + createDefaultExplorerSettings, + filePathOrderingKeysSchema, + objectOrderingKeysSchema +} from '../Explorer/store'; +import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; +import { useObjectsInfiniteQuery } from '../Explorer/useObjectsInfiniteQuery'; +import { usePathsInfiniteQuery } from '../Explorer/usePathsInfiniteQuery'; +import { usePageLayoutContext } from '../PageLayout/Context'; export const IconForCategory: Partial> = { Recents: iconNames.Collection, @@ -54,43 +61,45 @@ export const IconToDescription = { Trash: 'View all files in your trash' }; -const OBJECT_CATEGORIES: Category[] = ['Recents', 'Favorites']; +export const OBJECT_CATEGORIES: Category[] = ['Recents', 'Favorites']; // this is a gross function so it's in a separate hook :) -export function useItems( - category: Category, - explorerSettings: UseExplorerSettings -) { - const settings = explorerSettings.useSettingsSnapshot(); +export function useCategoryExplorer(category: Category) { const rspc = useRspcLibraryContext(); const { library } = useLibraryContext(); - - const kind = settings.layoutMode === 'media' ? [5, 7] : undefined; + const page = usePageLayoutContext(); const isObjectQuery = OBJECT_CATEGORIES.includes(category); - const objectFilter: ObjectFilterArgs = { category, kind }; + const pathsExplorerSettings = useExplorerSettings({ + settings: useMemo(() => createDefaultExplorerSettings(), []), + orderingKeys: filePathOrderingKeysSchema + }); + + const objectsExplorerSettings = useExplorerSettings({ + settings: useMemo(() => createDefaultExplorerSettings(), []), + orderingKeys: objectOrderingKeysSchema + }); + + const explorerSettings = isObjectQuery ? objectsExplorerSettings : pathsExplorerSettings; + const settings = explorerSettings.useSettingsSnapshot(); + + const take = 10; + + const objectFilter: ObjectFilterArgs = { + category, + ...(settings.layoutMode === 'media' && { + kind: [ObjectKindEnum.Image, ObjectKindEnum.Video] + }) + }; const objectsCount = useLibraryQuery(['search.objectsCount', { filter: objectFilter }]); - const objectsQuery = useInfiniteQuery({ + const objectsQuery = useObjectsInfiniteQuery({ enabled: isObjectQuery, - queryKey: [ - 'search.objects', - { - library_id: library.uuid, - arg: { take: 50, filter: objectFilter } - } - ] as const, - queryFn: ({ pageParam: cursor, queryKey }) => - rspc.client.query([ - 'search.objects', - { - ...queryKey[1].arg, - pagination: cursor ? { cursor: { pub_id: cursor } } : undefined - } - ]), - getNextPageParam: (lastPage) => lastPage.cursor ?? undefined + library, + arg: { take, filter: objectFilter }, + settings: objectsExplorerSettings }); const objectsItems = useMemo( @@ -104,25 +113,11 @@ export function useItems( // TODO: Make a custom double click handler for directories to take users to the location explorer. // For now it's not needed because folders shouldn't show. - const pathsQuery = useInfiniteQuery({ + const pathsQuery = usePathsInfiniteQuery({ enabled: !isObjectQuery, - queryKey: [ - 'search.paths', - { - library_id: library.uuid, - arg: { take: 50, filter: pathsFilter } - } - ] as const, - queryFn: ({ pageParam: cursor, queryKey }) => - rspc.client.query([ - 'search.paths', - { - ...queryKey[1].arg, - pagination: cursor ? { cursor: { pub_id: cursor } } : undefined - } - ]), - getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, - onSuccess: () => getExplorerStore().resetNewThumbnails() + library, + arg: { take, filter: pathsFilter }, + settings: pathsExplorerSettings }); const pathsItems = useMemo( @@ -135,17 +130,24 @@ export function useItems( if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage(); }; + const shared = { + loadMore, + scrollRef: page.ref + }; + return isObjectQuery - ? { + ? // eslint-disable-next-line + useExplorer({ items: objectsItems ?? null, count: objectsCount.data, - query: objectsQuery, - loadMore - } - : { + settings: objectsExplorerSettings, + ...shared + }) + : // eslint-disable-next-line + useExplorer({ items: pathsItems ?? null, count: pathsCount.data, - query: pathsQuery, - loadMore - }; + settings: pathsExplorerSettings, + ...shared + }); } diff --git a/interface/app/$libraryId/overview/index.tsx b/interface/app/$libraryId/overview/index.tsx index 98f45eadd..60afddb17 100644 --- a/interface/app/$libraryId/overview/index.tsx +++ b/interface/app/$libraryId/overview/index.tsx @@ -1,8 +1,8 @@ import { getIcon } from '@sd/assets/util'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import 'react-loading-skeleton/dist/skeleton.css'; import { useSnapshot } from 'valtio'; -import { Category, ObjectSearchOrdering } from '@sd/client'; +import { Category } from '@sd/client'; import { useIsDark } from '../../../hooks'; import { ExplorerContextProvider } from '../Explorer/Context'; import ContextMenu, { ObjectItems } from '../Explorer/ContextMenu'; @@ -10,46 +10,21 @@ import { Conditional } from '../Explorer/ContextMenu/ConditionalItem'; import { Inspector } from '../Explorer/Inspector'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; import View from '../Explorer/View'; -import { - createDefaultExplorerSettings, - objectOrderingKeysSchema, - useExplorerStore -} from '../Explorer/store'; -import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; +import { useExplorerStore } from '../Explorer/store'; import { usePageLayoutContext } from '../PageLayout/Context'; import { TopBarPortal } from '../TopBar/Portal'; import Statistics from '../overview/Statistics'; import { Categories } from './Categories'; -import { IconForCategory, IconToDescription, useItems } from './data'; +import { IconForCategory, IconToDescription, useCategoryExplorer } from './data'; export const Component = () => { const explorerStore = useExplorerStore(); const isDark = useIsDark(); const page = usePageLayoutContext(); - const explorerSettings = useExplorerSettings({ - settings: useMemo( - () => - createDefaultExplorerSettings({ - order: null - }), - [] - ), - onSettingsChanged: () => {}, - orderingKeys: objectOrderingKeysSchema - }); - const [selectedCategory, setSelectedCategory] = useState('Recents'); - const { items, count, loadMore } = useItems(selectedCategory, explorerSettings); - - const explorer = useExplorer({ - items, - count, - loadMore, - scrollRef: page.ref, - settings: explorerSettings - }); + const explorer = useCategoryExplorer(selectedCategory); useEffect(() => { if (!page.ref.current) return; diff --git a/interface/app/$libraryId/search.tsx b/interface/app/$libraryId/search.tsx index 668e033ad..17c2e2163 100644 --- a/interface/app/$libraryId/search.tsx +++ b/interface/app/$libraryId/search.tsx @@ -1,6 +1,6 @@ import { MagnifyingGlass } from 'phosphor-react'; import { Suspense, memo, useDeferredValue, useMemo } from 'react'; -import { type FilePathSearchOrdering, getExplorerItemData, useLibraryQuery } from '@sd/client'; +import { FilePathOrder, getExplorerItemData, useLibraryQuery } from '@sd/client'; import { type SearchParams, SearchParamsSchema } from '~/app/route-schemas'; import { useZodSearchParams } from '~/hooks'; import Explorer from './Explorer'; @@ -27,7 +27,7 @@ const SearchExplorer = memo((props: { args: SearchParams }) => { const explorerSettings = useExplorerSettings({ settings: useMemo( () => - createDefaultExplorerSettings({ + createDefaultExplorerSettings({ order: { field: 'name', value: 'Asc' diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx index 0b7ae46b0..8ece38635 100644 --- a/interface/app/$libraryId/tag/$id.tsx +++ b/interface/app/$libraryId/tag/$id.tsx @@ -1,6 +1,6 @@ import { getIcon, iconNames } from '@sd/assets/util'; import { useMemo } from 'react'; -import { ObjectSearchOrdering, useLibraryQuery } from '@sd/client'; +import { ObjectOrder, useLibraryQuery } from '@sd/client'; import { LocationIdParamsSchema } from '~/app/route-schemas'; import { useZodRouteParams } from '~/hooks'; import Explorer from '../Explorer'; @@ -17,9 +17,8 @@ export const Component = () => { const explorerData = useLibraryQuery([ 'search.objects', { - filter: { - tags: [tagId] - } + filter: { tags: [tagId] }, + take: 100 } ]); @@ -28,7 +27,7 @@ export const Component = () => { const explorerSettings = useExplorerSettings({ settings: useMemo( () => - createDefaultExplorerSettings({ + createDefaultExplorerSettings({ order: null }), [] @@ -39,13 +38,10 @@ export const Component = () => { const explorer = useExplorer({ items: explorerData.data?.items || null, - parent: tag.data - ? { - type: 'Tag', - tag: tag.data - } - : undefined, - settings: explorerSettings + settings: explorerSettings, + ...(tag.data && { + parent: { type: 'Tag', tag: tag.data } + }) }); return ( diff --git a/interface/app/route-schemas.ts b/interface/app/route-schemas.ts index dc89255f4..b44afdb54 100644 --- a/interface/app/route-schemas.ts +++ b/interface/app/route-schemas.ts @@ -21,11 +21,12 @@ export const PathParamsSchema = z.object({ path: z.string().optional() }); export type PathParams = z.infer; export const SearchParamsSchema = PathParamsSchema.extend({ - take: z.coerce.number().optional(), + take: z.coerce.number().default(100), order: z .union([ z.object({ field: z.literal('name'), value: SortOrderSchema }), - z.object({ field: z.literal('sizeInBytes'), value: SortOrderSchema }) + z.object({ field: z.literal('dateCreated'), value: SortOrderSchema }) + // z.object({ field: z.literal('sizeInBytes'), value: SortOrderSchema }) ]) .optional(), search: z.string().optional() diff --git a/interface/hooks/useZodSearchParams.ts b/interface/hooks/useZodSearchParams.ts index 92fc27eac..5f26f4108 100644 --- a/interface/hooks/useZodSearchParams.ts +++ b/interface/hooks/useZodSearchParams.ts @@ -17,7 +17,7 @@ export function useZodSearchParams(schema: Z) { typedSearchParams.data as z.infer, useCallback( ( - data: z.input | ((data: z.input) => z.infer), + data: z.input | ((data: z.input) => z.input), navigateOpts?: NavigateOptions ) => { if (typeof data === 'function') { @@ -26,7 +26,7 @@ export function useZodSearchParams(schema: Z) { if (!typedPrevParams.success) throw typedPrevParams.errors; - return data(typedPrevParams.data); + return schema.parse(data(typedPrevParams.data)); }, navigateOpts); } else { setSearchParams(data as any, navigateOpts); diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 940355096..0d711149b 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -28,7 +28,7 @@ export type Procedures = { { key: "notifications.get", input: never, result: Notification[] } | { key: "p2p.nlmState", input: never, result: { [key: string]: LibraryData } } | { key: "preferences.get", input: LibraryArgs, result: LibraryPreferences } | - { key: "search.ephemeralPaths", input: LibraryArgs, result: NonIndexedFileSystemEntries } | + { key: "search.ephemeralPaths", input: LibraryArgs, result: NonIndexedFileSystemEntries } | { key: "search.objects", input: LibraryArgs, result: SearchData } | { key: "search.objectsCount", input: LibraryArgs<{ filter?: ObjectFilterArgs }>, result: number } | { key: "search.paths", input: LibraryArgs, result: SearchData } | @@ -129,6 +129,8 @@ export type Composite = "Unknown" | "False" | "General" | "Live" export type CreateLibraryArgs = { name: LibraryName } +export type CursorOrderItem = { order: SortOrder; data: T } + export type Dimensions = { width: number; height: number } export type DiskType = "SSD" | "HDD" | "Removable" @@ -137,6 +139,10 @@ export type DoubleClickAction = "openFile" | "quickPreview" export type EditLibraryArgs = { id: string; name: LibraryName | null; description: MaybeUndefined } +export type EphemeralPathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } + +export type EphemeralPathSearchArgs = { path: string; withHiddenFiles: boolean; order?: EphemeralPathOrder | null } + export type Error = { code: ErrorCode; message: string } /** @@ -160,13 +166,17 @@ export type FileEraserJobInit = { location_id: number; file_path_ids: number[]; export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null } +export type FilePathCursor = { isDir: boolean; variant: FilePathCursorVariant } + +export type FilePathCursorVariant = { none: number[] } | { name: CursorOrderItem } | { dateCreated: CursorOrderItem } | { dateModified: CursorOrderItem } | { dateIndexed: CursorOrderItem } | { object: FilePathObjectCursor } + export type FilePathFilterArgs = { locationId?: number | null; search?: string | null; extension?: string | null; createdAt?: OptionalRange; path?: string | null; object?: ObjectFilterArgs | null } -export type FilePathPagination = { cursor: { pub_id: number[] } } | { offset: number } +export type FilePathObjectCursor = { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } -export type FilePathSearchArgs = { take?: number | null; order?: FilePathSearchOrdering | null; pagination?: FilePathPagination | null; filter?: FilePathFilterArgs; groupDirectories?: boolean } +export type FilePathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } | { field: "dateIndexed"; value: SortOrder } | { field: "object"; value: ObjectOrder } -export type FilePathSearchOrdering = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } | { field: "dateIndexed"; value: SortOrder } | { field: "object"; value: ObjectSearchOrdering } +export type FilePathSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: FilePathFilterArgs; groupDirectories?: boolean } export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null } @@ -247,7 +257,7 @@ export type Location = { id: number; pub_id: number[]; name: string | null; path */ export type LocationCreateArgs = { path: string; dry_run: boolean; indexer_rules_ids: number[] } -export type LocationSettings = { explorer: ExplorerSettings } +export type LocationSettings = { explorer: ExplorerSettings } /** * `LocationUpdateArgs` is the argument received from the client using `rspc` to update a location. @@ -280,12 +290,8 @@ export type NodeState = ({ id: string; name: string; p2p_port: number | null; fe export type NonIndexedFileSystemEntries = { entries: ExplorerItem[]; errors: Error[] } -export type NonIndexedPath = { path: string; withHiddenFiles: boolean; order?: NonIndexedPathOrdering | null } - export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[] } -export type NonIndexedPathOrdering = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } - /** * Represents a single notification. */ @@ -301,15 +307,15 @@ export type NotificationId = { type: "library"; id: [string, number] } | { type: export type Object = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null } +export type ObjectCursor = { none: number[] } | { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } + export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: ObjectHiddenFilter; dateAccessed?: MaybeNot | null; kind?: number[]; tags?: number[]; category?: Category | null } export type ObjectHiddenFilter = "exclude" | "include" -export type ObjectPagination = { cursor: { pub_id: number[] } } | { offset: number } +export type ObjectOrder = { field: "dateAccessed"; value: SortOrder } | { field: "kind"; value: SortOrder } -export type ObjectSearchArgs = { take?: number | null; order?: ObjectSearchOrdering | null; pagination?: ObjectPagination | null; filter?: ObjectFilterArgs } - -export type ObjectSearchOrdering = { field: "dateAccessed"; value: SortOrder } | { field: "kind"; value: SortOrder } +export type ObjectSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: ObjectFilterArgs } export type ObjectValidatorArgs = { id: number; path: string } @@ -323,6 +329,8 @@ export type OperatingSystem = "Windows" | "Linux" | "MacOS" | "Ios" | "Android" export type OptionalRange = { from: T | null; to: T | null } +export type OrderAndPagination = { orderOnly: TOrder } | { offset: { offset: number; order: TOrder | null } } | { cursor: TCursor } + export type Orientation = "Normal" | "MirroredHorizontal" | "CW90" | "MirroredVertical" | "MirroredHorizontalAnd270CW" | "MirroredHorizontalAnd90CW" | "CW180" | "CW270" /**