mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-24 00:09:19 -04:00
feat(recents): add RecentsView for displaying recently indexed files
This commit introduces the RecentsView component, which allows users to view recently indexed files sorted by the indexed_at timestamp. The view integrates seamlessly with existing components, ensuring consistent interactions such as keyboard navigation and drag-to-select. Additionally, it updates the Explorer context to support entering and exiting recents mode, enhancing the overall file browsing experience.
This commit is contained in:
44
TODO
44
TODO
@@ -22,36 +22,48 @@
|
||||
|
||||
Journey to v2.0.0-pre.1:
|
||||
✔ Get all desktop release CI working @done(25-12-16 23:51)
|
||||
☐ Remote file access on demand @critical
|
||||
☐ Remote file access on demand
|
||||
☐ Ensure updater and changelog are working @today
|
||||
☐ Ephemeral sidecars @critical
|
||||
☐ Ephemeral sidecars
|
||||
☐ Fix tag on file sync
|
||||
☐ Make a separate data folder for dev builds @critical
|
||||
☐ Sometimes quick preview reporting file not found @today (happens in the second column of column view)
|
||||
☐ Connection info on device panel (lan/relay)
|
||||
☐ Make a separate data folder for dev builds
|
||||
☐ Default path for paste to device (paste to /Downloads)
|
||||
☐ Mobile explorer
|
||||
☐ search
|
||||
☐ inspector
|
||||
☐ context menu
|
||||
☐ view settings
|
||||
☐ job manager
|
||||
☐ preview
|
||||
☐ share
|
||||
✔ Sometimes quick preview reporting file not found @today (happens in the second column of column view) @done(26-01-20 09:09)
|
||||
✔ Connection info on device panel (lan/relay) @done(26-01-20 09:09)
|
||||
☐ Improve job panel data display, layout and ordering @today
|
||||
☐ Pop out job viewer
|
||||
☐ More metadata in file copy jobs, to show more visually appealing transfer job @today
|
||||
✔ More metadata in file copy jobs, to show more visually appealing transfer job @today @done(26-01-20 09:09)
|
||||
☐ Drag between columns in column view
|
||||
✔ Rename files, create folders, new folder with items @done(25-12-24 09:08)
|
||||
✔ Inspector multi-select @done(26-01-10 22:22)
|
||||
✔ Open files with default app (cross platform) @critical @done(25-12-25 08:17)
|
||||
✔ Open files with default app (cross platform) @done(25-12-25 08:17)
|
||||
✔ Grid view render bug, shows as column for split second on first render of results @done(25-12-22 07:49)
|
||||
✔ Job sound: make copy sound a varient, not play also @done(25-12-24 07:24)
|
||||
✔ Sidebar active based on Explorer path @today @done(25-12-20 07:59)
|
||||
☐ Fix three device sync @critical
|
||||
☐ Fix three device sync
|
||||
☐ Run now button in Location Inspector doesn't work well @today
|
||||
☐ Delete location UX improvement
|
||||
☐ Delete jobs
|
||||
☐ Fix unique bytes calculation
|
||||
☐ Choose custom device name on mobile setup flow
|
||||
☐ Device state tombstone sync
|
||||
✔ Delete jobs @done(26-01-20 09:09)
|
||||
☐ Drop external items INTO spacedrive explorer @today
|
||||
☐ Eject volume button
|
||||
✔ Fix explorer arrow key navigation @critical @done(25-12-22 06:13)
|
||||
✔ Eject volume button @done(26-01-20 09:09)
|
||||
✔ Fix explorer arrow key navigation @done(25-12-22 06:13)
|
||||
✔ Complete Spaces Sidebar with customize palette @done(25-12-20 09:56)
|
||||
# It has an issue where it conflicts with the tab focus navigation and is out of order often
|
||||
☐ Device owned data count mismatch reconciliation / improve sync testing
|
||||
☐ Improve sync testing, synced directories seem to be missing relationships to form tree (sometimes)
|
||||
✔ Drag selection area + command to add to selection @today @done(25-12-25 08:18)
|
||||
✔ Back/forward button navigation not working @critical @done(25-12-22 05:43)
|
||||
✔ Back/forward button navigation not working @done(25-12-22 05:43)
|
||||
# Investigate sync integrity
|
||||
✔ Refetch all queries when window focus @today @done(25-12-20 09:55)
|
||||
✔ Fix job reactivity @today @done(25-12-21 06:14)
|
||||
@@ -62,19 +74,17 @@ Journey to v2.0.0-pre.1:
|
||||
✔ Explorer should render results for a device, show volumes @today @done(25-12-20 09:55)
|
||||
✔ Tags are not reactive, anywhere side from Inspector @today @done(26-01-10 22:21)
|
||||
☐ Tags should support ephemeral
|
||||
✔ Starting up screen when Core/daemon is initializing @critical @done(25-12-25 08:18)
|
||||
✔ Starting up screen when Core/daemon is initializing @done(25-12-25 08:18)
|
||||
# This is complex because ephemeral entries do not have a content identity
|
||||
☐ Fix unique bytes calculation
|
||||
✔ Disable knowledge view @today @done(25-12-25 09:24)
|
||||
✔ Fix zoom on videos / images @done(25-12-22 07:49)
|
||||
☐ Show alternate paths in inspector
|
||||
✔ Show alternate paths in inspector @done(26-01-20 09:10)
|
||||
☐ Make default home folders SpaceItems
|
||||
☐ Choose custom device name on mobile setup flow
|
||||
# Decide how this works in the context of sync, likely organize by device
|
||||
✔ New Space Item resource event not working @done(25-12-24 07:25)
|
||||
✔ Can't drag and drop onto space items to copy paste @done(25-12-16 23:51)
|
||||
☐ Sometimes device shows as online even if its not
|
||||
☐ Stale detection background discovery reindex @critical
|
||||
☐ Stale detection background discovery reindex
|
||||
# Looks like cache state issue, even though the devices due
|
||||
✔ Index mode not showing in location inspector @done(26-01-10 22:23)
|
||||
✔ Space Item by entry UUID, not entry id and should have constraints against duplicates, for both entries and categories like Overview and Favorites @done(26-01-10 22:23)
|
||||
|
||||
@@ -33,7 +33,7 @@ export function SettingsRow({
|
||||
"flex-row items-center px-6 py-3 bg-app-box min-h-[56px]",
|
||||
isFirst && "rounded-t-[32px]",
|
||||
isLast && "rounded-b-[32px]",
|
||||
onPress && "active:bg-app-hover",
|
||||
onPress && "active:bg-app-selected",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function BrowseScreen() {
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-sidebar">
|
||||
<View className="flex-1 bg-app">
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal
|
||||
|
||||
@@ -56,7 +56,7 @@ function FileCard({ file, onPress }: { file: File; onPress: () => void }) {
|
||||
<View className="rounded-lg p-2 items-center justify-center aspect-square mb-2">
|
||||
<Image
|
||||
source={iconSource}
|
||||
className="w-16 h-16"
|
||||
className="w-20 h-20"
|
||||
style={{ resizeMode: "contain" }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -178,6 +178,7 @@ fn passes_ephemeral_filters(
|
||||
DateField::ModifiedAt => metadata.modified,
|
||||
DateField::CreatedAt => metadata.created,
|
||||
DateField::AccessedAt => metadata.accessed,
|
||||
DateField::IndexedAt => None, // Ephemeral search doesn't have indexed_at
|
||||
};
|
||||
|
||||
if let Some(system_time) = system_time_opt {
|
||||
|
||||
@@ -43,6 +43,7 @@ impl FilterBuilder {
|
||||
DateField::CreatedAt => crate::infra::db::entities::entry::Column::CreatedAt,
|
||||
DateField::ModifiedAt => crate::infra::db::entities::entry::Column::ModifiedAt,
|
||||
DateField::AccessedAt => crate::infra::db::entities::entry::Column::AccessedAt,
|
||||
DateField::IndexedAt => crate::infra::db::entities::entry::Column::IndexedAt,
|
||||
};
|
||||
|
||||
if let Some(start) = range.start {
|
||||
|
||||
@@ -87,6 +87,7 @@ pub enum DateField {
|
||||
CreatedAt,
|
||||
ModifiedAt,
|
||||
AccessedAt,
|
||||
IndexedAt,
|
||||
}
|
||||
|
||||
/// Filter for file size in bytes
|
||||
@@ -111,6 +112,7 @@ pub enum SortField {
|
||||
Size,
|
||||
ModifiedAt,
|
||||
CreatedAt,
|
||||
IndexedAt,
|
||||
}
|
||||
|
||||
/// Sort direction
|
||||
|
||||
@@ -594,6 +594,7 @@ impl FileSearchQuery {
|
||||
crate::ops::search::input::DateField::CreatedAt => entry::Column::CreatedAt,
|
||||
crate::ops::search::input::DateField::ModifiedAt => entry::Column::ModifiedAt,
|
||||
crate::ops::search::input::DateField::AccessedAt => entry::Column::AccessedAt,
|
||||
crate::ops::search::input::DateField::IndexedAt => entry::Column::IndexedAt,
|
||||
};
|
||||
|
||||
if let Some(start) = date_range.start {
|
||||
@@ -988,6 +989,7 @@ impl FileSearchQuery {
|
||||
crate::ops::search::input::DateField::CreatedAt => Some(entry_model.created_at),
|
||||
crate::ops::search::input::DateField::ModifiedAt => Some(entry_model.modified_at),
|
||||
crate::ops::search::input::DateField::AccessedAt => entry_model.accessed_at,
|
||||
crate::ops::search::input::DateField::IndexedAt => entry_model.indexed_at,
|
||||
};
|
||||
|
||||
if let Some(date) = date_to_check {
|
||||
|
||||
@@ -42,6 +42,9 @@ impl SortBuilder {
|
||||
SortField::CreatedAt => {
|
||||
self.order.push(("created_at".to_string(), direction));
|
||||
}
|
||||
SortField::IndexedAt => {
|
||||
self.order.push(("indexed_at".to_string(), direction));
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
|
||||
@@ -6,6 +6,7 @@ import { JobsScreen } from "./components/JobManager";
|
||||
import { DaemonManager } from "./routes/daemon";
|
||||
import { TagView } from "./routes/tag";
|
||||
import { FileKindsView } from "./routes/file-kinds";
|
||||
import { RecentsView } from "./routes/explorer/views/RecentsView";
|
||||
|
||||
/**
|
||||
* Router routes configuration (without router instance)
|
||||
@@ -33,11 +34,7 @@ export const explorerRoutes = [
|
||||
},
|
||||
{
|
||||
path: "recents",
|
||||
element: (
|
||||
<div className="flex items-center justify-center h-full text-ink">
|
||||
Recents (coming soon)
|
||||
</div>
|
||||
),
|
||||
element: <RecentsView />,
|
||||
},
|
||||
{
|
||||
path: "file-kinds",
|
||||
|
||||
@@ -60,7 +60,8 @@ export interface SearchFilters {
|
||||
|
||||
export type ExplorerMode =
|
||||
| { type: "browse" }
|
||||
| { type: "search"; query: string; scope: SearchScope };
|
||||
| { type: "search"; query: string; scope: SearchScope }
|
||||
| { type: "recents" };
|
||||
|
||||
export type NavigationTarget =
|
||||
| { type: "path"; path: SdPath }
|
||||
@@ -190,6 +191,8 @@ type UIAction =
|
||||
| { type: "SET_TAG_MODE"; active: boolean }
|
||||
| { type: "ENTER_SEARCH_MODE"; query: string; scope: SearchScope }
|
||||
| { type: "EXIT_SEARCH_MODE" }
|
||||
| { type: "ENTER_RECENTS_MODE" }
|
||||
| { type: "EXIT_RECENTS_MODE" }
|
||||
| { type: "SET_SEARCH_FILTERS"; filters: SearchFilters }
|
||||
| {
|
||||
type: "LOAD_PREFERENCES";
|
||||
@@ -245,6 +248,18 @@ function uiReducer(state: UIState, action: UIAction): UIState {
|
||||
searchFilters: {},
|
||||
};
|
||||
|
||||
case "ENTER_RECENTS_MODE":
|
||||
return {
|
||||
...state,
|
||||
mode: { type: "recents" },
|
||||
};
|
||||
|
||||
case "EXIT_RECENTS_MODE":
|
||||
return {
|
||||
...state,
|
||||
mode: { type: "browse" },
|
||||
};
|
||||
|
||||
case "SET_SEARCH_FILTERS":
|
||||
return {
|
||||
...state,
|
||||
@@ -395,6 +410,8 @@ interface ExplorerContextValue {
|
||||
mode: ExplorerMode;
|
||||
enterSearchMode: (query: string, scope?: SearchScope) => void;
|
||||
exitSearchMode: () => void;
|
||||
enterRecentsMode: () => void;
|
||||
exitRecentsMode: () => void;
|
||||
searchFilters: SearchFilters;
|
||||
setSearchFilters: (filters: SearchFilters) => void;
|
||||
|
||||
@@ -712,6 +729,14 @@ export function ExplorerProvider({
|
||||
uiDispatch({ type: "EXIT_SEARCH_MODE" });
|
||||
}, []);
|
||||
|
||||
const enterRecentsMode = useCallback(() => {
|
||||
uiDispatch({ type: "ENTER_RECENTS_MODE" });
|
||||
}, []);
|
||||
|
||||
const exitRecentsMode = useCallback(() => {
|
||||
uiDispatch({ type: "EXIT_RECENTS_MODE" });
|
||||
}, []);
|
||||
|
||||
const setSearchFilters = useCallback((filters: SearchFilters) => {
|
||||
uiDispatch({ type: "SET_SEARCH_FILTERS", filters });
|
||||
}, []);
|
||||
@@ -767,6 +792,8 @@ export function ExplorerProvider({
|
||||
mode: uiState.mode,
|
||||
enterSearchMode,
|
||||
exitSearchMode,
|
||||
enterRecentsMode,
|
||||
exitRecentsMode,
|
||||
searchFilters: uiState.searchFilters,
|
||||
setSearchFilters,
|
||||
devices,
|
||||
@@ -808,6 +835,8 @@ export function ExplorerProvider({
|
||||
uiState.mode,
|
||||
enterSearchMode,
|
||||
exitSearchMode,
|
||||
enterRecentsMode,
|
||||
exitRecentsMode,
|
||||
uiState.searchFilters,
|
||||
setSearchFilters,
|
||||
devices,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useExplorer } from "../context";
|
||||
import type { SearchScope } from "../context";
|
||||
import { useVirtualListing } from "./useVirtualListing";
|
||||
|
||||
export type FileSource = "search" | "virtual" | "directory";
|
||||
export type FileSource = "search" | "virtual" | "directory" | "recents";
|
||||
|
||||
export interface ExplorerFilesResult {
|
||||
files: File[];
|
||||
@@ -30,6 +30,7 @@ export function useExplorerFiles(): ExplorerFilesResult {
|
||||
|
||||
// Check for search mode
|
||||
const isSearchMode = mode.type === "search";
|
||||
const isRecentsMode = mode.type === "recents";
|
||||
|
||||
// Build search query input
|
||||
const searchQueryInput = useMemo<FileSearchInput | null>(() => {
|
||||
@@ -80,6 +81,35 @@ export function useExplorerFiles(): ExplorerFilesResult {
|
||||
};
|
||||
}, [isSearchMode, mode, currentPath, sortBy]);
|
||||
|
||||
// Build recents query input
|
||||
const recentsQueryInput = useMemo<FileSearchInput | null>(() => {
|
||||
if (!isRecentsMode) return null;
|
||||
|
||||
return {
|
||||
query: "", // Empty query to match all files
|
||||
scope: "Library",
|
||||
filters: {
|
||||
file_types: null,
|
||||
tags: null,
|
||||
date_range: null,
|
||||
size_range: null,
|
||||
locations: null,
|
||||
content_types: null,
|
||||
include_hidden: null,
|
||||
include_archived: null,
|
||||
},
|
||||
mode: "Fast", // Fast mode since we're just sorting by indexed_at
|
||||
sort: {
|
||||
field: "IndexedAt", // Sort by when files were indexed
|
||||
direction: "Desc", // Most recent first
|
||||
},
|
||||
pagination: {
|
||||
limit: 100, // Reasonable limit for recents screen
|
||||
offset: 0,
|
||||
},
|
||||
};
|
||||
}, [isRecentsMode]);
|
||||
|
||||
// Search query
|
||||
const searchQuery = useNormalizedQuery<FileSearchInput, FileSearchOutput>({
|
||||
wireMethod: "query:search.files",
|
||||
@@ -92,6 +122,14 @@ export function useExplorerFiles(): ExplorerFilesResult {
|
||||
enabled: isSearchMode && !!searchQueryInput && searchQueryInput.query.length >= 2,
|
||||
});
|
||||
|
||||
// Recents query
|
||||
const recentsQuery = useNormalizedQuery<FileSearchInput, FileSearchOutput>({
|
||||
wireMethod: "query:search.files",
|
||||
input: recentsQueryInput!,
|
||||
resourceType: "file",
|
||||
enabled: isRecentsMode && !!recentsQueryInput,
|
||||
});
|
||||
|
||||
// Directory query
|
||||
const directoryQuery = useNormalizedQuery({
|
||||
wireMethod: "query:files.directory_listing",
|
||||
@@ -105,14 +143,23 @@ export function useExplorerFiles(): ExplorerFilesResult {
|
||||
}
|
||||
: null!,
|
||||
resourceType: "file",
|
||||
enabled: !!currentPath && !isVirtualView && !isSearchMode,
|
||||
enabled: !!currentPath && !isVirtualView && !isSearchMode && !isRecentsMode,
|
||||
pathScope: currentPath ?? undefined,
|
||||
});
|
||||
|
||||
// Determine source and files with priority: search > virtual > directory
|
||||
const source: FileSource = isSearchMode ? "search" : isVirtualView ? "virtual" : "directory";
|
||||
// Determine source and files with priority: recents > search > virtual > directory
|
||||
const source: FileSource = isRecentsMode
|
||||
? "recents"
|
||||
: isSearchMode
|
||||
? "search"
|
||||
: isVirtualView
|
||||
? "virtual"
|
||||
: "directory";
|
||||
|
||||
const files = useMemo(() => {
|
||||
if (isRecentsMode) {
|
||||
return (recentsQuery.data as FileSearchOutput | undefined)?.files || [];
|
||||
}
|
||||
if (isSearchMode) {
|
||||
return (searchQuery.data as FileSearchOutput | undefined)?.files || [];
|
||||
}
|
||||
@@ -120,13 +167,15 @@ export function useExplorerFiles(): ExplorerFilesResult {
|
||||
return virtualFiles || [];
|
||||
}
|
||||
return (directoryQuery.data as any)?.files || [];
|
||||
}, [isSearchMode, isVirtualView, searchQuery.data, virtualFiles, directoryQuery.data]);
|
||||
}, [isRecentsMode, isSearchMode, isVirtualView, recentsQuery.data, searchQuery.data, virtualFiles, directoryQuery.data]);
|
||||
|
||||
const isLoading = isSearchMode
|
||||
? searchQuery.isLoading
|
||||
: isVirtualView
|
||||
? false
|
||||
: directoryQuery.isLoading;
|
||||
const isLoading = isRecentsMode
|
||||
? recentsQuery.isLoading
|
||||
: isSearchMode
|
||||
? searchQuery.isLoading
|
||||
: isVirtualView
|
||||
? false
|
||||
: directoryQuery.isLoading;
|
||||
|
||||
return { files, isLoading, source };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useExplorer } from '../../context';
|
||||
import { GridView } from '../GridView';
|
||||
import { ListView } from '../ListView';
|
||||
import { MediaView } from '../MediaView';
|
||||
import { ColumnView } from '../ColumnView';
|
||||
import { SizeView } from '../SizeView';
|
||||
import { KnowledgeView } from '../KnowledgeView';
|
||||
|
||||
/**
|
||||
* RecentsView displays recently indexed files sorted by indexed_at timestamp.
|
||||
*
|
||||
* Similar to SearchView, it delegates to existing view components which automatically
|
||||
* read from useExplorerFiles. This ensures recents has the same interactions as normal
|
||||
* browsing: keyboard navigation, drag-to-select, context menus, etc.
|
||||
*/
|
||||
export function RecentsView() {
|
||||
const explorer = useExplorer();
|
||||
const { viewMode, enterRecentsMode, exitRecentsMode } = explorer;
|
||||
|
||||
// Enter recents mode on mount, exit on unmount
|
||||
useEffect(() => {
|
||||
enterRecentsMode();
|
||||
return () => exitRecentsMode();
|
||||
}, [enterRecentsMode, exitRecentsMode]);
|
||||
|
||||
// Route to the appropriate view based on viewMode
|
||||
// The views will automatically use recents results via useExplorerFiles
|
||||
switch (viewMode) {
|
||||
case 'grid':
|
||||
return <GridView />;
|
||||
case 'list':
|
||||
return <ListView />;
|
||||
case 'media':
|
||||
return <MediaView />;
|
||||
case 'column':
|
||||
return <ColumnView />;
|
||||
case 'size':
|
||||
return <SizeView />;
|
||||
case 'knowledge':
|
||||
return <KnowledgeView />;
|
||||
default:
|
||||
return <GridView />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RecentsView } from './RecentsView';
|
||||
Reference in New Issue
Block a user