Explorer Grid View (#334)

added grid view, as well as:
- moved location context to client lib
- merged library settings with main settings
- added some missing settings
- removed demo locations due to FileItem props syntax change, they are currently being replaced anyway by Oscar in another PR
- added functioning favorite button to the inspector, that works now
This commit is contained in:
Jamie Pine 2022-07-14 13:50:48 -07:00 committed by GitHub
parent fa1c39657d
commit 7839fe43e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 784 additions and 506 deletions

View file

@ -12,6 +12,7 @@
"ipfs",
"Keepsafe",
"nodestate",
"overscan",
"pathctx",
"prismjs",
"proptype",

View file

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LibraryCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };
export type LibraryCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileSetFavorite", params: { id: number, favorite: boolean, } } | { key: "FileDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocFullRescan", params: { id: number, } } | { key: "LocQuickRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };

View file

@ -168,3 +168,30 @@ pub async fn set_note(
Ok(CoreResponse::Success(()))
}
pub async fn favorite(
ctx: LibraryContext,
id: i32,
favorite: bool,
) -> Result<CoreResponse, CoreError> {
let _response = ctx
.db
.file()
.find_unique(file::id::equals(id))
.update(vec![file::favorite::set(favorite)])
.exec()
.await
.unwrap();
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
library_id: ctx.id.to_string(),
query: LibraryQuery::LibGetExplorerDir {
limit: 0,
path: "".to_string(),
location_id: 0,
},
}))
.await;
Ok(CoreResponse::Success(()))
}

View file

