diff --git a/apps/mobile/src/components/explorer/FileItem.tsx b/apps/mobile/src/components/explorer/FileItem.tsx
index c583a36c33ed..62d30d29d6f5 100644
--- a/apps/mobile/src/components/explorer/FileItem.tsx
+++ b/apps/mobile/src/components/explorer/FileItem.tsx
@@ -1,9 +1,10 @@
+import { ExplorerItem, Tag, getItemFilePath, getItemObject } from '@sd/client';
import { Text, View } from 'react-native';
-import { ExplorerItem, getItemFilePath } from '@sd/client';
import Layout from '~/constants/Layout';
import { tw, twStyle } from '~/lib/tailwind';
import { getExplorerStore } from '~/stores/explorerStore';
+import { useMemo } from 'react';
import FileThumb from './FileThumb';
type FileItemProps = {
@@ -14,6 +15,13 @@ const FileItem = ({ data }: FileItemProps) => {
const gridItemSize = Layout.window.width / getExplorerStore().gridNumColumns;
const filePath = getItemFilePath(data);
+ const object = getItemObject(data);
+
+ const maxTags = 3;
+ const tags = useMemo(() => {
+ if (!object) return [];
+ return 'tags' in object ? object.tags.slice(0, maxTags) : [];
+ }, [object]);
return (
{
{filePath?.extension && `.${filePath.extension}`}
+
+ {tags.map(({tag}: {tag: Tag}, idx: number) => {
+ return (
+
+ )
+ })}
+
);
};
diff --git a/apps/mobile/src/components/explorer/FileRow.tsx b/apps/mobile/src/components/explorer/FileRow.tsx
index 2e8cdcf4be2b..d376c0177e55 100644
--- a/apps/mobile/src/components/explorer/FileRow.tsx
+++ b/apps/mobile/src/components/explorer/FileRow.tsx
@@ -1,5 +1,5 @@
-import { ExplorerItem, getItemFilePath } from '@sd/client';
-import React from 'react';
+import { ExplorerItem, Tag, getItemFilePath, getItemObject } from '@sd/client';
+import React, { useMemo } from 'react';
import { Text, View } from 'react-native';
import { tw, twStyle } from '~/lib/tailwind';
import { getExplorerStore } from '~/stores/explorerStore';
@@ -12,21 +12,47 @@ type FileRowProps = {
const FileRow = ({ data }: FileRowProps) => {
const filePath = getItemFilePath(data);
+ const object = getItemObject(data);
+
+ const maxTags = 3;
+ const tags = useMemo(() => {
+ if (!object) return [];
+ return 'tags' in object ? object.tags.slice(0, maxTags) : [];
+ }, [object]);
return (
+ <>
-
-
+
+
+
{filePath?.name}
{filePath?.extension && `.${filePath.extension}`}
+
+ {tags.map(({tag}: {tag: Tag}, idx: number) => {
+ return (
+
+ )
+ })}
+
+
+ >
);
};
diff --git a/apps/mobile/src/components/explorer/sections/InfoTagPills.tsx b/apps/mobile/src/components/explorer/sections/InfoTagPills.tsx
index 234ab84b5c12..a8782d7c55dc 100644
--- a/apps/mobile/src/components/explorer/sections/InfoTagPills.tsx
+++ b/apps/mobile/src/components/explorer/sections/InfoTagPills.tsx
@@ -6,48 +6,100 @@ import {
isPath,
useLibraryQuery
} from '@sd/client';
-import React from 'react';
-import { Alert, Pressable, View, ViewStyle } from 'react-native';
+import React, { useRef, useState } from 'react';
+import { FlatList, NativeScrollEvent, Pressable, View, ViewStyle } from 'react-native';
+import Fade from '~/components/layout/Fade';
+import { ModalRef } from '~/components/layout/Modal';
+import AddTagModal from '~/components/modal/AddTagModal';
import { InfoPill, PlaceholderPill } from '~/components/primitive/InfoPill';
import { tw, twStyle } from '~/lib/tailwind';
type Props = {
data: ExplorerItem;
style?: ViewStyle;
+ contentContainerStyle?: ViewStyle;
+ columnCount?: number;
};
-const InfoTagPills = ({ data, style }: Props) => {
+const InfoTagPills = ({ data, style, contentContainerStyle, columnCount = 3 }: Props) => {
+
const objectData = getItemObject(data);
const filePath = getItemFilePath(data);
+ const [startedScrolling, setStartedScrolling] = useState(false);
+ const [reachedBottom, setReachedBottom] = useState(true); // needs to be set to true for initial rendering fade to be correct
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
- enabled: objectData != null
+ enabled: objectData != null,
});
- const items = tagsQuery.data;
+ const ref = useRef(null);
+ const tags = tagsQuery.data;
const isDir = data && isPath(data) ? data.item.is_dir : false;
+ // Fade the tag pills when scrolling
+ const fadeScroll = ({ layoutMeasurement, contentOffset, contentSize }: NativeScrollEvent) => {
+ const isScrolling = contentOffset.y > 0;
+ setStartedScrolling(isScrolling);
+
+ const hasReachedBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height;
+ setReachedBottom(hasReachedBottom);
+ }
+
return (
-
- {/* Kind */}
-
+ <>
+
+
+ ref.current?.present()}>
+
+
+ {/* Kind */}
+
{/* Extension */}
{filePath?.extension && (
-
+
)}
- {/* TODO: What happens if I have too many? */}
- {items?.map((tag) => (
-
- ))}
- Alert.alert('TODO')}>
-
-
+
+ {
+ if (e.nativeEvent.layout.height >= 80) {
+ setReachedBottom(false);
+ } else {
+ setReachedBottom(true);
+ }
+ }} style={twStyle(`relative flex-row flex-wrap gap-1 overflow-hidden`)}>
+
+ fadeScroll(e.nativeEvent)}
+ style={tw`max-h-20 w-full grow-0`}
+ data={tags}
+ scrollEventThrottle={1}
+ showsVerticalScrollIndicator={false}
+ numColumns={columnCount}
+ contentContainerStyle={twStyle(`gap-1`, contentContainerStyle)}
+ columnWrapperStyle={tags && twStyle(tags.length > 0 && `flex-wrap gap-1`)}
+ key={tags?.length}
+ keyExtractor={(item) => item.id.toString() + Math.floor(Math.random() * 10)}
+ renderItem={({ item }) => (
+
+ )}/>
+
+
+
+ >
);
};
diff --git a/apps/mobile/src/components/header/DynamicHeader.tsx b/apps/mobile/src/components/header/DynamicHeader.tsx
index 2bab43ce4815..2c27fdea4ae1 100644
--- a/apps/mobile/src/components/header/DynamicHeader.tsx
+++ b/apps/mobile/src/components/header/DynamicHeader.tsx
@@ -7,12 +7,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { tw, twStyle } from '~/lib/tailwind';
import { getExplorerStore, useExplorerStore } from '~/stores/explorerStore';
+import { FilterItem, TagItem, useSearchStore } from '~/stores/searchStore';
import { Icon } from '../icons/Icon';
type Props = {
headerRoute?: NativeStackHeaderProps; //supporting title from the options object of navigation
optionsRoute?: RouteProp; //supporting params passed
- kind: 'tag' | 'location'; //the kind of icon to display
+ kind: 'tags' | 'locations'; //the kind of icon to display
explorerMenu?: boolean; //whether to show the explorer menu
};
@@ -26,6 +27,28 @@ export default function DynamicHeader({
const headerHeight = useSafeAreaInsets().top;
const isAndroid = Platform.OS === 'android';
const explorerStore = useExplorerStore();
+ const searchStore = useSearchStore();
+ const params = headerRoute?.route.params as {
+ id: number;
+ color: string;
+ name: string;
+ }
+
+ //pressing the search icon will add a filter
+ //based on the screen
+
+ const searchHandler = (key: Props['kind']) => {
+ if (!params) return;
+ const keys: {
+ tags: TagItem;
+ locations: FilterItem;
+ } = {
+ tags: {id: params.id, color: params.color},
+ locations: {id: params.id, name: params.name},
+ }
+ searchStore.searchFrom(key, keys[key])
+ }
+
return (
{
+ searchHandler(kind)
navigation.navigate('SearchStack', {
screen: 'Search'
});
@@ -94,12 +118,12 @@ interface HeaderIconKindProps {
const HeaderIconKind = ({ routeParams, kind }: HeaderIconKindProps) => {
switch (kind) {
- case 'location':
+ case 'locations':
return ;
- case 'tag':
+ case 'tags':
return (
diff --git a/apps/mobile/src/components/locations/ListLocation.tsx b/apps/mobile/src/components/locations/ListLocation.tsx
index 36774aa3b9df..90fc66b8b4da 100644
--- a/apps/mobile/src/components/locations/ListLocation.tsx
+++ b/apps/mobile/src/components/locations/ListLocation.tsx
@@ -1,9 +1,9 @@
import { useNavigation } from '@react-navigation/native';
+import { Location, arraysEqual, humanizeSize, useOnlineLocations } 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 { arraysEqual, humanizeSize, Location, useOnlineLocations } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
@@ -25,7 +25,7 @@ const ListLocation = ({ location }: ListLocationProps) => {
return (
(
<>
diff --git a/apps/mobile/src/components/modal/AddTagModal.tsx b/apps/mobile/src/components/modal/AddTagModal.tsx
new file mode 100644
index 000000000000..463e151431f5
--- /dev/null
+++ b/apps/mobile/src/components/modal/AddTagModal.tsx
@@ -0,0 +1,177 @@
+import { Tag, getItemObject, useLibraryMutation, useLibraryQuery, useRspcContext } from "@sd/client";
+import { CaretLeft, Plus } from "phosphor-react-native";
+import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { FlatList, Pressable, Text, View } from "react-native";
+import useForwardedRef from "~/hooks/useForwardedRef";
+import { tw, twStyle } from "~/lib/tailwind";
+import { useActionsModalStore } from "~/stores/modalStore";
+import Card from "../layout/Card";
+import { Modal, ModalRef } from "../layout/Modal";
+import { Button } from "../primitive/Button";
+import CreateTagModal from "./tag/CreateTagModal";
+
+
+const AddTagModal = forwardRef((_, ref) => {
+
+ const {data} = useActionsModalStore();
+
+ // Wrapped in memo to ensure that the data is not undefined on initial render
+ const objectData = data && getItemObject(data);
+
+ const modalRef = useForwardedRef(ref);
+ const newTagRef = useRef(null);
+
+ const rspc = useRspcContext();
+ const tagsQuery = useLibraryQuery(['tags.list']);
+ const tagsObjectQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1]);
+ const mutation = useLibraryMutation(['tags.assign'], {
+ onSuccess: () => {
+ // this makes sure that the tags are updated in the UI
+ rspc.queryClient.invalidateQueries(['tags.getForObject'])
+ rspc.queryClient.invalidateQueries(['search.paths'])
+ modalRef.current?.dismiss();
+ }
+ });
+
+ const tagsData = tagsQuery.data;
+ const tagsObject = tagsObjectQuery.data;
+
+ const [selectedTags, setSelectedTags] = useState<{
+ id: number;
+ unassign: boolean;
+ selected: boolean;
+ }[]>([]);
+
+ // get the tags that are already applied to the object
+ const appliedTags = useMemo(() => {
+ if (!tagsObject) return [];
+ return tagsObject?.map((t) => t.id);
+ }, [tagsObject]);
+
+
+ // set selected tags when tagsOfObject.data is available
+ useEffect(() => {
+ if (!tagsObject) return;
+ //we want to set the selectedTags if there are applied tags
+ //this deals with an edge case of clearing the tags onDismiss of the Modal
+ if (selectedTags.length === 0 && appliedTags.length > 0) {
+ setSelectedTags((tagsObject ?? []).map((tag) => ({
+ id: tag.id,
+ unassign: false,
+ selected: true
+ })))}
+ }, [tagsObject, appliedTags, selectedTags])
+
+ // check if tag is selected
+ const isSelected = useCallback((id: number) => {
+ const findTag = selectedTags.find((t) => t.id === id);
+ return findTag?.selected ?? false;
+ }, [selectedTags]);
+
+ const selectTag = useCallback((id: number) => {
+ //check if tag is already selected
+ const findTag = selectedTags.find((t) => t.id === id);
+ if (findTag) {
+ //if tag is already selected, update its selected value
+ setSelectedTags((prev) => prev.map((t) => t.id === id ? { ...t, selected: !t.selected, unassign: !t.unassign } : t));
+ } else {
+ //if tag is not selected, select it
+ setSelectedTags((prev) => [...prev, { id, unassign: false, selected: true }]);
+ }
+ }, [selectedTags]);
+
+ const assignHandler = async () => {
+ const targets = data && 'id' in data.item && (data.type === 'Object' ? {
+ Object: data.item.id
+ } : {
+ FilePath: data.item.id
+ });
+
+ // in order to support assigning multiple tags
+ // we need to make multiple mutation calls
+ if (targets) await Promise.all([...selectedTags.map(async (tag) => await mutation.mutateAsync({
+ targets: [targets],
+ tag_id: tag.id,
+ unassign: tag.unassign
+ })),
+ ]
+ );
+ }
+
+ return (
+ <>
+ setSelectedTags([])}
+ enableContentPanningGesture={false}
+ enablePanDownToClose={false}
+ snapPoints={['50']}
+ title="Select Tags"
+ >
+ {/* Back Button */}
+ modalRef.current?.close()}
+ style={tw`absolute z-10 ml-6 rounded-full bg-app-button p-2`}
+ >
+
+
+ item.id.toString()}
+ contentContainerStyle={tw`mx-auto mt-4 p-4 pb-10`}
+ ItemSeparatorComponent={() => }
+ renderItem={({ item }) => (
+ isSelected(item.id)} select={() => selectTag(item.id)} tag={item} />
+ )}
+ />
+
+
+
+
+
+
+ >
+ )
+});
+
+interface Props {
+ tag: Tag;
+ select: () => void;
+ isSelected: () => boolean;
+}
+
+const TagItem = ({tag, select, isSelected}: Props) => {
+ return (
+
+
+
+ {tag?.name}
+
+
+ )
+}
+
+export default AddTagModal;
diff --git a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
index a6a228a633c0..b0dcd79ee4cf 100644
--- a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
+++ b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
@@ -1,3 +1,10 @@
+import {
+ getIndexedItemFilePath,
+ getItemObject,
+ humanizeSize,
+ useLibraryMutation,
+ useLibraryQuery
+} from '@sd/client';
import dayjs from 'dayjs';
import {
Copy,
@@ -13,13 +20,6 @@ import {
import { PropsWithChildren, useRef } from 'react';
import { Pressable, Text, View, ViewStyle } from 'react-native';
import FileViewer from 'react-native-file-viewer';
-import {
- getIndexedItemFilePath,
- getItemObject,
- humanizeSize,
- useLibraryMutation,
- useLibraryQuery
-} from '@sd/client';
import FileThumb from '~/components/explorer/FileThumb';
import FavoriteButton from '~/components/explorer/sections/FavoriteButton';
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
@@ -72,7 +72,6 @@ export const ActionsModal = () => {
const filePath = data && getIndexedItemFilePath(data);
// Open
-
const updateAccessTime = useLibraryMutation('files.updateAccessTime');
const queriedFullPath = useLibraryQuery(['files.getPath', filePath?.id ?? -1], {
enabled: filePath != null
@@ -100,7 +99,7 @@ export const ActionsModal = () => {
{data && (
-
+
{/* Thumbnail/Icon */}
{
{/* Name + Extension */}
{filePath?.name}
@@ -128,9 +127,9 @@ export const ActionsModal = () => {
- {objectData && }
+ {objectData && }
-
+
{/* Actions */}
diff --git a/apps/mobile/src/components/modal/inspector/FileInfoModal.tsx b/apps/mobile/src/components/modal/inspector/FileInfoModal.tsx
index 1bd33253199c..21ef7955e9eb 100644
--- a/apps/mobile/src/components/modal/inspector/FileInfoModal.tsx
+++ b/apps/mobile/src/components/modal/inspector/FileInfoModal.tsx
@@ -1,11 +1,12 @@
+import { getItemFilePath, humanizeSize, type ExplorerItem } from '@sd/client';
import dayjs from 'dayjs';
import { Barcode, CaretLeft, Clock, Cube, Icon, SealCheck, Snowflake } from 'phosphor-react-native';
import { forwardRef } from 'react';
import { Pressable, Text, View } from 'react-native';
-import { getItemFilePath, humanizeSize, type ExplorerItem } from '@sd/client';
import FileThumb from '~/components/explorer/FileThumb';
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
import { Modal, ModalScrollView, type ModalRef } from '~/components/layout/Modal';
+import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
import { Divider } from '~/components/primitive/Divider';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw } from '~/lib/tailwind';
@@ -49,25 +50,26 @@ const FileInfoModal = forwardRef((props, ref) => {
enablePanDownToClose={false}
snapPoints={['70']}
>
+
{data && (
-
+
{/* Back Button */}
modalRef.current?.close()}
- style={tw`absolute z-10 ml-4`}
+ style={tw`absolute left-2 z-10 rounded-full bg-app-button p-2`}
>
-
+
- {/* File Icon / Name */}
+ {/* File Icon / Name */}
-
+
{filePathData?.name}
-
+
{/* Details */}
-
+
<>
{/* Size */}
((props, ref) => {
>
)}
+
);
});
diff --git a/apps/mobile/src/components/overview/OverviewStats.tsx b/apps/mobile/src/components/overview/OverviewStats.tsx
index 967e0c85e31a..da17196297e3 100644
--- a/apps/mobile/src/components/overview/OverviewStats.tsx
+++ b/apps/mobile/src/components/overview/OverviewStats.tsx
@@ -1,10 +1,10 @@
import * as RNFS from '@dr.pogodin/react-native-fs';
import { AlphaRSPCError } from '@oscartbeaumont-sd/rspc-client/v2';
+import { Statistics, StatisticsResponse, humanizeSize, useLibraryContext } from '@sd/client';
import { UseQueryResult } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { Platform, Text, View } from 'react-native';
import { ClassInput } from 'twrnc/dist/esm/types';
-import { humanizeSize, Statistics, StatisticsResponse, useLibraryContext } from '@sd/client';
import useCounter from '~/hooks/useCounter';
import { tw, twStyle } from '~/lib/tailwind';
@@ -13,6 +13,7 @@ import Card from '../layout/Card';
const StatItemNames: Partial> = {
total_local_bytes_capacity: 'Total capacity',
total_library_preview_media_bytes: 'Preview media',
+ total_library_bytes: 'Total library size',
library_db_size: 'Index size',
total_local_bytes_free: 'Free space',
total_local_bytes_used: 'Total used space'
@@ -76,19 +77,18 @@ const OverviewStats = ({ stats }: Props) => {
}, []);
const renderStatItems = (isTotalStat = true) => {
+ const keysToFilter = ['total_local_bytes_capacity', 'total_local_bytes_used', 'total_library_bytes'];
if (!stats.data?.statistics) return null;
return Object.entries(stats.data.statistics).map(([key, bytesRaw]) => {
if (!displayableStatItems.includes(key)) return null;
- if (isTotalStat && !['total_bytes_capacity', 'total_bytes_used'].includes(key))
- return null;
- if (!isTotalStat && ['total_bytes_capacity', 'total_bytes_used'].includes(key))
- return null;
let bytes = BigInt(bytesRaw ?? 0);
- if (key === 'total_bytes_free') {
+ if (isTotalStat && !keysToFilter.includes(key)) return null;
+ if (!isTotalStat && keysToFilter.includes(key)) return null;
+ if (key === 'total_local_bytes_free') {
bytes = BigInt(sizeInfo.freeSpace);
- } else if (key === 'total_bytes_capacity') {
+ } else if (key === 'total_local_bytes_capacity') {
bytes = BigInt(sizeInfo.totalSpace);
- } else if (key === 'total_bytes_used' && Platform.OS === 'android') {
+ } else if (key === 'total_local_bytes_used' && Platform.OS === 'android') {
bytes = BigInt(sizeInfo.totalSpace - sizeInfo.freeSpace);
}
return (
@@ -97,7 +97,7 @@ const OverviewStats = ({ stats }: Props) => {
title={StatItemNames[key as keyof Statistics]!}
bytes={bytes}
isLoading={stats.isLoading}
- style={twStyle(isTotalStat && 'h-[101px]', 'w-full flex-1')}
+ style={tw`w-full`}
/>
);
});
@@ -106,11 +106,11 @@ const OverviewStats = ({ stats }: Props) => {
return (
Statistics
-
-
+
+
{renderStatItems()}
-
+
{renderStatItems(false)}
diff --git a/apps/mobile/src/components/primitive/InfoPill.tsx b/apps/mobile/src/components/primitive/InfoPill.tsx
index 865b283cde96..bd01f4bb2c49 100644
--- a/apps/mobile/src/components/primitive/InfoPill.tsx
+++ b/apps/mobile/src/components/primitive/InfoPill.tsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import { IconProps } from 'phosphor-react-native';
+import React, { ReactElement } from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { twStyle } from '~/lib/tailwind';
@@ -6,13 +7,14 @@ type Props = {
text: string;
containerStyle?: ViewStyle;
textStyle?: TextStyle;
+ icon?: ReactElement
};
export const InfoPill = (props: Props) => {
return (
@@ -27,11 +29,12 @@ export function PlaceholderPill(props: Props) {
return (
-
+ {props.icon && props.icon}
+
{props.text}
diff --git a/apps/mobile/src/components/search/filters/Locations.tsx b/apps/mobile/src/components/search/filters/Locations.tsx
index 3ea6f753c1f5..8d812803a542 100644
--- a/apps/mobile/src/components/search/filters/Locations.tsx
+++ b/apps/mobile/src/components/search/filters/Locations.tsx
@@ -32,14 +32,13 @@ const Locations = () => {
/>
-
+
}
numColumns={
locations ? Math.max(Math.ceil(locations.length / 2), 2) : 1
}
- contentContainerStyle={tw`w-full`}
ListEmptyComponent={
{
title="Tags"
sub="What tags would you like to filter by?"
/>
-
-
+
}
extraData={searchStore.filters.tags}
+ alwaysBounceVertical={false}
numColumns={tagsData ? Math.max(Math.ceil(tagsData.length / 2), 2) : 1}
key={tagsData ? 'tagsSearch' : '_'}
- contentContainerStyle={tw`w-full`}
- ListEmptyComponent={
-
- }
- scrollEnabled={false}
+ ListEmptyComponent={}
ItemSeparatorComponent={() => }
keyExtractor={(item) => item.id.toString()}
showsHorizontalScrollIndicator={false}
@@ -51,7 +47,6 @@ const Tags = () => {
/>
-
);
};
diff --git a/apps/mobile/src/components/tags/GridTag.tsx b/apps/mobile/src/components/tags/GridTag.tsx
index 364b3015c276..98fb764fb463 100644
--- a/apps/mobile/src/components/tags/GridTag.tsx
+++ b/apps/mobile/src/components/tags/GridTag.tsx
@@ -1,6 +1,6 @@
+import { Tag } from '@sd/client';
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { Pressable, Text, View } from 'react-native';
-import { Tag } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import Card from '../layout/Card';
diff --git a/apps/mobile/src/components/tags/ListTag.tsx b/apps/mobile/src/components/tags/ListTag.tsx
index 93b8f65db0a2..d7e0e0441f40 100644
--- a/apps/mobile/src/components/tags/ListTag.tsx
+++ b/apps/mobile/src/components/tags/ListTag.tsx
@@ -19,16 +19,16 @@ const ListTag = ({ tag, tagStyle }: ListTagProps) => {
return (
(
)}
>
-
+
diff --git a/apps/mobile/src/navigation/SearchStack.tsx b/apps/mobile/src/navigation/SearchStack.tsx
index e014259c4620..84139f37df8f 100644
--- a/apps/mobile/src/navigation/SearchStack.tsx
+++ b/apps/mobile/src/navigation/SearchStack.tsx
@@ -32,7 +32,7 @@ export default function SearchStack() {
name="Location"
component={LocationScreen}
options={({route: optionsRoute}) => ({
- header: (route) =>
+ header: (route) =>
})}
/>
diff --git a/apps/mobile/src/navigation/tabs/BrowseStack.tsx b/apps/mobile/src/navigation/tabs/BrowseStack.tsx
index 61080de23df8..50ca85a848f5 100644
--- a/apps/mobile/src/navigation/tabs/BrowseStack.tsx
+++ b/apps/mobile/src/navigation/tabs/BrowseStack.tsx
@@ -34,7 +34,7 @@ export default function BrowseStack() {
)
})}
@@ -58,7 +58,7 @@ export default function BrowseStack() {
component={TagScreen}
options={({ route: optionsRoute }) => ({
header: (route) => (
-
+
)
})}
/>
@@ -75,7 +75,7 @@ export default function BrowseStack() {
export type BrowseStackParamList = {
Browse: undefined;
- Location: { id: number; path?: string };
+ Location: { id: number; path?: string, name?: string };
Locations: undefined;
Tag: { id: number; color: string };
Tags: undefined;
diff --git a/apps/mobile/src/screens/browse/Location.tsx b/apps/mobile/src/screens/browse/Location.tsx
index 944f1b2e1636..9c26c1a5a4d6 100644
--- a/apps/mobile/src/screens/browse/Location.tsx
+++ b/apps/mobile/src/screens/browse/Location.tsx
@@ -1,6 +1,7 @@
import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import Explorer from '~/components/explorer/Explorer';
+import { useSortBy } from '~/hooks/useSortBy';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { getExplorerStore } from '~/stores/explorerStore';
@@ -9,11 +10,17 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
const location = useLibraryQuery(['locations.get', route.params.id]);
const locationData = location.data;
+ const order = useSortBy();
+ const title = useMemo(() => {
+ return path?.split('/')
+ .filter((x) => x !== '')
+ .pop();
+ }, [path])
const paths = usePathsExplorerQuery({
arg: {
filters: [
- // ...search.allFilters,
+ { filePath: { hidden: false }},
{ filePath: { locations: { in: [id] } } },
{
filePath: {
@@ -21,18 +28,13 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
location_id: id,
path: path ?? '',
include_descendants: false
- // include_descendants:
- // search.search !== '' ||
- // search.dynamicFilters.length > 0 ||
- // (layoutMode === 'media' && mediaViewWithDescendants)
}
}
}
- // !showHiddenFiles && { filePath: { hidden: false } }
].filter(Boolean) as any,
take: 30
},
- order: null,
+ order,
onSuccess: () => getExplorerStore().resetNewThumbnails()
});
@@ -41,17 +43,19 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
if (path && path !== '') {
// Nested location.
navigation.setOptions({
- title: path
- .split('/')
- .filter((x) => x !== '')
- .pop()
+ title
});
} else {
navigation.setOptions({
title: locationData?.name ?? 'Location'
});
}
- }, [locationData?.name, navigation, path]);
+ // sets params for handling when clicking on search within header
+ navigation.setParams({
+ id: id,
+ name: locationData?.name ?? 'Location'
+ })
+ }, [id, locationData?.name, navigation, path, title]);
useEffect(() => {
getExplorerStore().locationId = id;
diff --git a/apps/mobile/src/screens/browse/Locations.tsx b/apps/mobile/src/screens/browse/Locations.tsx
index 3efc40b68868..0afe969f8c3e 100644
--- a/apps/mobile/src/screens/browse/Locations.tsx
+++ b/apps/mobile/src/screens/browse/Locations.tsx
@@ -46,6 +46,7 @@ export default function LocationsScreen({ viewStyle }: Props) {
>
+
)}
/>
+
);
diff --git a/apps/mobile/src/screens/browse/Tag.tsx b/apps/mobile/src/screens/browse/Tag.tsx
index 9c2281ab51e7..012beb9ca759 100644
--- a/apps/mobile/src/screens/browse/Tag.tsx
+++ b/apps/mobile/src/screens/browse/Tag.tsx
@@ -1,4 +1,4 @@
-import { useLibraryQuery, useObjectsExplorerQuery } from '@sd/client';
+import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
import { useEffect } from 'react';
import Explorer from '~/components/explorer/Explorer';
import Empty from '~/components/layout/Empty';
@@ -11,17 +11,26 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
const tag = useLibraryQuery(['tags.get', id]);
const tagData = tag.data;
- const objects = useObjectsExplorerQuery({
- arg: { filters: [{ object: { tags: { in: [id] } } }], take: 30 },
+ const objects = usePathsExplorerQuery({
+ arg: { filters: [
+ { object: { tags: { in: [id] } } },
+ ], take: 30 },
+ enabled: typeof id === 'number',
order: null
});
useEffect(() => {
// Set screen title to tag name.
- navigation.setOptions({
- title: tagData?.name ?? 'Tag'
- });
- }, [tagData?.name, navigation]);
+ if (tagData) {
+ navigation.setParams({
+ id: tagData.id,
+ color: tagData.color as string
+ })
+ navigation.setOptions({
+ title: tagData.name ?? 'Tag',
+ });
+ }
+ }, [tagData, id, navigation]);
return
+
(
@@ -75,10 +76,11 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
showsHorizontalScrollIndicator={false}
ItemSeparatorComponent={() => }
contentContainerStyle={twStyle(
- `py-6`,
+ 'py-6',
tagsData?.length === 0 && 'h-full items-center justify-center'
)}
/>
+
);
diff --git a/apps/mobile/src/screens/search/Filters.tsx b/apps/mobile/src/screens/search/Filters.tsx
index bed24f49200d..1de25e88f502 100644
--- a/apps/mobile/src/screens/search/Filters.tsx
+++ b/apps/mobile/src/screens/search/Filters.tsx
@@ -6,7 +6,7 @@ const FiltersScreen = () => {
return (
<>
-
+
>
diff --git a/apps/mobile/src/stores/explorerStore.ts b/apps/mobile/src/stores/explorerStore.ts
index ffd26b453846..ca9bc9efad2f 100644
--- a/apps/mobile/src/stores/explorerStore.ts
+++ b/apps/mobile/src/stores/explorerStore.ts
@@ -1,6 +1,6 @@
+import { resetStore } from '@sd/client';
import { proxy, useSnapshot } from 'valtio';
import { proxySet } from 'valtio/utils';
-import { resetStore, type Ordering } from '@sd/client';
export type ExplorerLayoutMode = 'list' | 'grid' | 'media';
@@ -17,7 +17,7 @@ const state = {
toggleMenu: false as boolean,
// Using gridNumColumns instead of fixed size. We dynamically calculate the item size.
gridNumColumns: 3,
- listItemSize: 65,
+ listItemSize: 60,
newThumbnails: proxySet() as Set,
// sorting
// we will display different sorting options based on the kind of explorer we are in
diff --git a/apps/mobile/src/stores/modalStore.ts b/apps/mobile/src/stores/modalStore.ts
index 12d46c2cb605..dcd87151b70b 100644
--- a/apps/mobile/src/stores/modalStore.ts
+++ b/apps/mobile/src/stores/modalStore.ts
@@ -1,6 +1,6 @@
+import { ExplorerItem } from '@sd/client';
import { createRef } from 'react';
import { proxy, ref, useSnapshot } from 'valtio';
-import { ExplorerItem } from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
const store = proxy({
diff --git a/apps/mobile/src/stores/searchStore.ts b/apps/mobile/src/stores/searchStore.ts
index e8131364cab0..1ae39ec204c9 100644
--- a/apps/mobile/src/stores/searchStore.ts
+++ b/apps/mobile/src/stores/searchStore.ts
@@ -1,5 +1,5 @@
-import { proxy, useSnapshot } from 'valtio';
import { SearchFilterArgs } from '@sd/client';
+import { proxy, useSnapshot } from 'valtio';
import { IconName } from '~/components/icons/Icon';
export type SearchFilters = 'locations' | 'tags' | 'name' | 'extension' | 'hidden' | 'kind';
@@ -74,7 +74,7 @@ function updateArrayOrObject(
array: T[],
item: any,
filterByKey: string = 'id',
- isObject: boolean = false
+ isObject: boolean = false,
): T[] {
if (isObject) {
const index = (array as any).findIndex((i: any) => i.id === item[filterByKey]);
@@ -94,8 +94,10 @@ const searchStore = proxy<
updateFilters: (
filter: K,
value: State['filters'][K] extends Array ? U : State['filters'][K],
- apply?: boolean
+ apply?: boolean,
+ keepSame?: boolean
) => void;
+ searchFrom: (filter: 'tags' | 'locations', value: TagItem | FilterItem) => void;
applyFilters: () => void;
setSearch: (search: string) => void;
resetFilter: (filter: K, apply?: boolean) => void;
@@ -108,13 +110,15 @@ const searchStore = proxy<
...initialState,
//for updating the filters upon value selection
updateFilters: (filter, value, apply = false) => {
+ const currentFilter = searchStore.filters[filter];
+ const arrayCheck = Array.isArray(currentFilter);
+
if (filter === 'hidden') {
// Directly assign boolean values without an array operation
searchStore.filters['hidden'] = value as boolean;
} else {
// Handle array-based filters with more specific type handling
- const currentFilter = searchStore.filters[filter];
- if (Array.isArray(currentFilter)) {
+ if (arrayCheck) {
// Cast to the correct type based on the filter being updated
const updatedFilter = updateArrayOrObject(
currentFilter,
@@ -129,6 +133,21 @@ const searchStore = proxy<
// useful when you want to apply the filters from another screen
if (apply) searchStore.applyFilters();
},
+ searchFrom: (filter, value) => {
+ //reset state first
+ searchStore.resetFilters();
+ //update the filter with the value
+ switch (filter) {
+ case 'locations':
+ searchStore.filters[filter] = [value] as FilterItem[]
+ break;
+ case 'tags':
+ searchStore.filters[filter] = [value] as TagItem[]
+ break;
+ }
+ //apply the filters so it shows in the UI
+ searchStore.applyFilters();
+ },
//for clicking add filters and applying the selection
applyFilters: () => {
// loop through all filters and apply the ones with values
diff --git a/core/crates/prisma-helpers/src/lib.rs b/core/crates/prisma-helpers/src/lib.rs
index a2748c9afc9e..d562ab534952 100644
--- a/core/crates/prisma-helpers/src/lib.rs
+++ b/core/crates/prisma-helpers/src/lib.rs
@@ -138,8 +138,10 @@ file_path::select!(file_path_to_full_path {
});
// File Path includes!
-file_path::include!(file_path_with_object {
+file_path::include!(file_path_with_object { object });
+file_path::include!(file_path_for_frontend {
object: include {
+ tags: include { tag }
exif_data: select {
resolution
media_date
diff --git a/core/src/api/locations.rs b/core/src/api/locations.rs
index a6269f210f92..24d56e52b699 100644
--- a/core/src/api/locations.rs
+++ b/core/src/api/locations.rs
@@ -13,7 +13,7 @@ use crate::{
use sd_core_indexer_rules::IndexerRuleCreateArgs;
use sd_core_prisma_helpers::{
- file_path_with_object, label_with_objects, location_with_indexer_rules, object_with_file_paths,
+ file_path_for_frontend, label_with_objects, location_with_indexer_rules, object_with_file_paths,
};
use sd_prisma::prisma::{file_path, indexer_rule, indexer_rules_in_location, location, SortOrder};
@@ -42,7 +42,7 @@ pub enum ExplorerItem {
// this tells the frontend if a thumbnail actually exists or not
has_created_thumbnail: bool,
// we can't actually modify data from PCR types, thats why computed properties are used on ExplorerItem
- item: Box,
+ item: Box,
},
Object {
thumbnail: Option,
diff --git a/core/src/api/search/mod.rs b/core/src/api/search/mod.rs
index c203431ce607..a17d97997ebd 100644
--- a/core/src/api/search/mod.rs
+++ b/core/src/api/search/mod.rs
@@ -7,7 +7,7 @@ use crate::{
};
use prisma_client_rust::Operator;
-use sd_core_prisma_helpers::{file_path_with_object, object_with_file_paths};
+use sd_core_prisma_helpers::{file_path_for_frontend, object_with_file_paths};
use sd_prisma::prisma::{self, PrismaClient};
use std::path::PathBuf;
@@ -210,7 +210,7 @@ pub fn mount() -> AlphaRouter {
}
let file_paths = query
- .include(file_path_with_object::include())
+ .include(file_path_for_frontend::include())
.exec()
.await?;
diff --git a/interface/app/$libraryId/Explorer/ContextMenu/AssignTagMenuItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/AssignTagMenuItems.tsx
index c4a82643611f..8c80dc54b608 100644
--- a/interface/app/$libraryId/Explorer/ContextMenu/AssignTagMenuItems.tsx
+++ b/interface/app/$libraryId/Explorer/ContextMenu/AssignTagMenuItems.tsx
@@ -1,11 +1,11 @@
import { Plus } from '@phosphor-icons/react';
+import { ExplorerItem, useLibraryQuery } from '@sd/client';
+import { Button, ModifierKeys, dialogManager, tw } from '@sd/ui';
import { useQueryClient } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import clsx from 'clsx';
import { RefObject, useMemo, useRef } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
-import { ExplorerItem, useLibraryQuery } from '@sd/client';
-import { Button, dialogManager, ModifierKeys, tw } from '@sd/ui';
import CreateDialog, {
AssignTagItems,
useAssignItemsToTag
@@ -38,6 +38,7 @@ function useData({ items }: Props) {
{ suspense: true }
);
+
return {
tags: {
...tags,
diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx
index 51ea58aba079..ea94f8c8fb12 100644
--- a/interface/app/$libraryId/Explorer/Inspector/index.tsx
+++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx
@@ -12,23 +12,9 @@ import {
Icon as PhosphorIcon,
Snowflake
} from '@phosphor-icons/react';
-import clsx from 'clsx';
-import dayjs from 'dayjs';
-import {
- forwardRef,
- useCallback,
- useEffect,
- useMemo,
- useState,
- type HTMLAttributes,
- type ReactNode
-} from 'react';
-import { useLocation } from 'react-router';
-import { Link as NavLink } from 'react-router-dom';
-import Sticky from 'react-sticky-el';
import {
FilePath,
- FilePathWithObject,
+ FilePathForFrontend,
getExplorerItemData,
getItemFilePath,
humanizeSize,
@@ -42,6 +28,20 @@ import {
type ExplorerItem
} from '@sd/client';
import { Button, Divider, DropdownMenu, toast, Tooltip, tw } from '@sd/ui';
+import clsx from 'clsx';
+import dayjs from 'dayjs';
+import {
+ forwardRef,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+ type HTMLAttributes,
+ type ReactNode
+} from 'react';
+import { useLocation } from 'react-router';
+import { Link as NavLink } from 'react-router-dom';
+import Sticky from 'react-sticky-el';
import { LibraryIdParamsSchema } from '~/app/route-schemas';
import { Folder, Icon } from '~/components';
import { useLocale, useZodRouteParams } from '~/hooks';
@@ -171,7 +171,7 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => {
export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
let objectData: Object | ObjectWithFilePaths | null = null;
- let filePathData: FilePath | FilePathWithObject | null = null;
+ let filePathData: FilePath | FilePathForFrontend | null = null;
let ephemeralPathData: NonIndexedPathItem | null = null;
const { t, dateFormat } = useLocale();
@@ -484,7 +484,6 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
(metadata, item) => {
const { kind, size, dateCreated, dateAccessed, dateModified, dateIndexed } =
getExplorerItemData(item);
-
if (item.type !== 'NonIndexedPath' || !item.item.is_dir) {
metadata.size = (metadata.size ?? 0n) + size.bytes;
}
@@ -527,6 +526,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
const onlyNonIndexed = metadata.types.has('NonIndexedPath') && metadata.types.size === 1;
const filesSize = humanizeSize(metadata.size);
+
return (
<>
diff --git a/interface/app/$libraryId/Explorer/View/Grid/Item.tsx b/interface/app/$libraryId/Explorer/View/Grid/Item.tsx
index 390071052997..67bf05de07ae 100644
--- a/interface/app/$libraryId/Explorer/View/Grid/Item.tsx
+++ b/interface/app/$libraryId/Explorer/View/Grid/Item.tsx
@@ -1,8 +1,8 @@
+import { useSelector, type ExplorerItem } from '@sd/client';
import { HTMLAttributes, ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router';
-import { useSelector, type ExplorerItem } from '@sd/client';
-import { useOperatingSystem } from '~/hooks';
import { useRoutingContext } from '~/RoutingContext';
+import { useOperatingSystem } from '~/hooks';
import { useExplorerContext } from '../../Context';
import { explorerStore, isCut } from '../../store';
@@ -37,6 +37,7 @@ export const GridItem = ({ children, item, index, ...props }: Props) => {
[explorer.selectedItems, item]
);
+
const canGoBack = currentIndex !== 0;
const canGoForward = currentIndex !== maxIndex;
diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts
index be57285d8b98..5c9c249a7200 100644
--- a/packages/client/src/core.ts
+++ b/packages/client/src/core.ts
@@ -265,7 +265,7 @@ export type ExifDataOrder = { field: "epochTime"; value: SortOrder }
export type ExifMetadata = { resolution: Resolution; date_taken: MediaDate | null; location: MediaLocation | null; camera_data: CameraData; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null }
-export type ExplorerItem = { type: "Path"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: FilePathWithObject } | { type: "Object"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: ObjectWithFilePaths } | { type: "NonIndexedPath"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: NonIndexedPathItem } | { type: "Location"; item: Location } | { type: "SpacedropPeer"; item: PeerMetadata } | { type: "Label"; thumbnails: string[][]; item: LabelWithObjects }
+export type ExplorerItem = { type: "Path"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: FilePathForFrontend } | { type: "Object"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: ObjectWithFilePaths } | { type: "NonIndexedPath"; thumbnail: string[] | null; has_created_thumbnail: boolean; item: NonIndexedPathItem } | { type: "Location"; item: Location } | { type: "SpacedropPeer"; item: PeerMetadata } | { type: "Label"; thumbnails: string[][]; item: LabelWithObjects }
export type ExplorerLayout = "grid" | "list" | "media"
@@ -291,14 +291,14 @@ export type FilePathCursorVariant = "none" | { name: CursorOrderItem } |
export type FilePathFilterArgs = { locations: InOrNotIn } | { path: { location_id: number; path: string; include_descendants: boolean } } | { name: TextMatch } | { extension: InOrNotIn } | { createdAt: Range } | { modifiedAt: Range } | { indexedAt: Range } | { hidden: boolean }
+export type FilePathForFrontend = { 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; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; 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; tags: ({ object_id: number; tag_id: number; tag: Tag; date_created: string | null })[]; exif_data: { resolution: number[] | null; media_date: number[] | null; media_location: number[] | null; camera_data: number[] | null; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } | null } | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null }
+
export type FilePathObjectCursor = { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem }
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 FilePathSearchArgs = { take?: number | null; orderAndPagination?: OrderAndPagination | null; filters?: SearchFilterArgs[]; 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; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; 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; exif_data: { resolution: number[] | null; media_date: number[] | null; media_location: number[] | null; camera_data: number[] | null; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } | null } | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null }
-
export type Flash = {
/**
* Specifies how flash was used (on, auto, off, forced, onvalid)