mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[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
This commit is contained in:
parent
ae0f5c744e
commit
e7fbdb479c
|
@ -15,7 +15,8 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
|
|||
filter: {
|
||||
locationId: id,
|
||||
path: path ?? ''
|
||||
}
|
||||
},
|
||||
take: 100
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ const SearchScreen = ({ navigation }: RootStackScreenProps<'Search'>) => {
|
|||
// ...args,
|
||||
filter: {
|
||||
search: deferredSearch
|
||||
}
|
||||
},
|
||||
take: 100
|
||||
}
|
||||
],
|
||||
{
|
||||
|
|
|
@ -11,7 +11,8 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag'
|
|||
{
|
||||
filter: {
|
||||
tags: [id]
|
||||
}
|
||||
},
|
||||
take: 100
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<T> {
|
||||
cursor: Option<Vec<u8>>,
|
||||
|
@ -53,16 +55,16 @@ impl From<SortOrder> 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<ObjectSearchOrdering>),
|
||||
Object(Box<ObjectOrder>),
|
||||
}
|
||||
|
||||
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<T> {
|
||||
order: SortOrder,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FilePathObjectCursor {
|
||||
DateAccessed(CursorOrderItem<DateTime<FixedOffset>>),
|
||||
Kind(CursorOrderItem<i32>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FilePathCursorVariant {
|
||||
None(file_path::pub_id::Type),
|
||||
Name(CursorOrderItem<String>),
|
||||
// SizeInBytes(CursorOrderItem<Vec<u8>>),
|
||||
DateCreated(CursorOrderItem<DateTime<FixedOffset>>),
|
||||
DateModified(CursorOrderItem<DateTime<FixedOffset>>),
|
||||
DateIndexed(CursorOrderItem<DateTime<FixedOffset>>),
|
||||
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<DateTime<FixedOffset>>),
|
||||
Kind(CursorOrderItem<i32>),
|
||||
}
|
||||
|
||||
#[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<TOrder, TCursor> {
|
||||
OrderOnly(TOrder),
|
||||
Offset { offset: i32, order: Option<TOrder> },
|
||||
Cursor(TCursor),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Type, Debug, Default, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ObjectHiddenFilter {
|
||||
|
@ -277,7 +328,7 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
.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<Ctx> {
|
|||
|
||||
#[derive(Deserialize, Type, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NonIndexedPath {
|
||||
struct EphemeralPathSearchArgs {
|
||||
path: PathBuf,
|
||||
with_hidden_files: bool,
|
||||
#[specta(optional)]
|
||||
order: Option<NonIndexedPathOrdering>,
|
||||
order: Option<EphemeralPathOrder>,
|
||||
}
|
||||
|
||||
R.with2(library()).query(
|
||||
|(node, library),
|
||||
NonIndexedPath {
|
||||
EphemeralPathSearchArgs {
|
||||
path,
|
||||
with_hidden_files,
|
||||
order,
|
||||
|
@ -303,53 +354,36 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
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<Ctx> {
|
|||
)
|
||||
})
|
||||
.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<i32>,
|
||||
#[specta(optional)]
|
||||
order: Option<FilePathSearchOrdering>,
|
||||
#[specta(optional)]
|
||||
pagination: Option<FilePathPagination>,
|
||||
order_and_pagination: Option<OrderAndPagination<FilePathOrder, FilePathCursor>>,
|
||||
#[serde(default)]
|
||||
filter: FilePathFilterArgs,
|
||||
#[serde(default = "default_group_directories")]
|
||||
|
@ -387,19 +411,18 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
|(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<Ctx> {
|
|||
}
|
||||
|
||||
// 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<Ctx> {
|
|||
})
|
||||
}
|
||||
|
||||
Ok(SearchData { items, cursor })
|
||||
Ok(SearchData {
|
||||
items,
|
||||
cursor: None,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -478,22 +569,12 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
})
|
||||
.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<i32>,
|
||||
#[specta(optional)]
|
||||
order: Option<ObjectSearchOrdering>,
|
||||
#[specta(optional)]
|
||||
pagination: Option<ObjectPagination>,
|
||||
order_and_pagination: Option<OrderAndPagination<ObjectOrder, ObjectCursor>>,
|
||||
#[serde(default)]
|
||||
filter: ObjectFilterArgs,
|
||||
}
|
||||
|
@ -502,30 +583,55 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
|(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<Ctx> {
|
|||
.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);
|
||||
|
|
|
@ -52,7 +52,7 @@ impl LibraryPreferences {
|
|||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocationSettings {
|
||||
explorer: ExplorerSettings<search::FilePathSearchOrdering>,
|
||||
explorer: ExplorerSettings<search::FilePathOrder>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
|
|
|
@ -16,9 +16,9 @@ export const useExplorerContext = () => {
|
|||
return ctx;
|
||||
};
|
||||
|
||||
export const ExplorerContextProvider = <TOrdering extends Ordering>({
|
||||
export const ExplorerContextProvider = <TExplorer extends UseExplorer<any>>({
|
||||
explorer,
|
||||
children
|
||||
}: PropsWithChildren<{
|
||||
explorer: UseExplorer<TOrdering>;
|
||||
explorer: TExplorer;
|
||||
}>) => <ExplorerContext.Provider value={explorer as any}>{children}</ExplorerContext.Provider>;
|
||||
|
|
|
@ -25,7 +25,6 @@ const INSPECTOR_WIDTH = 260;
|
|||
export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorer = useExplorerContext();
|
||||
const [{ path }] = useExplorerSearchParams();
|
||||
|
||||
// Can we put this somewhere else -_-
|
||||
useLibrarySubscription(['jobs.newThumbnail'], {
|
||||
|
|
|
@ -65,13 +65,11 @@ export function getOrderingDirection(ordering: Ordering): SortOrder {
|
|||
else return ordering.value;
|
||||
}
|
||||
|
||||
export const createDefaultExplorerSettings = <TOrder extends Ordering>({
|
||||
order
|
||||
}: {
|
||||
order: TOrder | null;
|
||||
export const createDefaultExplorerSettings = <TOrder extends Ordering>(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<string>,
|
||||
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<CutCopyStat
|
|||
|
||||
export const filePathOrderingKeysSchema = z.union([
|
||||
z.literal('name').describe('Name'),
|
||||
z.literal('sizeInBytes').describe('Size'),
|
||||
// z.literal('sizeInBytes').describe('Size'),
|
||||
z.literal('dateModified').describe('Date Modified'),
|
||||
z.literal('dateIndexed').describe('Date Indexed'),
|
||||
z.literal('dateCreated').describe('Date Created'),
|
||||
|
@ -162,7 +159,7 @@ export const objectOrderingKeysSchema = z.union([
|
|||
|
||||
export const nonIndexedPathOrderingSchema = z.union([
|
||||
z.literal('name').describe('Name'),
|
||||
z.literal('sizeInBytes').describe('Size'),
|
||||
// z.literal('sizeInBytes').describe('Size'),
|
||||
z.literal('dateCreated').describe('Date Created'),
|
||||
z.literal('dateModified').describe('Date Modified')
|
||||
]);
|
||||
|
|
77
interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts
Normal file
77
interface/app/$libraryId/Explorer/useObjectsInfiniteQuery.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { UseInfiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ExplorerItem,
|
||||
LibraryConfigWrapped,
|
||||
ObjectCursor,
|
||||
ObjectOrder,
|
||||
ObjectSearchArgs,
|
||||
OrderAndPagination,
|
||||
SearchData,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { getExplorerStore } from './store';
|
||||
import { UseExplorerSettings } from './useExplorer';
|
||||
|
||||
export function useObjectsInfiniteQuery({
|
||||
library,
|
||||
arg,
|
||||
settings,
|
||||
...args
|
||||
}: {
|
||||
library: LibraryConfigWrapped;
|
||||
arg: ObjectSearchArgs;
|
||||
settings: UseExplorerSettings<ObjectOrder>;
|
||||
} & Pick<UseInfiniteQueryOptions<SearchData<ExplorerItem>>, '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<ExplorerItem, { type: 'Object' }> = pageParam;
|
||||
const { order } = explorerSettings;
|
||||
|
||||
let orderAndPagination: OrderAndPagination<ObjectOrder, ObjectCursor> | 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
|
||||
});
|
||||
}
|
154
interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts
Normal file
154
interface/app/$libraryId/Explorer/usePathsInfiniteQuery.ts
Normal file
|
@ -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<FilePathOrder>;
|
||||
} & Pick<UseInfiniteQueryOptions<SearchData<ExplorerItem>>, '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<ExplorerItem, { type: 'Path' }> = pageParam;
|
||||
const { order } = explorerSettings;
|
||||
|
||||
let orderAndPagination: OrderAndPagination<FilePathOrder, FilePathCursor> | 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
|
||||
});
|
||||
}
|
|
@ -15,7 +15,6 @@ export default () => {
|
|||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const platform = useOperatingSystem(false);
|
||||
const os = useOperatingSystem(true);
|
||||
const keybind = keybindForOs(os);
|
||||
|
||||
|
|
|
@ -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<NonIndexedPathOrdering>({
|
||||
createDefaultExplorerSettings<EphemeralPathOrder>({
|
||||
order: {
|
||||
field: 'name',
|
||||
value: 'Asc'
|
||||
|
|
|
@ -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<FilePathSearchOrdering>({
|
||||
order: {
|
||||
field: 'name',
|
||||
value: 'Asc'
|
||||
}
|
||||
const defaults = createDefaultExplorerSettings<FilePathOrder>({
|
||||
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<FilePathSearchOrdering>) => {
|
||||
async (settings: ExplorerSettings<FilePathOrder>) => {
|
||||
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<FilePathSearchOrdering>({
|
||||
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<FilePathSearchOrdering>;
|
||||
settings: UseExplorerSettings<FilePathOrder>;
|
||||
}) => {
|
||||
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 {
|
||||
|
|
|
@ -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<Record<Category, string>> = {
|
||||
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<ObjectSearchOrdering>
|
||||
) {
|
||||
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<FilePathOrder>(), []),
|
||||
orderingKeys: filePathOrderingKeysSchema
|
||||
});
|
||||
|
||||
const objectsExplorerSettings = useExplorerSettings({
|
||||
settings: useMemo(() => createDefaultExplorerSettings<ObjectOrder>(), []),
|
||||
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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<ObjectSearchOrdering>({
|
||||
order: null
|
||||
}),
|
||||
[]
|
||||
),
|
||||
onSettingsChanged: () => {},
|
||||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category>('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;
|
||||
|
|
|
@ -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<FilePathSearchOrdering>({
|
||||
createDefaultExplorerSettings<FilePathOrder>({
|
||||
order: {
|
||||
field: 'name',
|
||||
value: 'Asc'
|
||||
|
|
|
@ -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<ObjectSearchOrdering>({
|
||||
createDefaultExplorerSettings<ObjectOrder>({
|
||||
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 (
|
||||
|
|
|
@ -21,11 +21,12 @@ export const PathParamsSchema = z.object({ path: z.string().optional() });
|
|||
export type PathParams = z.infer<typeof PathParamsSchema>;
|
||||
|
||||
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()
|
||||
|
|
|
@ -17,7 +17,7 @@ export function useZodSearchParams<Z extends z.AnyZodObject>(schema: Z) {
|
|||
typedSearchParams.data as z.infer<Z>,
|
||||
useCallback(
|
||||
(
|
||||
data: z.input<Z> | ((data: z.input<Z>) => z.infer<Z>),
|
||||
data: z.input<Z> | ((data: z.input<Z>) => z.input<Z>),
|
||||
navigateOpts?: NavigateOptions
|
||||
) => {
|
||||
if (typeof data === 'function') {
|
||||
|
@ -26,7 +26,7 @@ export function useZodSearchParams<Z extends z.AnyZodObject>(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);
|
||||
|
|
|
@ -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<null>, result: LibraryPreferences } |
|
||||
{ key: "search.ephemeralPaths", input: LibraryArgs<NonIndexedPath>, result: NonIndexedFileSystemEntries } |
|
||||
{ key: "search.ephemeralPaths", input: LibraryArgs<EphemeralPathSearchArgs>, result: NonIndexedFileSystemEntries } |
|
||||
{ key: "search.objects", input: LibraryArgs<ObjectSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "search.objectsCount", input: LibraryArgs<{ filter?: ObjectFilterArgs }>, result: number } |
|
||||
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
|
@ -129,6 +129,8 @@ export type Composite = "Unknown" | "False" | "General" | "Live"
|
|||
|
||||
export type CreateLibraryArgs = { name: LibraryName }
|
||||
|
||||
export type CursorOrderItem<T> = { 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<string> }
|
||||
|
||||
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<string> } | { dateCreated: CursorOrderItem<string> } | { dateModified: CursorOrderItem<string> } | { dateIndexed: CursorOrderItem<string> } | { object: FilePathObjectCursor }
|
||||
|
||||
export type FilePathFilterArgs = { locationId?: number | null; search?: string | null; extension?: string | null; createdAt?: OptionalRange<string>; path?: string | null; object?: ObjectFilterArgs | null }
|
||||
|
||||
export type FilePathPagination = { cursor: { pub_id: number[] } } | { offset: number }
|
||||
export type FilePathObjectCursor = { dateAccessed: CursorOrderItem<string> } | { kind: CursorOrderItem<number> }
|
||||
|
||||
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<FilePathOrder, FilePathCursor> | 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<FilePathSearchOrdering> }
|
||||
export type LocationSettings = { explorer: ExplorerSettings<FilePathOrder> }
|
||||
|
||||
/**
|
||||
* `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<string> } | { kind: CursorOrderItem<number> }
|
||||
|
||||
export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: ObjectHiddenFilter; dateAccessed?: MaybeNot<string | null> | 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<ObjectOrder, ObjectCursor> | 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<T> = { from: T | null; to: T | null }
|
||||
|
||||
export type OrderAndPagination<TOrder, TCursor> = { orderOnly: TOrder } | { offset: { offset: number; order: TOrder | null } } | { cursor: TCursor }
|
||||
|
||||
export type Orientation = "Normal" | "MirroredHorizontal" | "CW90" | "MirroredVertical" | "MirroredHorizontalAnd270CW" | "MirroredHorizontalAnd90CW" | "CW180" | "CW270"
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue