Merge branch 'eng-259-identify-objectkind'

Conflicts:
	apps/landing/package.json
	apps/landing/vite.config.ts
	packages/interface/src/AppLayout.tsx
	packages/interface/src/components/explorer/Explorer.tsx
	packages/interface/src/components/explorer/ExplorerTopBar.tsx
	packages/ui/src/ContextMenu.tsx
	pnpm-lock.yaml
This commit is contained in:
Jamie Pine
2022-10-20 22:49:15 -07:00
parent db91ba80f9
commit 237c5a12af
29 changed files with 339 additions and 212 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -22,6 +22,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "1.1.1",
"@tauri-apps/tauricon": "github:tauri-apps/tauricon",
"@types/babel-core": "^6.25.7",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",

BIN
apps/mobile/pnpm-lock.yaml generated Normal file
View File

Binary file not shown.

View File

@@ -29,7 +29,7 @@ serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4.22", features = ["serde"] }
serde_json = "1.0"
futures = "0.3"
int-enum = "0.4.0"
int-enum = "0.5.0"
rmp = "^0.8.11"
rmp-serde = "^1.1.1"
blake3 = "1.3.1"

View File

@@ -3,9 +3,10 @@ use crate::{
library::LibraryContext,
prisma::{file_path, location, object},
};
use chrono::{DateTime, FixedOffset};
use int_enum::IntEnum;
use prisma_client_rust::{prisma_models::PrismaValue, raw::Raw, Direction};
use sd_file_ext::{extensions::Extension, kind::ObjectKind};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
@@ -131,8 +132,8 @@ impl StatefulJob for FileIdentifierJob {
) -> Result<(), JobError> {
let db = ctx.library_ctx().db;
// link file_path ids to a CreateFile struct containing unique file data
let mut chunk: HashMap<i32, CreateFile> = HashMap::new();
// link file_path ids to a CreateObject struct containing unique file data
let mut chunk: HashMap<i32, CreateObject> = HashMap::new();
let mut cas_lookup: HashMap<String, i32> = HashMap::new();
let data = state
@@ -222,6 +223,7 @@ impl StatefulJob for FileIdentifierJob {
PrismaValue::String(object.cas_id.clone()),
PrismaValue::Int(object.size_in_bytes),
PrismaValue::DateTime(object.date_created),
PrismaValue::Int(object.kind.int_value() as i64),
]);
}
@@ -230,9 +232,9 @@ impl StatefulJob for FileIdentifierJob {
let created_files: Vec<FileCreated> = db
._query_raw(Raw::new(
&format!(
"INSERT INTO object (cas_id, size_in_bytes, date_created) VALUES {}
"INSERT INTO object (cas_id, size_in_bytes, date_created, kind) VALUES {}
ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id",
vec!["({}, {}, {})"; new_objects.len()].join(",")
vec!["({}, {}, {}, {})"; new_objects.len()].join(",")
),
values,
))
@@ -360,10 +362,11 @@ async fn get_orphan_file_paths(
}
#[derive(Deserialize, Serialize, Debug)]
struct CreateFile {
struct CreateObject {
pub cas_id: String,
pub size_in_bytes: i64,
pub date_created: DateTime<FixedOffset>,
pub kind: ObjectKind,
}
#[derive(Deserialize, Serialize, Debug)]
@@ -375,7 +378,7 @@ struct FileCreated {
async fn assemble_object_metadata(
location_path: impl AsRef<Path>,
file_path: &file_path::Data,
) -> Result<CreateFile, io::Error> {
) -> Result<CreateObject, io::Error> {
let path = location_path
.as_ref()
.join(file_path.materialized_path.as_str());
@@ -384,7 +387,19 @@ async fn assemble_object_metadata(
let metadata = fs::metadata(&path).await?;
// let date_created: DateTime<Utc> = metadata.created().unwrap().into();
// derive Object kind
let object_kind: ObjectKind = match path.extension() {
Some(ext) => match ext.to_str() {
Some(ext) => {
let mut file = std::fs::File::open(&path).unwrap();
let resolved_ext = Extension::resolve_conflicting(ext, &mut file, true);
resolved_ext.map(Into::into).unwrap_or(ObjectKind::Unknown)
}
None => ObjectKind::Unknown,
},
None => ObjectKind::Unknown,
};
let size = metadata.len();
@@ -398,9 +413,10 @@ async fn assemble_object_metadata(
}
};
Ok(CreateFile {
Ok(CreateObject {
cas_id,
size_in_bytes: size as i64,
date_created: file_path.date_created,
kind: object_kind,
})
}

View File

@@ -3,8 +3,8 @@
use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, Copy, IntEnum)]
#[repr(u8)]
#[repr(i32)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, IntEnum)]
pub enum ObjectKind {
// A file that can not be identified by the indexer
Unknown = 0,

73
crates/sync/docs/HLC.md Normal file
View File

@@ -0,0 +1,73 @@
```rust
pub fn update_with_timestamp(&self, timestamp: &Timestamp) -> Result<(), String> {
let mut now = (self.clock)();
now.0 &= LMASK;
let msg_time = timestamp.get_time();
if *msg_time > now && *msg_time - now > self.delta {
let err_msg = format!(
"incoming timestamp from {} exceeding delta {}ms is rejected: {} vs. now: {}",
timestamp.get_id(),
self.delta.to_duration().as_millis(),
msg_time,
now
);
warn!("{}", err_msg);
Err(err_msg)
} else {
let mut last_time = lock!(self.last_time);
let max_time = cmp::max(cmp::max(now, *msg_time), *last_time);
if max_time == now {
*last_time = now;
} else if max_time == *msg_time {
*last_time = *msg_time + 1;
} else {
*last_time += 1;
}
Ok(())
}
}
```
```javascript
Timestamp.recv = function (msg) {
if (!clock) {
return null;
}
var now = Date.now();
var msg_time = msg.millis();
var msg_time = msg.counter();
if (msg_time - now > config.maxDrift) {
throw new Timestamp.ClockDriftError();
}
var last_time = clock.timestamp.millis();
var last_time = clock.timestamp.counter();
var max_time = Math.max(Math.max(last_time, now), msg_time);
var last_time =
max_time === last_time && lNew === msg_time
? Math.max(last_time, msg_time) + 1
: max_time === last_time
? last_time + 1
: max_time === msg_time
? msg_time + 1
: 0;
// 3.
if (max_time - phys > config.maxDrift) {
throw new Timestamp.ClockDriftError();
}
if (last_time > MAX_COUNTER) {
throw new Timestamp.OverflowError();
}
clock.timestamp.setMillis(max_time);
clock.timestamp.setCounter(last_time);
return new Timestamp(clock.timestamp.millis(), clock.timestamp.counter(), clock.timestamp.node());
};
```

View File

@@ -4,85 +4,15 @@ index: 12
# Sync
Spacedrive synchronizes data using a combination of master-slave replication and last-write-wins CRDTs,
with the synchronization method encoded into the Prisma schema using [record type attributes](#record-types).
Spacedrive synchronizes library data in realtime across the distributed network of Nodes.
In the cases where LWW CRDTs are used,
conflicts are resolved using a [Hybrid Logical Clock](https://github.com/atolab/uhlc-rs)
to determine the ordering of events.
Using a Unique Hybrid Logical Clock for distributed time synchronization.
We would be remiss to not credit credit [Actual Budget](https://actualbudget.com/)
with many of the CRDT concepts used in Spacedrive's sync system.
A combination of several property level CRDT types:
## Record Types
- **Local data** - migrations, statistics, sync events
- **Owned data** - locations, paths, volumes
- **Shared data** - objects, tags, spaces, jobs
- **Relationship data** - many to many tables
All data in a library conforms to one of the following types.
Each type uses a different strategy for syncing.
### Local Records
Local records exist entirely outside of the sync system.
They don't have Sync IDs and never leave the node they were created on.
Used for Nodes, Statistics, and Sync Events.
`@local`
### Owned Records
Owned records are only ever modified by the node they are created by,
so they can be synced in a master-slave fashion.
The creator of an owned record dictates the state of the record to other nodes,
who will simply accept new changes without considering conflicts.
File paths are owned records since they only exist on one node,
and that node can inform all other nodes about the correct state of the paths.
Used for Locations, Paths, and Volumes.
`@owned(owner: String, id?: String)`
- `owner` - Field that identifies the owner of this model.
If a scalar, will directly use that value in sync operations.
If a relation, the Sync ID of the related model will be resolved for sync operations.
- `id` - Scalar field to override the default Sync ID.
### Shared Records
Shared records encompass most data synced in the CRDT fashion.
Updates are applied per-field using a last-write-wins strategy.
Used for Objects, Tags, Spaces, and Jobs.
`@shared(create: SharedCreateType, id?: String)`
- `id` - Scalar field to override the default Sync ID.
- `create` - How the model should be created.
- `Unique` (default): Model can be created with many required arguemnts,
but ID provided _must_ be unique across all nodes.
Useful for Tags since their IDs are non-deterministic.
- `Atomic`: Require the model to have no required arguments apart from ID and apply all create arguments as atomic updates.
Necessary for models with the same ID that can be created on multiple nodes.
Useful for Objects since their ID is dependent on their content,
and could be the same across nodes.
### Relation Records
Similar to shared records, but represent a many-to-many relation between two records.
Sync ID is the combination of `item` and `group` Sync IDs.
Used for TagOnFile and FileInSpace.
`@relation(item: String, group: String)`
- `item` - Field that identifies the item that the relation is connecting.
Similar to the `owner` argument of `@owned`.
- `group` - Field that identifies the group that the item should be connected to.
Similar to the `owner` argument of `@owned`.
## Other Prisma Attributes
`@node`
Indicates that a relation field should be set to the current node.
This could be done manually,
but `@node` allows `node_id` fields to be resolved from the `node_id` field of a `CRDTOperation`,
saving on bandwidth
Built in Rust on top of Prisma, it uses the schema file to determine these sync rules.

View File

@@ -16,6 +16,7 @@ const state = {
gridItemSize: 100,
listItemSize: 40,
selectedRowIndex: 1,
tagAssignMode: false,
showInspector: true,
multiSelectIndexes: [] as number[],
contextMenuObjectId: null as number | null,

View File

@@ -46,6 +46,7 @@
"react-router": "6.4.2",
"react-router-dom": "6.4.2",
"rooks": "^5.14.0",
"tailwind-styled-components": "2.1.7",
"tailwindcss": "^3.1.8",
"use-count-up": "^3.0.1",
"use-debounce": "^8.0.4",

View File

@@ -21,7 +21,7 @@ export function AppLayout() {
className={clsx(
// App level styles
'flex flex-row overflow-hidden text-ink select-none cursor-default',
os === 'macOS' && 'rounded-xl',
os === 'macOS' && 'rounded-xl has-blur-effects',
os !== 'browser' && os !== 'windows' && 'border border-app-divider'
)}
onContextMenu={(e) => {

View File

@@ -1,4 +1,5 @@
import { ExplorerData, rspc, useCurrentLibrary, useExplorerStore } from '@sd/client';
import { useEffect, useState } from 'react';
import { Inspector } from '../explorer/Inspector';
import ExplorerContextMenu from './ExplorerContextMenu';
@@ -13,6 +14,18 @@ export default function Explorer(props: Props) {
const expStore = useExplorerStore();
const { library } = useCurrentLibrary();
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);
useEffect(() => {
setSeparateTopBar((oldValue) => {
const newValue = Object.values(scrollSegments).some((val) => val >= 5);
if (newValue !== oldValue) return newValue;
return oldValue;
});
}, [scrollSegments]);
rspc.useSubscription(['jobs.newThumbnail', { library_id: library!.uuid, arg: null }], {
onData: (cas_id) => {
expStore.addNewThumbnail(cas_id);
@@ -23,15 +36,36 @@ export default function Explorer(props: Props) {
<div className="relative">
<ExplorerContextMenu>
<div className="relative flex flex-col w-full bg-app">
<TopBar />
<TopBar showSeparator={separateTopBar} />
<div className="relative flex flex-row w-full max-h-full ">
<div className="relative flex flex-row w-full max-h-full app-bg">
{props.data && (
<VirtualizedList data={props.data.items || []} context={props.data.context} />
<VirtualizedList
data={props.data.items || []}
context={props.data.context}
onScroll={(y) => {
setScrollSegments((old) => {
return {
...old,
mainList: y
};
});
}}
/>
)}
{expStore.showInspector && (
<div className="flex min-w-[260px] max-w-[260px]">
<Inspector
onScroll={(e) => {
const y = (e.target as HTMLElement).scrollTop;
setScrollSegments((old) => {
return {
...old,
inspector: y
};
});
}}
key={props.data?.items[expStore.selectedRowIndex]?.id}
data={props.data?.items[expStore.selectedRowIndex]}
/>

View File

@@ -1,14 +1,7 @@
import {
getExplorerStore,
useExplorerStore,
useLibraryMutation,
useLibraryQuery
} from '@sd/client';
import { getExplorerStore, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { ContextMenu as CM } from '@sd/ui';
import {
ArrowBendUpRight,
FilePlus,
FileX,
LockSimple,
Package,
Plus,
@@ -18,7 +11,6 @@ import {
TrashSimple
} from 'phosphor-react';
import { PropsWithChildren } from 'react';
import { useSnapshot } from 'valtio';
const AssignTagMenuItems = (props: { objectId: number }) => {
const tags = useLibraryQuery(['tags.list'], { suspense: true });
@@ -28,12 +20,13 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
return (
<>
{tags.data?.map((tag) => {
{tags.data?.map((tag, index) => {
const active = !!tagsForObject.data?.find((t) => t.id === tag.id);
return (
<CM.Item
key={tag.id}
keybind={`${index + 1}`}
onClick={(e) => {
e.preventDefault();
if (props.objectId === null) return;
@@ -66,18 +59,18 @@ export default function ExplorerContextMenu(props: PropsWithChildren) {
return (
<div className="relative">
<CM.ContextMenu trigger={props.children}>
<CM.Item label="Open" />
<CM.Item label="Open" keybind="⌘O" />
<CM.Item label="Open with..." />
<CM.Separator />
<CM.Item label="Quick view" />
<CM.Item label="Open in Finder" />
<CM.Item label="Quick view" keybind="␣" />
<CM.Item label="Open in Finder" keybind="⌘Y" />
<CM.Separator />
<CM.Item label="Rename" />
<CM.Item label="Duplicate" />
<CM.Item label="Duplicate" keybind="⌘D" />
<CM.Separator />
@@ -103,8 +96,8 @@ export default function ExplorerContextMenu(props: PropsWithChildren) {
</CM.SubMenu>
)}
<CM.SubMenu label="More actions..." icon={Plus}>
<CM.Item label="Encrypt" icon={LockSimple} />
<CM.Item label="Compress" icon={Package} />
<CM.Item label="Encrypt" icon={LockSimple} keybind="⌘E" />
<CM.Item label="Compress" icon={Package} keybind="⌘B" />
<CM.SubMenu label="Convert to" icon={ArrowBendUpRight}>
<CM.Item label="PNG" />
<CM.Item label="WebP" />
@@ -114,7 +107,7 @@ export default function ExplorerContextMenu(props: PropsWithChildren) {
<CM.Separator />
<CM.Item icon={Trash} label="Delete" variant="danger" />
<CM.Item icon={Trash} label="Delete" variant="danger" keybind="⌘DEL" />
</CM.ContextMenu>
</div>
);

View File

@@ -1,4 +1,5 @@
import { ChevronLeftIcon, ChevronRightIcon, TagIcon } from '@heroicons/react/24/outline';
import { KeyIcon as KeyIconSolid, TagIcon as TagIconSolid } from '@heroicons/react/24/solid';
import {
OperatingSystem,
getExplorerStore,
@@ -55,7 +56,7 @@ const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
'rounded-r-none rounded-l-none': group && !left && !right,
'rounded-r-none': group && left,
'rounded-l-none': group && right,
'dark:bg-gray-500': active
'dark:bg-gray-550': active
},
className
)}
@@ -94,7 +95,7 @@ const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRe
else if (forwardedRef) forwardedRef.current = el;
}}
placeholder="Search"
className="peer w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-450 bg-[#F6F2F6] border border-gray-50 shadow-md dark:bg-gray-600 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-550 dark:focus:bg-gray-800 transition-all"
className="peer w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-450 bg-[#F6F2F6] border border-gray-50 shadow dark:shadow-gray-900/30 dark:bg-gray-600/70 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-550 dark:focus:bg-gray-800 transition-all"
{...searchField}
/>
@@ -117,7 +118,9 @@ const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRe
);
});
export type TopBarProps = DefaultProps;
export type TopBarProps = DefaultProps & {
showSeparator?: boolean;
};
export const TopBar: React.FC<TopBarProps> = (props) => {
const platform = useOperatingSystem(false);
@@ -211,9 +214,16 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
<>
<div
data-tauri-drag-region
className="flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center overflow-hidden rounded-tl-md"
// Backdrop blur was removed
// but the explorer still resides under the top bar
// in case you wanna turn it back on
// honestly its just work to revert
className={clsx(
'flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center border-transparent border-b app-bg overflow-hidden rounded-tl-md transition-[background-color] transition-[border-color] duration-250 ease-out',
props.showSeparator && 'top-bar-blur'
)}
>
<div className="flex ">
<div className="flex">
<Tooltip label="Navigate back">
<TopBarButton icon={ChevronLeftIcon} onClick={() => navigate(-1)} />
</Tooltip>
@@ -275,7 +285,11 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
</div>
</OverlayPanel>
<Tooltip label="Tag Assign Mode">
<TopBarButton icon={TagIcon} />
<TopBarButton
onClick={() => (getExplorerStore().tagAssignMode = !store.tagAssignMode)}
active={store.tagAssignMode}
icon={store.tagAssignMode ? TagIconSolid : TagIcon}
/>
</Tooltip>
<Tooltip label="Refresh">
<TopBarButton icon={ArrowsClockwise} />

View File

@@ -40,7 +40,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
className={clsx(
'border-2 border-transparent rounded-lg text-center mb-1 active:translate-y-[1px]',
{
'bg-gray-50 dark:bg-gray-750': selected
'bg-gray-50 dark:bg-gray-750/50': selected
}
)}
>
@@ -51,7 +51,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
>
<FileThumb
className={clsx(
'border-4 border-gray-250 shadow-md shadow-gray-750 object-cover max-w-full max-h-full w-auto overflow-hidden',
'border-4 border-gray-250 shadow shadow-gray-750 object-cover max-w-full max-h-full w-auto overflow-hidden',
isVid && 'border-gray-950 rounded border-x-0 border-y-[9px]'
)}
data={data}

View File

@@ -9,6 +9,7 @@ import dayjs from 'dayjs';
import { Link } from 'phosphor-react';
import { useEffect, useState } from 'react';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import FileThumb from './FileThumb';
import { Divider } from './inspector/Divider';
@@ -17,12 +18,14 @@ import { MetaItem } from './inspector/MetaItem';
import Note from './inspector/Note';
import { isObject } from './utils';
interface Props {
interface Props extends DefaultProps<HTMLDivElement> {
context?: ExplorerContext;
data?: ExplorerItem;
}
export const Inspector = (props: Props) => {
const { context, data, ...elementProps } = props;
const { data: types } = useQuery(
['_file-types'],
() => import('../../constants/file-types.json')
@@ -49,7 +52,10 @@ export const Inspector = (props: Props) => {
const isVid = isVideo(props.data?.extension || '');
return (
<div className="-mt-[50px] pt-[55px] pl-1.5 pr-1 w-full h-screen overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
<div
{...elementProps}
className="-mt-[50px] pt-[55px] pl-1.5 pr-1 w-full h-screen overflow-x-hidden custom-scroll inspector-scroll pb-[55px]"
>
{!!props.data && (
<>
<div className="flex bg-black items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg ">

View File

@@ -1,7 +1,7 @@
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client';
import { ExplorerContext, ExplorerItem } from '@sd/client';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { UIEventHandler, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useKey, useOnWindowResize } from 'rooks';
@@ -15,9 +15,10 @@ const GRID_TEXT_AREA_HEIGHT = 25;
interface Props {
context: ExplorerContext;
data: ExplorerItem[];
onScroll?: (posY: number) => void;
}
export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) => {
const scrollRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
@@ -45,6 +46,19 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
? explorerStore.gridItemSize + GRID_TEXT_AREA_HEIGHT
: explorerStore.listItemSize;
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const onElementScroll = (event: Event) => {
onScroll?.((event.target as HTMLElement).scrollTop);
};
el.addEventListener('scroll', onElementScroll);
return () => el.removeEventListener('scroll', onElementScroll);
}, [scrollRef, onScroll]);
const rowVirtualizer = useVirtualizer({
count: amountOfRows,
getScrollElement: () => scrollRef.current,
@@ -109,7 +123,7 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
ref={innerRef}
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
marginTop: `${TOP_BAR_HEIGHT}px`
marginTop: `${TOP_BAR_HEIGHT + 10}px`
}}
className="relative w-full"
>

View File

@@ -1,7 +1,14 @@
import { EyeIcon, FolderIcon, PhotoIcon, XMarkIcon } from '@heroicons/react/24/outline';
import {
EyeIcon,
FingerPrintIcon,
FolderIcon,
PhotoIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { QuestionMarkCircleIcon } from '@heroicons/react/24/solid';
import { useLibraryQuery } from '@sd/client';
import { JobReport } from '@sd/client';
import { Button } from '@sd/ui';
import { Button, CategoryHeading } from '@sd/ui';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { ArrowsClockwise } from 'phosphor-react';
@@ -13,20 +20,26 @@ interface JobNiceData {
icon: React.FC<React.ComponentProps<'svg'>>;
}
const NiceData: Record<string, JobNiceData> = {
const getNiceData = (job: JobReport): Record<string, JobNiceData> => ({
indexer: {
name: 'Indexed location',
name: `Indexed ${numberWithCommas(job.metadata?.data?.total_paths || 0)} paths at "${
job.metadata?.data?.location_path || '?'
}"`,
icon: FolderIcon
},
thumbnailer: {
name: 'Generated thumbnails',
name: `Generated ${numberWithCommas(job.task_count)} thumbnails`,
icon: PhotoIcon
},
file_identifier: {
name: 'Identified unique files',
name: `Extracted metadata for ${numberWithCommas(job.task_count)} files`,
icon: EyeIcon
},
object_validator: {
name: `Generated ${numberWithCommas(job.task_count)} full object hashes`,
icon: FingerPrintIcon
}
};
});
const StatusColors: Record<JobReport['status'], string> = {
Running: 'text-blue-500',
@@ -44,55 +57,66 @@ function elapsed(seconds: number) {
export function JobsManager() {
const jobs = useLibraryQuery(['jobs.getHistory']);
return (
<div className="h-full">
<div className="h-full pb-10 overflow-hidden">
{/* <div className="z-10 flex flex-row w-full h-10 bg-gray-500 border-b border-gray-700 bg-opacity-30"></div> */}
<div className="z-20 flex items-center w-full h-10 px-4 border-b border-gray-500 rounded-t-md">
<CategoryHeading className="mt-1 ">Recent Jobs</CategoryHeading>
</div>
<div className="h-full mr-1 overflow-x-hidden custom-scroll inspector-scroll">
<div className="py-1 pl-2">
<div className="fixed flex items-center h-10">
<h3 className="mt-1.5 ml-2 text-md font-medium opacity-40">Recent Jobs</h3>
</div>
<div className="h-10"></div>
{jobs.data?.map((job) => {
const color = StatusColors[job.status];
const niceData = NiceData[job.name];
<div className="">
<div className="py-1">
{jobs.data?.map((job) => {
// const color = StatusColors[job.status];
const niceData = getNiceData(job)[job.name] || {
name: job.name,
icon: QuestionMarkCircleIcon
};
return (
<div
className="flex items-center px-2 py-2 border-b border-gray-500 bg-opacity-60"
key={job.id}
>
<Tooltip label={job.status}>
<niceData.icon className={clsx('w-5 mr-3', color)} />
</Tooltip>
<div className="flex flex-1 flex-col">
<span className="mt-0.5 font-semibold">{niceData.name}</span>
<div className="flex items-center opacity-60">
<span className="text-xs">
{job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
{job.seconds_elapsed
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
: 'less than a second'}
return (
<div
className="flex items-center px-2 py-2 pl-4 border-b border-gray-500 bg-opacity-60"
key={job.id}
>
<Tooltip label={job.status}>
<niceData.icon className={clsx('w-5 mr-3')} />
</Tooltip>
<div className="flex flex-col">
<span className="flex mt-0.5 items-center font-semibold truncate">
{niceData.name}
</span>
<span className="mx-1 opacity-50">&#8226;</span>
<span className="text-xs">{dayjs(job.date_created).toNow(true)} ago</span>
<div className="flex items-center">
<span className="text-xs opacity-60">
{job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
{job.seconds_elapsed
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
: 'less than a second'}
</span>
<span className="mx-1 opacity-50">&#8226;</span>
<span className="text-xs">{dayjs(job.date_created).toNow(true)} ago</span>
</div>
<span className="text-xs">{job.data}</span>
</div>
<span className="text-xs">{job.data}</span>
</div>
<div className="space-x-2">
{job.status === 'Failed' && (
<Button padding="thin" variant="gray">
<ArrowsClockwise className="w-4 h-4" />
<div className="flex-grow" />
<div className="flex flex-row space-x-2">
{job.status === 'Failed' && (
<Button className="!p-1">
<ArrowsClockwise className="w-4" />
</Button>
)}
<Button className="!p-1">
<XMarkIcon className="w-4" />
</Button>
)}
<Button padding="thin" variant="gray">
<XMarkIcon className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
})}
);
})}
</div>
</div>
</div>
</div>
);
}
function numberWithCommas(x: number) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

View File

@@ -16,7 +16,7 @@ export default function DebugScreen() {
// });
const { mutate: identifyFiles } = useLibraryMutation('jobs.identifyUniqueFiles');
return (
<div className="flex flex-col w-full h-screen custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen custom-scroll page-scroll app-bg">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
<div className="flex flex-col p-5 pt-2 space-y-5 pb-7">
<h1 className="text-lg font-bold ">Developer Debugger</h1>

View File

@@ -139,7 +139,7 @@ export default function OverviewScreen() {
console.log(overviewStats);
return (
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll app-bg">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
{/* PAGE */}

View File

@@ -1,6 +1,6 @@
export default function PhotosScreen() {
return (
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll app-bg">
<div className="flex flex-col space-y-5 pb-7">
<p className="px-5 py-3 mb-3 text-sm text-gray-400 rounded-md bg-gray-50 dark:text-gray-400 dark:bg-gray-600">
<b>Note: </b>This is a pre-alpha build of Spacedrive, many features are yet to be

View File

@@ -5,7 +5,7 @@ import { SettingsSidebar } from '../../components/settings/SettingsSidebar';
export default function SettingsScreen() {
return (
<div className="flex flex-row w-full">
<div className="flex flex-row w-full app-bg">
<SettingsSidebar />
<div className="w-full">
<div data-tauri-drag-region className="w-full h-7" />

View File

@@ -1,12 +1,16 @@
import { useBridgeQuery, usePlatform } from '@sd/client';
import { Input } from '@sd/ui';
import { Database } from 'phosphor-react';
import tw from 'tailwind-styled-components';
import Card from '../../../components/layout/Card';
import { Toggle } from '../../../components/primitive';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
const NodePill = tw.div`px-1.5 py-[2px] rounded text-xs font-medium bg-gray-500`;
const NodeSettingLabel = tw.div`mb-1 text-xs font-medium text-gray-700 dark:text-gray-100`;
export default function GeneralSettings() {
const { data: node } = useBridgeQuery(['getNode']);
@@ -20,55 +24,43 @@ export default function GeneralSettings() {
/>
<Card className="px-5 dark:bg-gray-600">
<div className="flex flex-col w-full my-2">
<div className="flex">
<div className="flex flex-row items-center justify-between">
<span className="font-semibold">Connected Node</span>
<div className="flex-grow" />
<div className="space-x-2">
<span className="px-2 py-[2px] rounded text-xs font-medium bg-gray-500">0 Peers</span>
<span className="px-1.5 py-[2px] rounded text-xs font-medium bg-primary-600">
Running
</span>
<div className="grid grid-cols-2 gap-1">
<NodePill>0 Peers</NodePill>
<NodePill className="bg-primary-600">Running</NodePill>
</div>
</div>
<hr className="mt-2 mb-4 border-gray-500 " />
<div className="flex flex-row space-x-4">
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node Name
</span>
<NodeSettingLabel>Node Name</NodeSettingLabel>
<Input value={node?.name} />
</div>
<div className="flex flex-col w-[100px]">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node Port
</span>
<div className="flex flex-col ">
<NodeSettingLabel>Node Port</NodeSettingLabel>
<Input contentEditable={false} value={node?.p2p_port || 5795} />
</div>
<div className="flex flex-col w-[295px]">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node ID
</span>
<Input contentEditable={false} value={node?.id} />
</div>
</div>
<div className="flex items-center mt-5 space-x-3">
<Toggle size="sm" value />
<span className="text-sm text-gray-200">Run daemon when app closed</span>
</div>
<div className="mt-3">
<span
<div
onClick={() => {
if (node && platform?.openLink) {
platform.openLink(node.data_path);
}
}}
className="text-xs font-medium text-gray-700 dark:text-gray-400"
className="text-xs font-medium leading-relaxed text-gray-700 dark:text-gray-400"
>
<Database className="inline w-4 h-4 mr-2 -mt-[2px]" />
<b className="mr-2">Data Folder</b>
<b className="inline mr-2 truncate ">
<Database className="inline w-4 h-4 mr-1 -mt-[2px]" /> Data Folder
</b>
<span className="select-text">{node?.data_path}</span>
</span>
</div>
</div>
</div>
</Card>

View File

@@ -7,6 +7,16 @@ body {
font-family: 'InterVariable', sans-serif;
}
.app-bg {
@apply bg-app;
}
.has-blur-effects {
.app-bg {
@apply bg-app/90;
}
}
.no-scrollbar {
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */

View File

@@ -38,8 +38,6 @@
"tailwindcss-radix": "^2.6.0"
},
"devDependencies": {
"tailwindcss": "^3.1.8",
"storybook": "^6.5.12",
"@babel/core": "^7.19.3",
"@sd/config": "workspace:*",
"@storybook/addon-actions": "^6.5.12",
@@ -62,8 +60,11 @@
"postcss-loader": "^7.0.1",
"sass": "^1.55.0",
"sass-loader": "^13.0.2",
"storybook": "^6.5.12",
"storybook-tailwind-dark-mode": "^1.0.15",
"style-loader": "^3.3.1",
"tailwind-styled-components": "2.1.7",
"tailwindcss": "^3.1.8",
"typescript": "^4.8.4"
}
}

View File

@@ -10,12 +10,12 @@ interface Props extends RadixCM.MenuContentProps {
const MENU_CLASSES = `
flex flex-col
min-w-[11rem] p-2 space-y-1
min-w-[8rem] p-1
text-left text-sm dark:text-gray-100 text-gray-800
bg-gray-50 border-gray-200 dark:bg-gray-750 dark:bg-opacity-70 backdrop-blur
bg-gray-50 border-gray-200 dark:bg-gray-650 dark:bg-opacity-80 backdrop-blur
border border-transparent dark:border-gray-550
shadow-md shadow-gray-300 dark:shadow-gray-750
select-none cursor-default rounded-lg
select-none cursor-default rounded-md
`;
export const ContextMenu = ({
@@ -37,7 +37,7 @@ export const ContextMenu = ({
};
export const Separator = () => (
<RadixCM.Separator className="mx-2 border-0 border-b pointer-events-none border-b-gray-300 dark:border-b-gray-500" />
<RadixCM.Separator className="mx-2 border-0 border-b pointer-events-none border-b-gray-300 dark:border-b-gray-550" />
);
export const SubMenu = ({
@@ -48,7 +48,7 @@ export const SubMenu = ({
}: RadixCM.MenuSubContentProps & ItemProps) => {
return (
<RadixCM.Sub>
<RadixCM.SubTrigger className="[&[data-state='open']_div]:bg-primary focus:outline-none">
<RadixCM.SubTrigger className="[&[data-state='open']_div]:bg-primary focus:outline-none py-0.5">
<DivItem rightArrow {...{ label, icon }} />
</RadixCM.SubTrigger>
<RadixCM.Portal>
@@ -88,6 +88,7 @@ interface ItemProps extends VariantProps<typeof itemStyles> {
icon?: Icon;
rightArrow?: boolean;
label?: string;
keybind?: string;
}
export const Item = ({
@@ -95,11 +96,17 @@ export const Item = ({
label,
rightArrow,
children,
keybind,
variant,
...props
}: ItemProps & RadixCM.MenuItemProps) => (
<RadixCM.Item {...props} className={itemStyles({ variant })}>
{children ? children : <ItemInternals {...{ icon, label, rightArrow }} />}
<RadixCM.Item
{...props}
className="!cursor-default select-none group focus:outline-none py-0.5 active:opacity-80"
>
<div className={itemStyles({ variant })}>
{children ? children : <ItemInternals {...{ icon, label, rightArrow, keybind }} />}
</div>
</RadixCM.Item>
);
@@ -109,13 +116,18 @@ const DivItem = ({ variant, ...props }: ItemProps) => (
</div>
);
const ItemInternals = ({ icon, label, rightArrow }: ItemProps) => {
const ItemInternals = ({ icon, label, rightArrow, keybind }: ItemProps) => {
const ItemIcon = icon;
return (
<>
{ItemIcon && <ItemIcon size={18} />}
{label && <p>{label}</p>}
{keybind && (
<span className="absolute text-xs font-medium text-gray-500 right-3 flex-end dark:text-gray-400 group-hover:dark:text-white">
{keybind}
</span>
)}
{rightArrow && (
<>
<div className="flex-1" />

View File

@@ -17,7 +17,7 @@ const MENU_CLASSES = `
border border-gray-300 dark:border-gray-500
shadow-2xl shadow-gray-300 dark:shadow-gray-950
select-none cursor-default rounded-lg
!bg-opacity-80 backdrop-blur
!bg-opacity-90 backdrop-blur
`;
export const OverlayPanel = ({

View File

@@ -14,6 +14,11 @@
-webkit-backdrop-filter: blur(18px);
}
.top-bar-blur {
@apply border-app-divider;
backdrop-filter: saturate(120%) blur(18px);
}
.inset-center {
position: absolute;
top: 50%;

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.