mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
ui returns
This commit is contained in:
parent
fd745a4d36
commit
c62d507855
94
apps/desktop/src/App.tsx
Normal file
94
apps/desktop/src/App.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { Route, BrowserRouter as Router, Switch, Redirect } from 'react-router-dom';
|
||||
import { Sidebar } from './components/file/Sidebar';
|
||||
import { TopBar } from './components/layout/TopBar';
|
||||
import { useInputState } from './hooks/useInputState';
|
||||
import { SettingsScreen } from './screens/Settings';
|
||||
import { ExplorerScreen } from './screens/Explorer';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { DebugGlobalStore } from './Debug';
|
||||
import { useGlobalEvents } from './hooks/useGlobalEvents';
|
||||
import { AppState, useAppState } from './store/global';
|
||||
import { Modal } from './components/layout/Modal';
|
||||
import { useKey, useKeyBindings } from 'rooks';
|
||||
// import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { Button } from './components/primitive';
|
||||
import { useLocationStore, Location } from './store/locations';
|
||||
import { OverviewScreen } from './screens/Overview';
|
||||
import { SpacesScreen } from './screens/Spaces';
|
||||
|
||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
role="alert"
|
||||
className="flex border border-gray-200 dark:border-gray-650 h-screen justify-center items-center flex-col rounded-lg w-screen bg-gray-50 dark:bg-gray-950 dark:text-white p-4"
|
||||
>
|
||||
<p className="text-sm m-3 text-gray-400 font-bold">APP CRASHED</p>
|
||||
<h1 className="text-2xl font-bold">We're past the event horizon...</h1>
|
||||
<pre className="m-2">Error: {error.message}</pre>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button variant="primary" className="mt-2" onClick={resetErrorBoundary}>
|
||||
Reload
|
||||
</Button>
|
||||
<Button className="mt-2" onClick={resetErrorBoundary}>
|
||||
Send report
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useGlobalEvents();
|
||||
useEffect(() => {
|
||||
invoke<AppState>('get_config').then((state) => useAppState.getState().update(state));
|
||||
invoke<Location[]>('get_mounts').then((locations) =>
|
||||
useLocationStore.getState().setLocations(locations)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// useHotkeys('command+q', () => {
|
||||
// process.exit();
|
||||
// });
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onReset={() => {
|
||||
// reset the state of your app so the error doesn't happen again
|
||||
}}
|
||||
>
|
||||
<Router>
|
||||
<div className="flex flex-col select-none h-screen rounded-xl border border-gray-200 dark:border-gray-550 bg-white text-gray-900 dark:text-white dark:bg-gray-800 overflow-hidden">
|
||||
<DebugGlobalStore />
|
||||
<TopBar />
|
||||
<div className="flex flex-row min-h-full">
|
||||
<Sidebar />
|
||||
<div className="relative w-full flex bg-gray-50 dark:bg-gray-800">
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/explorer" />
|
||||
</Route>
|
||||
<Route path="/overview">
|
||||
<OverviewScreen />
|
||||
</Route>
|
||||
<Route path="/spaces">
|
||||
<SpacesScreen />
|
||||
</Route>
|
||||
<Route path="/explorer">
|
||||
<ExplorerScreen />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<SettingsScreen />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Modal />
|
||||
</div>
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
9
apps/desktop/src/Debug.tsx
Normal file
9
apps/desktop/src/Debug.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useAppState } from './store/global';
|
||||
import { useExplorerStore } from './store/explorer';
|
||||
|
||||
export function DebugGlobalStore() {
|
||||
useAppState();
|
||||
useExplorerStore();
|
||||
return <></>;
|
||||
}
|
1
apps/desktop/src/assets/spline/scene.json
Normal file
1
apps/desktop/src/assets/spline/scene.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/desktop/src/assets/spline/windows.json
Normal file
1
apps/desktop/src/assets/spline/windows.json
Normal file
File diff suppressed because one or more lines are too long
224
apps/desktop/src/components/file/FileList.tsx
Normal file
224
apps/desktop/src/components/file/FileList.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
import { DotsVerticalIcon } from '@heroicons/react/solid';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import clsx from 'clsx';
|
||||
import byteSize from 'pretty-bytes';
|
||||
import React, { forwardRef, useEffect, useMemo, useRef } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { useKey, useWindowSize } from 'rooks';
|
||||
import { DirectoryResponse } from '../../screens/Explorer';
|
||||
// import { List, ListRowRenderer } from 'react-virtualized';
|
||||
import { useAppState } from '../../store/global';
|
||||
import {
|
||||
useCurrentDir,
|
||||
useExplorerStore,
|
||||
useFile,
|
||||
useSelectedFileIndex
|
||||
} from '../../store/explorer';
|
||||
import { IFile } from '../../types';
|
||||
|
||||
interface IColumn {
|
||||
column: string;
|
||||
key: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const PADDING_SIZE = 130;
|
||||
|
||||
// Function ensure no types are loss, but guarantees that they are Column[]
|
||||
function ensureIsColumns<T extends IColumn[]>(data: T) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const columns = ensureIsColumns([
|
||||
{ column: 'Name', key: 'name', width: 280 } as const,
|
||||
{ column: 'Size', key: 'size_in_bytes', width: 120 } as const,
|
||||
{ column: 'Type', key: 'extension', width: 100 } as const
|
||||
// { column: 'Checksum', key: 'meta_checksum', width: 120 } as const
|
||||
// { column: 'Tags', key: 'tags', width: 120 } as const
|
||||
]);
|
||||
|
||||
type ColumnKey = typeof columns[number]['key'];
|
||||
|
||||
export const FileList: React.FC<{}> = (props) => {
|
||||
const tableContainer = useRef<null | HTMLDivElement>(null);
|
||||
const VList = useRef<null | VirtuosoHandle>(null);
|
||||
const currentDir = useCurrentDir();
|
||||
|
||||
// useOnWindowResize((e) => {
|
||||
|
||||
// })
|
||||
|
||||
const size = useWindowSize();
|
||||
|
||||
const explorer = useExplorerStore.getState();
|
||||
|
||||
const seletedRowIndex = useSelectedFileIndex(currentDir?.id as number);
|
||||
useEffect(() => {
|
||||
// VList.current?.scrollIntoView()
|
||||
if (seletedRowIndex != null) VList.current?.scrollIntoView({ index: seletedRowIndex });
|
||||
}, [seletedRowIndex]);
|
||||
|
||||
useKey('ArrowUp', (e) => {
|
||||
e.preventDefault();
|
||||
if (explorer.selectedFile) {
|
||||
explorer.selectFile(explorer.currentDir as number, explorer.selectedFile.id, 'above');
|
||||
}
|
||||
});
|
||||
useKey('ArrowDown', (e) => {
|
||||
e.preventDefault();
|
||||
if (explorer.selectedFile)
|
||||
explorer.selectFile(explorer.currentDir as number, explorer.selectedFile.id, 'below');
|
||||
});
|
||||
|
||||
const Row = (index: number) => {
|
||||
const row = currentDir?.children?.[index] as IFile;
|
||||
|
||||
return <RenderRow key={index} row={row} rowIndex={index} dirId={currentDir?.id as number} />;
|
||||
};
|
||||
|
||||
const Header = () => (
|
||||
<div>
|
||||
<h1 className="p-2 mt-10 ml-1 font-bold text-xl">{currentDir?.name}</h1>
|
||||
<div className="table-head">
|
||||
<div className="table-head-row flex flex-row p-2">
|
||||
{columns.map((col) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="table-head-cell flex flex-row items-center relative group pl-2"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<DotsVerticalIcon className="hidden absolute group-hover:block drag-handle w-5 h-5 opacity-10 -ml-5 cursor-move" />
|
||||
<span className="text-sm text-gray-500 font-medium">{col.column}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div
|
||||
ref={tableContainer}
|
||||
style={{ marginTop: -44 }}
|
||||
className="table-container w-full h-full bg-white dark:bg-gray-900 p-3 cursor-default"
|
||||
>
|
||||
<Virtuoso
|
||||
data={currentDir?.children}
|
||||
ref={VList}
|
||||
// style={{ height: '400px' }}
|
||||
totalCount={currentDir?.children_count || 0}
|
||||
itemContent={Row}
|
||||
components={{ Header }}
|
||||
className="table-body pb-10 outline-none"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[size.innerWidth, currentDir?.id, tableContainer.current]
|
||||
);
|
||||
};
|
||||
|
||||
const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: number }> = ({
|
||||
row,
|
||||
rowIndex,
|
||||
dirId
|
||||
}) => {
|
||||
const { selectFile, clearSelectedFiles, ingestDir } = useExplorerStore.getState();
|
||||
const selectedFileIndex = useSelectedFileIndex(dirId);
|
||||
|
||||
const isActive = selectedFileIndex === rowIndex;
|
||||
// console.log('hello from row id', rowIndex);
|
||||
|
||||
function selectFileHandler() {
|
||||
if (selectedFileIndex == rowIndex) clearSelectedFiles();
|
||||
else selectFile(dirId, row.id, undefined, rowIndex);
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div
|
||||
onClick={selectFileHandler}
|
||||
onDoubleClick={() => {
|
||||
if (row.is_dir) {
|
||||
invoke<DirectoryResponse>('get_files', { path: row.uri }).then((res) => {
|
||||
ingestDir(res.directory, res.contents);
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={clsx('table-body-row flex flex-row rounded-lg border-2 border-[#00000000]', {
|
||||
'bg-[#00000006] dark:bg-[#00000030]': rowIndex % 2 == 0,
|
||||
'border-primary-500': isActive
|
||||
})}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="table-body-cell px-4 py-2 flex items-center pr-2"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<RenderCell fileId={row.id} dirId={dirId} colKey={col?.key} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
[row.id, isActive]
|
||||
);
|
||||
};
|
||||
|
||||
const RenderCell: React.FC<{ colKey?: ColumnKey; dirId?: number; fileId?: number }> = ({
|
||||
colKey,
|
||||
fileId,
|
||||
dirId
|
||||
}) => {
|
||||
if (!fileId || !colKey || !dirId) return <></>;
|
||||
const row = useFile(fileId);
|
||||
if (!row) return <></>;
|
||||
const value = row[colKey];
|
||||
if (!value) return <></>;
|
||||
|
||||
// const icon = `${useAppState.getState().file_type_thumb_dir}/lol.png`;
|
||||
|
||||
switch (colKey) {
|
||||
case 'name':
|
||||
return (
|
||||
<div className="flex flex-row items-center overflow-hidden">
|
||||
<div className="w-6 h-6 mr-2">
|
||||
<img
|
||||
src={convertFileSrc(
|
||||
`${useAppState.getState().file_type_thumb_dir}/${
|
||||
row.is_dir ? 'folder' : row.extension
|
||||
}.png`
|
||||
)}
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
</div>
|
||||
{/* {colKey == 'name' &&
|
||||
(() => {
|
||||
switch (row.extension.toLowerCase()) {
|
||||
case 'mov' || 'mp4':
|
||||
return <FilmIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
|
||||
|
||||
default:
|
||||
if (row.is_dir)
|
||||
return <FolderIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
|
||||
return <DocumentIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
|
||||
}
|
||||
})()} */}
|
||||
<span className="truncate text-xs">{row[colKey]}</span>
|
||||
</div>
|
||||
);
|
||||
case 'size_in_bytes':
|
||||
return <span className="text-xs text-left">{byteSize(Number(value || 0))}</span>;
|
||||
case 'extension':
|
||||
return <span className="text-xs text-left">{value.toLowerCase()}</span>;
|
||||
// case 'meta_checksum':
|
||||
// return <span className="truncate">{value}</span>;
|
||||
// case 'tags':
|
||||
// return renderCellWithIcon(MusicNoteIcon);
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
88
apps/desktop/src/components/file/Inspector.tsx
Normal file
88
apps/desktop/src/components/file/Inspector.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { useExplorerStore, useSelectedFile } from '../../store/explorer';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { IFile } from '../../types';
|
||||
import { useAppState } from '../../store/global';
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import moment from 'moment';
|
||||
import { Button } from '../primitive';
|
||||
import { ShareIcon } from '@heroicons/react/solid';
|
||||
import { Heart, Link } from 'phosphor-react';
|
||||
|
||||
interface MetaItemProps {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const MetaItem = (props: MetaItemProps) => {
|
||||
return (
|
||||
<div className="meta-item flex flex-col px-3 py-1">
|
||||
<h5 className="font-bold text-xs">{props.title}</h5>
|
||||
<p className="break-all text-xs text-gray-600 dark:text-gray-300 truncate">{props.value}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-100 dark:bg-gray-600" />;
|
||||
|
||||
export const Inspector = () => {
|
||||
const selectedFile = useSelectedFile();
|
||||
|
||||
const isOpen = !!selectedFile;
|
||||
|
||||
const file = selectedFile;
|
||||
|
||||
return (
|
||||
<Transition
|
||||
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="h-full w-60 right-0 top-0 m-2 border border-gray-100 dark:border-gray-850 rounded-lg ">
|
||||
{!!file && (
|
||||
<div className="flex flex-col overflow-hidden h-full rounded-lg bg-white dark:bg-gray-700 select-text">
|
||||
<div className="h-32 bg-gray-50 dark:bg-gray-900 rounded-t-lg w-full flex justify-center items-center">
|
||||
<img
|
||||
src={convertFileSrc(
|
||||
`${useAppState.getState().file_type_thumb_dir}/${
|
||||
file?.is_dir ? 'folder' : file?.extension
|
||||
}.png`
|
||||
)}
|
||||
className="h-24"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-bold p-3 text-base">{file?.name}</h3>
|
||||
<div className="flex flex-row m-3 space-x-2">
|
||||
<Button size="sm">
|
||||
<Heart className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<ShareIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<Link className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<MetaItem title="Checksum" value={file?.meta_checksum as string} />
|
||||
<Divider />
|
||||
<MetaItem title="Uri" value={file?.uri as string} />
|
||||
<Divider />
|
||||
<MetaItem
|
||||
title="Date Created"
|
||||
value={moment(file?.date_created).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
/>
|
||||
{/* <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>
|
||||
</Transition>
|
||||
);
|
||||
};
|
125
apps/desktop/src/components/file/Sidebar.tsx
Normal file
125
apps/desktop/src/components/file/Sidebar.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
import {
|
||||
BookOpenIcon,
|
||||
CogIcon,
|
||||
CollectionIcon,
|
||||
CubeTransparentIcon,
|
||||
DatabaseIcon,
|
||||
FolderIcon,
|
||||
LibraryIcon,
|
||||
PhotographIcon,
|
||||
PlusIcon,
|
||||
ServerIcon
|
||||
} from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
Book,
|
||||
Camera,
|
||||
Circle,
|
||||
CirclesFour,
|
||||
Eject,
|
||||
EjectSimple,
|
||||
Folder,
|
||||
HandGrabbing,
|
||||
HardDrive,
|
||||
HardDrives,
|
||||
MonitorPlay,
|
||||
Package,
|
||||
Planet,
|
||||
Plus
|
||||
} from 'phosphor-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||
import { useLocations } from '../../store/locations';
|
||||
import { Button } from '../primitive';
|
||||
import { Dropdown } from '../primitive/Dropdown';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
|
||||
interface SidebarProps extends DefaultProps {}
|
||||
|
||||
const SidebarLink = (props: NavLinkProps) => (
|
||||
<NavLink
|
||||
{...props}
|
||||
className={clsx(
|
||||
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 flex flex-row flex-grow items-center hover:bg-gray-100 dark:hover:bg-gray-600 text-sm',
|
||||
props.className
|
||||
)}
|
||||
activeClassName="!bg-primary !text-white hover:bg-primary dark:hover:bg-primary"
|
||||
>
|
||||
{props.children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
const Icon = ({ component: Icon, ...props }: any) => (
|
||||
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
|
||||
);
|
||||
|
||||
const Heading: React.FC<{}> = ({ children }) => (
|
||||
<div className="mt-5 mb-1 ml-1 text-xs font-semibold text-gray-300">{children}</div>
|
||||
);
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
const locations = useLocations();
|
||||
return (
|
||||
<div className="w-46 flex flex-col flex-wrap flex-shrink-0 min-h-full bg-gray-50 dark:bg-gray-650 !bg-opacity-60 border-gray-100 border-r dark:border-gray-600 px-3 space-y-0.5">
|
||||
<Dropdown
|
||||
buttonProps={{
|
||||
justifyLeft: true,
|
||||
className:
|
||||
'mb-1 rounded !bg-gray-50 border-gray-150 hover:!bg-gray-1000 flex-shrink-0 w-[175px] dark:!bg-gray-600 dark:hover:!bg-gray-550 dark:!border-gray-550 dark:hover:!border-gray-500',
|
||||
variant: 'gray'
|
||||
}}
|
||||
// buttonIcon={<Book weight="bold" className="w-4 h-4 mt-0.5 mr-1" />}
|
||||
buttonText="Jamie's Library"
|
||||
items={[
|
||||
[{ name: `Jamie's Library`, selected: true }, { name: 'Subto' }],
|
||||
[
|
||||
{ name: 'Library Settings', icon: CogIcon },
|
||||
{ name: 'Add Library', icon: PlusIcon }
|
||||
]
|
||||
]}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<SidebarLink to="/overview">
|
||||
<Icon component={Planet} />
|
||||
Overview
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/spaces">
|
||||
<Icon component={CirclesFour} />
|
||||
Spaces
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/explorer">
|
||||
<Icon component={Folder} />
|
||||
Explorer
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings">
|
||||
<Icon component={MonitorPlay} />
|
||||
Media
|
||||
</SidebarLink>
|
||||
</div>
|
||||
<div>
|
||||
<Heading>Locations</Heading>
|
||||
{locations.map((location, index) => {
|
||||
return (
|
||||
<div className="flex flex-row items-center">
|
||||
<SidebarLink className="relative group" key={index} to={`/explorer/${location.name}`}>
|
||||
<Icon component={ServerIcon} />
|
||||
{location.name}
|
||||
<div className="flex-grow" />
|
||||
{location.is_removable && (
|
||||
<Button
|
||||
noBorder
|
||||
size="sm"
|
||||
className="w-7 h-7 top-0 right-0 absolute !bg-transparent group-hover:bg-gray-600 dark:hover:!bg-gray-550 !transition-none items-center !rounded-l-none"
|
||||
>
|
||||
<Icon className="w-3 h-3 mr-0 " component={EjectSimple} />
|
||||
</Button>
|
||||
)}
|
||||
</SidebarLink>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
35
apps/desktop/src/components/layout/Modal.tsx
Normal file
35
apps/desktop/src/components/layout/Modal.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Transition } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
export interface ModalProps {}
|
||||
|
||||
const modalContext = createContext({ open: false });
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
onClick={() => setOpen(false)}
|
||||
className={clsx(
|
||||
'transition-opacity w-screen h-screen p-5 absolute t-0 bg-black bg-opacity-30 m-[1px] rounded-lg',
|
||||
{ 'pointer-events-none hidden': !open }
|
||||
)}
|
||||
>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition-translate ease-in-out duration-200"
|
||||
enterFrom="-scale-2"
|
||||
enterTo="scale-0"
|
||||
leave="transition-translate ease-in-out duration-200"
|
||||
leaveFrom="scale-0"
|
||||
leaveTo="-scale-2"
|
||||
>
|
||||
<div className="w-full h-full bg-white rounded-lg shadow-xl dark:bg-gray-850">
|
||||
<h1 className="m-10">hi</h1>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
103
apps/desktop/src/components/layout/TopBar.tsx
Normal file
103
apps/desktop/src/components/layout/TopBar.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon, CogIcon } from '@heroicons/react/outline';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ArrowsLeftRight,
|
||||
Cloud,
|
||||
Columns,
|
||||
FolderPlus,
|
||||
HouseSimple,
|
||||
Key,
|
||||
SquaresFour,
|
||||
Tag,
|
||||
TerminalWindow
|
||||
} from 'phosphor-react';
|
||||
import React from 'react';
|
||||
import { useExplorerStore } from '../../store/explorer';
|
||||
import { TrafficLights } from '../os/TrafficLights';
|
||||
import { Button, ButtonProps, Input } from '../primitive';
|
||||
import { Shortcut } from '../primitive/Shortcut';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { HeartIcon } from '@heroicons/react/solid';
|
||||
|
||||
export interface TopBarProps extends DefaultProps {}
|
||||
export interface TopBarButtonProps extends ButtonProps {
|
||||
icon: any;
|
||||
group?: boolean;
|
||||
active?: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
}
|
||||
|
||||
const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) => {
|
||||
return (
|
||||
<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-600 dark:active:bg-gray-500 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
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon weight={'regular'} className="m-0.5 w-5 h-5 text-gray-450 dark:text-gray-150" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
const [goBack] = useExplorerStore((state) => [state.goBack]);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-[2.95rem] -mt-0.5 max-w z-50 rounded-t-2xl flex-shrink-0 items-center border-b bg-gray-50 dark:bg-gray-650 border-gray-100 dark:border-gray-600 !bg-opacity-60 backdrop-blur shadow-sm"
|
||||
>
|
||||
<div className="mr-32 ml-1">
|
||||
<TrafficLights
|
||||
onClose={appWindow.close}
|
||||
onFullscreen={appWindow.maximize}
|
||||
onMinimize={appWindow.minimize}
|
||||
className="p-1.5"
|
||||
/>
|
||||
</div>
|
||||
<TopBarButton icon={ChevronLeftIcon} onClick={goBack} />
|
||||
<TopBarButton icon={ChevronRightIcon} />
|
||||
{/* <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 className="flex-grow"></div>
|
||||
<div className="flex mx-8 space-x-2">
|
||||
<TopBarButton icon={Tag} />
|
||||
<TopBarButton icon={FolderPlus} />
|
||||
<TopBarButton icon={TerminalWindow} />
|
||||
</div>
|
||||
<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-gray-50 border border-gray-250 dark:bg-gray-700 dark:border-gray-600 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
|
||||
/>
|
||||
<div className="space-x-1 absolute top-[2px] right-1">
|
||||
<Shortcut chars="⌘S" />
|
||||
{/* <Shortcut chars="S" /> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mx-8 space-x-2">
|
||||
<TopBarButton icon={Key} />
|
||||
<TopBarButton icon={Cloud} />
|
||||
<TopBarButton icon={ArrowsLeftRight} />
|
||||
</div>
|
||||
<div className="flex-grow"></div>
|
||||
<div className="flex-grow"></div>
|
||||
<TopBarButton className="mr-[8px]" icon={CogIcon} />
|
||||
</div>
|
||||
{/* <div className="h-[1px] flex-shrink-0 max-w bg-gray-200 dark:bg-gray-700" /> */}
|
||||
</>
|
||||
);
|
||||
};
|
37
apps/desktop/src/components/os/TrafficLights.tsx
Normal file
37
apps/desktop/src/components/os/TrafficLights.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
|
||||
export interface TrafficLightsProps extends DefaultProps {
|
||||
onClose?: () => void;
|
||||
onMinimize?: () => void;
|
||||
onFullscreen?: () => void;
|
||||
}
|
||||
|
||||
export const TrafficLights: React.FC<TrafficLightsProps> = (props) => {
|
||||
return (
|
||||
<div className={clsx('flex flex-row space-x-2 px-3', props.className)}>
|
||||
<Light mode="close" action={props.onClose} />
|
||||
<Light mode="minimize" action={props.onMinimize} />
|
||||
<Light mode="fullscreen" action={props.onFullscreen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LightProps {
|
||||
mode: 'close' | 'minimize' | 'fullscreen';
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
const Light: React.FC<LightProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
onClick={props.action}
|
||||
className={clsx('w-[12px] h-[12px] rounded-full', {
|
||||
'bg-red-400': props.mode == 'close',
|
||||
'bg-green-400': props.mode == 'fullscreen',
|
||||
'bg-yellow-400': props.mode == 'minimize'
|
||||
})}
|
||||
></div>
|
||||
);
|
||||
};
|
100
apps/desktop/src/components/primitive/Button.tsx
Normal file
100
apps/desktop/src/components/primitive/Button.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import React from 'react';
|
||||
import { ButtonHTMLAttributes, useState } from 'react';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const sizes = {
|
||||
default: 'py-1 px-3 text-md font-medium',
|
||||
sm: 'py-1 px-2 text-sm font-medium'
|
||||
};
|
||||
|
||||
const variants = {
|
||||
default: `
|
||||
bg-gray-50
|
||||
shadow-sm
|
||||
hover:bg-gray-100
|
||||
active:bg-gray-50
|
||||
dark:bg-gray-650
|
||||
dark:hover:bg-gray-650
|
||||
dark:active:bg-gray-700
|
||||
dark:active:opacity-80
|
||||
|
||||
border-gray-100
|
||||
hover:border-gray-200
|
||||
active:border-gray-200
|
||||
dark:border-gray-700
|
||||
dark:active:border-gray-600
|
||||
dark:hover:border-gray-600
|
||||
|
||||
text-gray-700
|
||||
hover:text-gray-900
|
||||
active:text-gray-600
|
||||
dark:text-gray-200
|
||||
dark:active:text-white
|
||||
dark:hover:text-white
|
||||
`,
|
||||
gray: `
|
||||
bg-gray-100
|
||||
shadow-sm
|
||||
hover:bg-gray-200
|
||||
active:bg-gray-100
|
||||
dark:bg-gray-800
|
||||
dark:hover:bg-gray-700
|
||||
dark:active:bg-gray-700
|
||||
dark:active:opacity-80
|
||||
|
||||
border-gray-200
|
||||
hover:border-gray-300
|
||||
active:border-gray-200
|
||||
dark:border-gray-700
|
||||
dark:active:border-gray-600
|
||||
dark:hover:border-gray-600
|
||||
|
||||
text-gray-700
|
||||
hover:text-gray-900
|
||||
active:text-gray-600
|
||||
dark:text-gray-200
|
||||
dark:active:text-white
|
||||
dark:hover:text-white
|
||||
|
||||
`,
|
||||
primary:
|
||||
'bg-primary text-white shadow-sm border-primary-600 dark:border-primary-400 active:bg-primary-600 active:border-primary-700 hover:bg-primary-400 hover:border-primary-500',
|
||||
selected: `bg-gray-100 dark:bg-gray-500
|
||||
text-black hover:text-black active:text-black dark:hover:text-white dark:text-white
|
||||
`
|
||||
};
|
||||
|
||||
export type ButtonVariant = keyof typeof variants;
|
||||
export type ButtonSize = keyof typeof sizes;
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
noBorder?: boolean;
|
||||
pressEffect?: boolean;
|
||||
justifyLeft?: boolean;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({ loading, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx(
|
||||
'flex border rounded-md transition-colors duration-100 cursor-default',
|
||||
{ 'opacity-5': loading, '!p-1': props.noPadding },
|
||||
{ 'justify-center': !props.justifyLeft },
|
||||
sizes[props.size || 'default'],
|
||||
variants[props.variant || 'default'],
|
||||
{ 'active:translate-y-[1px]': props.pressEffect },
|
||||
{ 'border-0': props.noBorder },
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
41
apps/desktop/src/components/primitive/Checkbox.tsx
Normal file
41
apps/desktop/src/components/primitive/Checkbox.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
containerClasname?: string;
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = (props) => {
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
'flex items-center text-sm font-medium text-gray-700 dark:text-gray-100',
|
||||
props.containerClasname
|
||||
)}
|
||||
>
|
||||
<input
|
||||
{...props}
|
||||
type="checkbox"
|
||||
className={clsx(
|
||||
`
|
||||
bg-gray-50
|
||||
hover:bg-gray-100
|
||||
dark:bg-gray-800
|
||||
border-gray-100
|
||||
hover:border-gray-200
|
||||
dark:border-gray-700
|
||||
dark:hover:bg-gray-700
|
||||
dark:hover:border-gray-600
|
||||
transition
|
||||
rounded
|
||||
mr-2
|
||||
text-primary
|
||||
checked:ring-2 checked:ring-primary-500
|
||||
`,
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
<span className="select-none">Checkbox</span>
|
||||
</label>
|
||||
);
|
||||
};
|
84
apps/desktop/src/components/primitive/Dropdown.tsx
Normal file
84
apps/desktop/src/components/primitive/Dropdown.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { DefaultOptions } from '@apollo/client';
|
||||
import { Button, ButtonProps } from '.';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Section = {
|
||||
name: string;
|
||||
icon?: any;
|
||||
selected?: boolean;
|
||||
}[];
|
||||
|
||||
export interface DropdownProps extends DefaultOptions {
|
||||
items: Section[];
|
||||
buttonText: string;
|
||||
buttonProps: ButtonProps;
|
||||
buttonIcon?: any;
|
||||
}
|
||||
|
||||
export const Dropdown: React.FC<DropdownProps> = (props) => {
|
||||
return (
|
||||
<div className="flex mt-2">
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="outline-none">
|
||||
<Button size="sm" {...props.buttonProps}>
|
||||
{props.buttonIcon}
|
||||
{props.buttonText}
|
||||
<div className="flex-grow" />
|
||||
<ChevronDownIcon
|
||||
className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100 "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute left-0 w-44 mt-1 origin-top-left bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-700 border dark:border-gray-700 rounded-md shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||
{props.items.map((item, index) => (
|
||||
<div key={index} className="px-1 py-1">
|
||||
{item.map((button, index) => (
|
||||
<Menu.Item key={index}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={clsx(
|
||||
'text-sm group flex rounded-md items-center w-full px-2 py-1 mb-[2px] dark:hover:bg-gray-750',
|
||||
{
|
||||
'bg-gray-300 dark:bg-gray-600 dark:hover:!bg-gray-750':
|
||||
active || button.selected,
|
||||
'text-gray-900 dark:text-gray-200': !active
|
||||
}
|
||||
)}
|
||||
>
|
||||
{button.icon && (
|
||||
<button.icon
|
||||
className={clsx('mr-2 w-4 h-4', {
|
||||
'dark:text-gray-100': active,
|
||||
'text-gray-600 dark:text-gray-200': !active
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{button.name}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
53
apps/desktop/src/components/primitive/Input.tsx
Normal file
53
apps/desktop/src/components/primitive/Input.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
const variants = {
|
||||
default: `
|
||||
shadow-sm
|
||||
bg-white
|
||||
hover:bg-white
|
||||
focus:hover:bg-white
|
||||
focus:bg-white
|
||||
dark:bg-gray-800
|
||||
dark:hover:bg-gray-750
|
||||
dark:focus:bg-gray-800
|
||||
dark:focus:hover:bg-gray-800
|
||||
|
||||
border-gray-100
|
||||
hover:border-gray-200
|
||||
focus:border-white
|
||||
dark:border-gray-600
|
||||
dark:hover:border-gray-600
|
||||
dark:focus:border-gray-900
|
||||
|
||||
focus:ring-gray-100
|
||||
dark:focus:ring-gray-600
|
||||
|
||||
dark:text-white
|
||||
placeholder-gray-300
|
||||
`,
|
||||
primary: ''
|
||||
};
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
variant?: keyof typeof variants;
|
||||
}
|
||||
|
||||
export const Input = (props: InputProps) => {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
className={clsx(
|
||||
`px-3 py-1 rounded-md border leading-7 outline-none shadow-xs focus:ring-2 transition-all`,
|
||||
variants[props.variant || 'default'],
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Label: React.FC<{ slug?: string }> = (props) => (
|
||||
<label className="text-sm font-bold" htmlFor={props.slug}>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
22
apps/desktop/src/components/primitive/InputContainer.tsx
Normal file
22
apps/desktop/src/components/primitive/InputContainer.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DefaultProps } from './types';
|
||||
import { Label } from './Input';
|
||||
|
||||
interface InputContainerProps extends DefaultProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const InputContainer: React.FC<InputContainerProps> = (props) => {
|
||||
return (
|
||||
<div className={clsx('flex flex-col', props.className)} {...props}>
|
||||
<h3 className="text-gray-700 dark:text-gray-100 font-medium mb-1">{props.title}</h3>
|
||||
{!!props.description && (
|
||||
<p className="text-gray-400 text-sm max-w-md mb-2">{props.description}</p>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
33
apps/desktop/src/components/primitive/Shortcut.tsx
Normal file
33
apps/desktop/src/components/primitive/Shortcut.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { DefaultProps } from './types';
|
||||
|
||||
export interface ShortcutProps extends DefaultProps {
|
||||
chars: string;
|
||||
}
|
||||
|
||||
export const Shortcut: React.FC<ShortcutProps> = (props) => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
`
|
||||
px-1
|
||||
py-0.5
|
||||
text-xs
|
||||
font-bold
|
||||
text-gray-400
|
||||
bg-gray-200
|
||||
border-gray-300
|
||||
dark:text-gray-400
|
||||
dark:bg-gray-600
|
||||
dark:border-gray-500
|
||||
border-t-2
|
||||
rounded-lg
|
||||
`,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.chars}
|
||||
</span>
|
||||
);
|
||||
};
|
9
apps/desktop/src/components/primitive/Tag.tsx
Normal file
9
apps/desktop/src/components/primitive/Tag.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { DefaultProps } from './types';
|
||||
|
||||
export interface TagProps extends DefaultProps {}
|
||||
|
||||
export const Tag: React.FC<TagProps> = (props) => {
|
||||
return <div className={clsx('rounded px-2 py-1', props.className)}>{props.children}</div>;
|
||||
};
|
27
apps/desktop/src/components/primitive/Toggle.tsx
Normal file
27
apps/desktop/src/components/primitive/Toggle.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const Toggle = (props: { initialState: boolean }) => {
|
||||
const [enabled, setEnabled] = useState(props.initialState || false);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
className={clsx(
|
||||
'relative inline-flex items-center h-6 rounded-full w-11 bg-gray-200 dark:bg-gray-750',
|
||||
{
|
||||
'bg-primary-500 dark:bg-primary-500': enabled
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
} inline-block w-4 h-4 transform bg-white rounded-full`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
33
apps/desktop/src/components/primitive/Varients.tsx
Normal file
33
apps/desktop/src/components/primitive/Varients.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
interface StyleState {
|
||||
active: string[];
|
||||
hover: string[];
|
||||
normal: string[];
|
||||
}
|
||||
|
||||
interface Varient {
|
||||
base: string;
|
||||
light: StyleState;
|
||||
dark: StyleState;
|
||||
}
|
||||
|
||||
function tw(varient: Varient): string {
|
||||
return `${varient.base} ${varient.light}`;
|
||||
}
|
||||
|
||||
const variants: Record<string, string> = {
|
||||
default: tw({
|
||||
base: 'shadow-sm',
|
||||
light: {
|
||||
normal: ['bg-gray-50', 'border-gray-100', 'text-gray-700'],
|
||||
hover: ['bg-gray-100', 'border-gray-200', 'text-gray-900'],
|
||||
active: ['bg-gray-50', 'border-gray-200', 'text-gray-600']
|
||||
},
|
||||
dark: {
|
||||
normal: ['bg-gray-800 ', 'border-gray-100', ' text-gray-200'],
|
||||
active: ['bg-gray-700 ', 'border-gray-200 ', 'text-white'],
|
||||
hover: ['bg-gray-700 ', 'border-gray-600 ', 'text-white']
|
||||
}
|
||||
})
|
||||
};
|
3
apps/desktop/src/components/primitive/index.tsx
Normal file
3
apps/desktop/src/components/primitive/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './Button';
|
||||
export * from './Input';
|
||||
export * from './Toggle';
|
3
apps/desktop/src/components/primitive/types.ts
Normal file
3
apps/desktop/src/components/primitive/types.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface DefaultProps {
|
||||
className?: string;
|
||||
}
|
17
apps/desktop/src/hooks/useFocusState.tsx
Normal file
17
apps/desktop/src/hooks/useFocusState.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function useFocusState() {
|
||||
const [focused, setFocused] = useState(true);
|
||||
const focus = () => setFocused(true);
|
||||
const blur = () => setFocused(false);
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', focus);
|
||||
window.addEventListener('blur', blur);
|
||||
return () => {
|
||||
window.removeEventListener('focus', focus);
|
||||
window.removeEventListener('blur', blur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [focused];
|
||||
}
|
26
apps/desktop/src/hooks/useGlobalEvents.tsx
Normal file
26
apps/desktop/src/hooks/useGlobalEvents.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useEffect } from 'react';
|
||||
import { emit, listen, Event } from '@tauri-apps/api/event';
|
||||
import { useExplorerStore } from '../store/explorer';
|
||||
|
||||
export interface RustEvent {
|
||||
kind: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export function useGlobalEvents() {
|
||||
useEffect(() => {
|
||||
listen('message', (e: Event<RustEvent>) => {
|
||||
console.log({ e });
|
||||
|
||||
switch (e.payload?.kind) {
|
||||
case 'FileTypeThumb':
|
||||
if (e.payload?.data.icon_created)
|
||||
useExplorerStore.getState().nativeIconUpdated(e.payload.data.file_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}
|
10
apps/desktop/src/hooks/useInputState.tsx
Normal file
10
apps/desktop/src/hooks/useInputState.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
export function useInputState<T = any>(initialValue: T) {
|
||||
const [value, setValue] = useState<T>(initialValue);
|
||||
return {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(event.target.value as unknown as T),
|
||||
value
|
||||
};
|
||||
}
|
1
apps/desktop/src/index.d.ts
vendored
Normal file
1
apps/desktop/src/index.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'react-spline';
|
14
apps/desktop/src/index.html
Normal file
14
apps/desktop/src/index.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="http://localhost:8097"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>sd</title>
|
||||
</head>
|
||||
<body style="overflow: hidden">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
// import 'style.css';
|
||||
import App from './App';
|
||||
import './style.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<div style={{background:"red", display:"flex", flex:1}}>
|
||||
hello
|
||||
</div>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
36
apps/desktop/src/screens/Explorer.tsx
Normal file
36
apps/desktop/src/screens/Explorer.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FileList } from '../components/file/FileList';
|
||||
import { emit, listen } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { IFile } from '../types';
|
||||
import { useExplorerStore } from '../store/explorer';
|
||||
import { Inspector } from '../components/file/Inspector';
|
||||
|
||||
export interface DirectoryResponse {
|
||||
directory: IFile;
|
||||
contents: IFile[];
|
||||
}
|
||||
|
||||
export const ExplorerScreen: React.FC<{}> = () => {
|
||||
const [currentDir, tempWatchDir] = useExplorerStore((state) => [
|
||||
state.currentDir,
|
||||
state.tempWatchDir
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
invoke<DirectoryResponse>('get_files', { path: tempWatchDir }).then((res) => {
|
||||
console.log({ res });
|
||||
useExplorerStore.getState().ingestDir(res.directory, res.contents);
|
||||
invoke('get_thumbs_for_directory', { path: tempWatchDir });
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (currentDir === null) return <></>;
|
||||
|
||||
return (
|
||||
<div className="relative w-full flex flex-row bg-white dark:bg-gray-900">
|
||||
<FileList />
|
||||
<Inspector />
|
||||
</div>
|
||||
);
|
||||
};
|
36
apps/desktop/src/screens/Overview.tsx
Normal file
36
apps/desktop/src/screens/Overview.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
interface StatItemProps {
|
||||
name: string;
|
||||
value: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
const StatItem: React.FC<StatItemProps> = (props) => {
|
||||
return (
|
||||
<div className="flex flex-col p-4 rounded-md dark:bg-gray-800 mt-2">
|
||||
<span className="text-gray-400 text-sm">{props.name}</span>
|
||||
<span className="font-bold text-2xl">
|
||||
{props.value}
|
||||
<span className="text-sm text-gray-400 ml-1">{props.unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OverviewScreen: React.FC<{}> = (props) => {
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full bg-white dark:bg-gray-900 p-5">
|
||||
<h1 className=" font-bold text-xl">Jamie's Library</h1>
|
||||
<div className="flex flex-wrap space-x-2 mt-3">
|
||||
<StatItem name="Total capacity" value="26.5" unit="TB" />
|
||||
<StatItem name="Index size" value="103" unit="MB" />
|
||||
<StatItem name="Preview media" value="23.5" unit="GB" />
|
||||
<StatItem name="Free space" value="9.2" unit="TB" />
|
||||
|
||||
<StatItem name="Total at-risk" value="1.5" unit="TB" />
|
||||
<StatItem name="Total backed up" value="25.3" unit="TB" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
96
apps/desktop/src/screens/Settings.tsx
Normal file
96
apps/desktop/src/screens/Settings.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { DuplicateIcon, PencilAltIcon, TrashIcon } from '@heroicons/react/solid';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import React, { useRef } from 'react';
|
||||
// import { dummyIFile, FileList } from '../components/file/FileList';
|
||||
import { Input, Toggle } from '../components/primitive';
|
||||
import { Button } from '../components/primitive/Button';
|
||||
import { Checkbox } from '../components/primitive/Checkbox';
|
||||
import { Dropdown } from '../components/primitive/Dropdown';
|
||||
import { InputContainer } from '../components/primitive/InputContainer';
|
||||
import { Shortcut } from '../components/primitive/Shortcut';
|
||||
import { useInputState } from '../hooks/useInputState';
|
||||
import { useExplorerStore } from '../store/explorer';
|
||||
//@ts-ignore
|
||||
// import { Spline } from 'react-spline';
|
||||
// import WINDOWS_SCENE from '../assets/spline/scene.json';
|
||||
|
||||
export const SettingsScreen: React.FC<{}> = () => {
|
||||
const fileUploader = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [tempWatchDir, setTempWatchDir] = useExplorerStore((state) => [
|
||||
state.tempWatchDir,
|
||||
state.setTempWatchDir
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-5">
|
||||
{/* <FileList files={dummyIFile} /> */}
|
||||
{/* <Spline scene={WINDOWS_SCENE} /> */}
|
||||
{/* <iframe
|
||||
src="https://my.spline.design/windowscopy-8e92a2e9b7cb4d9237100441e8c4f688/"
|
||||
width="100%"
|
||||
height="100%"
|
||||
></iframe> */}
|
||||
<div className="flex space-x-2 mt-4">
|
||||
<InputContainer
|
||||
title="Quick scan directory"
|
||||
description="The directory for which this application will perform a detailed scan of the contents and sub directories"
|
||||
>
|
||||
<Input
|
||||
value={tempWatchDir}
|
||||
onChange={(e) => setTempWatchDir(e.target.value)}
|
||||
placeholder="/users/jamie/Desktop"
|
||||
/>
|
||||
</InputContainer>
|
||||
</div>
|
||||
<div className="space-x-2 flex flex-row mt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
invoke('scan_dir', {
|
||||
path: tempWatchDir
|
||||
});
|
||||
}}
|
||||
>
|
||||
Scan Now
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
invoke('test_scan');
|
||||
}}
|
||||
>
|
||||
Test Scan
|
||||
</Button>
|
||||
<Button size="sm">Test</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-x-2 flex flex-row mt-4">
|
||||
<Toggle initialState={false} />
|
||||
</div>
|
||||
<div className="space-x-2 flex flex-row mt-4 mb-5 ml-1">
|
||||
<Checkbox />
|
||||
<Checkbox />
|
||||
<Checkbox />
|
||||
</div>
|
||||
<Dropdown
|
||||
buttonProps={{}}
|
||||
buttonText="My Library"
|
||||
items={[
|
||||
[
|
||||
{ name: 'Edit', icon: PencilAltIcon },
|
||||
{ name: 'Copy', icon: DuplicateIcon }
|
||||
],
|
||||
[{ name: 'Delete', icon: TrashIcon }]
|
||||
]}
|
||||
/>
|
||||
<div className="mt-3 space-x-1">
|
||||
<Shortcut chars="⌘" />
|
||||
<Shortcut chars="S" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
9
apps/desktop/src/screens/Spaces.tsx
Normal file
9
apps/desktop/src/screens/Spaces.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
export const SpacesScreen: React.FC<{}> = (props) => {
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full bg-white dark:bg-gray-900 p-5">
|
||||
<h1 className=" font-bold text-xl">Spaces</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
33
apps/desktop/src/style.css
Normal file
33
apps/desktop/src/style.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
a,
|
||||
button {
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
/*
|
||||
.table-container {
|
||||
@apply w-full bg-gray-900;
|
||||
}
|
||||
.table-head {
|
||||
@apply w-full;
|
||||
}
|
||||
.table-head-row {
|
||||
@apply w-full flex flex-row;
|
||||
}
|
||||
.table-head-cell {
|
||||
}
|
||||
.table-body {
|
||||
@apply w-full;
|
||||
}
|
||||
.table-row {
|
||||
@apply w-full flex flex-row;
|
||||
}
|
||||
.table-cell {
|
||||
} */
|
40
apps/desktop/src/types/filesystem.ts
Normal file
40
apps/desktop/src/types/filesystem.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Encryption } from './library';
|
||||
import { ImageMeta, VideoMeta } from './media';
|
||||
|
||||
export interface IFile {
|
||||
id: number;
|
||||
meta_checksum: string;
|
||||
uri: string;
|
||||
is_dir: string;
|
||||
|
||||
date_created: Date;
|
||||
date_modified: Date;
|
||||
date_indexed: Date;
|
||||
|
||||
name: string;
|
||||
extension: string;
|
||||
size_in_bytes: string;
|
||||
|
||||
library_id: string;
|
||||
ipfs_id: string;
|
||||
storage_device_id: string;
|
||||
capture_device_id: string;
|
||||
parent_id: string;
|
||||
tags?: ITag[];
|
||||
|
||||
// this state is used to tell the renderer to look in the designated
|
||||
// folder for this media type
|
||||
has_native_icon?: boolean;
|
||||
has_thumb?: boolean;
|
||||
has_preview_media?: boolean;
|
||||
icon_b64?: string;
|
||||
}
|
||||
|
||||
export interface IDirectory extends IFile {
|
||||
children?: IFile[];
|
||||
children_count: number;
|
||||
}
|
||||
|
||||
export interface ITag {
|
||||
id: string;
|
||||
}
|
4
apps/desktop/src/types/index.ts
Normal file
4
apps/desktop/src/types/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './library';
|
||||
export * from './filesystem';
|
||||
export * from './job';
|
||||
export * from './media';
|
22
apps/desktop/src/types/job.ts
Normal file
22
apps/desktop/src/types/job.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export interface Job {
|
||||
// A job is used to define a task for the software to complete
|
||||
// These are intended to be stored in memory, or not persisted permanently
|
||||
object_ids: string[]; // array of object ids that concern this job
|
||||
type: JobType;
|
||||
created_at: Date;
|
||||
completed_at: Date;
|
||||
canceled_at: Date;
|
||||
parent_job_id: string;
|
||||
}
|
||||
|
||||
export enum JobType {
|
||||
SCAN,
|
||||
IMPORT,
|
||||
ENCRYPT,
|
||||
DECRYPT,
|
||||
COPY,
|
||||
MOVE,
|
||||
DELETE,
|
||||
RENDER_VIDEO,
|
||||
RENDER_IMAGE
|
||||
}
|
37
apps/desktop/src/types/library.ts
Normal file
37
apps/desktop/src/types/library.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
|
||||
google_access_token: string;
|
||||
google_refresh_token: string;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
object_count: number;
|
||||
total_size: number;
|
||||
encryption: Encryption;
|
||||
|
||||
public: boolean;
|
||||
date_created: Date;
|
||||
}
|
||||
|
||||
export interface UserLibrary {
|
||||
library_id: string;
|
||||
user_id: string;
|
||||
date_joined: Date;
|
||||
role: UserLibraryRole;
|
||||
}
|
||||
|
||||
export enum Encryption {
|
||||
NONE,
|
||||
'128-AES',
|
||||
'192-AES',
|
||||
'256-AES'
|
||||
}
|
||||
|
||||
export enum UserLibraryRole {
|
||||
OWNER,
|
||||
READ_ONLY
|
||||
}
|
30
apps/desktop/src/types/media.ts
Normal file
30
apps/desktop/src/types/media.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export interface ImageMeta {
|
||||
type: 'image';
|
||||
dimensions: {
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
color_space: string;
|
||||
aperture: number;
|
||||
exposure_mode: number;
|
||||
exposure_program: number;
|
||||
f_number: number;
|
||||
flash: boolean;
|
||||
focal_length: number;
|
||||
has_alpha_channel: boolean;
|
||||
iso_speed: number;
|
||||
orientation: number;
|
||||
metering_mode: number;
|
||||
}
|
||||
|
||||
export interface VideoMeta {
|
||||
type: 'video';
|
||||
codecs: Array<string>;
|
||||
bitrate: {
|
||||
video: string;
|
||||
audio: string;
|
||||
};
|
||||
duration_seconds: number;
|
||||
}
|
||||
|
||||
export interface AudioMeta {}
|
7
apps/desktop/src/vite-env.d.ts
vendored
Normal file
7
apps/desktop/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare interface ImportMetaEnv {
|
||||
VITE_OS: string;
|
||||
}
|
||||
|
||||
declare module '@babel/core' {}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "screens",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const jeff = true;
|
|
@ -1 +0,0 @@
|
|||
export const jeff = true;
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "style",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"globals.web.scss",
|
||||
"tailwind.config.js",
|
||||
"useTheme.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in a new issue