mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
x
This commit is contained in:
parent
65938b51bd
commit
f355679be2
|
@ -94,7 +94,7 @@ impl P2PManager {
|
|||
let config = self.node_config_manager.get().await;
|
||||
PeerMetadata {
|
||||
name: config.name.clone(),
|
||||
device_kind: Some(get_hardware_model_name().unwrap_or_else(|_| "Unknown".into())),
|
||||
device_model: Some(get_hardware_model_name().unwrap_or_else(|_| "Unknown".into())),
|
||||
operating_system: Some(OperatingSystem::get_os()),
|
||||
version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::node::Platform;
|
|||
pub struct PeerMetadata {
|
||||
pub name: String,
|
||||
pub operating_system: Option<OperatingSystem>,
|
||||
pub device_kind: Option<String>,
|
||||
pub device_model: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ impl Metadata for PeerMetadata {
|
|||
if let Some(version) = self.version {
|
||||
map.insert("version".to_owned(), version);
|
||||
}
|
||||
if let Some(device_kind) = self.device_kind {
|
||||
map.insert("device_kind".to_owned(), device_kind);
|
||||
if let Some(device_model) = self.device_model {
|
||||
map.insert("device_model".to_owned(), device_model);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ impl Metadata for PeerMetadata {
|
|||
.get("os")
|
||||
.map(|os| os.parse().map_err(|_| "Unable to parse 'OperationSystem'!"))
|
||||
.transpose()?,
|
||||
device_kind: data.get("device_kind").map(|v| v.to_owned()),
|
||||
device_model: data.get("device_model").map(|v| v.to_owned()),
|
||||
version: data.get("version").map(|v| v.to_owned()),
|
||||
})
|
||||
}
|
||||
|
|
95
interface/app/$libraryId/recents.tsx
Normal file
95
interface/app/$libraryId/recents.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { useMemo } from 'react';
|
||||
import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Icon } from '~/components';
|
||||
import { useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { useObjectsExplorerQuery } from './Explorer/queries/useObjectsExplorerQuery';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { EmptyNotice } from './Explorer/View';
|
||||
import SearchOptions, { SearchContextProvider, useSearch } from './Search';
|
||||
import SearchBar from './Search/SearchBar';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
||||
export function Component() {
|
||||
const { id: tagId } = useZodRouteParams(LocationIdParamsSchema);
|
||||
const result = useLibraryQuery(['tags.get', tagId], { suspense: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tag = useCache(result.data?.item);
|
||||
|
||||
useRouteTitle(tag!.name ?? 'Tag');
|
||||
|
||||
const explorerSettings = useExplorerSettings({
|
||||
settings: useMemo(() => {
|
||||
return createDefaultExplorerSettings<ObjectOrder>({ order: null });
|
||||
}, []),
|
||||
orderingKeys: objectOrderingKeysSchema
|
||||
});
|
||||
|
||||
const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
|
||||
const fixedFilters = useMemo(
|
||||
() => [
|
||||
{ object: { tags: { in: [tag!.id] } } },
|
||||
...(explorerSettingsSnapshot.layoutMode === 'media'
|
||||
? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }]
|
||||
: [])
|
||||
],
|
||||
[tag, explorerSettingsSnapshot.layoutMode]
|
||||
);
|
||||
|
||||
const search = useSearch({
|
||||
fixedFilters
|
||||
});
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: { take: 100, filters: search.allFilters },
|
||||
explorerSettings
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
...objects,
|
||||
isFetchingNextPage: objects.query.isFetchingNextPage,
|
||||
settings: explorerSettings,
|
||||
parent: { type: 'Tag', tag: tag! }
|
||||
});
|
||||
|
||||
return (
|
||||
<ExplorerContextProvider explorer={explorer}>
|
||||
<SearchContextProvider search={search}>
|
||||
<TopBarPortal
|
||||
center={<SearchBar />}
|
||||
left={
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div
|
||||
className="h-[14px] w-[14px] shrink-0 rounded-full"
|
||||
style={{ backgroundColor: tag!.color || '#efefef' }}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium">{tag?.name}</span>
|
||||
</div>
|
||||
}
|
||||
right={<DefaultTopBarOptions />}
|
||||
>
|
||||
{search.open && (
|
||||
<>
|
||||
<hr className="w-full border-t border-sidebar-divider bg-sidebar-divider" />
|
||||
<SearchOptions />
|
||||
</>
|
||||
)}
|
||||
</TopBarPortal>
|
||||
</SearchContextProvider>
|
||||
<Explorer
|
||||
emptyNotice={
|
||||
<EmptyNotice
|
||||
icon={<Icon name="Tags" size={128} />}
|
||||
message="No items assigned to this tag."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ExplorerContextProvider>
|
||||
);
|
||||
}
|
|
@ -6,17 +6,20 @@ import { tw } from '@sd/ui';
|
|||
|
||||
const ArrowButton = tw.div`absolute top-1/2 z-40 flex h-8 w-8 shrink-0 -translate-y-1/2 items-center p-2 cursor-pointer justify-center rounded-full border border-app-line bg-app/50 hover:opacity-95 backdrop-blur-md transition-all duration-200`;
|
||||
|
||||
export const useHorizontalScroll = () => {
|
||||
export const HorizontalScroll = ({ children }: { children: ReactNode }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { events } = useDraggable(ref as React.MutableRefObject<HTMLDivElement>);
|
||||
const [lastItemVisible, setLastItemVisible] = useState(false);
|
||||
const [scroll, setScroll] = useState(0);
|
||||
// If the content is overflowing, we need to show the arrows
|
||||
const [isContentOverflow, setIsContentOverflow] = useState(false);
|
||||
|
||||
const updateScrollState = () => {
|
||||
const element = ref.current;
|
||||
if (element) {
|
||||
setScroll(element.scrollLeft);
|
||||
setLastItemVisible(element.scrollWidth - element.clientWidth === element.scrollLeft);
|
||||
setIsContentOverflow(element.scrollWidth > element.clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -32,22 +35,35 @@ export const useHorizontalScroll = () => {
|
|||
};
|
||||
}, [ref]);
|
||||
|
||||
// Sets the initial scroll state on mount
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (element) {
|
||||
element.addEventListener('scroll', updateScrollState);
|
||||
updateScrollState();
|
||||
}
|
||||
return () => {
|
||||
if (element) {
|
||||
element.removeEventListener('scroll', updateScrollState);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleArrowOnClick = (direction: 'right' | 'left') => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const scrollAmount = element.clientWidth;
|
||||
|
||||
element.scrollTo({
|
||||
left: direction === 'left' ? element.scrollLeft - 200 : element.scrollLeft + 200,
|
||||
left:
|
||||
direction === 'left'
|
||||
? element.scrollLeft + scrollAmount
|
||||
: element.scrollLeft - scrollAmount,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
return { ref, events, handleArrowOnClick, lastItemVisible, scroll };
|
||||
};
|
||||
|
||||
export const HorizontalScroll = ({ children }: { children: ReactNode }) => {
|
||||
const { ref, events, handleArrowOnClick, lastItemVisible, scroll } = useHorizontalScroll();
|
||||
|
||||
const maskImage = `linear-gradient(90deg, transparent 0.1%, rgba(0, 0, 0, 1) ${
|
||||
scroll > 0 ? '10%' : '0%'
|
||||
}, rgba(0, 0, 0, 1) ${lastItemVisible ? '95%' : '85%'}, transparent 99%)`;
|
||||
|
@ -72,12 +88,14 @@ export const HorizontalScroll = ({ children }: { children: ReactNode }) => {
|
|||
{children}
|
||||
</div>
|
||||
|
||||
<ArrowButton
|
||||
onClick={() => handleArrowOnClick('left')}
|
||||
className={clsx('right-3', lastItemVisible && 'pointer-events-none opacity-0')}
|
||||
>
|
||||
<ArrowRight weight="bold" className="h-4 w-4 text-ink" />
|
||||
</ArrowButton>
|
||||
{isContentOverflow && (
|
||||
<ArrowButton
|
||||
onClick={() => handleArrowOnClick('left')}
|
||||
className={clsx('right-3', lastItemVisible && 'pointer-events-none opacity-0')}
|
||||
>
|
||||
<ArrowRight weight="bold" className="h-4 w-4 text-ink" />
|
||||
</ArrowButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ArrowsOut, CaretDown, CaretUp, FrameCorners } from '@phosphor-icons/react';
|
||||
import {
|
||||
DriveAmazonS3,
|
||||
DriveDropbox,
|
||||
|
@ -10,9 +11,9 @@ import {
|
|||
} from '@sd/assets/icons';
|
||||
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { forwardRef, useEffect, useMemo, useState } from 'react';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Button, Card, CircularProgress, tw } from '@sd/ui';
|
||||
import { Button, ButtonProps, Card, CircularProgress, tw } from '@sd/ui';
|
||||
|
||||
import { useIsDark } from '../../../hooks';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
@ -125,10 +126,10 @@ export const Component = () => {
|
|||
<TopBarPortal
|
||||
left={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">Overview</span>
|
||||
<Button className="!p-[5px]" variant="subtle">
|
||||
<span className="truncate text-sm font-medium">Library Overview</span>
|
||||
{/* <Button className="!p-[5px]" variant="subtle">
|
||||
<Ellipsis className="h- w-3 opacity-50" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -145,7 +146,7 @@ export const Component = () => {
|
|||
color="#0362FF"
|
||||
connection_type="lan"
|
||||
/>
|
||||
<StatisticItem
|
||||
{/* <StatisticItem
|
||||
name="Spacestudio"
|
||||
icon={SilverBox}
|
||||
total_space="4098046511104"
|
||||
|
@ -184,11 +185,11 @@ export const Component = () => {
|
|||
free_space="969004651119"
|
||||
color="#0362FF"
|
||||
connection_type="p2p"
|
||||
/>
|
||||
/> */}
|
||||
</OverviewSection>
|
||||
|
||||
<OverviewSection count={3} title="Cloud Drives">
|
||||
<StatisticItem
|
||||
{/* <OverviewSection count={3} title="Cloud Drives">
|
||||
<StatisticItem
|
||||
name="James Pine"
|
||||
icon={DriveDropbox}
|
||||
total_space="104877906944"
|
||||
|
@ -213,6 +214,7 @@ export const Component = () => {
|
|||
connection_type="cloud"
|
||||
/>
|
||||
</OverviewSection>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -220,6 +222,8 @@ export const Component = () => {
|
|||
|
||||
const COUNT_STYLE = `min-w-[20px] flex h-[20px] px-1 items-center justify-center rounded-full border border-app-button/40 text-[9px]`;
|
||||
|
||||
const BUTTON_STYLE = `!p-[5px] opacity-0 transition-opacity group-hover:opacity-100`;
|
||||
|
||||
const OverviewSection = ({
|
||||
children,
|
||||
title,
|
||||
|
@ -232,13 +236,18 @@ const OverviewSection = ({
|
|||
<div className="mb-3 flex w-full items-center gap-3 pl-8 pr-4">
|
||||
<div className="font-bold">{title}</div>
|
||||
{count && <div className={COUNT_STYLE}>{count}</div>}
|
||||
<Button
|
||||
className="!p-[5px] opacity-0 transition-opacity group-hover:opacity-100"
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
>
|
||||
<Ellipsis className="h-3 w-3 text-ink-faint " />
|
||||
</Button>
|
||||
<div className="grow" />
|
||||
<div className="flex flex-row gap-1 text-sidebar-inkFaint opacity-0 transition-all duration-300 hover:!opacity-100 group-hover:opacity-30">
|
||||
<Button className={BUTTON_STYLE} size="icon" variant="subtle">
|
||||
<CaretUp weight="fill" className="h-3 w-3 text-ink-faint " />
|
||||
</Button>
|
||||
<Button className={BUTTON_STYLE} size="icon" variant="subtle">
|
||||
<CaretDown weight="fill" className="h-3 w-3 text-ink-faint " />
|
||||
</Button>
|
||||
<Button className={BUTTON_STYLE} size="icon" variant="subtle">
|
||||
<Ellipsis className="h-3 w-3 text-ink-faint " />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<HorizontalScroll>{children}</HorizontalScroll>
|
||||
|
|
|
@ -480,7 +480,7 @@ export type PairingDecision = { decision: "accept"; libraryId: string } | { deci
|
|||
|
||||
export type PairingStatus = { type: "EstablishingConnection" } | { type: "PairingRequested" } | { type: "LibraryAlreadyExists" } | { type: "PairingDecisionRequest" } | { type: "PairingInProgress"; data: { library_name: string; library_description: string | null } } | { type: "InitialSyncProgress"; data: number } | { type: "PairingComplete"; data: string } | { type: "PairingRejected" }
|
||||
|
||||
export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; device_kind: string | null; version: string | null }
|
||||
export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; device_model: string | null; version: string | null }
|
||||
|
||||
export type PlusCode = string
|
||||
|
||||
|
|
Loading…
Reference in a new issue