mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 11:13:29 +00:00
[MOB-35] Explorer sort by (#2387)
* pnpm * hide search on some screens * translations * move shared order stuff to @sd/client * some ideas * wip - redesign otw * Merge remote-tracking branch 'origin' into mob-35-explorer-sort-by * header adjustments and more --------- Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com> Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
This commit is contained in:
parent
81c2b8bf51
commit
7cd33727b3
|
@ -3,11 +3,10 @@ import {
|
|||
ArchiveBox,
|
||||
Briefcase,
|
||||
Clock,
|
||||
DotsThreeOutline,
|
||||
DotsThree,
|
||||
Heart,
|
||||
Images,
|
||||
MapPin,
|
||||
Tag,
|
||||
UserFocus
|
||||
} from 'phosphor-react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
|
@ -42,7 +41,7 @@ const BrowseCategories = () => {
|
|||
style={tw`h-9 w-9 rounded-full`}
|
||||
variant="gray"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" size={16} color={'white'} />
|
||||
<DotsThree weight="bold" size={20} color={'white'} />
|
||||
</Button>
|
||||
</View>
|
||||
<View style={tw`flex-row flex-wrap gap-2`}>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { DotsThree, Plus } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
@ -43,7 +43,7 @@ const BrowseLocations = () => {
|
|||
style={tw`h-9 w-9 rounded-full`}
|
||||
variant="gray"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" size={16} color={'white'} />
|
||||
<DotsThree weight="bold" size={20} color={'white'} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { DotsThree, Plus } from 'phosphor-react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
@ -40,7 +40,7 @@ const BrowseTags = () => {
|
|||
style={tw`w-9 rounded-full`}
|
||||
variant="gray"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" size={16} color={'white'} />
|
||||
<DotsThree weight="bold" size={20} color={'white'} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -100,7 +100,8 @@ const TagColumn = ({ tags, dataAmount }: TagColumnProps) => {
|
|||
onPress={() =>
|
||||
navigation.navigate('BrowseStack', {
|
||||
screen: 'Tag',
|
||||
params: { id: tag.id, color: tag.color }
|
||||
params: { id: tag.id, color: tag.color },
|
||||
initial: false
|
||||
})
|
||||
}
|
||||
tagColor={tag.color as ColorValue}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { AnimatePresence, MotiView } from 'moti';
|
||||
import { MonitorPlay, Rows, SlidersHorizontal, SquaresFour } from 'phosphor-react-native';
|
||||
import { Rows, SquaresFour } from 'phosphor-react-native';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { toast } from '~/components/primitive/Toast';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
|
||||
|
||||
|
@ -12,43 +11,41 @@ const Menu = () => {
|
|||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{store.toggleMenu && (
|
||||
<MotiView
|
||||
{store.toggleMenu && (
|
||||
<MotiView
|
||||
from={{ translateY: -70 }}
|
||||
animate={{ translateY: 0 }}
|
||||
exit={{ translateY: -70 }}
|
||||
transition={{
|
||||
type: 'timing',
|
||||
duration: 300,
|
||||
repeat: 0,
|
||||
repeatReverse: false
|
||||
}}
|
||||
exit={{ translateY: -70 }}
|
||||
style={tw`w-screen flex-row items-center justify-between border-b border-app-cardborder bg-app-header px-5 py-3`}
|
||||
>
|
||||
<View
|
||||
style={tw`w-screen flex-row items-center justify-between border-b border-app-cardborder bg-app-header px-7 py-4`}
|
||||
>
|
||||
<SortByMenu />
|
||||
<View style={tw`flex-row gap-3`}>
|
||||
<Pressable onPress={() => (getExplorerStore().layoutMode = 'grid')}>
|
||||
{store.layoutMode === 'grid' ? (
|
||||
<Pressable hitSlop={12} onPress={() => (getExplorerStore().layoutMode = 'list')}>
|
||||
<Rows
|
||||
weight='fill'
|
||||
color={tw.color('text-ink-faint'
|
||||
)}
|
||||
size={23}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable hitSlop={12} onPress={() => (getExplorerStore().layoutMode = 'grid')}>
|
||||
<SquaresFour
|
||||
weight='fill'
|
||||
color={tw.color(
|
||||
store.layoutMode === 'grid'
|
||||
? 'text-accent'
|
||||
: 'text-ink-dull'
|
||||
'text-ink-faint'
|
||||
)}
|
||||
size={23}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => (getExplorerStore().layoutMode = 'list')}>
|
||||
<Rows
|
||||
color={tw.color(
|
||||
store.layoutMode === 'list'
|
||||
? 'text-accent'
|
||||
: 'text-ink-dull'
|
||||
)}
|
||||
size={23}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
)}
|
||||
{/* <Pressable
|
||||
onPress={() => toast.error('Media view is not available yet...')}
|
||||
// onPress={() => (getExplorerStore().layoutMode = 'media')}
|
||||
>
|
||||
|
@ -60,12 +57,10 @@ const Menu = () => {
|
|||
)}
|
||||
size={23}
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable> */}
|
||||
</View>
|
||||
<SortByMenu />
|
||||
</View>
|
||||
</MotiView>
|
||||
)}
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,61 +1,78 @@
|
|||
import { ArrowDown, ArrowUp } from 'phosphor-react-native';
|
||||
import { useState } from 'react';
|
||||
import { ArrowDown, ArrowUp, CaretDown, Check } from 'phosphor-react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Menu, MenuItem } from '~/components/primitive/Menu';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
|
||||
import { SortOptionsType, getSearchStore, useSearchStore } from '~/stores/searchStore';
|
||||
const sortOptions = {
|
||||
none: 'None',
|
||||
name: 'Name',
|
||||
kind: 'Kind',
|
||||
favorite: 'Favorite',
|
||||
date_created: 'Date Created',
|
||||
date_modified: 'Date Modified',
|
||||
date_last_opened: 'Date Last Opened'
|
||||
};
|
||||
sizeInBytes: 'Size',
|
||||
dateIndexed: 'Date Indexed',
|
||||
dateCreated: 'Date Created',
|
||||
dateModified: 'Date Modified',
|
||||
dateAccessed: 'Date Accessed',
|
||||
dateTaken: 'Date Taken',
|
||||
} satisfies Record<SortOptionsType['by'], string>;
|
||||
|
||||
type SortByType = keyof typeof sortOptions;
|
||||
const sortOrder = ['Asc', 'Desc'] as SortOptionsType['direction'][];
|
||||
|
||||
const ArrowUpIcon = () => <ArrowUp weight="bold" size={16} color={tw.color('ink-dull')} />;
|
||||
const ArrowDownIcon = () => <ArrowDown weight="bold" size={16} color={tw.color('ink-dull')} />;
|
||||
const ArrowUpIcon = <ArrowUp style={tw`ml-0.5`} weight="bold" size={14} color={tw.color('ink-dull')} />;
|
||||
const ArrowDownIcon = <ArrowDown style={tw`ml-0.5`} weight="bold" size={14} color={tw.color('ink-dull')} />;
|
||||
|
||||
const SortByMenu = () => {
|
||||
const [sortBy, setSortBy] = useState<SortByType>('name');
|
||||
const [sortDirection, setSortDirection] = useState('asc' as 'asc' | 'desc');
|
||||
|
||||
const searchStore = useSearchStore();
|
||||
return (
|
||||
<View style={tw`flex-row items-center gap-1.5`}>
|
||||
<Text style={tw`mr-1 font-medium text-ink-dull`}>Sort:</Text>
|
||||
<Menu
|
||||
trigger={
|
||||
<View style={tw`flex flex-row items-center`}>
|
||||
<Text style={tw`mr-0.5 font-medium text-ink-dull`}>{sortOptions[sortBy]}</Text>
|
||||
{sortDirection === 'asc' ? <ArrowUpIcon /> : <ArrowDownIcon />}
|
||||
</View>
|
||||
}
|
||||
trigger={<Trigger activeOption={sortOptions[searchStore.sort.by]} />}
|
||||
|
||||
>
|
||||
{Object.entries(sortOptions).map(([value, text]) => (
|
||||
{(Object.entries(sortOptions) as [[SortOptionsType['by'], string]]).map(([value, text], idx) => (
|
||||
<View key={value}>
|
||||
<MenuItem
|
||||
key={value}
|
||||
icon={
|
||||
value === sortBy
|
||||
? sortDirection === 'asc'
|
||||
? ArrowUpIcon
|
||||
: ArrowDownIcon
|
||||
: undefined
|
||||
}
|
||||
icon={value === searchStore.sort.by ? Check : undefined}
|
||||
text={text}
|
||||
value={value}
|
||||
onSelect={() => {
|
||||
if (value === sortBy) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
return;
|
||||
}
|
||||
// Reset sort direction to descending
|
||||
sortDirection === 'asc' && setSortDirection('desc');
|
||||
setSortBy(value as SortByType);
|
||||
}}
|
||||
onSelect={() => getSearchStore().sort.by = value}
|
||||
/>
|
||||
{idx !== Object.keys(sortOptions).length - 1 && <View style={tw`border-b border-app-cardborder`} />}
|
||||
</View>
|
||||
))}
|
||||
</Menu>
|
||||
<Menu
|
||||
trigger={<Trigger
|
||||
triggerIcon={searchStore.sort.direction === 'Asc' ? ArrowUpIcon : ArrowDownIcon}
|
||||
activeOption={searchStore.sort.direction}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{sortOrder.map((value, idx) => (
|
||||
<View key={value}>
|
||||
<MenuItem
|
||||
icon={value === searchStore.sort.direction ? Check : undefined}
|
||||
text={value === 'Asc' ? 'Ascending' : 'Descending'}
|
||||
onSelect={() => getSearchStore().sort.direction = value}
|
||||
/>
|
||||
{idx !== 1 && <View style={tw`border-b border-app-cardborder`} />}
|
||||
</View>
|
||||
))}
|
||||
</Menu>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
activeOption: string;
|
||||
triggerIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Trigger = ({activeOption, triggerIcon}: Props) => {
|
||||
return (
|
||||
<View style={tw`flex flex-row items-center rounded-md border border-app-inputborder p-1.5`}>
|
||||
<Text style={tw`mr-0.5 text-ink-dull`}>{activeOption}</Text>
|
||||
{triggerIcon ? triggerIcon : <CaretDown style={tw`ml-0.5`} weight="bold" size={16} color={tw.color('ink-dull')} />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SortByMenu;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
|
||||
import { RouteProp, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackHeaderProps } from '@react-navigation/native-stack';
|
||||
import { ArrowLeft, DotsThreeOutline, MagnifyingGlass } from 'phosphor-react-native';
|
||||
import { ArrowLeft, DotsThree, MagnifyingGlass } from 'phosphor-react-native';
|
||||
import { Platform, Pressable, Text, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -43,29 +43,14 @@ export default function DynamicHeader({
|
|||
<HeaderIconKind routeParams={optionsRoute?.params} kind={kind} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={tw`max-w-[200px] text-xl font-bold text-white`}
|
||||
style={tw`max-w-[200px] text-lg font-bold text-white`}
|
||||
>
|
||||
{headerRoute?.options.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={tw`flex-row gap-3`}>
|
||||
{explorerMenu && (
|
||||
<Pressable
|
||||
hitSlop={12}
|
||||
onPress={() => {
|
||||
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
|
||||
}}
|
||||
>
|
||||
<DotsThreeOutline
|
||||
size={24}
|
||||
color={tw.color(
|
||||
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
|
||||
)}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
<View style={tw`flex-row gap-6`}>
|
||||
<Pressable
|
||||
hitSlop={12}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchStack', {
|
||||
|
@ -74,11 +59,27 @@ export default function DynamicHeader({
|
|||
}}
|
||||
>
|
||||
<MagnifyingGlass
|
||||
size={24}
|
||||
size={20}
|
||||
weight="bold"
|
||||
color={tw.color('text-zinc-300')}
|
||||
/>
|
||||
</Pressable>
|
||||
{explorerMenu && (
|
||||
<Pressable
|
||||
hitSlop={12}
|
||||
onPress={() => {
|
||||
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
|
||||
}}
|
||||
>
|
||||
<DotsThree
|
||||
size={24}
|
||||
weight='bold'
|
||||
color={tw.color(
|
||||
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
|
||||
)}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -94,7 +95,7 @@ interface HeaderIconKindProps {
|
|||
const HeaderIconKind = ({ routeParams, kind }: HeaderIconKindProps) => {
|
||||
switch (kind) {
|
||||
case 'location':
|
||||
return <Icon size={30} name="Folder" />;
|
||||
return <Icon size={24} name="Folder" />;
|
||||
case 'tag':
|
||||
return (
|
||||
<View
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Location, arraysEqual, byteSize, useOnlineLocations } from '@sd/client';
|
||||
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
|
||||
import { DotsThreeVertical } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
|
@ -73,8 +73,8 @@ const ListLocation = ({ location }: ListLocationProps) => {
|
|||
</Text>
|
||||
</View>
|
||||
<Pressable hitSlop={24} onPress={() => swipeRef.current?.openRight()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
<DotsThreeVertical
|
||||
weight="bold"
|
||||
size={20}
|
||||
color={tw.color('ink-dull')}
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DotsThreeOutline } from 'phosphor-react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { DotsThree } from 'phosphor-react-native';
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
|
||||
|
||||
|
@ -24,7 +24,7 @@ export default function CategoriesScreen() {
|
|||
style={tw`h-9 w-9 rounded-full`}
|
||||
variant="gray"
|
||||
>
|
||||
<DotsThreeOutline weight="fill" size={16} color={'white'} />
|
||||
<DotsThree weight='bold' size={20} color={'white'} />
|
||||
</Button>
|
||||
</View>
|
||||
<View style={tw`flex-row flex-wrap gap-2`}>
|
||||
|
|
|
@ -8,23 +8,23 @@ import {
|
|||
Menu as PMenu,
|
||||
renderers
|
||||
} from 'react-native-popup-menu';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { ClassInput } from 'twrnc';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
type MenuProps = {
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
triggerStyle?: ClassInput;
|
||||
};
|
||||
|
||||
// TODO: Still looks a bit off...
|
||||
export const Menu = (props: MenuProps) => (
|
||||
<View>
|
||||
<PMenu renderer={renderers.NotAnimatedContextMenu}>
|
||||
<PMenu renderer={renderers.NotAnimatedContextMenu} style={twStyle(props.triggerStyle)}>
|
||||
<MenuTrigger>{props.trigger}</MenuTrigger>
|
||||
<MenuOptions optionsContainerStyle={tw`rounded bg-app-menu p-1`}>
|
||||
<MenuOptions optionsContainerStyle={tw`rounded-md border border-app-cardborder bg-app-menu p-1`}>
|
||||
{props.children}
|
||||
</MenuOptions>
|
||||
</PMenu>
|
||||
</View>
|
||||
);
|
||||
|
||||
type MenuItemProps = {
|
||||
|
@ -35,16 +35,16 @@ export const MenuItem = ({ icon, ...props }: MenuItemProps) => {
|
|||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<View style={tw`flex flex-row items-center`}>
|
||||
<View style={tw`flex flex-1 flex-row items-center`}>
|
||||
{Icon && (
|
||||
<View style={tw`ml-1`}>
|
||||
<Icon />
|
||||
<Icon size={16} style={tw`text-ink`} />
|
||||
</View>
|
||||
)}
|
||||
<MenuOption
|
||||
{...props}
|
||||
customStyles={{
|
||||
optionText: tw`py-0.5 text-sm font-medium text-ink`
|
||||
optionText: tw`w-full py-1 text-sm font-medium text-ink`
|
||||
}}
|
||||
style={tw`flex flex-row items-center`}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { tw } from '~/lib/tailwind';
|
|||
import { getSearchStore } from '~/stores/searchStore';
|
||||
|
||||
interface Props {
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export default function Search({ placeholder }: Props) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
|
||||
import { Tag } from '@sd/client';
|
||||
import { DotsThreeVertical } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { ClassInput } from 'twrnc';
|
||||
import { Tag } from '@sd/client';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
import RightActions from './RightActions';
|
||||
|
@ -40,11 +40,11 @@ const ListTag = ({ tag, tagStyle }: ListTagProps) => {
|
|||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => swipeRef.current?.openRight()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-dull')}
|
||||
/>
|
||||
<DotsThreeVertical
|
||||
weight="bold"
|
||||
size={20}
|
||||
color={tw.color('ink-dull')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Swipeable>
|
||||
|
|
30
apps/mobile/src/hooks/useSortBy.ts
Normal file
30
apps/mobile/src/hooks/useSortBy.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { FilePathOrder } from "@sd/client";
|
||||
import { SortOptionsType, useSearchStore } from "~/stores/searchStore";
|
||||
|
||||
/**
|
||||
* This hook provides a sorting order object based on user preferences
|
||||
* for constructing the order query.
|
||||
*/
|
||||
|
||||
export const useSortBy = (): FilePathOrder | null => {
|
||||
const searchStore = useSearchStore();
|
||||
const { by, direction } = searchStore.sort;
|
||||
|
||||
// if no sort by field is selected, return null
|
||||
if (by === 'none') return null;
|
||||
|
||||
// some sort by fields have common keys
|
||||
const common = { field: by, value: direction };
|
||||
|
||||
const fields: Record<Exclude<SortOptionsType['by'], 'none'>,any> = {
|
||||
name: common,
|
||||
sizeInBytes: common,
|
||||
dateIndexed: common,
|
||||
dateCreated: common,
|
||||
dateModified: common,
|
||||
dateAccessed: { field: "object", value: { field: "dateAccessed", value: direction} },
|
||||
dateTaken: { field: "object", value: {field: 'mediaData', value: { field: "epochTime", value: direction}} }
|
||||
};
|
||||
|
||||
return fields[by];
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { usePathsExplorerQuery } from '@sd/client';
|
||||
import { ArrowLeft, DotsThreeOutline, FunnelSimple } from 'phosphor-react-native';
|
||||
import { ArrowLeft, DotsThree, FunnelSimple } from 'phosphor-react-native';
|
||||
import { Suspense, useDeferredValue, useState } from 'react';
|
||||
import { ActivityIndicator, Platform, Pressable, TextInput, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
@ -8,6 +8,7 @@ import Explorer from '~/components/explorer/Explorer';
|
|||
import Empty from '~/components/layout/Empty';
|
||||
import FiltersBar from '~/components/search/filters/FiltersBar';
|
||||
import { useFiltersSearch } from '~/hooks/useFiltersSearch';
|
||||
import { useSortBy } from '~/hooks/useSortBy';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SearchStackScreenProps } from '~/navigation/SearchStack';
|
||||
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
|
||||
|
@ -20,35 +21,36 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
|
|||
const isFocused = useIsFocused();
|
||||
const [search, setSearch] = useState('');
|
||||
const deferredSearch = useDeferredValue(search);
|
||||
|
||||
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
const order = useSortBy();
|
||||
|
||||
const objects = usePathsExplorerQuery({
|
||||
order,
|
||||
arg: {
|
||||
take: 30,
|
||||
filters: searchStore.mergedFilters,
|
||||
},
|
||||
enabled: isFocused && searchStore.mergedFilters.length > 1, // only fetch when screen is focused & filters are applied
|
||||
suspense: true,
|
||||
order: null,
|
||||
onSuccess: () => getExplorerStore().resetNewThumbnails()
|
||||
});
|
||||
|
||||
useFiltersSearch(deferredSearch);
|
||||
|
||||
const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length;
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
|
||||
// Check if there are no objects or no search
|
||||
const noObjects = objects.items?.length === 0 || !objects.items;
|
||||
const noSearch = deferredSearch.length === 0 && appliedFiltersLength === 0;
|
||||
|
||||
useFiltersSearch(deferredSearch);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={twStyle('flex-1 bg-app-header', {
|
||||
style={twStyle('relative z-50 flex-1 bg-app-header', {
|
||||
paddingTop: headerHeight + (isAndroid ? 15 : 0)
|
||||
})}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={tw`relative z-20 border-b border-app-cardborder bg-app-header`}>
|
||||
<View style={tw`relative z-20 border-b border-app-cardborder bg-app-header pt-2`}>
|
||||
{/* Search area input container */}
|
||||
<View style={tw`flex-row items-center justify-between gap-4 px-5 pb-3`}>
|
||||
{/* Back Button */}
|
||||
|
@ -95,10 +97,11 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => {
|
|||
getExplorerStore().toggleMenu = !explorerStore.toggleMenu;
|
||||
}}
|
||||
>
|
||||
<DotsThreeOutline
|
||||
<DotsThree
|
||||
size={24}
|
||||
weight='bold'
|
||||
color={tw.color(
|
||||
explorerStore.toggleMenu ? 'text-accent' : 'text-zinc-300'
|
||||
explorerStore.toggleMenu ? 'text-accent' : 'text-ink-dull'
|
||||
)}
|
||||
/>
|
||||
</Pressable>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { proxySet } from 'valtio/utils';
|
||||
import { resetStore } from '@sd/client';
|
||||
import { resetStore, type Ordering } from '@sd/client';
|
||||
|
||||
export type ExplorerLayoutMode = 'list' | 'grid' | 'media';
|
||||
|
||||
|
@ -18,7 +18,12 @@ const state = {
|
|||
// Using gridNumColumns instead of fixed size. We dynamically calculate the item size.
|
||||
gridNumColumns: 3,
|
||||
listItemSize: 65,
|
||||
newThumbnails: proxySet() as Set<string>
|
||||
newThumbnails: proxySet() as Set<string>,
|
||||
// sorting
|
||||
// we will display different sorting options based on the kind of explorer we are in
|
||||
sortType: 'filePath' as 'filePath' | 'object' | 'ephemeral',
|
||||
orderKey: 'name',
|
||||
orderDirection: 'Asc' as 'Asc' | 'Desc'
|
||||
};
|
||||
|
||||
export function flattenThumbnailKey(thumbKey: string[]) {
|
||||
|
|
|
@ -3,6 +3,10 @@ import { proxy, useSnapshot } from 'valtio';
|
|||
import { IconName } from '~/components/icons/Icon';
|
||||
|
||||
export type SearchFilters = 'locations' | 'tags' | 'name' | 'extension' | 'hidden' | 'kind';
|
||||
export type SortOptionsType = {
|
||||
by: 'none' | 'name' | 'sizeInBytes' | 'dateIndexed' | 'dateCreated' | 'dateModified' | 'dateAccessed' | 'dateTaken';
|
||||
direction: 'Asc' | 'Desc';
|
||||
}
|
||||
|
||||
export interface FilterItem {
|
||||
id: number;
|
||||
|
@ -32,6 +36,7 @@ export interface Filters {
|
|||
interface State {
|
||||
search: string;
|
||||
filters: Filters;
|
||||
sort: SortOptionsType;
|
||||
appliedFilters: Partial<Filters>;
|
||||
mergedFilters: SearchFilterArgs[],
|
||||
disableActionButtons: boolean;
|
||||
|
@ -47,6 +52,10 @@ const initialState: State = {
|
|||
hidden: false,
|
||||
kind: []
|
||||
},
|
||||
sort: {
|
||||
by: 'none',
|
||||
direction: 'Asc'
|
||||
},
|
||||
appliedFilters: {},
|
||||
mergedFilters: [],
|
||||
disableActionButtons: true
|
||||
|
|
|
@ -2,7 +2,7 @@ import {
|
|||
createOrdering,
|
||||
explorerLayout,
|
||||
getOrderingDirection,
|
||||
orderingKey,
|
||||
getOrderingKey,
|
||||
useExplorerLayoutStore
|
||||
} from '@sd/client';
|
||||
import { RadixCheckbox, Select, SelectOption, Slider, tw, z } from '@sd/ui';
|
||||
|
@ -28,7 +28,7 @@ export default () => {
|
|||
<div className="flex flex-col">
|
||||
<Subheading>{t('sort_by')}</Subheading>
|
||||
<Select
|
||||
value={settings.order ? orderingKey(settings.order) : 'none'}
|
||||
value={settings.order ? getOrderingKey(settings.order) : 'none'}
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onChange={(key) => {
|
||||
|
@ -62,7 +62,7 @@ export default () => {
|
|||
if (explorer.settingsStore.order === null) return;
|
||||
|
||||
explorer.settingsStore.order = createOrdering(
|
||||
orderingKey(explorer.settingsStore.order),
|
||||
getOrderingKey(explorer.settingsStore.order),
|
||||
order
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -6,7 +6,12 @@ import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState
|
|||
import BasicSticky from 'react-sticky-el';
|
||||
import { useWindowEventListener } from 'rooks';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { createOrdering, getOrderingDirection, orderingKey, type ExplorerItem } from '@sd/client';
|
||||
import {
|
||||
createOrdering,
|
||||
getOrderingDirection,
|
||||
getOrderingKey,
|
||||
type ExplorerItem
|
||||
} from '@sd/client';
|
||||
import { ContextMenu } from '@sd/ui';
|
||||
import { TruncatedText } from '~/components';
|
||||
import { useShortcut } from '~/hooks';
|
||||
|
@ -804,7 +809,7 @@ export const ListView = memo(() => {
|
|||
|
||||
const orderKey =
|
||||
explorerSettings.order &&
|
||||
orderingKey(explorerSettings.order);
|
||||
getOrderingKey(explorerSettings.order);
|
||||
|
||||
const orderingDirection =
|
||||
orderKey &&
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { OrderingKey, getOrderingDirection, getOrderingKey } from '@sd/client';
|
||||
import { LoadMoreTrigger, useGrid, useScrollMargin, useVirtualizer } from '@virtual-grid/react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { getOrderingDirection, OrderingKey, orderingKey } from '@sd/client';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
import { useExplorerContext } from '../../Context';
|
||||
|
@ -28,7 +28,7 @@ export const MediaView = () => {
|
|||
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const orderBy = explorerSettings.order && orderingKey(explorerSettings.order);
|
||||
const orderBy = explorerSettings.order && getOrderingKey(explorerSettings.order);
|
||||
const orderDirection = explorerSettings.order && getOrderingDirection(explorerSettings.order);
|
||||
|
||||
const { dateFormat } = useLocale();
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { proxy } from 'valtio';
|
||||
import { proxySet } from 'valtio/utils';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
resetStore,
|
||||
type DoubleClickAction,
|
||||
|
@ -9,6 +6,9 @@ import {
|
|||
type ExplorerSettings,
|
||||
type Ordering
|
||||
} from '@sd/client';
|
||||
import { proxy } from 'valtio';
|
||||
import { proxySet } from 'valtio/utils';
|
||||
import { z } from 'zod';
|
||||
import i18n from '~/app/I18n';
|
||||
|
||||
import {
|
||||
|
|
|
@ -16,6 +16,17 @@ import { getSidebarStore, useSidebarStore } from '../store';
|
|||
import IsRunningJob from './IsRunningJob';
|
||||
import JobGroup from './JobGroup';
|
||||
|
||||
const sortByCreatedAt = (a: IJobGroup, b: IJobGroup) => {
|
||||
const aDate = dayjs(a.created_at);
|
||||
const bDate = dayjs(b.created_at);
|
||||
if (aDate.isBefore(bDate)) {
|
||||
return 1;
|
||||
} else if (bDate.isBefore(aDate)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
function sortJobData(jobs: IJobGroup[]) {
|
||||
const runningJobs: IJobGroup[] = [];
|
||||
const otherJobs: IJobGroup[] = [];
|
||||
|
@ -28,17 +39,6 @@ function sortJobData(jobs: IJobGroup[]) {
|
|||
}
|
||||
});
|
||||
|
||||
const sortByCreatedAt = (a: IJobGroup, b: IJobGroup) => {
|
||||
const aDate = dayjs(a.created_at);
|
||||
const bDate = dayjs(b.created_at);
|
||||
if (aDate.isBefore(bDate)) {
|
||||
return 1;
|
||||
} else if (bDate.isBefore(aDate)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
runningJobs.sort(sortByCreatedAt);
|
||||
otherJobs.sort(sortByCreatedAt);
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ import { memo, Suspense, useDeferredValue, useMemo } from 'react';
|
|||
import {
|
||||
ExplorerItem,
|
||||
getExplorerItemData,
|
||||
ItemData,
|
||||
nonIndexedPathOrderingSchema,
|
||||
SortOrder,
|
||||
useLibraryContext,
|
||||
useUnsafeStreamedQuery,
|
||||
type EphemeralPathOrder
|
||||
|
@ -27,11 +30,7 @@ import { useRouteTitle } from '~/hooks/useRouteTitle';
|
|||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import {
|
||||
createDefaultExplorerSettings,
|
||||
explorerStore,
|
||||
nonIndexedPathOrderingSchema
|
||||
} from './Explorer/store';
|
||||
import { createDefaultExplorerSettings, explorerStore } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { ObjectOrder, objectOrderingKeysSchema } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle } from '~/hooks';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
|
||||
import { createDefaultExplorerSettings } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectOrder, useLibraryQuery } from '@sd/client';
|
||||
import { ObjectOrder, objectOrderingKeysSchema, useLibraryQuery } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle } from '~/hooks';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
|
||||
import { createDefaultExplorerSettings } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { ArrowClockwise, Info } from '@phosphor-icons/react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { stringify } from 'uuid';
|
||||
import {
|
||||
arraysEqual,
|
||||
FilePathOrder,
|
||||
filePathOrderingKeysSchema,
|
||||
Location,
|
||||
useExplorerLayoutStore,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibrarySubscription,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { Loader, Tooltip } from '@sd/ui';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { stringify } from 'uuid';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Folder, Icon } from '~/components';
|
||||
import {
|
||||
|
@ -26,11 +25,7 @@ import { useQuickRescan } from '~/hooks/useQuickRescan';
|
|||
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import {
|
||||
createDefaultExplorerSettings,
|
||||
explorerStore,
|
||||
filePathOrderingKeysSchema
|
||||
} from '../Explorer/store';
|
||||
import { createDefaultExplorerSettings, explorerStore } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorerPreferences } from '../Explorer/useExplorerPreferences';
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useDiscoveredPeers } from '@sd/client';
|
||||
import { nonIndexedPathOrderingSchema, useDiscoveredPeers } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { useRouteTitle } from '~/hooks/useRouteTitle';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { createDefaultExplorerSettings, nonIndexedPathOrderingSchema } from './Explorer/store';
|
||||
import { createDefaultExplorerSettings } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { ObjectOrder, objectOrderingKeysSchema } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle } from '~/hooks';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
|
||||
import { createDefaultExplorerSettings } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
FilePathOrder,
|
||||
filePathOrderingKeysSchema,
|
||||
SearchFilterArgs,
|
||||
SearchTarget,
|
||||
useLibraryMutation,
|
||||
|
@ -15,11 +15,7 @@ import { useRouteTitle, useZodParams } from '~/hooks';
|
|||
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import {
|
||||
createDefaultExplorerSettings,
|
||||
explorerStore,
|
||||
filePathOrderingKeysSchema
|
||||
} from '../Explorer/store';
|
||||
import { createDefaultExplorerSettings, explorerStore } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectOrder } from '@sd/client';
|
||||
import { ObjectOrder, objectOrderingKeysSchema } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle } from '~/hooks';
|
||||
|
||||
import { SearchContextProvider, SearchOptions, useSearch } from '.';
|
||||
import { SearchContextProvider, SearchOptions } from '.';
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
|
||||
import { createDefaultExplorerSettings } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View/EmptyNotice';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ObjectOrder, objectOrderingKeysSchema, Tag, useLibraryQuery } from '@sd/client';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ObjectOrder, Tag, useLibraryQuery } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
|
@ -7,7 +7,7 @@ import { stringify } from '~/util/uuid';
|
|||
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
|
||||
import { createDefaultExplorerSettings } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorerPreferences } from '../Explorer/useExplorerPreferences';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { SortOrder } from '../core';
|
||||
|
||||
export * from './useExplorerInfiniteQuery';
|
||||
export * from './usePathsInfiniteQuery';
|
||||
export * from './usePathsOffsetInfiniteQuery';
|
||||
|
@ -7,51 +5,4 @@ export * from './usePathsExplorerQuery';
|
|||
export * from './useObjectsInfiniteQuery';
|
||||
export * from './useObjectsOffsetInfiniteQuery';
|
||||
export * from './useObjectsExplorerQuery';
|
||||
|
||||
export type Ordering = { field: string; value: SortOrder | Ordering };
|
||||
// branded type for added type-safety
|
||||
export type OrderingKey = string & {};
|
||||
|
||||
type OrderingValue<T extends Ordering, K extends string> = Extract<T, { field: K }>['value'];
|
||||
|
||||
export type OrderingKeys<T extends Ordering> = T extends Ordering
|
||||
? {
|
||||
[K in T['field']]: OrderingValue<T, K> extends SortOrder
|
||||
? K
|
||||
: OrderingValue<T, K> extends Ordering
|
||||
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
|
||||
: never;
|
||||
}[T['field']]
|
||||
: never;
|
||||
|
||||
export function orderingKey(ordering: Ordering): OrderingKey {
|
||||
let base = ordering.field;
|
||||
|
||||
if (typeof ordering.value === 'object') {
|
||||
base += `.${orderingKey(ordering.value)}`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
export function createOrdering<TOrdering extends Ordering = Ordering>(
|
||||
key: OrderingKey,
|
||||
value: SortOrder
|
||||
): TOrdering {
|
||||
return key
|
||||
.split('.')
|
||||
.reverse()
|
||||
.reduce((acc, field, i) => {
|
||||
if (i === 0)
|
||||
return {
|
||||
field,
|
||||
value
|
||||
};
|
||||
else return { field, value: acc };
|
||||
}, {} as any);
|
||||
}
|
||||
|
||||
export function getOrderingDirection(ordering: Ordering): SortOrder {
|
||||
if (typeof ordering.value === 'object') return getOrderingDirection(ordering.value);
|
||||
else return ordering.value;
|
||||
}
|
||||
export * from './order';
|
||||
|
|
74
packages/client/src/explorer/order.ts
Normal file
74
packages/client/src/explorer/order.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { SortOrder } from '../core';
|
||||
|
||||
export type Ordering = { field: string; value: SortOrder | Ordering };
|
||||
// branded type for added type-safety
|
||||
export type OrderingKey = string & {};
|
||||
|
||||
type OrderingValue<T extends Ordering, K extends string> = Extract<T, { field: K }>['value'];
|
||||
|
||||
export type OrderingKeys<T extends Ordering> = T extends Ordering
|
||||
? {
|
||||
[K in T['field']]: OrderingValue<T, K> extends SortOrder
|
||||
? K
|
||||
: OrderingValue<T, K> extends Ordering
|
||||
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
|
||||
: never;
|
||||
}[T['field']]
|
||||
: never;
|
||||
|
||||
export function getOrderingKey(ordering: Ordering): OrderingKey {
|
||||
let base = ordering.field;
|
||||
|
||||
if (typeof ordering.value === 'object') {
|
||||
base += `.${getOrderingKey(ordering.value)}`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
export function createOrdering<TOrdering extends Ordering = Ordering>(
|
||||
key: OrderingKey,
|
||||
value: SortOrder
|
||||
): TOrdering {
|
||||
return key
|
||||
.split('.')
|
||||
.reverse()
|
||||
.reduce((acc, field, i) => {
|
||||
if (i === 0)
|
||||
return {
|
||||
field,
|
||||
value
|
||||
};
|
||||
else return { field, value: acc };
|
||||
}, {} as any);
|
||||
}
|
||||
|
||||
export function getOrderingDirection(ordering: Ordering): SortOrder {
|
||||
if (typeof ordering.value === 'object') return getOrderingDirection(ordering.value);
|
||||
else return ordering.value;
|
||||
}
|
||||
|
||||
export const filePathOrderingKeysSchema = z.union([
|
||||
z.literal('name').describe('Name'),
|
||||
z.literal('sizeInBytes').describe('Size'),
|
||||
z.literal('dateModified').describe('Date Modified'),
|
||||
z.literal('dateIndexed').describe('Date Indexed'),
|
||||
z.literal('dateCreated').describe('Date Created'),
|
||||
z.literal('object.dateAccessed').describe('Date Accessed'),
|
||||
z.literal('object.mediaData.epochTime').describe('Date Taken')
|
||||
]);
|
||||
|
||||
export const objectOrderingKeysSchema = z.union([
|
||||
z.literal('dateAccessed').describe('Date Accessed'),
|
||||
z.literal('kind').describe('Kind'),
|
||||
z.literal('mediaData.epochTime').describe('Date Taken')
|
||||
]);
|
||||
|
||||
export const nonIndexedPathOrderingSchema = z.union([
|
||||
z.literal('name').describe('Name'),
|
||||
z.literal('sizeInBytes').describe('Size'),
|
||||
z.literal('dateCreated').describe('Date Created'),
|
||||
z.literal('dateModified').describe('Date Modified')
|
||||
]);
|
Loading…
Reference in a new issue