[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:
Utku 2024-05-08 01:12:09 +03:00 committed by GitHub
parent 81c2b8bf51
commit 7cd33727b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 319 additions and 239 deletions

View file

@ -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`}>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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

View file

@ -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')}
/>

View file

@ -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`}>

View file

@ -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`}
/>

View file

@ -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) {

View file

@ -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>

View 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];
};

View file

@ -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>

View file

@ -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[]) {

View file

@ -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

View file

@ -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
);
}}

View file

@ -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 &&

View file

@ -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();

View file

@ -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 {

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View 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')
]);