This commit is contained in:
James Pine 2023-12-18 18:04:33 -08:00
parent 65938b51bd
commit f355679be2
6 changed files with 158 additions and 36 deletions

View file

@ -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()),
}

View file

@ -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()),
})
}

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

View file

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

View file

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

View file

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