@ -175,7 +175,7 @@ impl Node {
description,
} => {
self.library_manager
.edit_library(id, name, description)
.edit(id, name, description)
.await
.unwrap();
CoreResponse::Success(())
@ -210,15 +210,19 @@ impl Node {
sys::delete_location(&ctx, id).await?;
CoreResponse::Success(())
}
LibraryCommand::LocRescan { id } => {
LibraryCommand::LocFullRescan { id } => {
sys::scan_location(&ctx, id, String::new()).await;
CoreResponse::Success(())
}
LibraryCommand::LocQuickRescan { id: _ } => todo!(),
// CRUD for files
LibraryCommand::FileReadMetaData { id: _ } => todo!(),
LibraryCommand::FileSetNote { id, note } => {
file::set_note(ctx, id, note).await?
}
LibraryCommand::FileSetFavorite { id, favorite } => {
file::favorite(ctx, id, favorite).await?
}
// ClientCommand::FileEncrypt { id: _, algorithm: _ } => todo!(),
LibraryCommand::FileDelete { id } => {
ctx.db
@ -345,6 +349,7 @@ pub enum LibraryCommand {
// Files
FileReadMetaData { id: i32 },
FileSetNote { id: i32, note: Option<String> },
FileSetFavorite { id: i32, favorite: bool },
// FileEncrypt { id: i32, algorithm: EncryptionAlgorithm },
FileDelete { id: i32 },
// Tags
@ -356,7 +361,8 @@ pub enum LibraryCommand {
LocCreate { path: String },
LocUpdate { id: i32, name: Option<String> },
LocDelete { id: i32 },
LocRescan { id: i32 },
LocFullRescan { id: i32 },
LocQuickRescan { id: i32 },
// System
SysVolumeUnmount { id: i32 },
GenerateThumbsForLocation { id: i32, path: String },

View file

@ -156,7 +156,7 @@ impl LibraryManager {
.collect()
}
pub(crate) async fn edit_library(
pub(crate) async fn edit(
&self,
id: String,
name: Option<String>,
@ -223,7 +223,7 @@ impl LibraryManager {
.find(|lib| lib.id.to_string() == library_id)
.map(|v| v.clone())
}
/// load the library from a given path
pub(crate) async fn load(
id: Uuid,

View file

@ -18,10 +18,10 @@ export function setTransport(_transport: BaseTransport) {
}
// extract keys from generated Rust query/command types
type QueryKeyType = ClientQuery['key'];
type LibraryQueryKeyType = LibraryQuery['key'];
type CommandKeyType = ClientCommand['key'];
type LibraryCommandKeyType = LibraryCommand['key'];
export type QueryKeyType = ClientQuery['key'];
export type LibraryQueryKeyType = LibraryQuery['key'];
export type CommandKeyType = ClientCommand['key'];
export type LibraryCommandKeyType = LibraryCommand['key'];
// extract the type from the union
type CQType<K> = Extract<ClientQuery, { key: K }>;

View file

@ -10,6 +10,7 @@ export interface AppProps {
transport: BaseTransport;
platform: Platform;
cdn_url?: CdnUrl;
data_path?: string;
convertFileSrc: (url: string) => string;
openDialog: (options: { directory?: boolean }) => Promise<string | string[] | null>;
onClose?: () => void;

View file

@ -0,0 +1,9 @@
import { createContext } from 'react';
export const LocationContext = createContext<{
location_id: number;
data_path: string;
}>({
location_id: 1,
data_path: ''
});

View file

@ -1 +1,2 @@
export * from './AppPropsContext';
export * from './LocationContext';

View file

@ -1,16 +1,21 @@
import create from 'zustand';
type LayoutMode = 'list' | 'grid';
type ExplorerStore = {
selectedRowIndex: number;
layoutMode: LayoutMode;
setSelectedRowIndex: (index: number) => void;
locationId: number;
setLocationId: (index: number) => void;
newThumbnails: Record<string, boolean>;
addNewThumbnail: (cas_id: string) => void;
setLayoutMode: (mode: LayoutMode) => void;
reset: () => void;
};
export const useExplorerStore = create<ExplorerStore>((set) => ({
layoutMode: 'grid',
selectedRowIndex: 1,
setSelectedRowIndex: (index) => set((state) => ({ ...state, selectedRowIndex: index })),
locationId: -1,
@ -21,5 +26,6 @@ export const useExplorerStore = create<ExplorerStore>((set) => ({
...state,
newThumbnails: { ...state.newThumbnails, [cas_id]: true }
})),
setLayoutMode: (mode: LayoutMode) => set((state) => ({ ...state, layoutMode: mode })),
reset: () => set(() => ({}))
}));

View file

@ -26,6 +26,7 @@
"@sd/client": "workspace:*",
"@sd/core": "workspace:*",
"@sd/ui": "workspace:*",
"@types/styled-components": "^5.1.25",
"@vitejs/plugin-react": "^1.3.2",
"autoprefixer": "^10.4.7",
"byte-size": "^8.1.0",
@ -54,6 +55,7 @@
"react-transition-group": "^4.4.2",
"react-virtuoso": "^2.12.1",
"rooks": "^5.11.2",
"styled-components": "^5.3.5",
"tailwindcss": "^3.0.24",
"use-debounce": "^8.0.1",
"zustand": "4.0.0-rc.1"

View file

@ -1,8 +1,8 @@
import '@fontsource/inter/variable.css';
import { BaseTransport, ClientProvider, setTransport } from '@sd/client';
import { BaseTransport, ClientProvider, setTransport, useBridgeQuery } from '@sd/client';
import { useCoreEvents } from '@sd/client';
import { AppProps, AppPropsContext } from '@sd/client';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
@ -13,12 +13,24 @@ import './style.scss';
const queryClient = new QueryClient();
function RouterContainer() {
function RouterContainer(props: { props: AppProps }) {
useCoreEvents();
const [appProps, setAppProps] = useState(props.props);
const { data: client } = useBridgeQuery('NodeGetState');
useEffect(() => {
setAppProps({
...appProps,
data_path: client?.data_path
});
}, [client?.data_path]);
return (
<MemoryRouter>
<AppRouter />
</MemoryRouter>
<AppPropsContext.Provider value={Object.assign({ isFocused: true }, appProps)}>
<MemoryRouter>
<AppRouter />
</MemoryRouter>
</AppPropsContext.Provider>
);
}
@ -34,11 +46,9 @@ export default function App(props: AppProps) {
<>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => {}}>
<QueryClientProvider client={queryClient} contextSharing={false}>
<AppPropsContext.Provider value={Object.assign({ isFocused: true }, props)}>
<ClientProvider>
<RouterContainer />
</ClientProvider>
</AppPropsContext.Provider>
<ClientProvider>
<RouterContainer props={props} />
</ClientProvider>
</QueryClientProvider>
</ErrorBoundary>
</>

View file

@ -1,4 +1,4 @@
import { AppPropsContext } from '@sd/client';
import { AppPropsContext, useBridgeQuery } from '@sd/client';
import clsx from 'clsx';
import React, { useContext } from 'react';
import { Outlet } from 'react-router-dom';

View file

@ -12,21 +12,22 @@ import { OverviewScreen } from './screens/Overview';
import { PhotosScreen } from './screens/Photos';
import { RedirectPage } from './screens/Redirect';
import { TagScreen } from './screens/Tag';
import { CurrentLibrarySettings } from './screens/settings/CurrentLibrarySettings';
import { SettingsScreen } from './screens/settings/Settings';
import AppearanceSettings from './screens/settings/client/AppearanceSettings';
import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
import GeneralSettings from './screens/settings/client/GeneralSettings';
import KeybindSettings from './screens/settings/client/KeybindSettings';
import ContactsSettings from './screens/settings/library/ContactsSettings';
import KeysSettings from './screens/settings/library/KeysSetting';
import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
import LocationSettings from './screens/settings/library/LocationSettings';
import NodesSettings from './screens/settings/library/NodesSettings';
import SecuritySettings from './screens/settings/library/SecuritySettings';
import SharingSettings from './screens/settings/library/SharingSettings';
import SyncSettings from './screens/settings/library/SyncSettings';
import TagsSettings from './screens/settings/library/TagsSettings';
import ExperimentalSettings from './screens/settings/node/ExperimentalSettings';
import LibrarySettings from './screens/settings/node/LibrariesSettings';
import NodesSettings from './screens/settings/node/NodesSettings';
import P2PSettings from './screens/settings/node/P2PSettings';
export function AppRouter() {
@ -57,28 +58,27 @@ export function AppRouter() {
<Route path="content" element={<ContentScreen />} />
<Route path="photos" element={<PhotosScreen />} />
<Route path="debug" element={<DebugScreen />} />
<Route path={'library-settings'} element={<CurrentLibrarySettings />}>
<Route index element={<LocationSettings />} />
<Route path="general" element={<LibraryGeneralSettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="keys" element={<KeysSettings />} />
</Route>
<Route path={'settings'} element={<SettingsScreen />}>
<Route index element={<GeneralSettings />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="appearance" element={<AppearanceSettings />} />
<Route path="nodes" element={<NodesSettings />} />
<Route path="keybinds" element={<KeybindSettings />} />
<Route path="extensions" element={<ExtensionSettings />} />
<Route path="p2p" element={<P2PSettings />} />
<Route path="contacts" element={<ContactsSettings />} />
<Route path="experimental" element={<ExperimentalSettings />} />
<Route path="keys" element={<KeysSettings />} />
<Route path="library" element={<LibrarySettings />} />
<Route path="libraries" element={<LibrarySettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="sharing" element={<SharingSettings />} />
<Route path="sync" element={<SyncSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="library" element={<LibraryGeneralSettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="nodes" element={<NodesSettings />} />
<Route path="keys" element={<KeysSettings />} />
</Route>
<Route path="explorer/:id" element={<ExplorerScreen />} />
<Route path="tag/:id" element={<TagScreen />} />

View file

@ -72,12 +72,11 @@ export function Device(props: DeviceProps) {
key={key}
selected={selectedFile === location.name}
onClick={() => handleSelect(location.name)}
fileName={location.name}
folder={location.folder}
format={location.format}
iconName={location.icon}
/>
))}
{props.locations.length === 0 && (
<div className="w-full my-5 text-center text-gray-450">No locations</div>
)}
</div>
</div>
);

View file

@ -1,151 +1,109 @@
import { LocationContext, useExplorerStore } from '@sd/client';
import { File, FilePath } from '@sd/core';
import clsx from 'clsx';
import { FilePlus, FileText, Plus, Share, Trash } from 'phosphor-react';
import React, { MouseEventHandler } from 'react';
import React, { MouseEventHandler, useContext } from 'react';
import icons from '../../assets/icons';
import { ReactComponent as Folder } from '../../assets/svg/folder.svg';
import { WithContextMenu } from '../layout/MenuOverlay';
import { DefaultProps } from '../primitive/types';
import FileThumb from './FileThumb';
interface Props extends DefaultProps {
fileName: string;
iconName?: string;
format?: string;
folder?: boolean;
interface Props extends React.HTMLAttributes<HTMLDivElement> {
file?: FilePath | null;
selected?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
}
export default function FileItem(props: Props) {
// const Shadow = () => {
// return (
// <div
// className={clsx(
// 'absolute opacity-100 transition-opacity duration-200 top-auto bottom-auto w-[64px] h-[40px] shadow-xl shadow-red-500',
// { 'opacity-100': props.selected }
// )}
// />
// );
// };
const location = useContext(LocationContext);
return (
<WithContextMenu
menu={[
[
<div {...props} className={clsx('inline-block w-[100px] mb-3', props.className)} draggable>
<div
className={clsx(
'border-2 border-transparent rounded-lg text-center w-[100px] h-[100px] mb-1',
{
label: 'Details',
icon: FileText,
onClick() {}
},
{
label: 'Share',
icon: Share,
onClick() {
navigator.share?.({
title: 'Spacedrive',
text: 'Check out this cool app',
url: 'https://spacedrive.com'
});
}
'bg-gray-50 dark:bg-gray-650': props.selected
}
],
[
{
label: 'More actions...',
icon: Plus,
onClick() {},
children: [
[
{
label: 'Move to library',
icon: FilePlus,
onClick() {}
}
]
]
}
],
[
{
label: 'Delete',
icon: Trash,
danger: true,
onClick() {}
}
]
]}
>
<div onClick={props.onClick} className="inline-block w-[100px] mb-3" draggable>
<div
className={clsx(
'border-2 border-transparent rounded-lg text-center w-[100px] h-[100px] mb-1',
{
'bg-gray-50 dark:bg-gray-650': props.selected
}
)}
>
{props.folder ? (
<div className="flex items-center justify-center w-full h-full active:translate-y-[1px]">
<div className="w-[70px]">
<Folder className="" />
</div>
)}
>
{props.file?.is_dir ? (
<div className="flex items-center justify-center w-full h-full active:translate-y-[1px]">
<div className="w-[70px]">
<Folder className="" />
</div>
) : (
<div
className={clsx(
'w-[64px] mt-1.5 m-auto transition duration-200 rounded-lg h-[90px] relative active:translate-y-[1px]',
{
'': props.selected
}
)}
>
<svg
className="absolute top-0 left-0 pointer-events-none fill-gray-150 dark:fill-gray-550"
width="65"
height="85"
viewBox="0 0 65 81"
>
<path d="M0 8C0 3.58172 3.58172 0 8 0H39.6863C41.808 0 43.8429 0.842855 45.3431 2.34315L53.5 10.5L62.6569 19.6569C64.1571 21.1571 65 23.192 65 25.3137V73C65 77.4183 61.4183 81 57 81H8C3.58172 81 0 77.4183 0 73V8Z" />
</svg>
<svg
width="22"
height="22"
className="absolute top-1 -right-[1px] z-10 fill-gray-50 dark:fill-gray-500 pointer-events-none"
viewBox="0 0 41 41"
>
<path d="M41.4116 40.5577H11.234C5.02962 40.5577 0 35.5281 0 29.3238V0L41.4116 40.5577Z" />
</svg>
<div className="absolute flex flex-col items-center justify-center w-full h-full">
{props.iconName && icons[props.iconName as keyof typeof icons] ? (
(() => {
const Icon = icons[props.iconName as keyof typeof icons];
return (
<Icon className="mt-2 pointer-events-none margin-auto w-[40px] h-[40px]" />
);
})()
) : (
<></>
)}
<span className="mt-1 text-xs font-bold text-center uppercase cursor-default text-gray-450">
{props.format}
</span>
</div>
</div>
)}
</div>
<div className="flex justify-center">
<span
</div>
) : props.file?.file?.has_thumbnail ? (
<div
className={clsx(
'px-1.5 py-[1px] rounded-md text-sm font-medium text-gray-550 dark:text-gray-300 cursor-default',
'flex items-center justify-center h-full p-1 overflow-hidden rounded border-gray-550 shrink-0',
props.selected && 'border-primary'
)}
>
<div className="border-4 rounded border-gray-550">
<FileThumb
className="rounded-sm"
file={props.file}
locationId={location.location_id}
/>
</div>
</div>
) : (
<div
className={clsx(
'w-[64px] mt-1.5 m-auto transition duration-200 rounded-lg h-[90px] relative active:translate-y-[1px]',
{
'bg-primary !text-white': props.selected
'': props.selected
}
)}
>
{props.fileName}
</span>
</div>
<svg
className="absolute top-0 left-0 pointer-events-none fill-gray-150 dark:fill-gray-550"
width="65"
height="85"
viewBox="0 0 65 81"
>
<path d="M0 8C0 3.58172 3.58172 0 8 0H39.6863C41.808 0 43.8429 0.842855 45.3431 2.34315L53.5 10.5L62.6569 19.6569C64.1571 21.1571 65 23.192 65 25.3137V73C65 77.4183 61.4183 81 57 81H8C3.58172 81 0 77.4183 0 73V8Z" />
</svg>
<svg
width="22"
height="22"
className="absolute top-1 -right-[1px] z-0 fill-gray-50 dark:fill-gray-500 pointer-events-none"
viewBox="0 0 41 41"
>
<path d="M41.4116 40.5577H11.234C5.02962 40.5577 0 35.5281 0 29.3238V0L41.4116 40.5577Z" />
</svg>
<div className="absolute flex flex-col items-center justify-center w-full h-full">
{props.file?.extension && icons[props.file.extension as keyof typeof icons] ? (
(() => {
const Icon = icons[props.file.extension as keyof typeof icons];
return (
<Icon className="mt-2 pointer-events-none margin-auto w-[40px] h-[40px]" />
);
})()
) : (
<></>
)}
<span className="mt-1 text-xs font-bold text-center uppercase cursor-default text-gray-450">
{props.file?.extension}
</span>
</div>
</div>
)}
</div>
</WithContextMenu>
<div className="flex justify-center">
<span
className={clsx(
'px-1.5 py-[1px] truncate text-center rounded-md text-xs font-medium text-gray-550 dark:text-gray-300 cursor-default',
{
'bg-primary !text-white': props.selected
}
)}
>
{props.file?.name}
</span>
</div>
</div>
);
}

View file

@ -1,14 +1,16 @@
import { DotsVerticalIcon } from '@heroicons/react/solid';
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
import { LocationContext, useBridgeQuery, useLibraryQuery } from '@sd/client';
import { useExplorerStore } from '@sd/client';
import { AppPropsContext } from '@sd/client';
import { FilePath } from '@sd/core';
import clsx from 'clsx';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Virtuoso, VirtuosoGrid, VirtuosoHandle } from 'react-virtuoso';
import { useKey, useWindowSize } from 'rooks';
import styled from 'styled-components';
import FileItem from './FileItem';
import FileThumb from './FileThumb';
interface IColumn {
@ -32,13 +34,18 @@ const columns = ensureIsColumns([
type ColumnKey = typeof columns[number]['key'];
const LocationContext = React.createContext<{
location_id: number;
data_path: string;
}>({
location_id: 1,
data_path: ''
});
// these styled components are out of place, but are here to follow the virtuoso docs. could probably be translated to tailwind somehow, since the `components` prop only accepts a styled div, not a react component.
const GridContainer = styled.div`
display: flex;
margin-top: 60px;
margin-left: 10px;
width: 100%;
flex-wrap: wrap;
`;
const GridItemContainer = styled.div`
display: flex;
flex-wrap: wrap;
`;
export const FileList: React.FC<{ location_id: number; path: string; limit: number }> = (props) => {
const size = useWindowSize();
@ -51,7 +58,7 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
const path = props.path;
const { selectedRowIndex, setSelectedRowIndex, setLocationId } = useExplorerStore();
const { selectedRowIndex, setSelectedRowIndex, setLocationId, layoutMode } = useExplorerStore();
const [goingUp, setGoingUp] = useState(false);
const { data: currentDir } = useLibraryQuery('LibGetExplorerDir', {
@ -64,7 +71,7 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
if (selectedRowIndex === 0 && goingUp) {
VList.current?.scrollTo({ top: 0, behavior: 'smooth' });
}
if (selectedRowIndex != -1) {
if (selectedRowIndex != -1 && typeof VList.current?.scrollIntoView === 'function') {
VList.current?.scrollIntoView({
index: goingUp ? selectedRowIndex - 1 : selectedRowIndex
});
@ -88,12 +95,12 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
setSelectedRowIndex(selectedRowIndex + 1);
});
const Row = (index: number) => {
const row = currentDir?.contents?.[index];
if (!row) return null;
return <RenderRow key={index} row={row} rowIndex={index} dirId={currentDir?.directory.id} />;
const createRenderItem = (RenderItem: React.FC<RenderItemProps>) => {
return (index: number) => {
const row = currentDir?.contents?.[index];
if (!row) return null;
return <RenderItem key={index} index={index} item={row} dirId={currentDir?.directory.id} />;
};
};
const Header = () => (
@ -116,46 +123,90 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
</div>
);
return useMemo(
() => (
<div
ref={tableContainer}
style={{ marginTop: -44 }}
className="w-full pl-2 bg-white cursor-default dark:bg-gray-650"
return (
<div
ref={tableContainer}
style={{ marginTop: -44 }}
className="w-full pl-2 bg-white cursor-default dark:bg-gray-600"
>
<LocationContext.Provider
value={{ location_id: props.location_id, data_path: client?.data_path as string }}
>
<LocationContext.Provider
value={{ location_id: props.location_id, data_path: client?.data_path as string }}
>
{layoutMode === 'grid' && (
<VirtuosoGrid
ref={VList}
overscan={5000}
components={{
Item: GridItemContainer,
List: GridContainer
}}
style={{ height: size.innerHeight ?? 600 }}
totalCount={currentDir?.contents.length || 0}
itemContent={createRenderItem(RenderGridItem)}
className="w-full overflow-x-hidden outline-none explorer-scroll"
/>
)}
{layoutMode === 'list' && (
<Virtuoso
data={currentDir?.contents}
data={currentDir?.contents} // this might be redundant, row data is retrieved by index in renderRow
ref={VList}
style={{ height: size.innerHeight ?? 600 }}
totalCount={currentDir?.contents.length || 0}
itemContent={Row}
components={{ Header, Footer: () => <div className="w-full " /> }}
itemContent={createRenderItem(RenderRow)}
components={{
Header,
Footer: () => <div className="w-full " />
}}
increaseViewportBy={{ top: 400, bottom: 200 }}
className="outline-none explorer-scroll"
/>
</LocationContext.Provider>
</div>
),
[props.location_id, size.innerWidth, currentDir?.directory.id, tableContainer.current]
)}
</LocationContext.Provider>
</div>
);
};
const RenderRow: React.FC<{
row: FilePath;
rowIndex: number;
interface RenderItemProps {
item: FilePath;
index: number;
dirId: number;
}> = ({ row, rowIndex, dirId }) => {
}
const RenderGridItem: React.FC<RenderItemProps> = ({ item, index, dirId }) => {
// return <div className="flex m-2 bg-red-600 w-44 h-44"></div>;
const { selectedRowIndex, setSelectedRowIndex } = useExplorerStore();
const isActive = selectedRowIndex === rowIndex;
const isActive = selectedRowIndex === index;
let [_, setSearchParams] = useSearchParams();
function selectFileHandler() {
if (selectedRowIndex == rowIndex) setSelectedRowIndex(-1);
else setSelectedRowIndex(rowIndex);
if (selectedRowIndex == index) setSelectedRowIndex(-1);
else setSelectedRowIndex(index);
}
return (
<FileItem
onDoubleClick={() => {
if (item.is_dir) {
setSearchParams({ path: item.materialized_path });
}
}}
file={item}
selected={isActive}
onClick={selectFileHandler}
/>
);
};
const RenderRow: React.FC<RenderItemProps> = ({ item, index, dirId }) => {
const { selectedRowIndex, setSelectedRowIndex } = useExplorerStore();
const isActive = selectedRowIndex === index;
let [_, setSearchParams] = useSearchParams();
function selectFileHandler() {
if (selectedRowIndex == index) setSelectedRowIndex(-1);
else setSelectedRowIndex(index);
}
return useMemo(
@ -163,14 +214,14 @@ const RenderRow: React.FC<{
<div
onClick={selectFileHandler}
onDoubleClick={() => {
if (row.is_dir) {
setSearchParams({ path: row.materialized_path });
if (item.is_dir) {
setSearchParams({ path: item.materialized_path });
}
}}
className={clsx(
'table-body-row mr-2 flex flex-row rounded-lg border-2',
isActive ? 'border-primary-500' : 'border-transparent',
rowIndex % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
)}
>
{columns.map((col) => (
@ -179,12 +230,12 @@ const RenderRow: React.FC<{
className="flex items-center px-4 py-2 pr-2 table-body-cell"
style={{ width: col.width }}
>
<RenderCell file={row} dirId={dirId} colKey={col?.key} />
<RenderCell file={item} dirId={dirId} colKey={col?.key} />
</div>
))}
</div>
),
[row.id, isActive]
[item.id, isActive]
);
};
@ -193,29 +244,22 @@ const RenderCell: React.FC<{
dirId?: number;
file?: FilePath;
}> = ({ colKey, file, dirId }) => {
const location = useContext(LocationContext);
if (!file || !colKey || !dirId) return <></>;
const row = file;
if (!row) return <></>;
const appProps = useContext(AppPropsContext);
const value = row[colKey];
if (!value) return <></>;
const location = useContext(LocationContext);
const { newThumbnails } = useExplorerStore();
const hasNewThumbnail = !!newThumbnails[row?.file?.cas_id ?? ''];
switch (colKey) {
case 'name':
return (
<div className="flex flex-row items-center overflow-hidden">
<div className="flex items-center justify-center w-6 h-6 mr-3 shrink-0">
<FileThumb
hasThumbnailOverride={hasNewThumbnail}
file={row}
locationId={location.location_id}
/>
<FileThumb file={row} locationId={location.location_id} />
</div>
{/* {colKey == 'name' &&
(() => {

View file

@ -1,4 +1,4 @@
import { useBridgeQuery } from '@sd/client';
import { useBridgeQuery, useExplorerStore } from '@sd/client';
import { AppPropsContext } from '@sd/client';
import { FilePath } from '@sd/core';
import clsx from 'clsx';
@ -10,22 +10,23 @@ import { Folder } from '../icons/Folder';
export default function FileThumb(props: {
file: FilePath;
locationId: number;
hasThumbnailOverride: boolean;
className?: string;
}) {
const appProps = useContext(AppPropsContext);
const { data: client } = useBridgeQuery('NodeGetState');
const { newThumbnails } = useExplorerStore();
const hasNewThumbnail = !!newThumbnails[props?.file?.file?.cas_id ?? ''];
if (props.file.is_dir) {
return <Folder size={100} />;
}
if (client?.data_path && (props.file.file?.has_thumbnail || props.hasThumbnailOverride)) {
if (appProps?.data_path && (props.file.file?.has_thumbnail || hasNewThumbnail)) {
return (
<img
className="pointer-events-none z-90"
className={clsx('pointer-events-none z-90', props.className)}
src={appProps?.convertFileSrc(
`${client.data_path}/thumbnails/${props.locationId}/${props.file.file?.cas_id}.webp`
`${appProps.data_path}/thumbnails/${props.locationId}/${props.file.file?.cas_id}.webp`
)}
/>
);

View file

@ -1,11 +1,11 @@
import { Transition } from '@headlessui/react';
import { ShareIcon } from '@heroicons/react/solid';
import { useInspectorStore } from '@sd/client';
import { useInspectorStore, useLibraryCommand } from '@sd/client';
import { FilePath, LocationResource } from '@sd/core';
import { Button, TextArea } from '@sd/ui';
import moment from 'moment';
import { Heart, Link } from 'phosphor-react';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { default as types } from '../../constants/file-types.json';
import FileThumb from './FileThumb';
@ -44,6 +44,26 @@ export const Inspector = (props: {
// when quickly navigating files, which cancels update function
const { notes, setNote, unCacheNote } = useInspectorStore();
const [favorite, setFavorite] = useState(false);
const { mutate: fileToggleFavorite, isLoading: isFavoriteLoading } = useLibraryCommand(
'FileSetFavorite',
{
onError: () => setFavorite(!!props.selectedFile?.file?.favorite)
}
);
const toggleFavorite = () => {
if (!isFavoriteLoading) {
fileToggleFavorite({ id: file_id, favorite: !favorite });
setFavorite(!favorite);
}
};
useEffect(() => {
setFavorite(!!props.selectedFile?.file?.favorite);
}, [props.selectedFile]);
// show cached note over server note, important to check for undefined not falsey
const note =
notes[file_id] === undefined ? props.selectedFile?.file?.note || null : notes[file_id];
@ -63,95 +83,80 @@ export const Inspector = (props: {
}, [note]);
return (
// <Transition
// as={React.Fragment}
// show={true}
// enter="transition-translate ease-in-out duration-200"
// enterFrom="translate-x-64"
// enterTo="translate-x-0"
// leave="transition-translate ease-in-out duration-200"
// leaveFrom="translate-x-0"
// leaveTo="translate-x-64"
// >
<div className="flex p-2 pr-1 mr-1 pb-[51px] w-[330px] flex-wrap overflow-x-hidden custom-scroll inspector-scroll">
<div className="p-2 pr-1 w-[330px] overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
{!!file_path && (
<div className="flex flex-col w-full pb-2 overflow-hidden bg-white rounded-lg select-text dark:bg-gray-600 bg-opacity-70">
<div className="flex items-center justify-center w-full h-64 overflow-hidden rounded-t-lg bg-gray-50 dark:bg-gray-900">
<div>
<div className="flex items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-900">
<FileThumb
hasThumbnailOverride={false}
className="!m-0 flex flex-shrink flex-grow-0"
file={file_path}
locationId={props.locationId}
/>
</div>
<h3 className="pt-3 pl-3 text-base font-bold">{file_path?.name}</h3>
<div className="flex flex-row m-3 space-x-2">
<Button size="sm" noPadding>
<Heart className="w-[18px] h-[18px]" />
</Button>
<Button size="sm" noPadding>
<ShareIcon className="w-[18px] h-[18px]" />
</Button>
<Button size="sm" noPadding>
<Link className="w-[18px] h-[18px]" />
</Button>
</div>
{file_path?.file?.cas_id && (
<MetaItem title="Unique Content ID" value={file_path.file.cas_id as string} />
)}
<Divider />
<MetaItem title="URI" value={full_path} />
<Divider />
<MetaItem
title="Date Created"
value={moment(file_path?.date_created).format('MMMM Do YYYY, h:mm:ss a')}
/>
<Divider />
<MetaItem
title="Date Indexed"
value={moment(file_path?.date_indexed).format('MMMM Do YYYY, h:mm:ss a')}
/>
{!file_path?.is_dir && (
<>
<Divider />
<div className="flex flex-row items-center px-3 py-2 meta-item">
{file_path?.extension && (
<span className="inline px-1 mr-1 text-xs font-bold uppercase bg-gray-500 rounded-md text-gray-150">
{file_path?.extension}
</span>
<div className="flex flex-col w-full pb-2 overflow-hidden bg-white rounded-lg select-text dark:bg-gray-550 dark:bg-opacity-40">
<h3 className="pt-3 pl-3 text-base font-bold">{file_path?.name}</h3>
<div className="flex flex-row m-3 space-x-2">
<Button onClick={toggleFavorite} size="sm" noPadding>
<Heart weight={favorite ? 'fill' : 'regular'} className="w-[18px] h-[18px]" />
</Button>
<Button size="sm" noPadding>
<ShareIcon className="w-[18px] h-[18px]" />
</Button>
<Button size="sm" noPadding>
<Link className="w-[18px] h-[18px]" />
</Button>
</div>
{file_path?.file?.cas_id && (
<MetaItem title="Unique Content ID" value={file_path.file.cas_id as string} />
)}
<Divider />
<MetaItem title="URI" value={full_path} />
<Divider />
<MetaItem
title="Date Created"
value={moment(file_path?.date_created).format('MMMM Do YYYY, h:mm:ss a')}
/>
<Divider />
<MetaItem
title="Date Indexed"
value={moment(file_path?.date_indexed).format('MMMM Do YYYY, h:mm:ss a')}
/>
{!file_path?.is_dir && (
<>
<Divider />
<div className="flex flex-row items-center px-3 py-2 meta-item">
{file_path?.extension && (
<span className="inline px-1 mr-1 text-xs font-bold uppercase bg-gray-500 rounded-md text-gray-150">
{file_path?.extension}
</span>
)}
<p className="text-xs text-gray-600 break-all truncate dark:text-gray-300">
{file_path?.extension
? //@ts-ignore
types[file_path.extension.toUpperCase()]?.descriptions.join(' / ')
: 'Unknown'}
</p>
</div>
{file_path.file && (
<>
<Divider />
<MetaItem
title="Note"
value={
<TextArea
className="mt-2 text-xs leading-snug !py-2"
value={note || ''}
onChange={handleNoteUpdate}
/>
}
/>
</>
)}
<p className="text-xs text-gray-600 break-all truncate dark:text-gray-300">
{file_path?.extension
? //@ts-ignore
types[file_path.extension.toUpperCase()]?.descriptions.join(' / ')
: 'Unknown'}
</p>
</div>
{file_path.file && (
<>
<Divider />
<MetaItem
title="Note"
value={
<TextArea
className="mt-2 text-xs leading-snug !py-2"
value={note || ''}
onChange={handleNoteUpdate}
/>
}
/>
</>
)}
</>
)}
{/* <div className="flex flex-row m-3">
<Button size="sm">Mint</Button>
</div> */}
{/* <MetaItem title="Date Last Modified" value={file?.date_modified} />
<MetaItem title="Date Indexed" value={file?.date_indexed} /> */}
</>
)}
</div>
</div>
)}
</div>
// </Transition>
);
};

View file

@ -86,13 +86,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
let locations = Array.isArray(locationsResponse) ? locationsResponse : [];
// initialize libraries
const { init: initLibraries, switchLibrary: _switchLibrary } = useLibraryStore();
const switchLibrary = (uuid: string) => {
navigate('overview');
_switchLibrary(uuid);
};
const { init: initLibraries, switchLibrary } = useLibraryStore();
const { currentLibrary, libraries, currentLibraryUuid } = useCurrentLibrary();
@ -163,7 +157,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
{
name: 'Library Settings',
icon: CogIcon,
onPress: () => navigate('library-settings/general')
onPress: () => navigate('settings/library')
},
{
name: 'Add Library',
@ -192,7 +186,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
</SidebarLink>
<SidebarLink to="content">
<Icon component={CirclesFour} />
Content
Spaces
</SidebarLink>
<SidebarLink to="photos">
<Icon component={PhotographIcon} />

View file

@ -1,6 +1,5 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline';
import { useLibraryCommand } from '@sd/client';
import { useExplorerStore } from '@sd/client';
import { AppPropsContext, useExplorerStore, useLibraryCommand } from '@sd/client';
import { Dropdown } from '@sd/ui';
import clsx from 'clsx';
import {
@ -10,12 +9,13 @@ import {
IconProps,
Key,
List,
Rows,
SquaresFour,
Tag,
TerminalWindow
} from 'phosphor-react';
import React, { DetailedHTMLProps, HTMLAttributes, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { AppPropsContext } from '../../AppPropsContext';
import { Shortcut } from '../primitive/Shortcut';
import { DefaultProps } from '../primitive/types';
@ -36,12 +36,12 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
<button
{...props}
className={clsx(
'mr-[1px] py-0.5 px-0.5 text-md font-medium hover:bg-gray-150 dark:transparent dark:hover:bg-gray-550 dark:active:bg-gray-500 rounded-md transition-colors duration-100',
'mr-[1px] py-0.5 px-0.5 text-md font-medium hover:bg-gray-150 dark:transparent dark:hover:bg-gray-550 rounded-md transition-colors duration-100',
{
'rounded-r-none rounded-l-none': props.group && !props.left && !props.right,
'rounded-r-none': props.group && props.left,
'rounded-l-none': props.group && props.right,
'dark:bg-gray-450 dark:hover:bg-gray-450 dark:active:bg-gray-450': props.active
'dark:bg-gray-550': props.active
},
props.className
)}
@ -51,25 +51,30 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
);
};
const SearchBar: React.FC<SearchBarProps> = (props) => { //TODO: maybe pass the appProps, so we can have the context in the TopBar if needed again
const SearchBar: React.FC<SearchBarProps> = (props) => {
//TODO: maybe pass the appProps, so we can have the context in the TopBar if needed again
const appProps = useContext(AppPropsContext);
return (
return (
<div className="relative flex h-7">
<input
placeholder="Search"
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-500 bg-[#F6F2F6] border border-gray-50 dark:bg-gray-650 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-450 bg-[#F6F2F6] border border-gray-50 shadow-md dark:bg-gray-600 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-550 dark:focus:bg-gray-800 transition-all"
/>
<div className="space-x-1 absolute top-[2px] right-1">
<Shortcut chars={ appProps?.platform === "macOS" || appProps?.platform === "browser" ? "⌘K" : "CTRL+K" } />
<Shortcut
chars={
appProps?.platform === 'macOS' || appProps?.platform === 'browser' ? '⌘K' : 'CTRL+K'
}
/>
{/* <Shortcut chars="S" /> */}
</div>
</div>
);
}
};
export const TopBar: React.FC<TopBarProps> = (props) => {
const { locationId } = useExplorerStore();
const { locationId, layoutMode, setLayoutMode } = useExplorerStore();
const { mutate: generateThumbsForLocation } = useLibraryCommand('GenerateThumbsForLocation', {
onMutate: (data) => {
console.log('GenerateThumbsForLocation', data);
@ -90,27 +95,40 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
<>
<div
data-tauri-drag-region
className="flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center border-b dark:bg-gray-600 border-gray-100 dark:border-gray-800 !bg-opacity-60 backdrop-blur"
className="flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center border-b dark:bg-gray-600 border-gray-100 dark:border-gray-800 !bg-opacity-90 backdrop-blur"
>
<div className="flex">
<div className="flex ">
<TopBarButton icon={ChevronLeftIcon} onClick={() => navigate(-1)} />
<TopBarButton icon={ChevronRightIcon} onClick={() => navigate(1)} />
</div>
{/* <div className="flex mx-8 space-x-[1px]">
<TopBarButton active group left icon={List} />
<TopBarButton group icon={Columns} />
<TopBarButton group right icon={SquaresFour} />
</div> */}
<div data-tauri-drag-region className="flex flex-row justify-center flex-grow ">
<div className="flex mx-8 space-x-2 pointer-events-auto">
<TopBarButton icon={Tag} />
<TopBarButton icon={FolderPlus} />
<TopBarButton icon={TerminalWindow} />
<div data-tauri-drag-region className="flex flex-row justify-center flex-grow">
<div className="flex mx-8">
<TopBarButton
group
left
active={layoutMode === 'list'}
icon={Rows}
onClick={() => setLayoutMode('list')}
/>
<TopBarButton
group
right
active={layoutMode === 'grid'}
icon={SquaresFour}
onClick={() => setLayoutMode('grid')}
/>
</div>
<SearchBar />
<div className="flex mx-8 space-x-2">
<TopBarButton icon={Key} />
<TopBarButton icon={Cloud} />
{/* <TopBarButton icon={Cloud} /> */}
<TopBarButton
icon={ArrowsClockwise}
onClick={() => {
@ -119,11 +137,6 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
/>
</div>
</div>
{/* <img
alt="spacedrive-logo"
src="/images/spacedrive_logo.png"
className="w-8 h-8 mt-[1px] mr-2 pointer-events-none"
/> */}
<div className="flex mr-3 space-x-2">
<Dropdown
// className="absolute block h-6 w-44 top-2 right-4"
@ -145,11 +158,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
buttonComponent={<TopBarButton icon={List} />}
/>
</div>
{/*<TopBarButton onClick={() => {*/}
{/* setSettingsOpen(!settingsOpen);*/}
{/*}} className="mr-[8px]" icon={CogIcon} />*/}
</div>
{/* <div className="h-[1px] flex-shrink-0 max-w bg-gray-200 dark:bg-gray-700" /> */}
</>
);
};

View file

@ -16,7 +16,7 @@ interface LocationListItemProps {
export default function LocationListItem({ location }: LocationListItemProps) {
const [hide, setHide] = useState(false);
const { mutate: locRescan } = useLibraryCommand('LocRescan');
const { mutate: locRescan } = useLibraryCommand('LocFullRescan');
const { mutate: deleteLoc, isLoading: locDeletePending } = useLibraryCommand('LocDelete', {
onSuccess: () => {
@ -28,7 +28,6 @@ export default function LocationListItem({ location }: LocationListItemProps) {
return (
<div className="flex w-full px-4 py-2 border border-gray-500 rounded-lg bg-gray-550">
<DotsVerticalIcon className="w-5 h-5 mt-3 mr-1 -ml-3 cursor-move drag-handle opacity-10" />
<Folder size={30} className="mr-3" />
<div className="flex flex-col">
<h1 className="pt-0.5 text-sm font-semibold">{location.name}</h1>

View file

@ -14,7 +14,7 @@ export const SettingsHeading: React.FC<{ className?: string; children: string }>
children,
className
}) => (
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-300', className)}>
<div className={clsx('mt-5 mb-1 ml-1 text-xs font-semibold text-gray-400', className)}>
{children}
</div>
);

View file

@ -1,14 +1,20 @@
import { LockClosedIcon } from '@heroicons/react/solid';
import { Input } from '@sd/ui';
import React from 'react';
export const ContentScreen: React.FC<{}> = (props) => {
const [address, setAddress] = React.useState('');
return (
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll">
<div className="flex flex-col space-y-5 pb-7">
<p className="px-5 py-3 mb-3 text-sm text-gray-400 rounded-md bg-gray-50 dark:text-gray-400 dark:bg-gray-600">
<b>Note: </b>This is a pre-alpha build of Spacedrive, many features are yet to be
functional.
</p>
</div>
{/* <div className="relative flex flex-col space-y-5 pb-7">
<LockClosedIcon className="absolute w-4 h-4 ml-3 text-gray-250 top-[30px]" />
<Input
className="pl-9"
placeholder="0f2z49zA"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
</div> */}
</div>
);
};

View file

@ -29,7 +29,7 @@ export const ExplorerScreen: React.FC<{}> = () => {
);
return (
<div className="relative flex flex-col w-full">
<div className="relative flex flex-col w-full bg-gray-600">
<TopBar />
<div className="relative flex flex-row w-full max-h-full">
<FileList location_id={location_id} path={path} limit={limit} />

View file

@ -217,40 +217,9 @@ export const OverviewScreen = () => {
</div>
</div>
<div className="flex flex-col pb-4 mt-4 space-y-4">
<Device
name={`James' MacBook Pro`}
size="1TB"
locations={[
{ name: 'Documents', folder: true },
{ name: 'Movies', folder: true },
{ name: 'Downloads', folder: true },
{ name: 'Minecraft', folder: true },
{ name: 'Projects', folder: true },
{ name: 'Notes', folder: true }
]}
type="desktop"
/>
<Device
name={`James' iPhone 12`}
size="47.7GB"
locations={[
{ name: 'Camera Roll', folder: true },
{ name: 'Notes', folder: true },
{ name: 'App.tsx', format: 'tsx', icon: 'reactts' },
{ name: 'vite.config.js', format: 'js', icon: 'vite' }
]}
type="phone"
/>
<Device
name={`Spacedrive Server`}
size="5GB"
locations={[
{ name: 'Cached', folder: true },
{ name: 'Photos', folder: true },
{ name: 'Documents', folder: true }
]}
type="server"
/>
<Device name={`James' MacBook Pro`} size="1TB" locations={[]} type="desktop" />
<Device name={`James' iPhone 12`} size="47.7GB" locations={[]} type="phone" />
<Device name={`Spacedrive Server`} size="5GB" locations={[]} type="server" />
</div>
<div className="px-5 py-3 text-sm text-gray-400 rounded-md bg-gray-50 dark:text-gray-400 dark:bg-gray-600">
<b>Note: </b>This is a pre-alpha build of Spacedrive, many features are yet to be

View file

@ -1,42 +0,0 @@
import { CogIcon, DatabaseIcon, KeyIcon, TagIcon } from '@heroicons/react/outline';
import { HardDrive, ShareNetwork } from 'phosphor-react';
import React from 'react';
import { SidebarLink } from '../../components/file/Sidebar';
import {
SettingsHeading,
SettingsIcon,
SettingsScreenContainer
} from '../../components/settings/SettingsScreenContainer';
export const CurrentLibrarySettings: React.FC = () => {
return (
<SettingsScreenContainer>
<SettingsHeading className="!mt-0">Library Settings</SettingsHeading>
<SidebarLink to="/library-settings/general">
<SettingsIcon component={CogIcon} />
General
</SidebarLink>
<SidebarLink to="/library-settings/locations">
<SettingsIcon component={HardDrive} />
Locations
</SidebarLink>
<SidebarLink to="/library-settings/tags">
<SettingsIcon component={TagIcon} />
Tags
</SidebarLink>
<SidebarLink to="/library-settings/keys">
<SettingsIcon component={KeyIcon} />
Keys
</SidebarLink>
<SidebarLink to="/library-settings/backups">
<SettingsIcon component={DatabaseIcon} />
Backups
</SidebarLink>
<SidebarLink to="/library-settings/backups">
<SettingsIcon component={ShareNetwork} />
Sync
</SidebarLink>
</SettingsScreenContainer>
);
};

View file

@ -1,30 +0,0 @@
import { useLibraryQuery } from '@sd/client';
import React from 'react';
import LocationListItem from '../../components/location/LocationListItem';
import { InputContainer } from '../../components/primitive/InputContainer';
import { SettingsContainer } from '../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../components/settings/SettingsHeader';
// const exampleLocations = [
// { option: 'Macintosh HD', key: 'macintosh_hd' },
// { option: 'LaCie External', key: 'lacie_external' },
// { option: 'Seagate 8TB', key: 'seagate_8tb' }
// ];
export default function LocationSettings() {
const { data: locations } = useLibraryQuery('SysGetLocations');
return (
<SettingsContainer>
{/*<Button size="sm">Add Location</Button>*/}
<SettingsHeader title="Locations" description="Manage your storage locations." />
<div className="grid space-y-2">
{locations?.map((location) => (
<LocationListItem key={location.id} location={location} />
))}
</div>
</SettingsContainer>
);
}

View file

@ -1,11 +1,28 @@
import {
CogIcon,
CollectionIcon,
DatabaseIcon,
GlobeAltIcon,
HeartIcon,
InformationCircleIcon,
KeyIcon,
LibraryIcon,
LightBulbIcon,
TagIcon,
TerminalIcon
} from '@heroicons/react/outline';
import { HardDrive, PaintBrush, ShareNetwork } from 'phosphor-react';
import {
BookOpen,
Cloud,
HardDrive,
Hash,
Info,
KeyReturn,
PaintBrush,
PuzzlePiece,
ShareNetwork,
UsersFour
} from 'phosphor-react';
import React from 'react';
import { SidebarLink } from '../../components/file/Sidebar';
@ -23,61 +40,75 @@ export const SettingsScreen: React.FC = () => {
<SettingsIcon component={CogIcon} />
General
</SidebarLink>
<SidebarLink to="/settings/libraries">
<SettingsIcon component={CollectionIcon} />
Libraries
</SidebarLink>
<SidebarLink to="/settings/appearance">
<SettingsIcon component={PaintBrush} />
Appearance
</SidebarLink>
<SidebarLink to="/settings/keybinds">
<SettingsIcon component={KeyReturn} />
Keybinds
</SidebarLink>
<SidebarLink to="/settings/extensions">
<SettingsIcon component={PuzzlePiece} />
Extensions
</SidebarLink>
<SettingsHeading>Node</SettingsHeading>
<SettingsHeading>Library</SettingsHeading>
<SidebarLink to="/settings/library">
<SettingsIcon component={CogIcon} />
General
</SidebarLink>
<SidebarLink to="/settings/nodes">
<SettingsIcon component={GlobeAltIcon} />
<SettingsIcon component={ShareNetwork} />
Nodes
</SidebarLink>
<SidebarLink to="/settings/locations">
<SettingsIcon component={HardDrive} />
Locations
</SidebarLink>
<SidebarLink to="/settings/tags">
<SettingsIcon component={TagIcon} />
Tags
</SidebarLink>
<SidebarLink to="/settings/keys">
<SettingsIcon component={KeyIcon} />
Keys
</SidebarLink>
{/* <SidebarLink to="/settings/backups">
<SettingsIcon component={DatabaseIcon} />
Backups
</SidebarLink>
<SidebarLink to="/settings/backups">
<SettingsIcon component={ShareNetwork} />
Sync
</SidebarLink> */}
<SettingsHeading>Advanced</SettingsHeading>
<SidebarLink to="/settings/p2p">
<SettingsIcon component={ShareNetwork} />
P2P
Networking
</SidebarLink>
<SidebarLink to="/settings/library">
<SettingsIcon component={CollectionIcon} />
Libraries
</SidebarLink>
<SidebarLink to="/settings/security">
<SettingsIcon component={KeyIcon} />
Security
</SidebarLink>
<SettingsHeading>Developer</SettingsHeading>
<SidebarLink to="/settings/experimental">
<SettingsIcon component={TerminalIcon} />
Experimental
Developer
</SidebarLink>
{/* <SettingsHeading>Library</SettingsHeading>
<SidebarLink to="/settings/library">
<SettingsIcon component={CollectionIcon} />
My Libraries
</SidebarLink>
<SidebarLink to="/settings/locations">
<SettingsIcon component={HardDrive} />
Locations
</SidebarLink>
<SidebarLink to="/settings/keys">
<SettingsIcon component={KeyIcon} />
Keys
</SidebarLink>
<SidebarLink to="/settings/tags">
<SettingsIcon component={TagIcon} />
Tags
</SidebarLink> */}
{/* <SettingsHeading>Cloud</SettingsHeading>
<SidebarLink to="/settings/sync">
<SettingsIcon component={CloudIcon} />
Sync
</SidebarLink>
<SidebarLink to="/settings/contacts">
<SettingsIcon component={UsersIcon} />
Contacts
</SidebarLink> */}
<SettingsHeading>Resources</SettingsHeading>
<SidebarLink to="/settings/about">
<SettingsIcon component={BookOpen} />
About
</SidebarLink>
<SidebarLink to="/settings/changelog">
<SettingsIcon component={LightBulbIcon} />
Changelog
</SidebarLink>
<SidebarLink to="/settings/support">
<SettingsIcon component={HeartIcon} />
Support
</SidebarLink>
</SettingsScreenContainer>
);
};

View file

@ -0,0 +1,83 @@
import { SearchIcon } from '@heroicons/react/solid';
import { Button, Input } from '@sd/ui';
import React from 'react';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
// extensions should cache their logos in the app data folder
interface ExtensionItemData {
name: string;
uuid: string;
platforms: ['windows' | 'macOS' | 'linux'];
installed: boolean;
description: string;
logoUri: string;
}
const extensions: ExtensionItemData[] = [
{
name: 'Apple Photos',
uuid: 'com.apple.photos',
installed: true,
platforms: ['macOS'],
description: 'Import photos and videos with metadata from Apple Photos.',
logoUri: 'https://apple.com/apple-logo.png'
},
{
name: 'Twitch VOD Archiver',
uuid: 'com.apple.photos',
installed: false,
platforms: ['macOS'],
description: 'Apple Photos is a photo management application for Mac.',
logoUri: 'https://apple.com/apple-logo.png'
},
{
name: 'Shared Clipboard',
uuid: 'com.apple.photos',
installed: false,
platforms: ['macOS'],
description: 'Apple Photos is a photo management application for Mac.',
logoUri: 'https://apple.com/apple-logo.png'
}
];
function ExtensionItem(props: { extension: ExtensionItemData }) {
const { installed, name, description } = props.extension;
return (
<div className="flex flex-col w-[290px] px-4 py-4 bg-gray-600 border border-gray-500 rounded">
<h3 className="m-0 text-sm font-bold">{name}</h3>
<p className="mt-1 mb-1 text-xs text-gray-300 ">{description}</p>
<Button size="sm" className="mt-2" variant={installed ? 'gray' : 'primary'}>
{installed ? 'Installed' : 'Install'}
</Button>
</div>
);
}
export default function ExtensionSettings() {
// const { data: volumes } = useBridgeQuery('SysGetVolumes');
return (
<SettingsContainer>
<SettingsHeader
title="Extensions"
description="Install extensions to extend the functionality of this client."
rightArea={
<div className="relative mt-6">
<SearchIcon className="absolute w-[18px] h-auto top-[8px] left-[11px] text-gray-350" />
<Input className="w-56 !p-0.5 !pl-9" placeholder="Search extensions" />
</div>
}
/>
<div className="flex flex-wrap gap-3">
{extensions.map((extension) => (
<ExtensionItem key={extension.uuid} extension={extension} />
))}
</div>
</SettingsContainer>
);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function AppearanceSettings() {
return (
<SettingsContainer>
<SettingsHeader title="Keybinds" description="Manage client keybinds" />
<InputContainer
mini
title="Sync with Library"
description="If enabled your keybinds will be synced with library, otherwise they will apply only to this client."
>
<Toggle value />
</InputContainer>
</SettingsContainer>
);
}

View file

@ -6,7 +6,7 @@ import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function NodesSettings() {
return (
<SettingsContainer>
<SettingsHeader title="Nodes" description="Manage the nodes in your Spacedrive network." />
<SettingsHeader title="Backups" description="Manage database backups." />
</SettingsContainer>
);
}

View file

@ -17,6 +17,8 @@ export default function LibraryGeneralSettings() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [encryptLibrary, setEncryptLibrary] = useState(false);
// prevent auto update when switching library
const [blockAutoUpdate, setBlockAutoUpdate] = useState(false);
const [nameDebounced] = useDebounce(name, 500);
const [descriptionDebounced] = useDebounce(description, 500);
@ -42,6 +44,18 @@ export default function LibraryGeneralSettings() {
}
}, [libraries]);
useEffect(() => {
if (currentLibrary) {
setBlockAutoUpdate(true);
setName(currentLibrary.config.name);
setDescription(currentLibrary.config.description);
}
}, [currentLibraryUuid]);
useEffect(() => {
if (blockAutoUpdate) setBlockAutoUpdate(false);
}, [blockAutoUpdate]);
return (
<SettingsContainer>
<SettingsHeader
@ -49,8 +63,8 @@ export default function LibraryGeneralSettings() {
description="General settings related to the currently active library."
/>
<div className="flex flex-row pb-3 space-x-5">
<div className="flex flex-col flex-grow ">
<span className="mt-2 mb-1 text-xs font-semibold text-gray-300">Name</span>
<div className="flex flex-col flex-grow">
<span className="mb-1 font-medium text-gray-700 dark:text-gray-100">Name</span>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
@ -58,7 +72,7 @@ export default function LibraryGeneralSettings() {
/>
</div>
<div className="flex flex-col flex-grow">
<span className="mt-2 mb-1 text-xs font-semibold text-gray-300">Description</span>
<span className="mb-1 font-medium text-gray-700 dark:text-gray-100">Description</span>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
@ -76,13 +90,21 @@ export default function LibraryGeneralSettings() {
<Toggle value={encryptLibrary} onChange={setEncryptLibrary} />
</div>
</InputContainer>
<InputContainer mini title="Export Library" description="Export this library to a file.">
<div className="mt-2">
<Button size="sm" variant="gray">
Export
</Button>
</div>
</InputContainer>
<InputContainer
mini
title="Delete Library"
description="This is permanent, your files will not be deleted, only the Spacedrive library."
>
<div className="mt-2">
<Button size="sm" variant="colored" className="bg-red-500 border-red-500">
Delete Library
Delete
</Button>
</div>
</InputContainer>

View file

@ -0,0 +1,15 @@
import React from 'react';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function NodesSettings() {
return (
<SettingsContainer>
<SettingsHeader
title="Nodes"
description="Manage the nodes connected to this library. A node is an instance of Spacedrive's backend, running on a device or server. Each node carries a copy of the database and synchronizes via peer-to-peer connections in realtime."
/>
</SettingsContainer>
);
}

View file

@ -4,6 +4,7 @@ import { useBridgeCommand, useBridgeQuery } from '@sd/client';
import { AppPropsContext } from '@sd/client';
import { LibraryConfig, LibraryConfigWrapped } from '@sd/core';
import { Button, Input } from '@sd/ui';
import { DotsSixVertical } from 'phosphor-react';
import React, { useContext, useState } from 'react';
import Card from '../../../components/layout/Card';
@ -26,6 +27,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped }) {
return (
<Card>
<DotsSixVertical weight="bold" className="mt-[15px] mr-3 opacity-30" />
<div className="flex-grow my-0.5">
<h3 className="font-semibold">{props.library.config.name}</h3>
<p className="mt-0.5 text-xs text-gray-200">{props.library.uuid}</p>

View file

@ -29,7 +29,7 @@ body {
width: 8px;
}
&::-webkit-scrollbar-track {
@apply bg-[#00000006] dark:bg-[#00000030] mt-[55px] rounded-[6px];
@apply bg-[#00000006] dark:bg-[#00000030] mt-[53px] rounded-[6px];
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-gray-300 dark:bg-gray-550;

View file

@ -283,6 +283,7 @@ importers:
'@types/react-router-dom': ^5.3.3
'@types/react-table': ^7.7.12
'@types/react-window': ^1.8.5
'@types/styled-components': ^5.1.25
'@types/tailwindcss': ^3.0.10
'@vitejs/plugin-react': ^1.3.2
autoprefixer: ^10.4.7
@ -314,6 +315,7 @@ importers:
react-transition-group: ^4.4.2
react-virtuoso: ^2.12.1
rooks: ^5.11.2
styled-components: ^5.3.5
tailwindcss: ^3.0.24
typescript: ^4.7.2
use-debounce: ^8.0.1
@ -333,6 +335,7 @@ importers:
'@sd/client': link:../client
'@sd/core': link:../../core
'@sd/ui': link:../ui
'@types/styled-components': 5.1.25
'@vitejs/plugin-react': 1.3.2
autoprefixer: 10.4.7
byte-size: 8.1.0
@ -361,6 +364,7 @@ importers:
react-transition-group: 4.4.2_ef5jwxihqo6n7gxfmzogljlgcm
react-virtuoso: 2.13.2_ef5jwxihqo6n7gxfmzogljlgcm
rooks: 5.11.2_ef5jwxihqo6n7gxfmzogljlgcm
styled-components: 5.3.5_ef5jwxihqo6n7gxfmzogljlgcm
tailwindcss: 3.1.3
use-debounce: 8.0.1_react@18.1.0
zustand: 4.0.0-rc.1_immer@9.0.15+react@18.1.0
@ -2058,6 +2062,24 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/traverse/7.18.2_supports-color@5.5.0:
resolution: {integrity: sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.16.7
'@babel/generator': 7.18.2
'@babel/helper-environment-visitor': 7.18.2
'@babel/helper-function-name': 7.17.9
'@babel/helper-hoist-variables': 7.16.7
'@babel/helper-split-export-declaration': 7.16.7
'@babel/parser': 7.18.4
'@babel/types': 7.18.4
debug: 4.3.4_supports-color@5.5.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: false
/@babel/types/7.13.0:
resolution: {integrity: sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==}
dependencies:
@ -2182,6 +2204,24 @@ packages:
engines: {node: '>=10.0.0'}
dev: true
/@emotion/is-prop-valid/1.1.3:
resolution: {integrity: sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA==}
dependencies:
'@emotion/memoize': 0.7.5
dev: false
/@emotion/memoize/0.7.5:
resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==}
dev: false
/@emotion/stylis/0.8.5:
resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==}
dev: false
/@emotion/unitless/0.7.5:
resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
dev: false
/@fiahfy/icns/0.0.7:
resolution: {integrity: sha512-0apAtbUXTU3Opy/Z4h69o53voBa+am8FmdZauyagUMskAVYN1a5yIRk48Sf+tEdBLlefbvqLWPJ4pxr/Y/QtTg==}
engines: {node: '>=8'}
@ -5193,6 +5233,13 @@ packages:
resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
dev: true
/@types/hoist-non-react-statics/3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
dependencies:
'@types/react': 18.0.9
hoist-non-react-statics: 3.3.2
dev: false
/@types/html-minifier-terser/5.1.2:
resolution: {integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==}
dev: true
@ -5412,6 +5459,14 @@ packages:
resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==}
dev: true
/@types/styled-components/5.1.25:
resolution: {integrity: sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==}
dependencies:
'@types/hoist-non-react-statics': 3.3.1
'@types/react': 18.0.9
csstype: 3.1.0
dev: false
/@types/tailwindcss/3.0.10:
resolution: {integrity: sha512-1UnZIHO0NOPyPlPFV0HuMjki2SHkvG9uBA1ZehWj/OQMSROk503nuNyyfmJSIT289yewxTbKoPG+KLxYRvfIIg==}
dev: true
@ -6455,6 +6510,23 @@ packages:
- supports-color
dev: true
/babel-plugin-styled-components/2.0.7_styled-components@5.3.5:
resolution: {integrity: sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==}
peerDependencies:
styled-components: '>= 2'
dependencies:
'@babel/helper-annotate-as-pure': 7.16.7
'@babel/helper-module-imports': 7.16.7
babel-plugin-syntax-jsx: 6.18.0
lodash: 4.17.21
picomatch: 2.3.1
styled-components: 5.3.5_ef5jwxihqo6n7gxfmzogljlgcm
dev: false
/babel-plugin-syntax-jsx/6.18.0:
resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
dev: false
/babel-runtime/6.26.0:
resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==}
dependencies:
@ -7059,6 +7131,10 @@ packages:
engines: {node: '>=10'}
dev: true
/camelize/1.0.0:
resolution: {integrity: sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg==}
dev: false
/caniuse-lite/1.0.30001352:
resolution: {integrity: sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==}
@ -7773,6 +7849,11 @@ packages:
util.promisify: 1.0.0
dev: true
/css-color-keywords/1.0.0:
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
engines: {node: '>=4'}
dev: false
/css-loader/3.6.0:
resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==}
engines: {node: '>= 8.9.0'}
@ -7861,6 +7942,14 @@ packages:
nth-check: 2.1.1
dev: true
/css-to-react-native/3.0.0:
resolution: {integrity: sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==}
dependencies:
camelize: 1.0.0
css-color-keywords: 1.0.0
postcss-value-parser: 4.2.0
dev: false
/css-what/6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@ -7946,6 +8035,19 @@ packages:
dependencies:
ms: 2.1.2
/debug/4.3.4_supports-color@5.5.0:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
supports-color: 5.5.0
dev: false
/decamelize/1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
@ -15030,6 +15132,10 @@ packages:
kind-of: 6.0.3
dev: true
/shallowequal/1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: false
/sharp/0.30.5:
resolution: {integrity: sha512-0T28KxqY4DzUMLSAp1/IhGVeHpPIQyp1xt7esmuXCAfyi/+6tYMUeRhQok+E/+E52Yk5yFjacXp90cQOkmkl4w==}
engines: {node: '>=12.13.0'}
@ -15605,6 +15711,29 @@ packages:
inline-style-parser: 0.1.1
dev: true
/styled-components/5.3.5_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==}
engines: {node: '>=10'}
requiresBuild: true
peerDependencies:
react: '>= 16.8.0'
react-dom: '>= 16.8.0'
react-is: '>= 16.8.0'
dependencies:
'@babel/helper-module-imports': 7.16.7
'@babel/traverse': 7.18.2_supports-color@5.5.0
'@emotion/is-prop-valid': 1.1.3
'@emotion/stylis': 0.8.5
'@emotion/unitless': 0.7.5
babel-plugin-styled-components: 2.0.7_styled-components@5.3.5
css-to-react-native: 3.0.0
hoist-non-react-statics: 3.3.2
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
shallowequal: 1.1.0
supports-color: 5.5.0
dev: false
/supports-color/2.0.0:
resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
engines: {node: '>=0.8.0'}