mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-18 13:09:15 +00:00
Merge branch 'main' of github.com:spacedriveapp/spacedrive
This commit is contained in:
commit
471babf4c0
|
@ -1,9 +1,15 @@
|
|||
use sdcore::{ClientCommand, ClientQuery, CoreEvent, CoreResponse, Node, NodeController};
|
||||
use std::{env, path::Path};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix::{
|
||||
Actor, AsyncContext, ContextFutureSpawner, Handler, Message, StreamHandler,
|
||||
WrapFuture,
|
||||
Actor, ActorContext, Addr, AsyncContext, Context, ContextFutureSpawner, Handler,
|
||||
Message, StreamHandler, WrapFuture,
|
||||
};
|
||||
use actix_web::{
|
||||
get, http::StatusCode, web, App, Error, HttpRequest, HttpResponse, HttpServer,
|
||||
|
@ -16,10 +22,85 @@ use tokio::sync::mpsc;
|
|||
|
||||
const DATA_DIR_ENV_VAR: &'static str = "DATA_DIR";
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Event(CoreEvent);
|
||||
|
||||
impl Message for Event {
|
||||
type Result = ();
|
||||
}
|
||||
|
||||
struct EventServer {
|
||||
clients: Arc<RwLock<HashSet<Addr<Socket>>>>,
|
||||
}
|
||||
|
||||
impl Actor for EventServer {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl EventServer {
|
||||
pub fn listen(mut event_receiver: mpsc::Receiver<CoreEvent>) -> Addr<Self> {
|
||||
let server = Self {
|
||||
clients: Arc::new(RwLock::new(HashSet::new())),
|
||||
};
|
||||
let clients = server.clients.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut last = Instant::now();
|
||||
while let Some(event) = event_receiver.recv().await {
|
||||
match event {
|
||||
CoreEvent::InvalidateQueryDebounced(_) => {
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last) > Duration::from_millis(1000 / 60)
|
||||
{
|
||||
last = current;
|
||||
for client in clients.read().unwrap().iter() {
|
||||
client.do_send(Event(event.clone()));
|
||||
}
|
||||
}
|
||||
},
|
||||
event => {
|
||||
for client in clients.read().unwrap().iter() {
|
||||
client.do_send(Event(event.clone()));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
server.start()
|
||||
}
|
||||
}
|
||||
|
||||
enum EventServerOperation {
|
||||
Connect(Addr<Socket>),
|
||||
Disconnect(Addr<Socket>),
|
||||
}
|
||||
|
||||
impl Message for EventServerOperation {
|
||||
type Result = ();
|
||||
}
|
||||
|
||||
impl Handler<EventServerOperation> for EventServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: EventServerOperation,
|
||||
_: &mut Context<Self>,
|
||||
) -> Self::Result {
|
||||
match msg {
|
||||
EventServerOperation::Connect(addr) => {
|
||||
self.clients.write().unwrap().insert(addr)
|
||||
},
|
||||
EventServerOperation::Disconnect(addr) => {
|
||||
self.clients.write().unwrap().remove(&addr)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Define HTTP actor
|
||||
struct Socket {
|
||||
_event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
core: web::Data<NodeController>,
|
||||
node_controller: web::Data<NodeController>,
|
||||
event_server: web::Data<Addr<EventServer>>,
|
||||
}
|
||||
|
||||
impl Actor for Socket {
|
||||
|
@ -62,18 +143,21 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
|
|||
},
|
||||
};
|
||||
|
||||
let core = self.core.clone();
|
||||
let core = self.node_controller.clone();
|
||||
self.event_server
|
||||
.do_send(EventServerOperation::Connect(ctx.address()));
|
||||
|
||||
let recipient = ctx.address().recipient();
|
||||
|
||||
let fut = async move {
|
||||
match msg.payload {
|
||||
SocketMessagePayload::Query(query) => {
|
||||
match core.query(query).await {
|
||||
Ok(response) => recipient.do_send(SocketResponse {
|
||||
id: msg.id.clone(),
|
||||
payload: SocketResponsePayload::Query(response),
|
||||
}),
|
||||
Ok(response) => {
|
||||
recipient.do_send(SocketResponse::Response {
|
||||
id: msg.id.clone(),
|
||||
payload: response,
|
||||
})
|
||||
},
|
||||
Err(err) => {
|
||||
println!("query error: {:?}", err);
|
||||
// Err(err.to_string())
|
||||
|
@ -82,10 +166,12 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
|
|||
},
|
||||
SocketMessagePayload::Command(command) => {
|
||||
match core.command(command).await {
|
||||
Ok(response) => recipient.do_send(SocketResponse {
|
||||
id: msg.id.clone(),
|
||||
payload: SocketResponsePayload::Query(response),
|
||||
}),
|
||||
Ok(response) => {
|
||||
recipient.do_send(SocketResponse::Response {
|
||||
id: msg.id.clone(),
|
||||
payload: response,
|
||||
})
|
||||
},
|
||||
Err(err) => {
|
||||
println!("command error: {:?}", err);
|
||||
// Err(err.to_string())
|
||||
|
@ -102,27 +188,35 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||
self.event_server
|
||||
.do_send(EventServerOperation::Disconnect(ctx.address()));
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
|
||||
pub enum SocketResponsePayload {
|
||||
Query(CoreResponse),
|
||||
impl Handler<Event> for Socket {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Event, ctx: &mut Self::Context) {
|
||||
ctx.text(serde_json::to_string(&SocketResponse::Event(msg.0)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
|
||||
#[rtype(result = "()")]
|
||||
struct SocketResponse {
|
||||
id: String,
|
||||
payload: SocketResponsePayload,
|
||||
enum SocketResponse {
|
||||
Response { id: String, payload: CoreResponse },
|
||||
Event(CoreEvent),
|
||||
}
|
||||
|
||||
impl Handler<SocketResponse> for Socket {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: SocketResponse, ctx: &mut Self::Context) {
|
||||
let string = serde_json::to_string(&msg).unwrap();
|
||||
ctx.text(string);
|
||||
ctx.text(serde_json::to_string(&msg).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,13 +234,13 @@ async fn healthcheck() -> impl Responder {
|
|||
async fn ws_handler(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
controller: web::Data<NodeController>,
|
||||
server: web::Data<Addr<EventServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let resp = ws::start(
|
||||
Socket {
|
||||
_event_receiver: event_receiver,
|
||||
core: controller,
|
||||
node_controller: controller,
|
||||
event_server: server,
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
|
@ -154,12 +248,6 @@ async fn ws_handler(
|
|||
resp
|
||||
}
|
||||
|
||||
#[get("/file/{file:.*}")]
|
||||
async fn file() -> impl Responder {
|
||||
// TODO
|
||||
format!("OK")
|
||||
}
|
||||
|
||||
async fn not_found() -> impl Responder {
|
||||
HttpResponse::build(StatusCode::OK).body("We're past the event horizon...")
|
||||
}
|
||||
|
@ -168,15 +256,16 @@ async fn not_found() -> impl Responder {
|
|||
async fn main() -> std::io::Result<()> {
|
||||
let (event_receiver, controller) = setup().await;
|
||||
|
||||
let server = web::Data::new(EventServer::listen(event_receiver));
|
||||
|
||||
println!("Listening http://localhost:8080");
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(event_receiver.clone())
|
||||
.app_data(controller.clone())
|
||||
.app_data(server.clone())
|
||||
.service(index)
|
||||
.service(healthcheck)
|
||||
.service(ws_handler)
|
||||
.service(file)
|
||||
.default_service(web::route().to(not_found))
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
|
@ -184,10 +273,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn setup() -> (
|
||||
web::Data<mpsc::Receiver<CoreEvent>>,
|
||||
web::Data<NodeController>,
|
||||
) {
|
||||
async fn setup() -> (mpsc::Receiver<CoreEvent>, web::Data<NodeController>) {
|
||||
let data_dir_path = match env::var(DATA_DIR_ENV_VAR) {
|
||||
Ok(path) => Path::new(&path).to_path_buf(),
|
||||
Err(_e) => {
|
||||
|
@ -207,5 +293,5 @@ async fn setup() -> (
|
|||
let (controller, event_receiver, node) = Node::new(data_dir_path).await;
|
||||
tokio::spawn(node.start());
|
||||
|
||||
(web::Data::new(event_receiver), web::Data::new(controller))
|
||||
(event_receiver, web::Data::new(controller))
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ class Transport extends BaseTransport {
|
|||
.then(() => {
|
||||
this.websocket = ws;
|
||||
this.attachEventListeners();
|
||||
console.log('Reconnected!');
|
||||
})
|
||||
.catch((err) => this.reconnect(timeoutIndex++));
|
||||
}, timeout);
|
||||
|
@ -44,33 +43,34 @@ class Transport extends BaseTransport {
|
|||
this.websocket.addEventListener('message', (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
const { id, payload } = JSON.parse(event.data);
|
||||
const { type: msg_type, data: msg_data } = JSON.parse(event.data);
|
||||
|
||||
const { type, data } = payload;
|
||||
if (type === 'event') {
|
||||
this.emit('core_event', data);
|
||||
} else if (type === 'query' || type === 'command') {
|
||||
if (msg_type === 'response') {
|
||||
const id = msg_data.id;
|
||||
if (this.requestMap.has(id)) {
|
||||
this.requestMap.get(id)?.(data);
|
||||
this.requestMap.get(id)?.({ data: msg_data.payload.data });
|
||||
this.requestMap.delete(id);
|
||||
}
|
||||
} else if (msg_type === 'event') {
|
||||
this.emit('core_event', msg_data);
|
||||
} else {
|
||||
console.error(`Received response message of type ${msg_type} which is not valid!`);
|
||||
}
|
||||
});
|
||||
|
||||
this.websocket.addEventListener('close', () => {
|
||||
console.log('GONE');
|
||||
this.reconnect();
|
||||
});
|
||||
}
|
||||
|
||||
async query(query: ClientQuery) {
|
||||
if (websocket.readyState == 0) {
|
||||
if (this.websocket.readyState == 0) {
|
||||
let resolve: () => void;
|
||||
const promise = new Promise((res) => {
|
||||
resolve = () => res(undefined);
|
||||
});
|
||||
// @ts-ignore
|
||||
websocket.addEventListener('open', resolve);
|
||||
this.websocket.addEventListener('open', resolve);
|
||||
await promise;
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,16 @@ class Transport extends BaseTransport {
|
|||
return await promise;
|
||||
}
|
||||
async command(command: ClientCommand) {
|
||||
if (this.websocket.readyState == 0) {
|
||||
let resolve: () => void;
|
||||
const promise = new Promise((res) => {
|
||||
resolve = () => res(undefined);
|
||||
});
|
||||
// @ts-ignore
|
||||
this.websocket.addEventListener('open', resolve);
|
||||
await promise;
|
||||
}
|
||||
|
||||
const id = randomId();
|
||||
let resolve: (data: any) => void;
|
||||
|
||||
|
|
|
@ -372,7 +372,7 @@ pub enum LibraryCommand {
|
|||
}
|
||||
|
||||
/// is a query destined for the core
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum ClientQuery {
|
||||
|
@ -388,7 +388,7 @@ pub enum ClientQuery {
|
|||
}
|
||||
|
||||
/// is a query destined for a specific library which is loaded into the core.
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[serde(tag = "key", content = "params")]
|
||||
#[ts(export)]
|
||||
pub enum LibraryQuery {
|
||||
|
@ -407,7 +407,7 @@ pub enum LibraryQuery {
|
|||
}
|
||||
|
||||
// represents an event this library can emit
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[serde(tag = "key", content = "data")]
|
||||
#[ts(export)]
|
||||
pub enum CoreEvent {
|
||||
|
@ -462,7 +462,7 @@ pub enum CoreError {
|
|||
LibraryError(#[from] library::LibraryError),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[ts(export)]
|
||||
pub enum CoreResource {
|
||||
Client,
|
||||
|
|
|
@ -165,8 +165,21 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
icon: CogIcon,
|
||||
onPress: () => navigate('library-settings/general')
|
||||
},
|
||||
{ name: 'Add Library', icon: PlusIcon },
|
||||
{ name: 'Lock', icon: LockClosedIcon }
|
||||
{
|
||||
name: 'Add Library',
|
||||
icon: PlusIcon,
|
||||
onPress: () => {
|
||||
alert('todo');
|
||||
// TODO: Show Dialog defined in `LibrariesSettings.tsx`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Lock',
|
||||
icon: LockClosedIcon,
|
||||
onPress: () => {
|
||||
alert('todo');
|
||||
}
|
||||
}
|
||||
// { name: 'Hide', icon: EyeOffIcon }
|
||||
]
|
||||
]}
|
||||
|
|
|
@ -13,8 +13,9 @@ import {
|
|||
Tag,
|
||||
TerminalWindow
|
||||
} from 'phosphor-react';
|
||||
import React, { DetailedHTMLProps, HTMLAttributes } from 'react';
|
||||
import React, { DetailedHTMLProps, HTMLAttributes, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppPropsContext } from '../../AppPropsContext';
|
||||
|
||||
import { Shortcut } from '../primitive/Shortcut';
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
|
@ -28,6 +29,7 @@ export interface TopBarButtonProps
|
|||
left?: boolean;
|
||||
right?: boolean;
|
||||
}
|
||||
interface SearchBarProps extends DefaultProps {}
|
||||
|
||||
const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) => {
|
||||
return (
|
||||
|
@ -49,6 +51,23 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
|
|||
);
|
||||
};
|
||||
|
||||
const SearchBar: React.FC<SearchBarProps> = (props) => { //TODO: maybe pass the appProps, so we can have the context in the TopBar if needed again
|
||||
const appProps = useContext(AppPropsContext);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-7">
|
||||
<input
|
||||
placeholder="Search"
|
||||
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-500 bg-[#F6F2F6] border border-gray-50 dark:bg-gray-650 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
|
||||
/>
|
||||
<div className="space-x-1 absolute top-[2px] right-1">
|
||||
<Shortcut chars={ appProps?.platform === "macOS" || appProps?.platform === "browser" ? "⌘K" : "CTRL+K" } />
|
||||
{/* <Shortcut chars="S" /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||
const { locationId } = useExplorerStore();
|
||||
const { mutate: generateThumbsForLocation } = useLibraryCommand('GenerateThumbsForLocation', {
|
||||
|
@ -88,16 +107,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
|||
<TopBarButton icon={FolderPlus} />
|
||||
<TopBarButton icon={TerminalWindow} />
|
||||
</div>
|
||||
<div className="relative flex h-7">
|
||||
<input
|
||||
placeholder="Search"
|
||||
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-500 bg-[#F6F2F6] border border-gray-50 dark:bg-gray-650 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
|
||||
/>
|
||||
<div className="space-x-1 absolute top-[2px] right-1">
|
||||
<Shortcut chars="⌘K" />
|
||||
{/* <Shortcut chars="S" /> */}
|
||||
</div>
|
||||
</div>
|
||||
<SearchBar />
|
||||
<div className="flex mx-8 space-x-2">
|
||||
<TopBarButton icon={Key} />
|
||||
<TopBarButton icon={Cloud} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useBridgeQuery } from '@sd/client';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import React from 'react';
|
||||
|
||||
import LocationListItem from '../../components/location/LocationListItem';
|
||||
|
@ -13,9 +13,7 @@ import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
|||
// ];
|
||||
|
||||
export default function LocationSettings() {
|
||||
const { data: locations } = useBridgeQuery('SysGetLocations');
|
||||
|
||||
console.log({ locations });
|
||||
const { data: locations } = useLibraryQuery('SysGetLocations');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
|
|
Loading…
Reference in a new issue