mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-03 02:23:27 +00:00
[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:
parent
7b739d0b33
commit
3f44d6f521
|
@ -48,6 +48,7 @@ sharma
|
|||
skippable
|
||||
spacedrive
|
||||
spacedriveapp
|
||||
spacetunnel
|
||||
specta
|
||||
storedkey
|
||||
stringly
|
||||
|
|
|
@ -19,6 +19,11 @@ export default defineConfig({
|
|||
}
|
||||
})
|
||||
],
|
||||
css: {
|
||||
modules: {
|
||||
localsConvention: 'camelCaseOnly'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: [relativeAliasResolver]
|
||||
},
|
||||
|
|
|
@ -17,6 +17,11 @@ export default defineConfig({
|
|||
md({ mode: [Mode.REACT] }),
|
||||
visualizer()
|
||||
],
|
||||
css: {
|
||||
modules: {
|
||||
localsConvention: 'camelCaseOnly'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
33
core/src/object/tag.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
packages/assets/images/Compressed.png
Normal file
BIN
packages/assets/images/Compressed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
packages/assets/images/Encrypted.png
Normal file
BIN
packages/assets/images/Encrypted.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
BIN
packages/assets/images/GoogleDrive.png
Normal file
BIN
packages/assets/images/GoogleDrive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
packages/assets/images/Mega.png
Normal file
BIN
packages/assets/images/Mega.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
BIN
packages/assets/images/iCloud.png
Normal file
BIN
packages/assets/images/iCloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)} />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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"
|
||||
|
|
10
packages/interface/src/components/layout/DragRegion.tsx
Normal file
10
packages/interface/src/components/layout/DragRegion.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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) => (
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
)}
|
||||
|
|
14
packages/interface/src/components/primitive/SubtleButton.tsx
Normal file
14
packages/interface/src/components/primitive/SubtleButton.tsx
Normal 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`;
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
25
packages/interface/src/screens/Spacedrop.module.scss
Normal file
25
packages/interface/src/screens/Spacedrop.module.scss
Normal 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));
|
||||
}
|
151
packages/interface/src/screens/Spacedrop.tsx
Normal file
151
packages/interface/src/screens/Spacedrop.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
19
packages/interface/src/screens/_Layout.tsx
Normal file
19
packages/interface/src/screens/_Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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')) }
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 />
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -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]"
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)} />;
|
||||
};
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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==}
|
||||
|
|
Loading…
Reference in a new issue