mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 11:03:27 +00:00
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:
parent
7115a2d346
commit
3c440783b3
|
@ -82,7 +82,7 @@
|
|||
"alwaysOnTop": false,
|
||||
"focus": false,
|
||||
"visible": false,
|
||||
"fileDropEnabled": false,
|
||||
"fileDropEnabled": true,
|
||||
"decorations": true,
|
||||
"transparent": true,
|
||||
"center": true
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
134
interface/app/Spacedrop.tsx
Normal 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>
|
||||
);
|
||||
}
|
11
interface/hooks/useSpacedropState.ts
Normal file
11
interface/hooks/useSpacedropState.ts
Normal 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);
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"exports": {
|
||||
".": "./index.tsx",
|
||||
"./assets/*": "./assets/*",
|
||||
"./components/*": "./components/*"
|
||||
"./components/*": "./components/*",
|
||||
"./hooks/*": "./hooks/*"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --cache",
|
||||
|
|
|
@ -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 }
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue