feat: add basic tauri client app

This commit is contained in:
Lin Onetwo
2023-01-04 17:16:26 +08:00
parent 87451a19bb
commit 64ca6a6b35
57 changed files with 12747 additions and 54 deletions
+4
View File
@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
+4957
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "AFFiNE"
version = "0.0.0"
description = "Development Tool for BlockSuite"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = {version = "1.2", features = [] }
[dependencies]
bytes = "1.3.0"
ipc_types = { path = "./types" }
futures = "^0.3.25"
js-sys = "0.3.60"
jwst = { path = "../src-OctoBase/libs/jwst" }
jwst-storage = { path = "../src-OctoBase/libs/jwst-storage", features = ["sqlite"] }
lib0 = "0.12.0"
project-root = "0.2.2"
schemars = "0.8.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = {version = "1.2", features = ["api-all", "devtools"] }
tokio = { version = "1.23.0", features = ["rt", "macros"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
[profile.release.package.wry]
debug = true
debug-assertions = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1,30 @@
use ipc_types::{
blob::IBlobParameters, document::YDocumentUpdate, workspace::CreateWorkspace,
};
/**
* convert serde to jsonschema: https://imfeld.dev/writing/generating_typescript_types_from_rust
* with way to optimize
* convert jsonschema to ts: https://github.com/bcherny/json-schema-to-typescript
*/
use project_root::get_project_root;
use schemars::{schema_for, JsonSchema};
use std::{
fs::write,
path::{Path, PathBuf},
};
fn generate<T>(path: PathBuf)
where
T: ?Sized + JsonSchema, // Sized or ?Sized are both ok, click https://zhuanlan.zhihu.com/p/21820917 to learn why
{
let schema = schema_for!(T);
let output = serde_json::to_string_pretty(&schema).unwrap();
write(path, output).expect("can not write json-schema file")
}
fn main() {
let project_root = &get_project_root().unwrap();
generate::<YDocumentUpdate>(Path::join(project_root, "../src/types/ipc/document.json"));
generate::<CreateWorkspace>(Path::join(project_root, "../src/types/ipc/workspace.json"));
generate::<IBlobParameters>(Path::join(project_root, "../src/types/ipc/blob.json"));
}
+1
View File
@@ -0,0 +1 @@
tab_spaces = 2
+12
View File
@@ -0,0 +1,12 @@
pub mod blob;
pub mod workspace;
use workspace::{__cmd__create_workspace, __cmd__update_y_document};
use blob::__cmd__put_blob;
use blob::__cmd__get_blob;
use crate::{commands::{workspace::{create_workspace, update_y_document}, blob::{put_blob, get_blob}}};
pub fn invoke_handler() -> impl Fn(tauri::Invoke) + Send + Sync + 'static {
tauri::generate_handler![update_y_document, create_workspace, put_blob, get_blob]
}
+68
View File
@@ -0,0 +1,68 @@
use bytes::Bytes;
use futures::{
stream::{self},
StreamExt,
};
use ipc_types::blob::{GetBlob, PutBlob};
use jwst::BlobStorage;
use crate::state::AppState;
#[tauri::command]
pub async fn put_blob<'s>(
state: tauri::State<'s, AppState>,
parameters: PutBlob,
) -> Result<String, String> {
let blob_storage = &state.0.lock().await.blob_storage;
if let Ok(path) = blob_storage
.put_blob(
// TODO: ask octobase to accept blob directly or wrap/await tauri command to create a real stream, so we don't need to construct stream manually
Some(parameters.workspace_id.to_string()),
stream::iter::<Vec<Bytes>>(vec![Bytes::from(parameters.blob)]),
)
.await
{
Ok(path)
} else {
Err("Failed to create".to_string())
}
}
#[tauri::command]
pub async fn get_blob<'s>(
state: tauri::State<'s, AppState>,
parameters: GetBlob,
) -> Result<Vec<u8>, String> {
let GetBlob { workspace_id, id } = parameters;
// TODO: check user permission? Or just assume there will only be one user
let blob_storage = &state.0.lock().await.blob_storage;
if let Ok(mut file_stream) = blob_storage.get_blob(Some(workspace_id.to_string()), id.clone()).await {
// Read all of the chunks into a vector.
let mut stream_contents = Vec::new();
let mut error_message = "".to_string();
while let Some(chunk) = file_stream.next().await {
match chunk {
Ok(chunk_bytes) => stream_contents.extend_from_slice(&chunk_bytes),
Err(err) => {
error_message = format!(
"Failed to read blob file {}/{} from stream, error: {}",
workspace_id.to_string(),
id,
err
);
}
}
}
if error_message.len() > 0 {
return Err(error_message);
}
Ok(stream_contents)
} else {
Err(format!(
"Failed to read blob file {}/{} ",
workspace_id.to_string(),
id
))
}
}
@@ -0,0 +1,38 @@
use ipc_types::{document::YDocumentUpdate, workspace::CreateWorkspace};
use jwst::{DocStorage, Workspace};
use lib0::any::Any;
use crate::state::AppState;
#[tauri::command]
pub async fn create_workspace<'s>(
state: tauri::State<'s, AppState>,
parameters: CreateWorkspace,
) -> Result<bool, String> {
let workspace = Workspace::new(parameters.id.to_string());
workspace.with_trx(|mut workspace_transaction| {
// TODO: why this Any here?
workspace_transaction.set_metadata("name", Any::String(parameters.name.into_boxed_str()));
workspace_transaction.set_metadata(
"avatar",
Any::String(parameters.avatar.clone().into_boxed_str()),
);
});
if let Err(error_message) = &state
.0
.lock()
.await
.doc_storage
.write_doc(parameters.id, workspace.doc())
.await
{
Err(error_message.to_string())
} else {
Ok(true)
}
}
#[tauri::command]
pub fn update_y_document(parameters: YDocumentUpdate) -> Result<bool, String> {
Ok(true)
}
+35
View File
@@ -0,0 +1,35 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod commands;
mod state;
use state::AppState;
use tokio::sync::Mutex;
use tauri::TitleBarStyle;
#[tokio::main]
async fn main() {
tauri::async_runtime::set(tokio::runtime::Handle::current());
let preload = include_str!("../../public/preload/index.js");
tauri::Builder::default()
.manage(AppState(Mutex::new(
state::AppStateRaw::new().await.unwrap(),
)))
// manually create window here, instead of in the tauri.conf.json, to add `initialization_script` here
.setup(move |app| {
let _window =
tauri::WindowBuilder::new(app, "label", tauri::WindowUrl::App("index.html".into()))
.title("AFFiNE")
.inner_size(1000.0, 800.0)
.title_bar_style(TitleBarStyle::Overlay)
.hidden_title(true)
.initialization_script(&preload)
.build()?;
Ok(())
})
.invoke_handler(commands::invoke_handler())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+39
View File
@@ -0,0 +1,39 @@
use jwst_storage::{BlobFsStorage, DBContext, DocFsStorage};
use std::path::Path;
use tauri::api::path::desktop_dir;
use tokio::sync::Mutex;
pub struct AppStateRaw {
pub blob_storage: BlobFsStorage,
pub doc_storage: DocFsStorage,
pub metadata_db: DBContext,
}
impl AppStateRaw {
pub async fn new() -> Option<AppStateRaw> {
let doc_env = Path::new(&desktop_dir()?.into_os_string())
.join("affine-dev")
.join("doc");
let blob_env = Path::new(&(desktop_dir()?.into_os_string()))
.join("affine-dev")
.join("blob");
let db_env = format!(
"sqlite://{}?mode=rwc",
Path::new(&(desktop_dir()?.into_os_string()))
.join("affine-dev")
.join("db")
.join("metadata.db")
.into_os_string()
.into_string()
.unwrap()
);
Some(Self {
doc_storage: DocFsStorage::new(Some(16), 500, Path::new(&doc_env).into()).await,
blob_storage: BlobFsStorage::new(Some(16), Path::new(&blob_env).into()).await,
metadata_db: DBContext::new(db_env).await,
})
}
}
pub struct AppState(pub Mutex<AppStateRaw>); // need pub, otherwise will be "field `0` of struct `types::state::AppState` is private"
+58
View File
@@ -0,0 +1,58 @@
{
"build": {
"beforeDevCommand": "pnpm dev:web",
"beforeBuildCommand": "pnpm build:web",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "AFFiNE",
"version": "0.0.2"
},
"tauri": {
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": ["$RESOURCE", "$RESOURCE/*", "$APP/*"]
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.affine.client",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"updater": {
"active": false
}
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "ipc_types"
version = "0.1.0"
[dependencies]
project-root = "0.2.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = "0.8.3"
+20
View File
@@ -0,0 +1,20 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PutBlob {
pub workspace_id: u64,
pub blob: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GetBlob {
pub workspace_id: u64,
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum IBlobParameters {
Put(PutBlob),
Get(GetBlob),
}
@@ -0,0 +1,8 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct YDocumentUpdate<'a> {
update: Vec<u8>,
room: &'a str,
}
+7
View File
@@ -0,0 +1,7 @@
#[allow(unused_imports)]
extern crate serde;
extern crate schemars;
pub mod blob;
pub mod document;
pub mod workspace;
@@ -0,0 +1,9 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CreateWorkspace {
pub id: i64,
pub name: String,
pub avatar: String,
}