[ENG-363] Spacedrop UI + Misc Improvements (#568)

* begin spacedrop ui + misc ui improvements

* better 404 xox

* Update extensions.rs

I think I prefer Container

* added DragRegion component,  ot tested cuz im on my fone

* Update DragRegion.tsx

fix import

* added dummy drop items

* better dummy data

* added clouds & search bar

* added action buttons to spacedrop items

* customize subtle button

* added support for apng, thanks luka big pants

* use relative path in sidebar

* use BYTES const

---------

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
Jamie Pine 2023-02-24 00:12:21 -08:00 committed by GitHub
parent 7b739d0b33
commit 3f44d6f521
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 521 additions and 151 deletions

View file

@ -48,6 +48,7 @@ sharma
skippable
spacedrive
spacedriveapp
spacetunnel
specta
storedkey
stringly

View file

@ -19,6 +19,11 @@ export default defineConfig({
}
})
],
css: {
modules: {
localsConvention: 'camelCaseOnly'
}
},
resolve: {
alias: [relativeAliasResolver]
},

View file

@ -17,6 +17,11 @@ export default defineConfig({
md({ mode: [Mode.REACT] }),
visualizer()
],
css: {
modules: {
localsConvention: 'camelCaseOnly'
}
},
resolve: {
alias: [
{

View file

@ -113,6 +113,19 @@ pub(crate) fn mount() -> RouterBuilder {
.await?)
})
})
// .library_mutation("create", |t| {
// #[derive(Type, Deserialize)]
// pub struct TagCreateArgs {
// pub name: String,
// pub color: String,
// }
// t(|_, args: TagCreateArgs, library| async move {
// let created_tag = Tag::new(args.name, args.color);
// created_tag.save(&library.db).await?;
// invalidate_query!(library, "tags.list");
// Ok(created_tag)
// })
// })
.library_mutation("create", |t| {
#[derive(Type, Deserialize)]
pub struct TagCreateArgs {

View file

@ -9,7 +9,7 @@ use tokio::fs::File;
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
use super::{context_menu_fs_info, FsInfo};
use super::{context_menu_fs_info, FsInfo, BYTES};
pub struct FileDecryptorJob;
#[derive(Serialize, Deserialize, Debug)]
pub struct FileDecryptorJobState {}
@ -74,7 +74,7 @@ impl StatefulJob for FileDecryptorJob {
|| {
let mut path = info.fs_path.clone();
let extension = path.extension().map_or("decrypted", |ext| {
if ext == ".sdenc" {
if ext == BYTES {
""
} else {
"decrypted"

View file

@ -98,7 +98,7 @@ impl StatefulJob for FileEncryptorJob {
|| {
let mut path = info.fs_path.clone();
let extension = path.extension().map_or_else(
|| Ok("sdenc".to_string()),
|| Ok("bytes".to_string()),
|extension| {
Ok::<String, JobError>(
extension
@ -108,7 +108,7 @@ impl StatefulJob for FileEncryptorJob {
"path contents when converted to string",
),
})?
.to_string() + ".sdenc",
.to_string() + ".bytes",
)
},
)?;

View file

@ -22,6 +22,8 @@ pub mod error;
pub mod erase;
pub const BYTES: &str = "bytes";
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub enum ObjectType {
File,

View file

@ -2,6 +2,7 @@ pub mod cas;
pub mod fs;
pub mod identifier_job;
pub mod preview;
pub mod tag;
pub mod validation;
// Objects are primarily created by the identifier from Paths

33
core/src/object/tag.rs Normal file
View file

@ -0,0 +1,33 @@
use prisma_client_rust::QueryError;
use rspc::Type;
use serde::Deserialize;
use uuid::Uuid;
use crate::prisma::{tag, PrismaClient};
#[derive(Type, Deserialize)]
pub struct Tag {
pub name: String,
pub color: String,
}
impl Tag {
pub fn new(name: String, color: String) -> Self {
Self { name, color }
}
pub async fn save(self, db: &PrismaClient) -> Result<(), QueryError> {
db.tag()
.create(
Uuid::new_v4().as_bytes().to_vec(),
vec![
tag::name::set(Some(self.name)),
tag::color::set(Some(self.color)),
],
)
.exec()
.await?;
Ok(())
}
}

View file

@ -67,6 +67,7 @@ extension_category_enum! {
Jpg = [0xFF, 0xD8],
Jpeg = [0xFF, 0xD8],
Png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
Apng = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52],
Gif = [0x47, 0x49, 0x46, 0x38, _, 0x61],
Bmp = [0x42, 0x4D],
Tiff = [0x49, 0x49, 0x2A, 0x00],
@ -183,11 +184,11 @@ extension_category_enum! {
extension_category_enum! {
EncryptedExtension _ALL_ENCRYPTED_EXTENSIONS {
// Spacedrive encrypted file
SdEnc = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70],
Bytes = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70],
// Spacedrive container
SdContainer = [0x73, 0x64, 0x62, 0x6F, 0x78],
Container = [0x73, 0x64, 0x62, 0x6F, 0x78],
// Spacedrive block storage,
SdBlock = [0x73, 0x64, 0x62, 0x6C, 0x6F, 0x63, 0x6B],
Block = [0x73, 0x64, 0x62, 0x6C, 0x6F, 0x63, 0x6B],
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -1,5 +1,6 @@
import { captureException } from '@sentry/browser';
import { FallbackProps } from 'react-error-boundary';
import { useDebugState } from '@sd/client';
import { Button } from '@sd/ui';
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
@ -8,6 +9,8 @@ export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
resetErrorBoundary();
};
const debug = useDebugState();
return (
<div
data-tauri-drag-region
@ -17,6 +20,11 @@ export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
<p className="text-ink-faint m-3 text-sm font-bold">APP CRASHED</p>
<h1 className="text-ink text-2xl font-bold">We're past the event horizon...</h1>
<pre className="text-ink m-2">Error: {error.message}</pre>
{debug.enabled && (
<pre className="text-ink-dull m-2 text-sm">
Check the console (CMD/CRTL + OPTION + i) for stack trace.
</pre>
)}
<div className="text-ink flex flex-row space-x-2">
<Button variant="accent" className="mt-2" onClick={resetErrorBoundary}>
Reload

View file

@ -4,6 +4,7 @@ import {
Copy,
FileX,
Image,
Info,
LockSimple,
LockSimpleOpen,
Package,
@ -12,6 +13,7 @@ import {
Scissors,
Share,
ShieldCheck,
Sidebar,
TagSimple,
Trash,
TrashSimple
@ -244,6 +246,19 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Separator />
{!store.showInspector && (
<>
<CM.Item
label="Details"
// icon={Sidebar}
onClick={(e) => {
getExplorerStore().showInspector = true;
}}
/>
<CM.Separator />
</>
)}
<CM.Item label="Quick view" keybind="␣" />
<OpenInNativeExplorer />
@ -259,7 +274,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
source_path_id: data.item.id,
target_location_id: store.locationId!,
target_path: params.path,
target_file_name_suffix: ' - Clone'
target_file_name_suffix: ' copy'
});
}}
/>

View file

@ -16,6 +16,7 @@ import { forwardRef, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { Button, Input, Popover, cva } from '@sd/ui';
import DragRegion from '~/components/layout/DragRegion';
import { getExplorerStore, useExplorerStore } from '../../hooks/useExplorerStore';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import { KeybindEvent } from '../../util/keybind';
@ -69,7 +70,7 @@ const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
}
);
const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRef) => {
export const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRef) => {
const {
register,
handleSubmit,
@ -97,11 +98,14 @@ const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRe
else if (forwardedRef) forwardedRef.current = el;
}}
placeholder="Search"
className="w-32 transition-all focus:w-52"
className={clsx('w-32 transition-all focus:w-52', props.className)}
{...searchField}
/>
<div className={clsx('pointer-events-none absolute right-1 space-x-1 peer-focus:invisible')}>
<div
className={clsx(
'pointer-events-none absolute right-1 flex h-7 items-center space-x-1 opacity-70 peer-focus:invisible'
)}
>
{platform === 'browser' ? (
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
) : os === 'macOS' ? (
@ -331,11 +335,10 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
onClick={() => (getExplorerStore().showInspector = !store.showInspector)}
className="my-2"
>
{store.showInspector ? (
<SidebarSimple className={TOP_BAR_ICON_STYLE} />
) : (
<SidebarSimple className={TOP_BAR_ICON_STYLE} />
)}
<SidebarSimple
weight={store.showInspector ? 'fill' : 'regular'}
className={clsx(TOP_BAR_ICON_STYLE, 'scale-x-[-1] transform')}
/>
</TopBarButton>
</Tooltip>
{/* <Dropdown

View file

@ -1,8 +1,10 @@
import archive from '@sd/assets/images/Archive.png';
import documentPdf from '@sd/assets/images/Document_pdf.png';
import executable from '@sd/assets/images/Executable.png';
import file from '@sd/assets/images/File.png';
import video from '@sd/assets/images/Video.png';
import Archive from '@sd/assets/images/Archive.png';
import Compressed from '@sd/assets/images/Compressed.png';
import DocumentPdf from '@sd/assets/images/Document_pdf.png';
import Encrypted from '@sd/assets/images/Encrypted.png';
import Executable from '@sd/assets/images/Executable.png';
import File from '@sd/assets/images/File.png';
import Video from '@sd/assets/images/Video.png';
import clsx from 'clsx';
import { ExplorerItem, isObject, isPath } from '@sd/client';
import { useExplorerStore } from '~/hooks/useExplorerStore';
@ -35,30 +37,33 @@ export default function FileThumb({ data, ...props }: Props) {
if (isPath(data) && data.item.is_dir) return <Folder size={props.size * 0.7} />;
const cas_id = isObject(data) ? data.item.file_paths[0]?.cas_id : data.item.cas_id;
if (data.has_thumbnail) {
const cas_id = isObject(data) ? data.item.file_paths[0]?.cas_id : data.item.cas_id;
if (!cas_id) return <div></div>;
if (!cas_id) return <div></div>;
const url = platform.getThumbnailUrlById(cas_id);
const url = platform.getThumbnailUrlById(cas_id);
if (data.has_thumbnail && url)
return (
<img
style={props.style}
decoding="async"
// width={props.size}
className={clsx('z-90 pointer-events-none', props.className)}
src={url}
/>
);
if (url)
return (
<img
style={props.style}
decoding="async"
// width={props.size}
className={clsx('z-90 pointer-events-none', props.className)}
src={url}
/>
);
}
let icon = file;
let icon = File;
// Hacky (and temporary) way to integrate thumbnails
if (props.kind === 'Archive') icon = archive;
else if (props.kind === 'Video') icon = video;
else if (props.kind === 'Document' && data.item.extension === 'pdf') icon = documentPdf;
else if (props.kind === 'Executable') icon = executable;
else if (props.kind === 'Encrypted') icon = archive;
if (props.kind === 'Archive') icon = Archive;
else if (props.kind === 'Video') icon = Video;
else if (props.kind === 'Document' && data.item.extension === 'pdf') icon = DocumentPdf;
else if (props.kind === 'Executable') icon = Executable;
else if (props.kind === 'Encrypted') icon = Encrypted;
else if (props.kind === 'Compressed') icon = Compressed;
return <img src={icon} className={clsx('h-full overflow-hidden', props.iconClassNames)} />;
}

View file

@ -1,7 +1,7 @@
// import types from '../../constants/file-types.json';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { Barcode, CircleWavyCheck, Clock, Cube, Link, Lock, Snowflake } from 'phosphor-react';
import { Barcode, CircleWavyCheck, Clock, Cube, Hash, Link, Lock, Snowflake } from 'phosphor-react';
import { useEffect, useState } from 'react';
import {
ExplorerContext,
@ -27,6 +27,8 @@ export const MetaContainer = tw.div`flex flex-col px-4 py-1.5`;
export const MetaTitle = tw.h5`text-xs font-bold`;
export const MetaKeyName = tw.h5`text-xs flex-shrink-0 flex-wrap-0`;
export const MetaValue = tw.p`text-xs break-all text-ink truncate`;
const MetaTextLine = tw.div`flex items-center my-0.5 text-xs text-ink-dull`;
@ -66,6 +68,9 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
const item = data?.item;
// map array of numbers into string
const pub_id = fullObjectData?.data?.pub_id.map((n: number) => n.toString(16)).join('');
return (
<div
{...elementProps}
@ -154,14 +159,14 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
<Tooltip label={dayjs(item?.date_created).format('h:mm:ss a')}>
<MetaTextLine>
<InspectorIcon component={Clock} />
<span className="mr-1.5">Created</span>
<MetaKeyName className="mr-1.5">Created</MetaKeyName>
<MetaValue>{dayjs(item?.date_created).format('MMM Do YYYY')}</MetaValue>
</MetaTextLine>
</Tooltip>
<Tooltip label={dayjs(item?.date_created).format('h:mm:ss a')}>
<MetaTextLine>
<InspectorIcon component={Barcode} />
<span className="mr-1.5">Indexed</span>
<MetaKeyName className="mr-1.5">Indexed</MetaKeyName>
<MetaValue>{dayjs(item?.date_indexed).format('MMM Do YYYY')}</MetaValue>
</MetaTextLine>
</Tooltip>
@ -175,7 +180,7 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
<Tooltip label={filePathData?.cas_id || ''}>
<MetaTextLine>
<InspectorIcon component={Snowflake} />
<span className="mr-1.5">Content ID</span>
<MetaKeyName className="mr-1.5">Content ID</MetaKeyName>
<MetaValue>{filePathData?.cas_id || ''}</MetaValue>
</MetaTextLine>
</Tooltip>
@ -183,11 +188,20 @@ export const Inspector = ({ data, context, ...elementProps }: Props) => {
<Tooltip label={filePathData?.integrity_checksum || ''}>
<MetaTextLine>
<InspectorIcon component={CircleWavyCheck} />
<span className="mr-1.5">Checksum</span>
<MetaKeyName className="mr-1.5">Checksum</MetaKeyName>
<MetaValue>{filePathData?.integrity_checksum}</MetaValue>
</MetaTextLine>
</Tooltip>
)}
{pub_id && (
<Tooltip label={pub_id || ''}>
<MetaTextLine>
<InspectorIcon component={Hash} />
<MetaKeyName className="mr-1.5">Object ID</MetaKeyName>
<MetaValue>{pub_id}</MetaValue>
</MetaTextLine>
</Tooltip>
)}
</MetaContainer>
</>
)}

View file

@ -115,7 +115,7 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
// );
return (
<div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full cursor-default pl-2">
<div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full cursor-default pl-4">
<div
ref={scrollRef}
className="custom-scroll explorer-scroll h-screen"

View file

@ -0,0 +1,10 @@
import { PropsWithChildren } from 'react';
import { cx } from '@sd/ui';
export default function DragRegion(props: PropsWithChildren & { className?: string }) {
return (
<div data-tauri-drag-region className={cx('flex flex-shrink-0 w-full h-5', props.className)}>
{props.children}
</div>
);
}

View file

@ -1,17 +1,21 @@
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
import clsx from 'clsx';
import {
ArchiveBox,
Broadcast,
CheckCircle,
CirclesFour,
CopySimple,
Crosshair,
Eraser,
FilmStrip,
Gear,
Lock,
MonitorPlay,
Planet,
Plus,
UsersThree
Plus
} from 'phosphor-react';
import React, { PropsWithChildren, useEffect } from 'react';
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import { Link, NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import {
Location,
LocationCreateArgs,
@ -46,6 +50,7 @@ import { Folder } from '../icons/Folder';
import { JobsManager } from '../jobs/JobManager';
import { MacTrafficLights } from '../os/TrafficLights';
import { InputContainer } from '../primitive/InputContainer';
import { SubtleButton } from '../primitive/SubtleButton';
import { Tooltip } from '../tooltip/Tooltip';
const SidebarBody = tw.div`flex relative flex-col flex-grow-0 flex-shrink-0 w-44 min-h-full border-r border-sidebar-divider bg-sidebar`;
@ -136,17 +141,43 @@ export function Sidebar() {
<Icon component={CirclesFour} />
Spaces
</SidebarLink>
<SidebarLink to="people">
{/* <SidebarLink to="people">
<Icon component={UsersThree} />
People
</SidebarLink>
</SidebarLink> */}
<SidebarLink to="media">
<Icon component={MonitorPlay} />
Media
</SidebarLink>
<SidebarLink to="spacedrop">
<Icon component={Broadcast} />
Spacedrop
</SidebarLink>
<SidebarLink to="imports">
<Icon component={ArchiveBox} />
Imports
</SidebarLink>
</div>
{library && <LibraryScopedSection key={library.uuid} />}
<div className="grow" />
{library && <LibraryScopedSection />}
<SidebarSection name="Tools" actionArea={<SubtleButton />}>
<SidebarLink to="duplicate-finder">
<Icon component={CopySimple} />
Duplicate Finder
</SidebarLink>
<SidebarLink to="lost-and-found">
<Icon component={Crosshair} />
Find a File
</SidebarLink>
<SidebarLink to="cache-cleaner">
<Icon component={Eraser} />
Cache Cleaner
</SidebarLink>
<SidebarLink to="media-encoder">
<Icon component={FilmStrip} />
Media Encoder
</SidebarLink>
</SidebarSection>
<div className="flex-grow" />
</SidebarContents>
<SidebarFooter>
<div className="flex">
@ -326,17 +357,6 @@ const SidebarSection = (
);
};
const SidebarHeadingOptionsButton: React.FC<{ to: string; icon?: React.FC }> = (props) => {
const Icon = props.icon ?? Ellipsis;
return (
<NavLink to={props.to}>
<Button className="!p-[5px]" variant="subtle">
<Icon className="h-3 w-3" />
</Button>
</NavLink>
);
};
function LibraryScopedSection() {
const platform = usePlatform();
@ -352,10 +372,9 @@ function LibraryScopedSection() {
<SidebarSection
name="Locations"
actionArea={
<>
{/* <SidebarHeadingOptionsButton to="/settings/locations" icon={CogIcon} /> */}
<SidebarHeadingOptionsButton to="settings/locations" />
</>
<Link to="settings/locations">
<SubtleButton />
</Link>
}
>
{locations.data?.map((location) => {
@ -399,7 +418,11 @@ function LibraryScopedSection() {
{!!tags.data?.length && (
<SidebarSection
name="Tags"
actionArea={<SidebarHeadingOptionsButton to="/settings/tags" />}
actionArea={
<NavLink to="settings/tags">
<SubtleButton />
</NavLink>
}
>
<div className="mt-1 mb-2">
{tags.data?.slice(0, 6).map((tag, index) => (

View file

@ -4,6 +4,7 @@ import { useEffect } from 'react';
import { Navigate, Outlet, RouteObject, useNavigate } from 'react-router';
import { getOnboardingStore } from '@sd/client';
import { tw } from '@sd/ui';
import DragRegion from '~/components/layout/DragRegion';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import OnboardingCreatingLibrary from './OnboardingCreatingLibrary';
import OnboardingMasterPassword from './OnboardingMasterPassword';
@ -64,7 +65,7 @@ export default function OnboardingRoot() {
'bg-sidebar text-ink flex h-screen flex-col'
)}
>
<div data-tauri-drag-region className="z-50 flex h-9 w-full shrink-0" />
<DragRegion className="z-50 h-9" />
<div className="-mt-5 flex grow flex-col p-10">
<div className="flex grow flex-col items-center justify-center">

View file

@ -19,7 +19,7 @@ export const PopoverPicker = ({ className, ...props }: PopoverPickerProps) => {
return (
<div className={clsx('relative mt-3 flex items-center', className)}>
<div
className={clsx('h-5 w-5 rounded-full shadow ', isOpen && 'dark:border-gray-500')}
className={clsx('h-4 w-4 rounded-full shadow', isOpen && 'dark:border-gray-500')}
style={{ backgroundColor: field.value }}
onClick={() => toggle(true)}
/>

View file

@ -11,8 +11,8 @@ export const Shortcut: React.FC<ShortcutProps> = (props) => {
return (
<kbd
className={clsx(
`border border-b-2 px-1`,
`rounded-md text-xs font-bold`,
`px-1 border border-b-2`,
`rounded-md text-xs font-ink-dull font-bold`,
`border-app-line dark:border-transparent`,
className
)}

View file

@ -0,0 +1,14 @@
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
import { Button, tw } from '@sd/ui';
export const SubtleButton: React.FC<{ icon?: React.FC }> = (props) => {
const Icon = props.icon ?? Ellipsis;
return (
<Button className="!p-[5px]" variant="subtle">
{/* @ts-expect-error */}
<Icon weight="bold" className="w-3 h-3" />
</Button>
);
};
export const SubtleButtonContainer = tw.div`opacity-0 text-ink-faint group-hover:opacity-30 hover:!opacity-100`;

View file

@ -2,6 +2,7 @@ import { ReactComponent as CaretDown } from '@sd/assets/svgs/caret.svg';
import { PropsWithChildren } from 'react';
import { useNavigate } from 'react-router';
import { Button, tw } from '@sd/ui';
import DragRegion from '~/components/layout/DragRegion';
import { Divider } from '../explorer/inspector/Divider';
interface Props extends PropsWithChildren {
@ -20,7 +21,7 @@ export const SettingsSubPage = ({ children, title, topRight }: Props) => {
return (
<PageOuter>
<div data-tauri-drag-region className="absolute h-5 w-full" />
<DragRegion />
<Page>
<PageInner>
<HeaderArea>

View file

@ -20,7 +20,7 @@ const state = {
listItemSize: 40,
selectedRowIndex: 1,
tagAssignMode: false,
showInspector: true,
showInspector: false,
multiSelectIndexes: [] as number[],
contextMenuObjectId: null as number | null,
contextMenuActiveObject: null as object | null,

View file

@ -1,6 +1,7 @@
import { useBridgeQuery, useLibraryMutation, useLibraryQuery } from '@sd/client';
import CodeBlock from '~/components/primitive/Codeblock';
import { usePlatform } from '~/util/Platform';
import { ScreenContainer } from './_Layout';
// TODO: Bring this back with a button in the sidebar near settings at the bottom
export default function DebugScreen() {
@ -16,9 +17,8 @@ export default function DebugScreen() {
// });
const { mutate: identifyFiles } = useLibraryMutation('jobs.identifyUniqueFiles');
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col">
<div data-tauri-drag-region className="flex h-5 w-full shrink-0" />
<div className="flex flex-col space-y-5 p-5 pt-2 pb-7">
<ScreenContainer>
<div className="flex flex-col p-5 pt-2 space-y-5 pb-7">
<h1 className="text-lg font-bold ">Developer Debugger</h1>
{/* <div className="flex flex-row pb-4 space-x-2">
<Button
@ -43,6 +43,6 @@ export default function DebugScreen() {
<h1 className="text-sm font-bold ">Libraries</h1>
<CodeBlock src={{ ...libraryState }} />
</div>
</div>
</ScreenContainer>
);
}

View file

@ -1,9 +1,10 @@
import { ScreenHeading } from '@sd/ui';
import { ScreenContainer } from './_Layout';
export default function MediaScreen() {
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col p-5">
<ScreenContainer>
<ScreenHeading>Media</ScreenHeading>
</div>
</ScreenContainer>
);
}

View file

@ -4,17 +4,21 @@ import { Button } from '@sd/ui';
export default function NotFound() {
const navigate = useNavigate();
return (
<div
data-tauri-drag-region
role="alert"
className="flex h-full w-full flex-col items-center justify-center rounded-lg p-4"
>
<p className="text-ink-faint m-3 text-sm font-semibold uppercase">Error: 404</p>
<h1 className="text-4xl font-bold">You chose nothingness.</h1>
<div className="flex flex-row space-x-2">
<Button variant="accent" className="mt-4" onClick={() => navigate(-1)}>
Go Back
</Button>
<div className="bg-app/80 w-full">
<div
role="alert"
className="flex h-full w-full flex-col items-center justify-center rounded-lg p-4"
>
<p className="text-ink-faint m-3 text-sm font-semibold uppercase">Error: 404</p>
<h1 className="text-4xl font-bold">There's nothing here.</h1>
<p className="text-ink-dull mt-2 text-sm">
Its likely that this page has not been built yet, if so we're on it!
</p>
<div className="flex flex-row space-x-2">
<Button variant="outline" className="mt-4" onClick={() => navigate(-1)}>
Go Back
</Button>
</div>
</div>
</div>
);

View file

@ -18,6 +18,7 @@ import { Card } from '@sd/ui';
import useCounter from '~/hooks/useCounter';
import { useLibraryId } from '~/util';
import { usePlatform } from '~/util/Platform';
import { ScreenContainer } from './_Layout';
interface StatItemProps {
title: string;
@ -97,11 +98,8 @@ export default function OverviewScreen() {
overviewMounted = true;
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col overflow-x-hidden">
<div data-tauri-drag-region className="flex h-5 w-full shrink-0" />
{/* PAGE */}
<div className="flex h-screen w-full flex-col px-4">
<ScreenContainer>
<div className="flex h-screen w-full flex-col">
{/* STAT HEADER */}
<div className="flex w-full">
{/* STAT CONTAINER */}
@ -138,7 +136,7 @@ export default function OverviewScreen() {
</Card>
<div className="flex h-4 w-full shrink-0" />
</div>
</div>
</ScreenContainer>
);
}

View file

@ -1,9 +1,10 @@
import { ScreenHeading } from '@sd/ui';
import { ScreenContainer } from './_Layout';
export default function PeopleScreen() {
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col p-5">
<ScreenContainer>
<ScreenHeading>People</ScreenHeading>
</div>
</ScreenContainer>
);
}

View file

@ -0,0 +1,25 @@
.honeycomb-outer {
font-size: 0; /*disable white space between inline block element */
display: flex;
--s: 150px; /* size */
--m: 4px; /* margin */
--f: calc(1.732 * var(--s) + 4 * var(--m) - 1px);
}
.honeycomb-container .honeycomb-item {
width: var(--s);
margin: var(--m);
height: calc(var(--s) * 1.1547);
display: inline-block;
clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
// background: rgba(48, 48, 55, 0.272);
margin-bottom: calc(var(--m) - var(--s) * 0.2885);
}
.honeycomb-container::before {
content: '';
width: calc(var(--s) / 2 + var(--m));
float: left;
height: 120%;
shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f));
}

View file

@ -0,0 +1,151 @@
import GoogleDrive from '@sd/assets/images/GoogleDrive.png';
import Mega from '@sd/assets/images/Mega.png';
import iCloud from '@sd/assets/images/iCloud.png';
import clsx from 'clsx';
import { DeviceMobile, HardDrives, Heart, Icon, Laptop, PhoneX, Star, User } from 'phosphor-react';
import { useRef } from 'react';
import { Button, tw } from '@sd/ui';
import { SearchBar } from '../components/explorer/ExplorerTopBar';
import { SubtleButton, SubtleButtonContainer } from '../components/primitive/SubtleButton';
import { OperatingSystem } from '../util/Platform';
import classes from './Spacedrop.module.scss';
import { ScreenContainer } from './_Layout';
// TODO: move this to UI, copied from Inspector
const Pill = tw.span`mt-1 inline border border-transparent px-0.5 text-[9px] font-medium shadow shadow-app-shade/5 bg-app-selected rounded text-ink-dull`;
type DropItemProps = {
// TODO: remove optionals when dummy data is removed (except for icon)
name?: string;
connectionType?: 'lan' | 'bluetooth' | 'usb' | 'p2p' | 'cloud';
receivingNodeOsType?: Omit<OperatingSystem, 'unknown'>;
} & ({ image: string } | { icon?: Icon } | { brandIcon: string });
function DropItem(props: DropItemProps) {
let icon;
if ('image' in props) {
icon = <img className="rounded-full" src={props.image} alt={props.name} />;
} else if ('brandIcon' in props) {
let brandIconSrc;
switch (props.brandIcon) {
case 'google-drive':
brandIconSrc = GoogleDrive;
break;
case 'icloud':
brandIconSrc = iCloud;
break;
case 'mega':
brandIconSrc = Mega;
break;
}
if (brandIconSrc) {
icon = (
<div className="flex items-center justify-center h-full p-3">
<img className="rounded-full " src={brandIconSrc} alt={props.name} />
</div>
);
}
} else {
//
const Icon = props.icon || User;
icon = <Icon className={clsx('w-8 h-8 m-3', !props.name && 'opacity-20')} />;
}
return (
<div
className={clsx(classes.honeycombItem, 'overflow-hidden bg-app-box/20 hover:bg-app-box/50')}
>
<div className="relative flex flex-col items-center justify-center w-full h-full group ">
<SubtleButtonContainer className="absolute left-[12px] top-[55px]">
<SubtleButton icon={Star} />
</SubtleButtonContainer>
<div className="rounded-full w-14 h-14 bg-app-button">{icon}</div>
<SubtleButtonContainer className="absolute right-[12px] top-[55px] rotate-90">
<SubtleButton />
</SubtleButtonContainer>
{props.name && <span className="mt-1 text-xs font-medium">{props.name}</span>}
<div className="flex flex-row space-x-1">
{props.receivingNodeOsType && <Pill>{props.receivingNodeOsType}</Pill>}
{props.connectionType && (
<Pill
className={clsx(
'!text-white uppercase',
props.connectionType === 'lan' && 'bg-green-500',
props.connectionType === 'p2p' && 'bg-blue-500'
)}
>
{props.connectionType}
</Pill>
)}
</div>
</div>
</div>
);
}
export default function SpacedropScreen() {
const searchRef = useRef<HTMLInputElement>(null);
return (
<ScreenContainer
dragRegionChildren={
<div className="flex flex-row items-center justify-center w-full h-8 pt-3">
<SearchBar className="ml-[13px]" ref={searchRef} />
{/* <Button variant="outline">Add</Button> */}
</div>
}
className={classes.honeycombOuter}
>
<div className={clsx(classes.honeycombContainer, 'mt-8')}>
<DropItem
name="Jamie's MacBook Pro"
receivingNodeOsType="macOs"
connectionType="lan"
icon={Laptop}
/>
<DropItem
name="Jamie's iPhone"
receivingNodeOsType="iOS"
connectionType="lan"
icon={DeviceMobile}
/>
<DropItem
name="Titan NAS"
receivingNodeOsType="linux"
connectionType="p2p"
icon={HardDrives}
/>
<DropItem
name="Jamie's iPad"
receivingNodeOsType="iOS"
connectionType="lan"
icon={DeviceMobile}
/>
<DropItem name="Jamie's Google Drive" brandIcon="google-drive" connectionType="cloud" />
<DropItem name="Jamie's iCloud" brandIcon="icloud" connectionType="cloud" />
<DropItem name="Mega" brandIcon="mega" connectionType="cloud" />
<DropItem
name="maxichrome"
image="https://github.com/maxichrome.png"
connectionType="p2p"
/>
<DropItem
name="Brendan Alan"
image="https://github.com/brendonovich.png"
connectionType="p2p"
/>
<DropItem
name="Oscar Beaumont"
image="https://github.com/oscartbeaumont.png"
connectionType="p2p"
/>
<DropItem name="Polar" image="https://github.com/polargh.png" connectionType="p2p" />
<DropItem
name="Andrew Haskell"
image="https://github.com/andrewtechx.png"
connectionType="p2p"
/>
</div>
</ScreenContainer>
);
}

View file

@ -1,9 +1,10 @@
import { ScreenHeading } from '@sd/ui';
import { ScreenContainer } from './_Layout';
export default function SpacesScreen() {
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col p-5">
<ScreenContainer>
<ScreenHeading>Spaces</ScreenHeading>
</div>
</ScreenContainer>
);
}

View file

@ -0,0 +1,19 @@
import clsx from 'clsx';
import { PropsWithChildren, ReactNode, createContext } from 'react';
import DragRegion from '~/components/layout/DragRegion';
export function ScreenContainer(
props: PropsWithChildren & { className?: string; dragRegionChildren?: ReactNode }
) {
return (
<div
className={clsx(
'custom-scroll page-scroll app-background flex h-screen w-full flex-col',
props.className
)}
>
<DragRegion>{props.dragRegionChildren}</DragRegion>
<div className="flex h-screen w-full flex-col p-5 pt-0">{props.children}</div>
</div>
);
}

View file

@ -11,11 +11,12 @@ const screens: RouteObject[] = [
{ path: 'media', element: lazyEl(() => import('./Media')) },
{ path: 'spaces', element: lazyEl(() => import('./Spaces')) },
{ path: 'debug', element: lazyEl(() => import('./Debug')) },
{ path: 'spacedrop', element: lazyEl(() => import('./Spacedrop')) },
{ path: 'location/:id', element: lazyEl(() => import('./LocationExplorer')) },
{ path: 'tag/:id', element: lazyEl(() => import('./TagExplorer')) },
{
path: 'settings',
element: lazyEl(() => import('./settings/Layout')),
element: lazyEl(() => import('./settings/_Layout')),
children: settingsScreens
},
{ path: '*', element: lazyEl(() => import('./NotFound')) }

View file

@ -1,7 +1,7 @@
import { Suspense } from 'react';
import { Outlet } from 'react-router';
export default function SettingsScreen() {
export default function SettingsSubPageScreen() {
return (
<div className="app-background flex w-full flex-row">
<div className="w-full">

View file

@ -2,7 +2,7 @@ import { Suspense } from 'react';
import { Outlet } from 'react-router';
import { SettingsSidebar } from '~/components/settings/SettingsSidebar';
export default function SettingsScreen() {
export default function SettingsScreenContainer() {
return (
<div className="app-background flex w-full flex-row">
<SettingsSidebar />

View file

@ -1,5 +1,5 @@
import { MagnifyingGlass } from 'phosphor-react';
import { Button, Card, GridLayout, Input } from '@sd/ui';
import { Button, Card, GridLayout, Input, SearchInput } from '@sd/ui';
import { SettingsContainer } from '~/components/settings/SettingsContainer';
import { SettingsHeader } from '~/components/settings/SettingsHeader';
@ -63,12 +63,7 @@ export default function ExtensionSettings() {
<SettingsHeader
title="Extensions"
description="Install extensions to extend the functionality of this client."
rightArea={
<div className="relative mt-6">
<MagnifyingGlass className="text-gray-350 absolute top-[8px] left-[11px] h-auto w-[18px]" />
<Input className="w-56 !p-0.5 !pl-9" placeholder="Search extensions" />
</div>
}
rightArea={<SearchInput outerClassnames="mt-1.5" placeholder="Search extensions" />}
/>
<GridLayout>

View file

@ -1,26 +1,30 @@
import Logo from '@sd/assets/images/logo.png';
import { useBridgeQuery } from '@sd/client';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
import { SettingsContainer } from '~/components/settings/SettingsContainer';
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
export default function AboutSpacedrive() {
const buildInfo = useBridgeQuery(['buildInfo']);
const os = useOperatingSystem();
const currentPlatformNiceName =
os === 'browser' ? 'Web' : os == 'macOS' ? os : os.charAt(0).toUpperCase() + os.slice(1);
return (
<SettingsContainer>
<SettingsHeader
title="Spacedrive"
description={
<div className="flex flex-col">
<span>The file manager from the future.</span>
<span className="text-ink-faint/80 mt-2 text-xs">
v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
</span>
</div>
}
>
<img src={Logo} className="mr-8 w-[88px]" />
</SettingsHeader>
<div className="flex flex-row items-center">
<img src={Logo} className="w-[88px] h-[88px] mr-8" />
<div className="flex flex-col">
<h1 className="text-2xl font-bold">
Spacedrive {os !== 'unknown' && <>for {currentPlatformNiceName}</>}
</h1>
<span className="mt-1 text-sm text-ink-dull">The file manager from the future.</span>
<span className="mt-1 text-xs text-ink-faint/80">
v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
</span>
</div>
</div>
</SettingsContainer>
);
}

View file

@ -31,11 +31,15 @@ export default function LibraryGeneralSettings() {
<div className="flex flex-row space-x-5 pb-3">
<div className="flex grow flex-col">
<span className="mb-1 text-sm font-medium">Name</span>
<Input {...form.register('name', { required: true })} defaultValue="My Default Library" />
<Input
size="md"
{...form.register('name', { required: true })}
defaultValue="My Default Library"
/>
</div>
<div className="flex grow flex-col">
<span className="mb-1 text-sm font-medium">Description</span>
<Input {...form.register('description')} placeholder="" />
<Input size="md" {...form.register('description')} placeholder="" />
</div>
</div>

View file

@ -1,7 +1,7 @@
import { MagnifyingGlass } from 'phosphor-react';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { LocationCreateArgs } from '@sd/client';
import { Button, Input, dialogManager } from '@sd/ui';
import { Button, Input, SearchInput, dialogManager } from '@sd/ui';
import AddLocationDialog from '~/components/dialog/AddLocationDialog';
import LocationListItem from '~/components/location/LocationListItem';
import { SettingsContainer } from '~/components/settings/SettingsContainer';
@ -20,14 +20,11 @@ export default function LocationSettings() {
description="Manage your storage locations."
rightArea={
<div className="flex flex-row items-center space-x-5">
<div className="relative hidden lg:block">
<MagnifyingGlass className="text-gray-350 absolute top-[8px] left-[11px] h-auto w-[18px]" />
<Input className="!p-0.5 !pl-9" placeholder="Search locations" />
</div>
<SearchInput placeholder="Search locations" />
<Button
variant="accent"
size="sm"
size="md"
onClick={() => {
if (platform.platform === 'web') {
dialogManager.create((dp) => <AddLocationDialog {...dp} />);

View file

@ -89,7 +89,7 @@ export default function TagsSettings() {
</span>
<div className="relative">
<PopoverPicker
className="!absolute left-[9px] top-[-3px]"
className="!absolute left-[9px] -top-[5px]"
{...updateForm.register('color')}
/>
<Input className="w-28 pl-[40px]" {...updateForm.register('color')} />
@ -163,7 +163,7 @@ function CreateTagDialog(props: UseDialogProps) {
ctaLabel="Create"
>
<div className="relative mt-3 ">
<PopoverPicker className="!absolute left-[9px] top-[-3px]" {...form.register('color')} />
<PopoverPicker className="!absolute left-[9px] -top-[5px]" {...form.register('color')} />
<Input
{...form.register('name', { required: true })}
className="w-full pl-[40px]"

View file

@ -34,7 +34,8 @@ const styles = cva(
},
size: {
icon: '!p-1',
md: 'text-md py-1 px-3 font-medium',
lg: 'py-1.5 px-3 text-md font-medium',
md: 'py-1.5 px-2.5 text-sm font-medium',
sm: 'py-1 px-2 text-sm font-medium'
},
variant: {
@ -60,7 +61,7 @@ const styles = cva(
}
},
defaultVariants: {
size: 'md',
size: 'sm',
variant: 'default'
}
}

View file

@ -1,6 +1,6 @@
import { VariantProps, cva } from 'class-variance-authority';
import clsx from 'clsx';
import { Eye, EyeSlash } from 'phosphor-react';
import { Eye, EyeSlash, MagnifyingGlass } from 'phosphor-react';
import { PropsWithChildren, forwardRef, useState } from 'react';
import { Button } from './Button';
@ -12,8 +12,8 @@ export type TextareaProps = InputBaseProps & React.ComponentProps<'textarea'>;
const styles = cva(
[
'rounded-md border px-3 py-1 text-sm leading-7',
'shadow-sm outline-none transition-all focus:ring-2'
'px-3 text-sm rounded-md border leading-7',
'outline-none shadow-sm focus:ring-2 transition-all'
],
{
variants: {
@ -24,8 +24,8 @@ const styles = cva(
]
},
size: {
sm: 'text-sm',
md: 'text-base'
sm: 'text-sm py-0.5',
md: 'text-sm py-1'
}
},
defaultVariants: {
@ -40,6 +40,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
)
);
export const SearchInput = forwardRef<HTMLInputElement, InputProps & { outerClassnames?: string }>(
({ variant, size, className, outerClassnames, ...props }, ref) => (
<div className={clsx('relative', outerClassnames)}>
<MagnifyingGlass className="text-gray-350 absolute top-[8px] left-[11px] h-auto w-[18px]" />
<Input
{...props}
ref={ref}
className={clsx(styles({ variant, size, className }), '!p-0.5 !pl-9')}
/>
</div>
)
);
export const TextArea = ({ size, variant, ...props }: TextareaProps) => {
return <textarea {...props} className={clsx(styles({ size, variant }), props.className)} />;
};

View file

@ -2,4 +2,4 @@ import { tw } from './utils';
export const CategoryHeading = tw.h3`text-xs font-semibold text-ink-dull`;
export const ScreenHeading = tw.h3`text-2xl font-bold`;
export const ScreenHeading = tw.h3`ml-1 text-xl font-medium`;

View file

@ -28,7 +28,7 @@
.mask-fade-out {
// -webkit-mask-image: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));
mask-image: linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100px);
mask-image: linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 50px);
}
.cool-shadow {

View file

@ -8394,7 +8394,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12
magic-string: 0.26.7
react-refresh: 0.14.0
vite: 4.0.4_sass@1.57.1
vite: 4.0.4_@types+node@18.11.18
transitivePeerDependencies:
- supports-color
@ -21023,7 +21023,7 @@ packages:
dependencies:
'@rollup/pluginutils': 5.0.2
'@svgr/core': 6.5.1
vite: 4.0.4_sass@1.57.1
vite: 4.0.4_@types+node@18.11.18
transitivePeerDependencies:
- rollup
- supports-color
@ -21119,7 +21119,6 @@ packages:
rollup: 3.10.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/vite/4.0.4_ovmyjmuuyckt3r3gpaexj2onji:
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==}
@ -21187,6 +21186,7 @@ packages:
sass: 1.57.1
optionalDependencies:
fsevents: 2.3.2
dev: true
/vlq/1.0.1:
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}