ui returns

This commit is contained in:
Jamie 2021-12-24 04:56:19 -08:00
parent fd745a4d36
commit c62d507855
48 changed files with 1546 additions and 25 deletions

94
apps/desktop/src/App.tsx Normal file
View 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>
);
}

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,3 @@
export * from './Button';
export * from './Input';
export * from './Toggle';

View file

@ -0,0 +1,3 @@
export interface DefaultProps {
className?: string;
}

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

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

View 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
View file

@ -0,0 +1 @@
declare module 'react-spline';

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

View file

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

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

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

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

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

View 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 {
} */

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

View file

@ -0,0 +1,4 @@
export * from './library';
export * from './filesystem';
export * from './job';
export * from './media';

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

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

View 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
View file

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare interface ImportMetaEnv {
VITE_OS: string;
}
declare module '@babel/core' {}

View file

@ -1,7 +0,0 @@
{
"name": "screens",
"version": "0.0.0",
"main": "index.js",
"license": "MIT"
}

View file

@ -1 +0,0 @@
export const jeff = true;

View file

@ -1 +0,0 @@
export const jeff = true;

View file

@ -1,12 +0,0 @@
{
"name": "style",
"version": "0.0.0",
"private": true,
"main": "index.js",
"files": [
"globals.web.scss",
"tailwind.config.js",
"useTheme.ts"
]
}