Spacedrop (#761)

* A hint of file drop

* backport from #671

* accept/reject Spacedrop working

* final cleanup

* Rename spacedrop.tsx to Spacedrop.tsx

* Update index.tsx
This commit is contained in:
Oscar Beaumont 2023-04-27 12:41:33 +08:00 committed by GitHub
parent 7115a2d346
commit 3c440783b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 548 additions and 267 deletions

View file

@ -82,7 +82,7 @@
"alwaysOnTop": false,
"focus": false,
"visible": false,
"fileDropEnabled": false,
"fileDropEnabled": true,
"decorations": true,
"transparent": true,
"center": true

View file

@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { dialog, invoke, os, shell } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { createMemoryRouter } from 'react-router-dom';
import { getDebugState, hooks } from '@sd/client';
@ -16,6 +17,7 @@ import {
SpacedriveInterface,
routes
} from '@sd/interface';
import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState';
import '@sd/ui/style';
const client = hooks.createClient({
@ -88,8 +90,15 @@ export default function App() {
document.dispatchEvent(new KeybindEvent(input.payload as string));
});
const dropEventListener = appWindow.onFileDropEvent((event) => {
if (event.payload.type === 'drop') {
getSpacedropState().droppedFiles = event.payload.paths;
}
});
return () => {
keybindListener.then((unlisten) => unlisten());
dropEventListener.then((unlisten) => unlisten());
};
}, []);

View file

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<body style="overflow: hidden">
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>

View file

@ -256,7 +256,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
.procedure(
"online",
R.subscription(|ctx, _: ()| async move {
let location_manager = ctx.library_manager.node_context.location_manager.clone();
let location_manager = ctx.location_manager.clone();
let mut rx = location_manager.online_rx();

View file

@ -209,19 +209,23 @@ pub(crate) fn mount() -> Arc<Router> {
.merge("files.", files::mount())
.merge("jobs.", jobs::mount())
.merge("p2p.", p2p::mount())
.merge("nodes.", nodes::mount())
.merge("sync.", sync::mount())
.merge("invalidation.", utils::mount_invalidate())
.build({
let config = Config::new().set_ts_bindings_header("/* eslint-disable */");
.build(
#[allow(clippy::let_and_return)]
{
let config = Config::new().set_ts_bindings_header("/* eslint-disable */");
#[cfg(all(debug_assertions, not(feature = "mobile")))]
let config = config.export_ts_bindings(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../packages/client/src/core.ts"),
);
#[cfg(all(debug_assertions, not(feature = "mobile")))]
let config = config.export_ts_bindings(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../packages/client/src/core.ts"),
);
config
})
config
},
)
.arced();
InvalidRequests::validate(r.clone()); // This validates all invalidation calls.

View file

@ -1 +1,28 @@
use rspc::alpha::AlphaRouter;
use serde::Deserialize;
use specta::Type;
use crate::api::R;
use super::Ctx;
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router().procedure("changeNodeName", {
#[derive(Deserialize, Type)]
pub struct ChangeNodeNameArgs {
pub name: String,
}
// TODO: validate name isn't empty or too long
R.mutation(|ctx, args: ChangeNodeNameArgs| async move {
ctx.config
.write(|mut config| {
config.name = args.name;
})
.await
.unwrap();
Ok(())
})
})
}

View file

@ -3,6 +3,7 @@ use sd_p2p::PeerId;
use serde::Deserialize;
use specta::Type;
use std::path::PathBuf;
use uuid::Uuid;
use crate::p2p::P2PEvent;
@ -38,13 +39,26 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
#[derive(Type, Deserialize)]
pub struct SpacedropArgs {
peer_id: PeerId,
file_path: String,
file_path: Vec<String>,
}
R.mutation(|ctx, args: SpacedropArgs| async move {
// TODO: Handle multiple files path and error if zero paths
ctx.p2p
.big_bad_spacedrop(args.peer_id, PathBuf::from(args.file_path))
.big_bad_spacedrop(args.peer_id, PathBuf::from(args.file_path.first().unwrap()))
.await;
})
})
.procedure("acceptSpacedrop", {
R.mutation(|ctx, (id, path): (Uuid, Option<String>)| async move {
match path {
Some(path) => {
ctx.p2p.accept_spacedrop(id, path).await;
}
None => {
ctx.p2p.reject_spacedrop(id).await;
}
}
})
})
}

View file

@ -244,12 +244,13 @@ pub(crate) fn mount_invalidate() -> AlphaRouter<Ctx> {
}
} else {
warn!("Shutting down invalidation manager thread due to the core event bus being droppped!");
break;
}
},
// Given human reaction time of ~250 milli this should be a good ballance.
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let x = buf.drain().map(|(_k, v)| v).collect::<Vec<_>>();
if x.len() > 0 {
if !x.is_empty() {
match tx.send(x) {
Ok(_) => {},
// All receivers are shutdown means that all clients are disconnected.

View file

@ -4,7 +4,7 @@ use crate::{
library::LibraryManager,
location::{LocationManager, LocationManagerError},
node::NodeConfigManager,
p2p::{P2PEvent, P2PManager},
p2p::P2PManager,
};
use std::{path::Path, sync::Arc};
@ -42,6 +42,7 @@ pub struct NodeContext {
pub struct Node {
config: Arc<NodeConfigManager>,
library_manager: Arc<LibraryManager>,
location_manager: Arc<LocationManager>,
jobs: Arc<JobManager>,
p2p: Arc<P2PManager>,
event_bus: (broadcast::Sender<CoreEvent>, broadcast::Receiver<CoreEvent>),
@ -157,23 +158,17 @@ impl Node {
let library_manager = library_manager.clone();
async move {
while let Ok(ops) = p2p_rx.recv().await {
if let P2PEvent::SyncOperation {
library_id,
operations,
} = ops
{
debug!("going to ingest {} operations", operations.len());
while let Ok((library_id, operations)) = p2p_rx.recv().await {
debug!("going to ingest {} operations", operations.len());
let Some(library) = library_manager.get_ctx(library_id).await else {
let Some(library) = library_manager.get_ctx(library_id).await else {
warn!("no library found!");
continue;
};
for op in operations {
println!("ingest lib id: {}", library.id);
library.sync.ingest_op(op).await.unwrap();
}
for op in operations {
println!("ingest lib id: {}", library.id);
library.sync.ingest_op(op).await.unwrap();
}
}
}
@ -183,6 +178,7 @@ impl Node {
let node = Node {
config,
library_manager,
location_manager,
jobs,
p2p,
event_bus,

View file

@ -32,9 +32,9 @@ pub struct LibraryManager {
/// libraries_dir holds the path to the directory where libraries are stored.
libraries_dir: PathBuf,
/// libraries holds the list of libraries which are currently loaded into the node.
pub libraries: RwLock<Vec<Library>>,
libraries: RwLock<Vec<Library>>,
/// node_context holds the context for the node which this library manager is running on.
pub node_context: NodeContext,
node_context: NodeContext,
}
#[derive(Error, Debug)]

View file

@ -1,7 +1,13 @@
use std::{path::PathBuf, sync::Arc, time::Instant};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};
use sd_p2p::{
spaceblock::{BlockSize, TransferRequest},
spaceblock::{BlockSize, SpacedropRequest},
spacetime::SpaceTimeStream,
Event, Manager, MetadataManager, PeerId,
};
use sd_sync::CRDTOperation;
@ -9,8 +15,9 @@ use serde::Serialize;
use specta::Type;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt, BufReader},
sync::broadcast,
io::{self, AsyncReadExt, AsyncWriteExt, BufReader},
sync::{broadcast, oneshot, Mutex},
time::sleep,
};
use tracing::{debug, error, info};
use uuid::Uuid;
@ -22,6 +29,9 @@ use crate::{
use super::{Header, PeerMetadata};
/// The amount of time to wait for a Spacedrop request to be accepted or rejected before it's automatically rejected
const SPACEDROP_TIMEOUT: Duration = Duration::from_secs(60);
/// TODO: P2P event for the frontend
#[derive(Debug, Clone, Type, Serialize)]
#[serde(tag = "type")]
@ -30,23 +40,25 @@ pub enum P2PEvent {
peer_id: PeerId,
metadata: PeerMetadata,
},
// TODO: Expire peer + connection/disconnect
SyncOperation {
library_id: Uuid,
operations: Vec<CRDTOperation>,
SpacedropRequest {
id: Uuid,
peer_id: PeerId,
name: String,
},
// TODO: Expire peer + connection/disconnect
}
pub struct P2PManager {
pub events: broadcast::Sender<P2PEvent>,
pub manager: Arc<Manager<PeerMetadata>>,
spacedrop_pairing_reqs: Arc<Mutex<HashMap<Uuid, oneshot::Sender<Option<String>>>>>,
pub metadata_manager: Arc<MetadataManager<PeerMetadata>>,
}
impl P2PManager {
pub async fn new(
node_config: Arc<NodeConfigManager>,
) -> (Arc<Self>, broadcast::Receiver<P2PEvent>) {
) -> (Arc<Self>, broadcast::Receiver<(Uuid, Vec<CRDTOperation>)>) {
let (config, keypair) = {
let config = node_config.get().await;
(Self::config_to_metadata(&config), config.keypair)
@ -65,10 +77,14 @@ impl P2PManager {
manager.listen_addrs().await
);
let (tx, rx) = broadcast::channel(100);
let (tx, _) = broadcast::channel(100);
let (tx2, rx2) = broadcast::channel(100);
let spacedrop_pairing_reqs = Arc::new(Mutex::new(HashMap::new()));
tokio::spawn({
let events = tx.clone();
// let sync_events = tx2.clone();
let spacedrop_pairing_reqs = spacedrop_pairing_reqs.clone();
async move {
let mut shutdown = false;
@ -93,6 +109,8 @@ impl P2PManager {
}
Event::PeerMessage(mut event) => {
let events = events.clone();
let sync_events = tx2.clone();
let spacedrop_pairing_reqs = spacedrop_pairing_reqs.clone();
tokio::spawn(async move {
let header = Header::from_stream(&mut event.stream).await.unwrap();
@ -102,20 +120,60 @@ impl P2PManager {
debug!("Received ping from peer '{}'", event.peer_id);
}
Header::Spacedrop(req) => {
info!("Received Spacedrop from peer '{}' for file '{}' with file length '{}'", event.peer_id, req.name, req.size);
let mut stream = match event.stream {
SpaceTimeStream::Unicast(stream) => stream,
_ => {
// TODO: Return an error to the remote client
error!("Received Spacedrop request from peer '{}' but it's not a unicast stream!", event.peer_id);
return;
}
};
let id = Uuid::new_v4();
let (tx, rx) = oneshot::channel();
// TODO: Ask the user if they wanna reject/accept it
info!("spacedrop({id}): received from peer '{}' for file '{}' with file length '{}'", event.peer_id, req.name, req.size);
// TODO: Deal with binary data. Deal with blocking based on `req.block_size`, etc
let mut s = String::new();
event.stream.read_to_string(&mut s).await.unwrap();
spacedrop_pairing_reqs.lock().await.insert(id, tx);
events
.send(P2PEvent::SpacedropRequest {
id,
peer_id: event.peer_id,
name: req.name,
})
.unwrap();
println!(
"Recieved file '{}' with content '{}' through Spacedrop!",
req.name, s
);
let file_path = tokio::select! {
_ = sleep(SPACEDROP_TIMEOUT) => {
info!("spacedrop({id}): timeout, rejecting!");
// TODO: Save to the filesystem
return;
}
file_path = rx => {
match file_path {
Ok(Some(file_path)) => {
info!("spacedrop({id}): accepted saving to '{:?}'", file_path);
file_path
}
Ok(None) => {
info!("spacedrop({id}): rejected");
return;
}
Err(_) => {
info!("spacedrop({id}): error with Spacedrop pairing request receiver!");
return;
}
}
}
};
stream.write_all(&[1]).await.unwrap();
let mut f = File::create(file_path).await.unwrap();
// TODO: Use binary block protocol instead of this
io::copy(&mut stream, &mut f).await.unwrap();
info!("spacedrop({id}): complete");
}
Header::Sync(library_id, len) => {
let mut buf = vec![0; len as usize]; // TODO: Designed for easily being able to be DOS the current Node
@ -126,12 +184,7 @@ impl P2PManager {
println!("Received sync events for library '{library_id}': {operations:?}");
events
.send(P2PEvent::SyncOperation {
library_id,
operations,
})
.ok();
sync_events.send((library_id, operations)).unwrap();
}
}
});
@ -159,6 +212,7 @@ impl P2PManager {
let this = Arc::new(Self {
events: tx,
manager,
spacedrop_pairing_reqs,
metadata_manager,
});
@ -227,7 +281,7 @@ impl P2PManager {
// });
}
(this, rx)
(this, rx2)
}
fn config_to_metadata(config: &NodeConfig) -> PeerMetadata {
@ -245,6 +299,18 @@ impl P2PManager {
.update(Self::config_to_metadata(&node_config_manager.get().await));
}
pub async fn accept_spacedrop(&self, id: Uuid, path: String) {
if let Some(chan) = self.spacedrop_pairing_reqs.lock().await.remove(&id) {
chan.send(Some(path)).unwrap();
}
}
pub async fn reject_spacedrop(&self, id: Uuid) {
if let Some(chan) = self.spacedrop_pairing_reqs.lock().await.remove(&id) {
chan.send(None).unwrap();
}
}
pub fn subscribe(&self) -> broadcast::Receiver<P2PEvent> {
self.events.subscribe()
}
@ -273,7 +339,7 @@ impl P2PManager {
stream
.write_all(
&Header::Spacedrop(TransferRequest {
&Header::Spacedrop(SpacedropRequest {
name: path.file_name().unwrap().to_str().unwrap().to_string(), // TODO: Encode this as bytes instead
size: metadata.len(),
block_size: BlockSize::from_size(metadata.len()),
@ -283,6 +349,15 @@ impl P2PManager {
.await
.unwrap();
debug!("Waiting for Spacedrop to be accepted from peer '{peer_id}'");
let mut buf = [0; 1];
// TODO: Add timeout so the connection is dropped if they never response
stream.read_exact(&mut buf).await.unwrap();
if buf[0] != 1 {
debug!("Spacedrop was rejected from peer '{peer_id}'");
return;
}
debug!("Starting Spacedrop to peer '{peer_id}'");
let i = Instant::now();

View file

@ -1,13 +1,13 @@
use tokio::io::AsyncReadExt;
use uuid::Uuid;
use sd_p2p::{spaceblock::TransferRequest, spacetime::SpaceTimeStream};
use sd_p2p::{spaceblock::SpacedropRequest, spacetime::SpaceTimeStream};
/// TODO
#[derive(Debug, PartialEq, Eq)]
pub enum Header {
Ping,
Spacedrop(TransferRequest),
Spacedrop(SpacedropRequest),
Sync(Uuid, u32),
}
@ -19,9 +19,9 @@ impl Header {
match discriminator {
0 => match stream {
SpaceTimeStream::Unicast(stream) => {
Ok(Self::Spacedrop(TransferRequest::from_stream(stream).await?))
}
SpaceTimeStream::Unicast(stream) => Ok(Self::Spacedrop(
SpacedropRequest::from_stream(stream).await?,
)),
_ => todo!(),
},
1 => Ok(Self::Ping),

View file

@ -13,7 +13,7 @@ use tokio::io::AsyncReadExt;
use crate::spacetime::{SpaceTimeStream, UnicastStream};
/// TODO
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockSize(i64);
impl BlockSize {
@ -26,15 +26,15 @@ impl BlockSize {
}
/// TODO
#[derive(Debug, PartialEq, Eq)]
pub struct TransferRequest {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpacedropRequest {
pub name: String,
pub size: u64,
// TODO: Include file permissions
pub block_size: BlockSize,
}
impl TransferRequest {
impl SpacedropRequest {
pub async fn from_stream(stream: &mut UnicastStream) -> Result<Self, ()> {
let name_len = stream.read_u8().await.map_err(|_| ())?; // TODO: Error handling
let mut name = vec![0u8; name_len as usize];

View file

@ -85,86 +85,86 @@ function DropItem(props: DropItemProps) {
);
}
const schema = z.object({
target_peer: z.string(),
file_path: z.string()
});
// const schema = z.object({
// target_peer: z.string(),
// file_path: z.string()
// });
// TODO: This will be removed and properly hooked up to the UI in the future
function TemporarySpacedropDemo() {
const [[discoveredPeers], setDiscoveredPeer] = useState([new Map<string, PeerMetadata>()]);
const doSpacedrop = useBridgeMutation('p2p.spacedrop');
// // TODO: This will be removed and properly hooked up to the UI in the future
// function TemporarySpacedropDemo() {
// const [[discoveredPeers], setDiscoveredPeer] = useState([new Map<string, PeerMetadata>()]);
// const doSpacedrop = useBridgeMutation('p2p.spacedrop');
const form = useZodForm({
schema
});
// const form = useZodForm({
// schema
// });
useBridgeSubscription(['p2p.events'], {
onData(data) {
if (data.type === 'DiscoveredPeer') {
setDiscoveredPeer([discoveredPeers.set(data.peer_id, data.metadata)]);
// if (!form.getValues().target_peer) form.setValue('target_peer', data.peer_id);
}
}
});
// useBridgeSubscription(['p2p.events'], {
// onData(data) {
// if (data.type === 'DiscoveredPeer') {
// setDiscoveredPeer([discoveredPeers.set(data.peer_id, data.metadata)]);
// // if (!form.getValues().target_peer) form.setValue('target_peer', data.peer_id);
// }
// }
// });
const onSubmit = form.handleSubmit((data) => {
doSpacedrop.mutate({
peer_id: data.target_peer,
file_path: data.file_path
});
});
// const onSubmit = form.handleSubmit((data) => {
// doSpacedrop.mutate({
// peer_id: data.target_peer,
// file_path: data.file_path
// });
// });
// TODO: Input select
return (
<Form onSubmit={onSubmit} form={form}>
<ScreenHeading>Spacedrop Demo</ScreenHeading>
<p className="text-xs text-ink-dull">
Note: Right now the file must be less than 255 bytes long and only contain UTF-8
chars. Create a txt file in Vscode to test (note macOS TextEdit cause that is rtf by
default)
</p>
<div className="mt-2 flex flex-row items-center space-x-4">
<Input
size="sm"
placeholder="/Users/oscar/Desktop/sd/demo.txt"
value="/Users/jamie/Desktop/Jeff.txt"
className="w-full"
{...form.register('file_path')}
/>
// // TODO: Input select
// return (
// <Form onSubmit={onSubmit} form={form}>
// <ScreenHeading>Spacedrop Demo</ScreenHeading>
// <p className="text-xs text-ink-dull">
// Note: Right now the file must be less than 255 bytes long and only contain UTF-8
// chars. Create a txt file in Vscode to test (note macOS TextEdit cause that is rtf by
// default)
// </p>
// <div className="mt-2 flex flex-row items-center space-x-4">
// <Input
// size="sm"
// placeholder="/Users/oscar/Desktop/sd/demo.txt"
// value="/Users/jamie/Desktop/Jeff.txt"
// className="w-full"
// {...form.register('file_path')}
// />
<Button className="block shrink-0" variant="gray">
Select File
</Button>
// <Button className="block shrink-0" variant="gray">
// Select File
// </Button>
<Select
onChange={(e) => form.setValue('target_peer', e)}
value={form.watch('target_peer')}
>
{[...discoveredPeers.entries()].map(([peerId, metadata], index) => (
<SelectOption default={index === 0} key={peerId} value={peerId}>
{metadata.name}
</SelectOption>
))}
</Select>
// <Select
// onChange={(e) => form.setValue('target_peer', e)}
// value={form.watch('target_peer')}
// >
// {[...discoveredPeers.entries()].map(([peerId, metadata], index) => (
// <SelectOption default={index === 0} key={peerId} value={peerId}>
// {metadata.name}
// </SelectOption>
// ))}
// </Select>
<Button
disabled={!form.getValues().target_peer}
className="block shrink-0"
variant="accent"
type="submit"
>
Send
</Button>
</div>
</Form>
);
}
// <Button
// disabled={!form.getValues().target_peer}
// className="block shrink-0"
// variant="accent"
// type="submit"
// >
// Send
// </Button>
// </div>
// </Form>
// );
// }
export const Component = () => {
return (
<>
<TemporarySpacedropDemo />
{/* <TemporarySpacedropDemo /> */}
<div className={classes.honeycombOuter}>
<div className={clsx(classes.honeycombContainer, 'mt-8')}>
<DropItem

134
interface/app/Spacedrop.tsx Normal file
View file

@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { PeerMetadata, useBridgeMutation, useBridgeSubscription } from '@sd/client';
import {
Dialog,
Select,
SelectOption,
UseDialogProps,
dialogManager,
forms,
useDialog
} from '@sd/ui';
import { getSpacedropState, subscribeSpacedropState } from '../hooks/useSpacedropState';
const { Input, useZodForm, z } = forms;
export function SpacedropUI() {
useEffect(() =>
subscribeSpacedropState(() => {
dialogManager.create((dp) => <SpacedropDialog {...dp} />);
})
);
useBridgeSubscription(['p2p.events'], {
onData(data) {
if (data.type === 'SpacedropRequest') {
dialogManager.create((dp) => (
<SpacedropRequestDialog
dropId={data.id}
name={data.name}
peerId={data.peer_id}
{...dp}
/>
));
}
}
});
return null;
}
function SpacedropDialog(props: UseDialogProps) {
const [[discoveredPeers], setDiscoveredPeer] = useState([new Map<string, PeerMetadata>()]);
const dialog = useDialog(props);
const form = useZodForm({
// We aren't using this but it's required for the Dialog :(
schema: z.object({
target_peer: z.string()
})
});
useBridgeSubscription(['p2p.events'], {
onData(data) {
if (data.type === 'DiscoveredPeer') {
setDiscoveredPeer([discoveredPeers.set(data.peer_id, data.metadata)]);
}
}
});
const doSpacedrop = useBridgeMutation('p2p.spacedrop');
const onSubmit = form.handleSubmit((data) =>
doSpacedrop.mutate({
file_path: getSpacedropState().droppedFiles,
peer_id: data.target_peer
})
);
return (
<Dialog
form={form}
dialog={dialog}
title="Spacedrop a File"
loading={doSpacedrop.isLoading}
ctaLabel="Send"
closeLabel="Cancel"
onSubmit={onSubmit}
>
<div className="space-y-2 py-2">
<Select
onChange={(e) => form.setValue('target_peer', e)}
value={form.watch('target_peer')}
>
{[...discoveredPeers.entries()].map(([peerId, metadata], index) => (
<SelectOption default={index === 0} key={peerId} value={peerId}>
{metadata.name}
</SelectOption>
))}
</Select>
</div>
</Dialog>
);
}
function SpacedropRequestDialog(
props: { dropId: string; name: string; peerId: string } & UseDialogProps
) {
const dialog = useDialog(props);
const form = useZodForm({
// We aren't using this but it's required for the Dialog :(
schema: z.object({
file_path: z.string()
})
});
const acceptSpacedrop = useBridgeMutation('p2p.acceptSpacedrop');
const onSubmit = form.handleSubmit((data) =>
acceptSpacedrop.mutate([props.dropId, data.file_path])
);
// TODO: Automatically close this after 60 seconds cause the Spacedrop would have expired
return (
<Dialog
form={form}
dialog={dialog}
title="Received Spacedrop"
loading={acceptSpacedrop.isLoading}
ctaLabel="Send"
closeLabel="Cancel"
onSubmit={onSubmit}
onCancelled={() => acceptSpacedrop.mutate([props.dropId, null])}
>
<div className="space-y-2 py-2">
<p>File Name: {props.name}</p>
<p>Peer Id: {props.peerId}</p>
<Input
size="sm"
placeholder="/Users/oscar/Desktop/demo.txt"
className="w-full"
{...form.register('file_path')}
/>
</div>
</Dialog>
);
}

View file

@ -0,0 +1,11 @@
import { proxy, subscribe, useSnapshot } from 'valtio';
const state = proxy({
droppedFiles: [] as string[]
});
export const useSpacedropState = () => useSnapshot(state);
export const getSpacedropState = () => state;
export const subscribeSpacedropState = (callback: () => void) => subscribe(state, callback);

View file

@ -10,6 +10,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import { RouterProvider, RouterProviderProps } from 'react-router-dom';
import { useDebugState } from '@sd/client';
import ErrorFallback from './ErrorFallback';
import { SpacedropUI } from './app/Spacedrop';
export * from './util/keybind';
export * from './util/Platform';
@ -48,6 +49,7 @@ export const SpacedriveInterface = (props: { router: RouterProviderProps['router
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Devtools />
<SpacedropUI />
<RouterProvider router={props.router} />
</ErrorBoundary>
);

View file

@ -8,7 +8,8 @@
"exports": {
".": "./index.tsx",
"./assets/*": "./assets/*",
"./components/*": "./components/*"
"./components/*": "./components/*",
"./hooks/*": "./hooks/*"
},
"scripts": {
"lint": "eslint . --cache",

View file

@ -2,88 +2,90 @@
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
export type Procedures = {
queries:
{ key: "buildInfo", input: never, result: BuildInfo } |
{ key: "files.get", input: LibraryArgs<GetArgs>, result: { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; file_paths: FilePath[]; media_data: MediaData | null } | null } |
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: JobReport[] } |
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: JobReport[] } |
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
{ key: "keys.getKey", input: LibraryArgs<string>, result: string } |
{ key: "keys.getSecretKey", input: LibraryArgs<null>, result: string | null } |
{ key: "keys.isKeyManagerUnlocking", input: LibraryArgs<null>, result: boolean | null } |
{ key: "keys.isSetup", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.isUnlocked", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.list", input: LibraryArgs<null>, result: StoredKey[] } |
{ key: "keys.listMounted", input: LibraryArgs<null>, result: string[] } |
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
{ key: "library.list", input: never, result: LibraryConfigWrapped[] } |
{ key: "locations.getById", input: LibraryArgs<number>, result: LocationWithIndexerRules | null } |
{ key: "locations.getExplorerData", input: LibraryArgs<LocationExplorerArgs>, result: ExplorerData } |
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: ({ id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; node: Node })[] } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "search", input: LibraryArgs<{ locationId?: number | null; afterFileId?: [number, number] | null; take?: number | null; order?: Ordering | null; search?: string | null; extension?: string | null; kind?: number | null; tags?: number[] | null; createdAtFrom?: string | null; createdAtTo?: string | null; path?: string | null }>, result: ExplorerItem[] } |
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Tag[] } |
{ key: "tags.list", input: LibraryArgs<null>, result: Tag[] } |
queries:
{ key: "buildInfo", input: never, result: BuildInfo } |
{ key: "files.get", input: LibraryArgs<GetArgs>, result: { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; file_paths: FilePath[]; media_data: MediaData | null } | null } |
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: JobReport[] } |
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: JobReport[] } |
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
{ key: "keys.getKey", input: LibraryArgs<string>, result: string } |
{ key: "keys.getSecretKey", input: LibraryArgs<null>, result: string | null } |
{ key: "keys.isKeyManagerUnlocking", input: LibraryArgs<null>, result: boolean | null } |
{ key: "keys.isSetup", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.isUnlocked", input: LibraryArgs<null>, result: boolean } |
{ key: "keys.list", input: LibraryArgs<null>, result: StoredKey[] } |
{ key: "keys.listMounted", input: LibraryArgs<null>, result: string[] } |
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
{ key: "library.list", input: never, result: LibraryConfigWrapped[] } |
{ key: "locations.getById", input: LibraryArgs<number>, result: LocationWithIndexerRules | null } |
{ key: "locations.getExplorerData", input: LibraryArgs<LocationExplorerArgs>, result: ExplorerData } |
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: ({ id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; node: Node })[] } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "search", input: LibraryArgs<{ locationId?: number | null; afterFileId?: string | null; take?: number | null; order?: Ordering | null; search?: string | null; extension?: string | null; kind?: number | null; tags?: number[] | null; createdAtFrom?: string | null; createdAtTo?: string | null; path?: string | null }>, result: ExplorerItem[] } |
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
{ key: "tags.get", input: LibraryArgs<number>, result: Tag | null } |
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Tag[] } |
{ key: "tags.list", input: LibraryArgs<null>, result: Tag[] } |
{ key: "volumes.list", input: never, result: Volume[] },
mutations:
{ key: "files.copyFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
{ key: "files.cutFiles", input: LibraryArgs<FileCutterJobInit>, result: null } |
{ key: "files.decryptFiles", input: LibraryArgs<FileDecryptorJobInit>, result: null } |
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
{ key: "files.duplicateFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
{ key: "files.encryptFiles", input: LibraryArgs<FileEncryptorJobInit>, result: null } |
{ key: "files.eraseFiles", input: LibraryArgs<FileEraserJobInit>, result: null } |
{ key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } |
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
{ key: "files.setNote", input: LibraryArgs<SetNoteArgs>, result: null } |
{ key: "jobs.clear", input: LibraryArgs<string>, result: null } |
{ key: "jobs.clearAll", input: LibraryArgs<null>, result: null } |
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
{ key: "keys.backupKeystore", input: LibraryArgs<string>, result: null } |
{ key: "keys.changeMasterPassword", input: LibraryArgs<MasterPasswordChangeArgs>, result: null } |
{ key: "keys.clearMasterPassword", input: LibraryArgs<null>, result: null } |
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
{ key: "keys.mount", input: LibraryArgs<string>, result: null } |
{ key: "keys.restoreKeystore", input: LibraryArgs<RestoreBackupArgs>, result: number } |
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
{ key: "keys.setup", input: LibraryArgs<OnboardingConfig>, result: null } |
{ key: "keys.syncKeyToLibrary", input: LibraryArgs<string>, result: null } |
{ key: "keys.unlockKeyManager", input: LibraryArgs<UnlockKeyManagerArgs>, result: null } |
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
{ key: "keys.updateAutomountStatus", input: LibraryArgs<AutomountUpdateArgs>, result: null } |
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
{ key: "library.delete", input: string, result: null } |
{ key: "library.edit", input: EditLibraryArgs, result: null } |
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: null } |
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: null } |
{ key: "locations.delete", input: LibraryArgs<number>, result: null } |
{ key: "locations.fullRescan", input: LibraryArgs<number>, result: null } |
{ key: "locations.indexer_rules.create", input: LibraryArgs<IndexerRuleCreateArgs>, result: IndexerRule } |
{ key: "locations.indexer_rules.delete", input: LibraryArgs<number>, result: null } |
{ key: "locations.quickRescan", input: LibraryArgs<LightScanArgs>, result: null } |
{ key: "locations.relink", input: LibraryArgs<string>, result: null } |
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
{ key: "p2p.spacedrop", input: SpacedropArgs, result: null } |
{ key: "tags.assign", input: LibraryArgs<TagAssignArgs>, result: null } |
{ key: "tags.create", input: LibraryArgs<TagCreateArgs>, result: Tag } |
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
mutations:
{ key: "files.copyFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
{ key: "files.cutFiles", input: LibraryArgs<FileCutterJobInit>, result: null } |
{ key: "files.decryptFiles", input: LibraryArgs<FileDecryptorJobInit>, result: null } |
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
{ key: "files.deleteFiles", input: LibraryArgs<FileDeleterJobInit>, result: null } |
{ key: "files.duplicateFiles", input: LibraryArgs<FileCopierJobInit>, result: null } |
{ key: "files.encryptFiles", input: LibraryArgs<FileEncryptorJobInit>, result: null } |
{ key: "files.eraseFiles", input: LibraryArgs<FileEraserJobInit>, result: null } |
{ key: "files.renameFile", input: LibraryArgs<RenameFileArgs>, result: null } |
{ key: "files.setFavorite", input: LibraryArgs<SetFavoriteArgs>, result: null } |
{ key: "files.setNote", input: LibraryArgs<SetNoteArgs>, result: null } |
{ key: "jobs.clear", input: LibraryArgs<string>, result: null } |
{ key: "jobs.clearAll", input: LibraryArgs<null>, result: null } |
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs<GenerateThumbsForLocationArgs>, result: null } |
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs<IdentifyUniqueFilesArgs>, result: null } |
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
{ key: "keys.add", input: LibraryArgs<KeyAddArgs>, result: null } |
{ key: "keys.backupKeystore", input: LibraryArgs<string>, result: null } |
{ key: "keys.changeMasterPassword", input: LibraryArgs<MasterPasswordChangeArgs>, result: null } |
{ key: "keys.clearMasterPassword", input: LibraryArgs<null>, result: null } |
{ key: "keys.deleteFromLibrary", input: LibraryArgs<string>, result: null } |
{ key: "keys.mount", input: LibraryArgs<string>, result: null } |
{ key: "keys.restoreKeystore", input: LibraryArgs<RestoreBackupArgs>, result: number } |
{ key: "keys.setDefault", input: LibraryArgs<string>, result: null } |
{ key: "keys.setup", input: LibraryArgs<OnboardingConfig>, result: null } |
{ key: "keys.syncKeyToLibrary", input: LibraryArgs<string>, result: null } |
{ key: "keys.unlockKeyManager", input: LibraryArgs<UnlockKeyManagerArgs>, result: null } |
{ key: "keys.unmount", input: LibraryArgs<string>, result: null } |
{ key: "keys.unmountAll", input: LibraryArgs<null>, result: null } |
{ key: "keys.updateAutomountStatus", input: LibraryArgs<AutomountUpdateArgs>, result: null } |
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
{ key: "library.delete", input: string, result: null } |
{ key: "library.edit", input: EditLibraryArgs, result: null } |
{ key: "locations.addLibrary", input: LibraryArgs<LocationCreateArgs>, result: null } |
{ key: "locations.create", input: LibraryArgs<LocationCreateArgs>, result: null } |
{ key: "locations.delete", input: LibraryArgs<number>, result: null } |
{ key: "locations.fullRescan", input: LibraryArgs<number>, result: null } |
{ key: "locations.indexer_rules.create", input: LibraryArgs<IndexerRuleCreateArgs>, result: IndexerRule } |
{ key: "locations.indexer_rules.delete", input: LibraryArgs<number>, result: null } |
{ key: "locations.quickRescan", input: LibraryArgs<LightScanArgs>, result: null } |
{ key: "locations.relink", input: LibraryArgs<string>, result: null } |
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
{ key: "nodes.changeNodeName", input: ChangeNodeNameArgs, result: null } |
{ key: "p2p.acceptSpacedrop", input: [string, string | null], result: null } |
{ key: "p2p.spacedrop", input: SpacedropArgs, result: null } |
{ key: "tags.assign", input: LibraryArgs<TagAssignArgs>, result: null } |
{ key: "tags.create", input: LibraryArgs<TagCreateArgs>, result: Tag } |
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
{ key: "tags.update", input: LibraryArgs<TagUpdateArgs>, result: null },
subscriptions:
{ key: "invalidation.listen", input: never, result: InvalidateOperationEvent[] } |
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string } |
{ key: "locations.online", input: never, result: number[][] } |
{ key: "p2p.events", input: never, result: P2PEvent } |
subscriptions:
{ key: "invalidation.listen", input: never, result: InvalidateOperationEvent[] } |
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string } |
{ key: "locations.online", input: never, result: number[][] } |
{ key: "p2p.events", input: never, result: P2PEvent } |
{ key: "sync.newMessage", input: LibraryArgs<null>, result: CRDTOperation }
};
@ -93,44 +95,36 @@ export type PeerMetadata = { name: string; operating_system: OperatingSystem | n
export type MasterPasswordChangeArgs = { password: Protected<string>; algorithm: Algorithm; hashing_algorithm: HashingAlgorithm }
/**
* `LocationUpdateArgs` is the argument received from the client using `rspc` to update a location.
* It contains the id of the location to be updated, possible a name to change the current location's name
* and a vector of indexer rules ids to add or remove from the location.
*
* It is important to note that only the indexer rule ids in this vector will be used from now on.
* Old rules that aren't in this vector will be purged.
*/
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] }
/**
* NodeConfig is the configuration for a node. This is shared between all libraries and is stored in a JSON file on disk.
*/
export type NodeConfig = { id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }
export type Ordering = { name: boolean }
/**
* This denotes the `StoredKey` version.
*/
export type StoredKeyVersion = "V1"
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
/**
* This should be used for passing an encrypted key around.
*
*
* This is always `ENCRYPTED_KEY_LEN` (which is `KEY_LEM` + `AEAD_TAG_LEN`)
*/
export type EncryptedKey = number[]
export type PeerId = string
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string }
export type GenerateThumbsForLocationArgs = { id: number; path: string }
export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
/**
* These parameters define the password-hashing level.
*
*
* The greater the parameter, the longer the password will take to hash.
*/
export type Params = "Standard" | "Hardened" | "Paranoid"
@ -147,11 +141,9 @@ export type RenameFileArgs = { location_id: number; file_name: string; new_file_
*/
export type OperatingSystem = "Windows" | "Linux" | "MacOS" | "Ios" | "Android" | { Other: string }
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
/**
* This is a stored key, and can be freely written to the database.
*
*
* It contains no sensitive information that is not encrypted.
*/
export type StoredKey = { uuid: string; version: StoredKeyVersion; key_type: StoredKeyType; algorithm: Algorithm; hashing_algorithm: HashingAlgorithm; content_salt: Salt; master_key: EncryptedKey; master_key_nonce: Nonce; key_nonce: Nonce; key: number[]; salt: Salt; memory_only: boolean; automount: boolean }
@ -160,41 +152,49 @@ export type OnboardingConfig = { password: Protected<string>; algorithm: Algorit
export type LocationExplorerArgs = { location_id: number; path: string | null; limit: number; cursor: string | null; kind: number[] | null }
export type NodeState = ({ id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string }
export type EditLibraryArgs = { id: string; name: string | null; description: string | null }
export type BuildInfo = { version: string; commit: string }
/**
* This should be used for providing a nonce to encrypt/decrypt functions.
*
*
* You may also generate a nonce for a given algorithm with `Nonce::generate()`
*/
export type Nonce = { XChaCha20Poly1305: number[] } | { Aes256Gcm: number[] }
export type UnlockKeyManagerArgs = { password: Protected<string>; secret_key: Protected<string> }
export type FilePathWithObject = { id: number; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; parent_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null }
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; parent_id: number[] | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null }
export type FileEncryptorJobInit = { location_id: number; path_id: number; key_uuid: string; algorithm: Algorithm; metadata: boolean; preview_media: boolean; output_path: string | null }
export type InvalidateOperationEvent = { key: string; arg: any; result: any | null }
export type BuildInfo = { version: string; commit: string }
export type CRDTOperation = { node: string; timestamp: number; id: string; typ: CRDTOperationType }
export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; file_paths: FilePath[] }
/**
* This should be used for passing a salt around.
*
*
* You may also generate a salt with `Salt::generate()`
*/
export type Salt = number[]
export type GetArgs = { id: number }
export type FileCutterJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string }
/**
* `LocationUpdateArgs` is the argument received from the client using `rspc` to update a location.
* It contains the id of the location to be updated, possible a name to change the current location's name
* and a vector of indexer rules ids to add or remove from the location.
*
* It is important to note that only the indexer rule ids in this vector will be used from now on.
* Old rules that aren't in this vector will be purged.
*/
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] }
export type FilePath = { id: number; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; parent_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
export type FileCutterJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string }
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused"
@ -202,10 +202,7 @@ export type ObjectValidatorArgs = { id: number; path: string }
export type FileEraserJobInit = { location_id: number; path_id: number; passes: string }
/**
* TODO: P2P event for the frontend
*/
export type P2PEvent = { type: "DiscoveredPeer"; peer_id: PeerId; metadata: PeerMetadata } | { type: "SyncOperation"; library_id: string; operations: CRDTOperation[] }
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
export type Volume = { name: string; mount_point: string; total_capacity: string; available_capacity: string; is_removable: boolean; disk_type: string | null; file_system: string | null; is_root_filesystem: boolean }
@ -218,20 +215,29 @@ export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
export type ExplorerItem = { type: "Path"; has_thumbnail: boolean; item: FilePathWithObject } | { type: "Object"; has_thumbnail: boolean; item: ObjectWithFilePaths }
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: any | null; is_background: boolean; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; message: string }
export type OwnedOperationItem = { id: any; data: OwnedOperationData }
export type SpacedropArgs = { peer_id: PeerId; file_path: string }
export type SetFavoriteArgs = { id: number; favorite: boolean }
export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
/**
* TODO: P2P event for the frontend
*/
export type P2PEvent = { type: "DiscoveredPeer"; peer_id: PeerId; metadata: PeerMetadata } | { type: "SpacedropRequest"; id: string; peer_id: PeerId; name: string }
export type SpacedropArgs = { peer_id: PeerId; file_path: string[] }
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: ({ indexer_rule: IndexerRule })[] }
export type NodeState = ({ id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string }
export type TagCreateArgs = { name: string; color: string }
export type OwnedOperation = { model: string; items: OwnedOperationItem[] }
@ -240,21 +246,12 @@ export type SharedOperation = { record_id: any; model: string; data: SharedOpera
export type RelationOperationData = "Create" | { Update: { field: string; value: any } } | "Delete"
export type FileEncryptorJobInit = { location_id: number; path_id: number; key_uuid: string; algorithm: Algorithm; metadata: boolean; preview_media: boolean; output_path: string | null }
export type SharedOperationCreateData = { u: { [key: string]: any } } | "a"
export type KeyAddArgs = { algorithm: Algorithm; hashing_algorithm: HashingAlgorithm; key: Protected<string>; library_sync: boolean; automount: boolean }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
/**
* `LocationCreateArgs` is the argument received from the client using `rspc` to create a new location.
* It has the actual path and a vector of indexer rules ids, to create many-to-many relationships
* between the location and indexer rules.
*/
export type LocationCreateArgs = { path: string; indexer_rules_ids: number[] }
/**
* Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries.
*/
@ -262,6 +259,13 @@ export type LibraryArgs<T> = { library_id: string; arg: T }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
/**
* `LocationCreateArgs` is the argument received from the client using `rspc` to create a new location.
* It has the actual path and a vector of indexer rules ids, to create many-to-many relationships
* between the location and indexer rules.
*/
export type LocationCreateArgs = { path: string; indexer_rules_ids: number[] }
export type IdentifyUniqueFilesArgs = { id: number; path: string }
export type OwnedOperationData = { Create: { [key: string]: any } } | { CreateMany: { values: ([any, { [key: string]: any }])[]; skip_duplicates: boolean } } | { Update: { [key: string]: any } } | "Delete"
@ -272,6 +276,8 @@ export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boole
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
export type ChangeNodeNameArgs = { name: string }
/**
* This defines all available password hashing algorithms.
*/
@ -281,7 +287,7 @@ export type ExplorerContext = ({ type: "Location" } & Location) | ({ type: "Tag"
export type SetNoteArgs = { id: number; note: string | null }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: ({ indexer_rule: IndexerRule })[] }
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; parent_id: number[] | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
/**
* LibraryConfig holds the configuration for a specific library. This is stored as a '{uuid}.sdlibrary' file.
@ -299,23 +305,21 @@ export type Protected<T> = T
/**
* `IndexerRuleCreateArgs` is the argument received from the client using rspc to create a new indexer rule.
* Note that `parameters` field **MUST** be a JSON object serialized to bytes.
*
*
* In case of `RuleKind::AcceptFilesByGlob` or `RuleKind::RejectFilesByGlob`, it will be a
* single string containing a glob pattern.
*
*
* In case of `RuleKind::AcceptIfChildrenDirectoriesArePresent` or `RuleKind::RejectIfChildrenDirectoriesArePresent` the
* `parameters` field must be a vector of strings containing the names of the directories.
*/
export type IndexerRuleCreateArgs = { kind: RuleKind; name: string; parameters: string[] }
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
export type LightScanArgs = { location_id: number; sub_path: string }
export type RestoreBackupArgs = { password: Protected<string>; secret_key: Protected<string>; path: string }
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
export type Ordering = { name: boolean }
export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData }
/**

View file

@ -109,6 +109,7 @@ export interface DialogProps<S extends FieldValues>
dialog: ReturnType<typeof useDialog>;
trigger?: ReactNode;
ctaLabel?: string;
closeLabel?: string;
ctaDanger?: boolean;
title?: string;
description?: string;
@ -116,6 +117,7 @@ export interface DialogProps<S extends FieldValues>
transformOrigin?: string;
loading?: boolean;
submitDisabled?: boolean;
onCancelled?: () => void;
}
export function Dialog<S extends FieldValues>({
@ -187,8 +189,9 @@ export function Dialog<S extends FieldValues>({
disabled={props.loading}
size="sm"
variant="gray"
onClick={props.onCancelled}
>
Close
{props.closeLabel || 'Close'}
</Button>
</DialogPrimitive.Close>
<Button