Merge pull request #935 from toeverything/feat/client-app
feat: tauri desktop client app
@@ -0,0 +1,74 @@
|
||||
name: Release App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
- '.vscode'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'README.md'
|
||||
- '.vscode'
|
||||
|
||||
concurrency:
|
||||
group: release-ci-group
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
MacOS:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- name: Get npm cache directory
|
||||
uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i -r
|
||||
|
||||
- name: Make macOS (x64)
|
||||
run: pnpm build:app
|
||||
working-directory: client-app
|
||||
env:
|
||||
CI: true
|
||||
CI_PULL_REQUEST: ${{ github.event_name == 'pull_request' }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: client-app/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -58,3 +58,7 @@ module-resolve.cjs
|
||||
|
||||
# Cache
|
||||
.eslintcache
|
||||
|
||||
# generated assets
|
||||
client-app/public/affine-out
|
||||
client-app/public/preload
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "client-app/src-OctoBase"]
|
||||
path = client-app/src-OctoBase
|
||||
url = https://github.com/toeverything/OctoBase
|
||||
@@ -8,13 +8,24 @@
|
||||
"datacenter",
|
||||
"livedemo",
|
||||
"pnpm",
|
||||
"jwst",
|
||||
"testid",
|
||||
"octobase",
|
||||
"selfhosted",
|
||||
"testid"
|
||||
"testid",
|
||||
"schemars"
|
||||
],
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
|
||||
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.config.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json",
|
||||
"Cargo.toml": "Cargo.lock",
|
||||
"README.md": "LICENSE, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md"
|
||||
}
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
"[toml]": {
|
||||
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
||||
},
|
||||
"rust-analyzer.linkedProjects": ["client-app/src-tauri/Cargo.toml"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Client App
|
||||
|
||||
AFFiNE App client powered by Tauri.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Please follow the Tauri [getting started guide](https://tauri.app/v1/guides/getting-started/setup/) for environment setup.
|
||||
|
||||
After the environment is ready, start development build:
|
||||
|
||||
```sh
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Currently client-app depends on a rapidly developing rust library "Octobase", we use git-submodule to link it currently.
|
||||
|
||||
We will provide its binary binding soon, to replace the git-submodule, before Octobase become opensource.
|
||||
|
||||
### Scripts
|
||||
|
||||
On this folder:
|
||||
|
||||
- `pnpm dev:app` will start a vite server
|
||||
- `pnpm build:prerequisite` will link the Octobase and prepare affine dist html and tauri preload script, also will generate ts type from rs. You should run this before start your first development time.
|
||||
|
||||
On project root folder:
|
||||
|
||||
### Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AFFiNE</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root" />
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@affine/client-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "MPL-2.0",
|
||||
"module": "true",
|
||||
"scripts": {
|
||||
"dev:app": "pnpm build:preload && cross-env NODE_ENV=development tauri dev",
|
||||
"dev:prerequisite": "concurrently \"cd ../packages/data-center && pnpm dev\" \"cd ../packages/app && pnpm dev\"",
|
||||
"build:prerequisite": "pnpm build:submodules && pnpm build:rs-types && pnpm build:affine && pnpm build:preload",
|
||||
"build:rs-types": "zx scripts/generateTsTypingsFromJsonSchema.mjs",
|
||||
"build:submodules": "zx scripts/buildSubModules.mjs",
|
||||
"build:affine": "zx scripts/buildAffine.mjs",
|
||||
"build:preload": "esbuild src/preload/index.ts --outdir=public/preload",
|
||||
"build:app": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/store": "^0.3.1",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"json-schema-to-typescript": "^11.0.2",
|
||||
"lib0": "^0.2.58",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-is": "^18.2.0",
|
||||
"react-router": "^6.5.0",
|
||||
"react-router-dom": "^6.5.0",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.5.43"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "5.47.0",
|
||||
"@typescript-eslint/parser": "5.47.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.16.10",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-config-standard-with-typescript": "24.0.0",
|
||||
"eslint-import-resolver-alias": "1.1.2",
|
||||
"eslint-import-resolver-typescript": "3.5.2",
|
||||
"eslint-plugin-autofix": "1.1.0",
|
||||
"eslint-plugin-html": "7.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.6.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-security": "1.5.0",
|
||||
"eslint-plugin-security-node": "1.1.1",
|
||||
"eslint-plugin-typescript-sort-keys": "2.1.0",
|
||||
"eslint-plugin-unicorn": "45.0.2",
|
||||
"eslint-plugin-unused-imports": "2.0.0",
|
||||
"prettier": "2.8.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.9.4",
|
||||
"typesync": "^0.9.2",
|
||||
"vite": "^4.0.2",
|
||||
"zx": "^7.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
const repoDirectory = path.join(__dirname, '..', '..');
|
||||
const clientAppDirectory = path.join(__dirname, '..');
|
||||
const publicDistributionDirectory = path.join(clientAppDirectory, 'public');
|
||||
const affineSrcDirectory = path.join(repoDirectory, 'packages', 'app');
|
||||
const affineSrcOutDirectory = path.join(affineSrcDirectory, 'out');
|
||||
const publicAffineOutDirectory = path.join(
|
||||
publicDistributionDirectory,
|
||||
'affine-out'
|
||||
);
|
||||
|
||||
/**
|
||||
* Build affine dist html
|
||||
*/
|
||||
cd(repoDirectory);
|
||||
await $`pnpm i -r`;
|
||||
await $`pnpm build`;
|
||||
cd(affineSrcDirectory);
|
||||
$.env.BASE_PATH = '/affine-out';
|
||||
await $`pnpm build`;
|
||||
await $`pnpm export`;
|
||||
await fs.remove(publicAffineOutDirectory);
|
||||
await fs.move(affineSrcOutDirectory, publicAffineOutDirectory);
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
const repoDirectory = path.join(__dirname, '..');
|
||||
const publicDistributionDirectory = path.join(repoDirectory, 'public');
|
||||
|
||||
const octoBaseBranchName = 'master';
|
||||
/**
|
||||
* 1. Until OctoBase become public, we link it using submodule too.
|
||||
*/
|
||||
cd(`${path.join(repoDirectory, 'src-OctoBase')}`);
|
||||
await $`git checkout ${octoBaseBranchName}`;
|
||||
await $`git submodule update --recursive && git submodule update --remote`;
|
||||
await $`git pull origin ${octoBaseBranchName}`;
|
||||
await $`git reset --hard origin/${octoBaseBranchName}`;
|
||||
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// TODO: use https://github.com/quicktype/quicktype#installation instead
|
||||
import { compileFromFile } from 'json-schema-to-typescript';
|
||||
import { cd } from 'zx/core';
|
||||
|
||||
const projectRoot = path.join(__dirname, '..', '..');
|
||||
const tsTypingsFolder = path.join(
|
||||
projectRoot,
|
||||
'packages/data-center/src/provider/tauri-ipc/ipc/types'
|
||||
);
|
||||
|
||||
/**
|
||||
* 1. generate JSONSchema using rs crate `schemars`, this happened on rs side script `src-tauri/examples/generate-jsonschema.rs`
|
||||
*/
|
||||
cd('./src-tauri');
|
||||
try {
|
||||
fs.mkdirSync(tsTypingsFolder);
|
||||
} catch {}
|
||||
await $`cargo run --example generate-jsonschema`;
|
||||
|
||||
/**
|
||||
* 2. generate TS from JSON schema, this is efficient on NodeJS side.
|
||||
*/
|
||||
const fileNames = fs.readdirSync(tsTypingsFolder);
|
||||
const jsonSchemaFilePaths = fileNames
|
||||
.filter(fileName => fileName.endsWith('.json'))
|
||||
.map(fileName => path.join(tsTypingsFolder, fileName));
|
||||
|
||||
await Promise.all(
|
||||
jsonSchemaFilePaths.map(
|
||||
async fileName =>
|
||||
await compileFromFile(fileName).then(tsContent =>
|
||||
fs.writeFileSync(fileName.replace('.json', '.ts'), tsContent)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 3. fix eslint error on generated ts files
|
||||
*/
|
||||
cd(path.join(projectRoot, 'packages/data-center'));
|
||||
await $`eslint ${tsTypingsFolder} --ext ts --fix`;
|
||||
|
||||
/**
|
||||
* 4. // TODO: parse all #[tauri::command] and generate ts method code
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
[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",
|
||||
] }
|
||||
cloud-database = { path = "../src-OctoBase/libs/cloud-database", features = [
|
||||
"sqlite",
|
||||
] }
|
||||
project-root = "0.2.2"
|
||||
schemars = "0.8.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dotenvy = "0.15.6"
|
||||
tauri = { version = "1.2", features = ["api-all", "devtools"] }
|
||||
tokio = { version = "1.23.0", features = ["rt", "macros"] }
|
||||
lib0 = "0.12.0"
|
||||
moka = { version = "0.9.6", features = ["future"] }
|
||||
yrs = { path = "../src-OctoBase/libs/vendors/y-crdt/yrs" }
|
||||
y-sync = { path = "../src-OctoBase/libs/vendors/y-sync" }
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use ipc_types::{
|
||||
blob::IBlobParameters, document::IDocumentParameters, user::IUserParameters,
|
||||
workspace::IWorkspaceParameters,
|
||||
};
|
||||
/**
|
||||
* 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();
|
||||
let mono_repo_root = Path::join(project_root, "../..");
|
||||
let data_center_ipc_type_folder = Path::join(
|
||||
&mono_repo_root,
|
||||
"packages/data-center/src/provider/tauri-ipc/ipc/types",
|
||||
);
|
||||
generate::<IDocumentParameters>(Path::join(&data_center_ipc_type_folder, "document.json"));
|
||||
generate::<IWorkspaceParameters>(Path::join(&data_center_ipc_type_folder, "workspace.json"));
|
||||
generate::<IBlobParameters>(Path::join(&data_center_ipc_type_folder, "blob.json"));
|
||||
generate::<IUserParameters>(Path::join(&data_center_ipc_type_folder, "user.json"));
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 947 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1 @@
|
||||
tab_spaces = 2
|
||||
@@ -0,0 +1,25 @@
|
||||
pub mod blob;
|
||||
pub mod workspace;
|
||||
pub mod document;
|
||||
pub mod user;
|
||||
|
||||
use blob::*;
|
||||
use workspace::*;
|
||||
use document::*;
|
||||
use user::*;
|
||||
|
||||
pub fn invoke_handler() -> impl Fn(tauri::Invoke) + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
update_y_document,
|
||||
create_workspace,
|
||||
update_workspace,
|
||||
get_workspaces,
|
||||
get_workspace,
|
||||
create_user,
|
||||
get_user,
|
||||
create_doc,
|
||||
get_doc,
|
||||
put_blob,
|
||||
get_blob
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
parameters.workspace_id,
|
||||
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(workspace_id.clone(), 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.clone().unwrap_or_default().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.unwrap_or_default().to_string(),
|
||||
id
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use ipc_types::document::{
|
||||
CreateDocumentParameter, GetDocumentParameter, GetDocumentResponse, YDocumentUpdate,
|
||||
};
|
||||
use jwst::DocStorage;
|
||||
use jwst::Workspace as OctoBaseWorkspace;
|
||||
use lib0::any::Any;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
/// get yDoc created by create_workspace, using same id
|
||||
pub async fn create_doc<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: CreateDocumentParameter,
|
||||
) -> Result<(), String> {
|
||||
let workspace_doc = OctoBaseWorkspace::new(parameters.workspace_id.clone());
|
||||
|
||||
workspace_doc.with_trx(|mut workspace_doc_transaction| {
|
||||
workspace_doc_transaction.set_metadata(
|
||||
"name",
|
||||
Any::String(parameters.workspace_name.clone().into_boxed_str()),
|
||||
);
|
||||
});
|
||||
if let Err(error_message) = &state
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.doc_db
|
||||
.write_doc(parameters.workspace_id.clone(), workspace_doc.doc())
|
||||
.await
|
||||
{
|
||||
Err(format!(
|
||||
"Failed to write_doc during create_workspace with error {}",
|
||||
error_message.to_string()
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
/// get yDoc created by create_workspace, using same id
|
||||
pub async fn get_doc<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: GetDocumentParameter,
|
||||
) -> Result<GetDocumentResponse, String> {
|
||||
// TODO: check user permission
|
||||
let state = &state.0.lock().await;
|
||||
let doc_db = &state.doc_db;
|
||||
|
||||
if let Ok(all_updates_of_workspace) = doc_db.all(¶meters.id).await {
|
||||
let all_updates = all_updates_of_workspace
|
||||
.iter()
|
||||
.map(|model| model.blob.clone())
|
||||
.collect::<Vec<Vec<u8>>>();
|
||||
Ok(GetDocumentResponse {
|
||||
updates: all_updates,
|
||||
})
|
||||
} else {
|
||||
Err(format!(
|
||||
"Failed to get yDoc from workspace {}",
|
||||
parameters.id
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_y_document<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: YDocumentUpdate,
|
||||
) -> Result<bool, String> {
|
||||
let state = &state.0.lock().await;
|
||||
let doc_db = &state.doc_db;
|
||||
|
||||
doc_db
|
||||
.replace_with(¶meters.id.clone(), parameters.update)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use cloud_database::{CreateUser, User};
|
||||
use ipc_types::{document::CreateDocumentParameter, user::GetUserParameters};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::document::create_doc;
|
||||
|
||||
#[tauri::command]
|
||||
/// create new user and a private workspace
|
||||
pub async fn create_user<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: CreateUser,
|
||||
) -> Result<User, String> {
|
||||
let new_user_result = &state
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.metadata_db
|
||||
.create_user(parameters.clone())
|
||||
.await;
|
||||
match new_user_result {
|
||||
Ok(new_user_option) => match new_user_option {
|
||||
Some((new_user, new_workspace)) => {
|
||||
// a new private workspace is created, we have to create a yDoc for it
|
||||
create_doc(
|
||||
state,
|
||||
CreateDocumentParameter {
|
||||
workspace_id: new_workspace.id.clone(),
|
||||
workspace_name: parameters.name.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
Ok(new_user.clone())
|
||||
}
|
||||
None => Err("User creation failed".to_string()),
|
||||
},
|
||||
Err(error_message) => Err(error_message.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
/// get the only one user in local sqlite
|
||||
pub async fn get_user<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: GetUserParameters,
|
||||
) -> Result<User, String> {
|
||||
let db = &state.0.lock().await.metadata_db;
|
||||
match db.get_user_by_email(¶meters.email).await.ok().unwrap() {
|
||||
Some(user) => Ok(user),
|
||||
None => Err("User not found".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use ipc_types::{
|
||||
document::CreateDocumentParameter,
|
||||
workspace::{
|
||||
CreateWorkspace, CreateWorkspaceResult, GetWorkspace, GetWorkspaceResult, GetWorkspaces,
|
||||
GetWorkspacesResult, UpdateWorkspace,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::document::create_doc;
|
||||
|
||||
#[tauri::command]
|
||||
/// create yDoc for a workspace
|
||||
pub async fn get_workspaces<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: GetWorkspaces,
|
||||
) -> Result<GetWorkspacesResult, String> {
|
||||
match &state
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.metadata_db
|
||||
.get_user_workspaces(parameters.user_id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(user_workspaces) => Ok(GetWorkspacesResult {
|
||||
workspaces: user_workspaces.clone(),
|
||||
}),
|
||||
Err(error_message) => Err(error_message.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
/// create yDoc for a workspace
|
||||
pub async fn get_workspace<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: GetWorkspace,
|
||||
) -> Result<GetWorkspaceResult, String> {
|
||||
match &state
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.metadata_db
|
||||
.get_workspace_by_id(parameters.id)
|
||||
.await
|
||||
{
|
||||
Ok(user_workspace_option) => match user_workspace_option {
|
||||
Some(user_workspace) => Ok(GetWorkspaceResult {
|
||||
workspace: user_workspace.clone(),
|
||||
}),
|
||||
None => Err("Get workspace has no result".to_string()),
|
||||
},
|
||||
Err(error_message) => Err(error_message.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
/// create yDoc for a workspace
|
||||
pub async fn create_workspace<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: CreateWorkspace,
|
||||
) -> Result<CreateWorkspaceResult, String> {
|
||||
let new_workspace_result = &state
|
||||
.0
|
||||
.lock()
|
||||
.await
|
||||
.metadata_db
|
||||
.create_normal_workspace(parameters.user_id.to_string())
|
||||
.await;
|
||||
match new_workspace_result {
|
||||
Ok(new_workspace) => {
|
||||
create_doc(
|
||||
state,
|
||||
CreateDocumentParameter {
|
||||
workspace_id: new_workspace.id.clone(),
|
||||
workspace_name: parameters.name.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
Ok(CreateWorkspaceResult {
|
||||
id: new_workspace.id.clone(),
|
||||
name: parameters.name,
|
||||
})
|
||||
}
|
||||
Err(error_message) => Err(format!(
|
||||
"Failed to create_workspace with error {}",
|
||||
error_message.to_string()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_workspace<'s>(
|
||||
state: tauri::State<'s, AppState>,
|
||||
parameters: UpdateWorkspace,
|
||||
) -> Result<bool, String> {
|
||||
// TODO: check user permission
|
||||
// No thing to update now. The avatar is update in YDoc using websocket or yrs.update
|
||||
Ok(true)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod commands;
|
||||
mod state;
|
||||
use dotenvy::dotenv;
|
||||
use state::AppState;
|
||||
use std::env;
|
||||
use tauri::TitleBarStyle;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tauri::async_runtime::set(tokio::runtime::Handle::current());
|
||||
dotenv().ok();
|
||||
let preload = include_str!("../../public/preload/index.js");
|
||||
let is_dev = env::var("NODE_ENV").unwrap_or_default() == "development";
|
||||
let initial_path = if is_dev {
|
||||
"index.html"
|
||||
} else {
|
||||
"affine-out/index.html"
|
||||
};
|
||||
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(initial_path.into()))
|
||||
.title("AFFiNE")
|
||||
.inner_size(1000.0, 800.0)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.hidden_title(true)
|
||||
.initialization_script(&preload)
|
||||
.build()?;
|
||||
#[cfg(debug_assertions)]
|
||||
_window.open_devtools();
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(commands::invoke_handler())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use cloud_database::SqliteDBContext;
|
||||
use jwst::Workspace;
|
||||
use jwst_storage::{BlobAutoStorage, DocAutoStorage};
|
||||
use std::{fs, path::Path};
|
||||
use tauri::api::path::document_dir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct AppStateRaw {
|
||||
pub doc_db: DocAutoStorage,
|
||||
pub blob_storage: BlobAutoStorage,
|
||||
pub metadata_db: SqliteDBContext,
|
||||
}
|
||||
|
||||
impl AppStateRaw {
|
||||
pub async fn new() -> Option<AppStateRaw> {
|
||||
let affine_document_path = Path::new(&document_dir()?.into_os_string()).join("affine");
|
||||
let metadata_db_env = format!(
|
||||
"sqlite://{}?mode=rwc",
|
||||
affine_document_path
|
||||
.join("metadata")
|
||||
.with_extension("db")
|
||||
.display()
|
||||
);
|
||||
let blob_db_env = format!(
|
||||
"sqlite://{}?mode=rwc",
|
||||
affine_document_path
|
||||
.join("blob")
|
||||
.with_extension("db")
|
||||
.display()
|
||||
);
|
||||
let doc_db_env = format!(
|
||||
"sqlite://{}?mode=rwc",
|
||||
affine_document_path
|
||||
.join("doc")
|
||||
.with_extension("db")
|
||||
.display()
|
||||
);
|
||||
fs::create_dir_all(affine_document_path.clone()).unwrap();
|
||||
|
||||
Some(Self {
|
||||
doc_db: DocAutoStorage::init_pool(&doc_db_env).await.unwrap(),
|
||||
blob_storage: BlobAutoStorage::init_pool(&blob_db_env).await.unwrap(),
|
||||
metadata_db: SqliteDBContext::new(metadata_db_env).await,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppState(pub Mutex<AppStateRaw>); // need pub, otherwise will be "field `0` of struct `types::state::AppState` is private"
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev:prerequisite",
|
||||
"beforeBuildCommand": "pnpm build:preload && pnpm build:affine",
|
||||
"devPath": "http://localhost:8080",
|
||||
"distDir": "../public",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ipc_types"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
jwst-storage = { path = "../../src-OctoBase/libs/jwst-storage", features = [
|
||||
"sqlite",
|
||||
] }
|
||||
cloud-database = { path = "../../src-OctoBase/libs/cloud-database", features = [
|
||||
"sqlite",
|
||||
] }
|
||||
project-root = "0.2.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
schemars = "0.8.3"
|
||||
@@ -0,0 +1,20 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PutBlob {
|
||||
pub workspace_id: Option<String>,
|
||||
pub blob: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetBlob {
|
||||
pub workspace_id: Option<String>,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum IBlobParameters {
|
||||
Put(PutBlob),
|
||||
Get(GetBlob),
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct YDocumentUpdate {
|
||||
pub update: Vec<u8>,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetDocumentParameter {
|
||||
pub id: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDocumentParameter {
|
||||
pub workspace_id: String,
|
||||
pub workspace_name: String,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetDocumentResponse {
|
||||
pub updates: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum IDocumentParameters {
|
||||
YDocumentUpdate(YDocumentUpdate),
|
||||
CreateDocumentParameter(CreateDocumentParameter),
|
||||
GetDocumentParameter(GetDocumentParameter),
|
||||
GetDocumentResponse(GetDocumentResponse),
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[allow(unused_imports)]
|
||||
extern crate serde;
|
||||
extern crate schemars;
|
||||
extern crate jwst_storage;
|
||||
extern crate cloud_database;
|
||||
|
||||
pub mod blob;
|
||||
pub mod document;
|
||||
pub mod workspace;
|
||||
pub mod user;
|
||||
@@ -0,0 +1,15 @@
|
||||
use cloud_database::{CreateUser, User};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetUserParameters {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum IUserParameters {
|
||||
CreateUser(CreateUser),
|
||||
User(User),
|
||||
GetUserParameters(GetUserParameters),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use cloud_database::{WorkspaceWithPermission, WorkspaceDetail};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateWorkspace {
|
||||
pub user_id: String,
|
||||
/**
|
||||
* only set name, avatar is update in datacenter to yDoc directly
|
||||
*/
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetWorkspaces {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetWorkspace {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateWorkspaceResult {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetWorkspacesResult {
|
||||
pub workspaces: Vec<WorkspaceWithPermission>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GetWorkspaceResult {
|
||||
pub workspace: WorkspaceDetail,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct UpdateWorkspace {
|
||||
pub id: i64,
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum IWorkspaceParameters {
|
||||
CreateWorkspace(CreateWorkspace),
|
||||
GetWorkspace(GetWorkspace),
|
||||
GetWorkspaces(GetWorkspaces),
|
||||
GetWorkspaceResult(GetWorkspaceResult),
|
||||
GetWorkspacesResult(GetWorkspacesResult),
|
||||
UpdateWorkspace(UpdateWorkspace),
|
||||
CreateWorkspaceResult(CreateWorkspaceResult),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Preload Scripts
|
||||
|
||||
Here are preload scripts (See [tauri's doc](https://tauri.app/v1/references/architecture/inter-process-communication/isolation)). This is simillar to Electron's [preload script](https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts).
|
||||
|
||||
We pass env variables to AFFiNE side from here.
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
// tauri preload script can't have `export {}`
|
||||
// @ts-ignore 'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.ts(1208)
|
||||
window.__TAURI_ISOLATION_HOOK__ = payload => {
|
||||
console.log('Tauri isolation hook', payload);
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
/**
|
||||
* Give AFFiNE app code some env to know it is inside a tauri app.
|
||||
*/
|
||||
function setEnvironmentVariables() {
|
||||
window.CLIENT_APP = true;
|
||||
}
|
||||
|
||||
setEnvironmentVariables();
|
||||
@@ -0,0 +1,8 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
CLIENT_APP?: boolean;
|
||||
__editoVersion?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node" /* can't use NodeNext, otherwise can't find styled-components and @emotion/styled 's type, because ts won't follow `type` field in @emotion/styled 's packagejson, will follow `main` instead */,
|
||||
"strict": true,
|
||||
"strictNullChecks": true /* Enable strict null checks. */,
|
||||
"strictFunctionTypes": true /* Enable strict checking of function types. */,
|
||||
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
|
||||
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
|
||||
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||
"allowJs": false /* Allow javascript files to be compiled. */,
|
||||
"esModuleInterop": true,
|
||||
"types": ["vite/client"],
|
||||
"typeRoots": ["types"],
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"noImplicitReturns": true
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: ['es2021', 'chrome100', 'safari13'],
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
esbuild: {
|
||||
jsxInject: `import React from 'react';`,
|
||||
},
|
||||
});
|
||||
@@ -6,10 +6,11 @@
|
||||
"license": "MPL-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev",
|
||||
"dev:ac": "pnpm --filter=!@affine/app build && NODE_API_SERVER=ac pnpm --filter @affine/app dev",
|
||||
"dev": "cross-env NODE_ENV=development pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev",
|
||||
"dev:ac": "pnpm --filter=!@affine/app build && cross-env NODE_API_SERVER=ac pnpm --filter @affine/app dev",
|
||||
"dev:local": "pnpm --filter=!@affine/app build && cross-env NODE_API_SERVER=local pnpm --filter @affine/app dev",
|
||||
"build": " pnpm --filter=!@affine/app build && pnpm --filter!=@affine/datacenter -r build",
|
||||
"build:client": " pnpm --filter=@affine/client-app build:app",
|
||||
"build:storybook": " pnpm -r build-storybook",
|
||||
"export": "pnpm --filter @affine/app export",
|
||||
"start": "pnpm --filter @affine/app start",
|
||||
@@ -32,6 +33,7 @@
|
||||
"@types/node": "^18.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
@@ -80,6 +82,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@tauri-apps/api@1.2.0": "patches/@tauri-apps__api@1.2.0.patch",
|
||||
"next@13.1.0": "patches/next@13.1.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const EditorHeader = () => {
|
||||
>
|
||||
{title && (
|
||||
<StyledTitle
|
||||
data-tauri-drag-region
|
||||
onMouseEnter={() => {
|
||||
if (isTrash) return;
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@ export const Header = ({
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
<StyledHeader hasWarning={showWarning} data-testid="editor-header-items">
|
||||
<StyledHeader
|
||||
hasWarning={showWarning}
|
||||
data-testid="editor-header-items"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{children}
|
||||
<StyledHeaderRightSide>
|
||||
{rightItems.map(itemName => {
|
||||
|
||||
@@ -79,7 +79,7 @@ const AppDefender = ({ children }: PropsWithChildren) => {
|
||||
const { synced } = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
if (router.asPath === '/') {
|
||||
if (['/index.html', '/'].includes(router.asPath)) {
|
||||
router.replace('/workspace');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
@@ -32,7 +32,9 @@ export const AppStateProvider = ({
|
||||
if (dataCenter.workspaces.length === 0) {
|
||||
await createDefaultWorkspace(dataCenter);
|
||||
}
|
||||
setUser((await dataCenter.getUserInfo()) || null);
|
||||
setUser(
|
||||
(await dataCenter.getUserInfo(dataCenter.providers[0]?.id)) || null
|
||||
);
|
||||
setAppState({
|
||||
dataCenter,
|
||||
workspaceList: dataCenter.workspaces,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "0.4.0-20230210031655-264744e",
|
||||
"@blocksuite/store": "0.4.0-20230210031655-264744e",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"debug": "^4.3.4",
|
||||
"encoding": "^0.1.13",
|
||||
"firebase": "^9.15.0",
|
||||
|
||||
@@ -46,7 +46,16 @@ export class DataCenter {
|
||||
};
|
||||
};
|
||||
// TODO: switch different provider
|
||||
await dc.registerProvider(new LocalProvider(getInitParams()));
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.CLIENT_APP &&
|
||||
typeof window.__TAURI_IPC__ === 'function'
|
||||
) {
|
||||
const { TauriIPCProvider } = await import('./provider/tauri-ipc');
|
||||
await dc.registerProvider(new TauriIPCProvider(getInitParams()));
|
||||
} else {
|
||||
await dc.registerProvider(new LocalProvider(getInitParams()));
|
||||
}
|
||||
await dc.registerProvider(new AffineProvider(getInitParams()));
|
||||
|
||||
for (const provider of dc.providerMap.values()) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
CLIENT_APP?: boolean;
|
||||
__editoVersion?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -27,7 +27,7 @@ export enum WorkspaceType {
|
||||
export enum PermissionType {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 2,
|
||||
Admin = 10,
|
||||
Owner = 99,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { PutBlob, GetBlob } from '../ipc/types/blob';
|
||||
import {
|
||||
YDocumentUpdate,
|
||||
GetDocumentParameter,
|
||||
GetDocumentResponse,
|
||||
} from '../ipc/types/document';
|
||||
import { CreateUser, GetUserParameters, User } from '../ipc/types/user';
|
||||
import {
|
||||
CreateWorkspace,
|
||||
CreateWorkspaceResult,
|
||||
GetWorkspaces,
|
||||
GetWorkspacesResult,
|
||||
GetWorkspace,
|
||||
GetWorkspaceResult,
|
||||
} from '../ipc/types/workspace';
|
||||
|
||||
export const updateYDocument = async (parameters: YDocumentUpdate) =>
|
||||
await true;
|
||||
|
||||
export const getYDocument = async (
|
||||
parameters: GetDocumentParameter
|
||||
): Promise<GetDocumentResponse> =>
|
||||
await {
|
||||
updates: [],
|
||||
};
|
||||
|
||||
export const createWorkspace = async (
|
||||
parameters: CreateWorkspace
|
||||
): Promise<CreateWorkspaceResult> => await { id: 'xxx', name: 'xxx' };
|
||||
|
||||
export const getWorkspaces = async (
|
||||
parameters: GetWorkspaces
|
||||
): Promise<GetWorkspacesResult> =>
|
||||
await {
|
||||
workspaces: [],
|
||||
};
|
||||
|
||||
export const getWorkspace = async (
|
||||
parameters: GetWorkspace
|
||||
): Promise<GetWorkspaceResult> =>
|
||||
await {
|
||||
workspace: {
|
||||
created_at: 0,
|
||||
id: '',
|
||||
member_count: 1,
|
||||
public: true,
|
||||
type: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const putBlob = async (parameters: PutBlob): Promise<string> =>
|
||||
await 'path/xxx';
|
||||
|
||||
export const getBlob = async (parameters: GetBlob): Promise<number[]> =>
|
||||
await [];
|
||||
|
||||
/**
|
||||
* This will create a private workspace too.
|
||||
* @returns
|
||||
*/
|
||||
export const createUser = async (parameters: CreateUser): Promise<User> =>
|
||||
await {
|
||||
created_at: 0,
|
||||
id: '1',
|
||||
email: 'xxx@xxx.xxx',
|
||||
name: 'xxx',
|
||||
};
|
||||
|
||||
export const getUser = async (parameters: GetUserParameters): Promise<User> =>
|
||||
await {
|
||||
created_at: 0,
|
||||
id: '1',
|
||||
email: 'xxx@xxx.xxx',
|
||||
name: 'xxx',
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WorkspaceUnitCollection } from '../../../workspace-unit-collection.js';
|
||||
import { TauriIPCProvider } from '../index.js';
|
||||
import { MessageCenter } from '../../../message/index.js';
|
||||
import * as ipcMethods from './mock-apis.js';
|
||||
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
test.describe.serial('tauri-ipc provider', async () => {
|
||||
const workspaceMetaCollection = new WorkspaceUnitCollection();
|
||||
const provider = new TauriIPCProvider({
|
||||
workspaces: workspaceMetaCollection.createScope(),
|
||||
messageCenter: new MessageCenter(),
|
||||
});
|
||||
provider.init(ipcMethods);
|
||||
|
||||
const workspaceName = 'workspace-test';
|
||||
let workspaceId: string | undefined;
|
||||
|
||||
test('create workspace', async () => {
|
||||
const workspaceUnit = await provider.createWorkspace({
|
||||
name: workspaceName,
|
||||
});
|
||||
workspaceId = workspaceUnit?.id;
|
||||
|
||||
expect(workspaceMetaCollection.workspaces.length).toEqual(1);
|
||||
expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName);
|
||||
});
|
||||
|
||||
test('workspace list', async () => {
|
||||
const workspacesMetaCollection1 = new WorkspaceUnitCollection();
|
||||
const provider1 = new TauriIPCProvider({
|
||||
workspaces: workspacesMetaCollection1.createScope(),
|
||||
messageCenter: new MessageCenter(),
|
||||
});
|
||||
provider1.init(ipcMethods);
|
||||
|
||||
await provider1.getUserInfo();
|
||||
await provider1.loadWorkspaces();
|
||||
expect(workspacesMetaCollection1.workspaces.length).toEqual(0);
|
||||
await provider1.createWorkspace({
|
||||
name: workspaceName,
|
||||
});
|
||||
expect(workspacesMetaCollection1.workspaces[0].name).toEqual(workspaceName);
|
||||
expect(workspacesMetaCollection1.workspaces[0].id).toEqual(workspaceId);
|
||||
});
|
||||
|
||||
test('update workspace', async () => {
|
||||
await provider.updateWorkspaceMeta(workspaceId!, {
|
||||
name: '1111',
|
||||
});
|
||||
expect(workspaceMetaCollection.workspaces[0].name).toEqual('1111');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { BlobSyncState } from '@blocksuite/store';
|
||||
import * as ipcMethods from '../ipc/methods.js';
|
||||
import { Signal } from '@blocksuite/store';
|
||||
import type {
|
||||
BlobProvider,
|
||||
BlobSyncStateChangeEvent,
|
||||
BlobId,
|
||||
BlobURL,
|
||||
} from '@blocksuite/store/dist/persistence/blob/types.js';
|
||||
|
||||
export class IPCBlobProvider implements BlobProvider {
|
||||
#ipc = ipcMethods;
|
||||
|
||||
#workspace: string;
|
||||
|
||||
readonly signals = {
|
||||
onBlobSyncStateChange: new Signal<BlobSyncStateChangeEvent>(),
|
||||
};
|
||||
|
||||
private constructor(workspace: string) {
|
||||
this.#workspace = workspace;
|
||||
}
|
||||
|
||||
static async init(workspace: string): Promise<IPCBlobProvider> {
|
||||
const provider = new IPCBlobProvider(workspace);
|
||||
return provider;
|
||||
}
|
||||
|
||||
get uploading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get blobs() {
|
||||
// TODO: implement blob list in Octobase
|
||||
return Promise.resolve([]) as Promise<string[]>;
|
||||
}
|
||||
|
||||
async get(id: BlobId): Promise<BlobURL | null> {
|
||||
const blobArray = await this.#ipc.getBlob({
|
||||
id,
|
||||
});
|
||||
// Make a Blob from the bytes
|
||||
const blob = new Blob([new Uint8Array(blobArray)], { type: 'image/bmp' });
|
||||
this.signals.onBlobSyncStateChange.emit({
|
||||
id,
|
||||
state: BlobSyncState.Success,
|
||||
});
|
||||
return window.URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async set(blob: Blob): Promise<BlobId> {
|
||||
// TODO: skip if already has
|
||||
const blobID = await this.#ipc.putBlob({
|
||||
blob: Array.from(new Uint8Array(await blob.arrayBuffer())),
|
||||
});
|
||||
this.signals.onBlobSyncStateChange.emit({
|
||||
id: blobID,
|
||||
state: BlobSyncState.Success,
|
||||
});
|
||||
return blobID;
|
||||
}
|
||||
|
||||
async delete(id: BlobId): Promise<void> {
|
||||
// TODO: implement blob delete in Octobase
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
// TODO: implement blob clear in Octobase, use workspace id #workspace
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import * as Y from 'yjs';
|
||||
import assert from 'assert';
|
||||
|
||||
import { LocalProvider } from '../local/index.js';
|
||||
import type { IPCMethodsType } from './ipc/methods.js';
|
||||
import {
|
||||
CreateWorkspaceInfoParams,
|
||||
ProviderConstructorParams,
|
||||
} from '../base.js';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { IPCBlobProvider } from './blocksuite-provider/blob.js';
|
||||
import type { WorkspaceUnit } from 'src/workspace-unit.js';
|
||||
import { createWorkspaceUnit, loadWorkspaceUnit } from '../local/utils.js';
|
||||
import { WorkspaceWithPermission } from './ipc/types/workspace.js';
|
||||
import { applyUpdate } from '../../utils/index.js';
|
||||
import { User } from 'src/types/index.js';
|
||||
|
||||
/**
|
||||
* init - createUser - create first workspace and ydoc - loadWorkspace - return the first workspace - wrapWorkspace - #initDocFromIPC - applyUpdate - on('update') - updateYDocument
|
||||
*
|
||||
* (init - createUser - error) loadWorkspace - return the first workspace - wrapWorkspace - #initDocFromIPC - applyUpdate - on('update') - updateYDocument
|
||||
*/
|
||||
export class TauriIPCProvider extends LocalProvider {
|
||||
public id = 'tauri-ipc';
|
||||
static defaultUserEmail = 'xxx@xx.xx';
|
||||
/**
|
||||
* // TODO: We only have one user in this version of app client. But may support switch user later.
|
||||
*/
|
||||
#userID?: string;
|
||||
#ipc: IPCMethodsType | undefined;
|
||||
|
||||
constructor(params: ProviderConstructorParams) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
async init(ipc?: IPCMethodsType) {
|
||||
if (ipc) {
|
||||
this.#ipc = ipc;
|
||||
} else {
|
||||
this.#ipc = await import('./ipc/methods.js');
|
||||
}
|
||||
try {
|
||||
const user = await this.#ipc?.getUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
});
|
||||
this.#userID = user.id;
|
||||
} catch (error) {
|
||||
// maybe user not existed, we create a default user if we don't have one.
|
||||
try {
|
||||
const user = await this.#ipc?.createUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
name: 'xxx',
|
||||
password: 'xxx',
|
||||
avatar_url: '',
|
||||
});
|
||||
this.#userID = user.id;
|
||||
} catch (error) {
|
||||
// maybe user existed, which can be omited
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get auth user info
|
||||
* @returns
|
||||
*/
|
||||
public async getUserInfo(): Promise<User | undefined> {
|
||||
const user = await this.#ipc?.getUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
});
|
||||
if (user?.name !== undefined) {
|
||||
return {
|
||||
...user,
|
||||
avatar: user?.avatar_url || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async #initDocFromIPC(
|
||||
workspaceID: string,
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) {
|
||||
this._logger(`Loading ${workspaceID}...`);
|
||||
const result = await this.#ipc?.getYDocument({ id: workspaceID });
|
||||
if (result) {
|
||||
const updates = result.updates.map(
|
||||
binaryUpdate => new Uint8Array(binaryUpdate)
|
||||
);
|
||||
|
||||
const mergedUpdate = Y.mergeUpdates(updates);
|
||||
await applyUpdate(blocksuiteWorkspace, mergedUpdate);
|
||||
this._logger(`Loaded: ${workspaceID}`);
|
||||
}
|
||||
}
|
||||
|
||||
async #connectDocToIPC(
|
||||
workspaceID: string,
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) {
|
||||
this._logger(`Connecting yDoc for ${workspaceID}...`);
|
||||
blocksuiteWorkspace.doc.on('update', async (update: Uint8Array) => {
|
||||
try {
|
||||
const binary = Y.encodeStateAsUpdate(blocksuiteWorkspace.doc);
|
||||
const success = await this.#ipc?.updateYDocument({
|
||||
update: Array.from(binary),
|
||||
id: workspaceID,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(`YDoc update failed, id: ${workspaceID}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: write error log to disk, and add button to open them in settings panel
|
||||
console.error("#yDocument.on('update'", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await super.clear();
|
||||
}
|
||||
|
||||
override async warpWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
|
||||
const { room } = blocksuiteWorkspace;
|
||||
assert(room);
|
||||
|
||||
(await blocksuiteWorkspace.blobs)?.setProvider(
|
||||
await IPCBlobProvider.init(room)
|
||||
);
|
||||
await this.#initDocFromIPC(room, blocksuiteWorkspace);
|
||||
await this.#connectDocToIPC(room, blocksuiteWorkspace);
|
||||
|
||||
return blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
this._logger('Creating client app workspace');
|
||||
assert(this.#ipc);
|
||||
assert(this.#userID);
|
||||
const { id } = await this.#ipc.createWorkspace({
|
||||
name: meta.name,
|
||||
// TODO: get userID here
|
||||
user_id: this.#userID,
|
||||
});
|
||||
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
name: meta.name,
|
||||
id,
|
||||
published: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
});
|
||||
this._workspaces.add(workspaceUnit);
|
||||
const doc = workspaceUnit?.blocksuiteWorkspace?.doc;
|
||||
if (doc) {
|
||||
const update = Y.encodeStateAsUpdate(doc);
|
||||
const success = await this.#ipc?.updateYDocument({
|
||||
update: Array.from(update),
|
||||
id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(`YDoc update failed, id: ${id}`);
|
||||
}
|
||||
}
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
override async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
assert(this.#ipc);
|
||||
assert(this.#userID);
|
||||
const { workspaces } = await this.#ipc.getWorkspaces({
|
||||
user_id: this.#userID,
|
||||
});
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspaces.map((meta: WorkspaceWithPermission) => {
|
||||
return loadWorkspaceUnit({
|
||||
...meta,
|
||||
memberCount: 1,
|
||||
// TODO: load name here
|
||||
name: '',
|
||||
provider: this.id,
|
||||
syncMode: 'all',
|
||||
});
|
||||
})
|
||||
);
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import {
|
||||
GetDocumentParameter,
|
||||
GetDocumentResponse,
|
||||
YDocumentUpdate,
|
||||
} from './types/document';
|
||||
import {
|
||||
CreateWorkspace,
|
||||
CreateWorkspaceResult,
|
||||
GetWorkspace,
|
||||
GetWorkspaceResult,
|
||||
GetWorkspaces,
|
||||
GetWorkspacesResult,
|
||||
User,
|
||||
} from './types/workspace';
|
||||
import { GetBlob, PutBlob } from './types/blob';
|
||||
import { CreateUser, GetUserParameters } from './types/user';
|
||||
|
||||
export interface IPCMethodsType {
|
||||
updateYDocument: typeof updateYDocument;
|
||||
getYDocument: typeof getYDocument;
|
||||
createWorkspace: typeof createWorkspace;
|
||||
getWorkspaces: typeof getWorkspaces;
|
||||
getWorkspace: typeof getWorkspace;
|
||||
putBlob: typeof putBlob;
|
||||
getBlob: typeof getBlob;
|
||||
createUser: typeof createUser;
|
||||
getUser: typeof getUser;
|
||||
}
|
||||
|
||||
export const updateYDocument = async (parameters: YDocumentUpdate) =>
|
||||
await invoke<boolean>('update_y_document', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getYDocument = async (parameters: GetDocumentParameter) =>
|
||||
await invoke<GetDocumentResponse>('get_doc', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const createWorkspace = async (parameters: CreateWorkspace) =>
|
||||
await invoke<CreateWorkspaceResult>('create_workspace', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getWorkspaces = async (parameters: GetWorkspaces) =>
|
||||
await invoke<GetWorkspacesResult>('get_workspaces', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getWorkspace = async (parameters: GetWorkspace) =>
|
||||
await invoke<GetWorkspaceResult>('get_workspace', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const putBlob = async (parameters: PutBlob) =>
|
||||
await invoke<string>('put_blob', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getBlob = async (parameters: GetBlob) =>
|
||||
await invoke<number[]>('get_blob', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
/**
|
||||
* This will create a private workspace too.
|
||||
* @returns
|
||||
*/
|
||||
export const createUser = async (parameters: CreateUser) =>
|
||||
await invoke<User>('create_user', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getUser = async (parameters: GetUserParameters) =>
|
||||
await invoke<User>('get_user', {
|
||||
parameters,
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IBlobParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["Put"],
|
||||
"properties": {
|
||||
"Put": {
|
||||
"$ref": "#/definitions/PutBlob"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["Get"],
|
||||
"properties": {
|
||||
"Get": {
|
||||
"$ref": "#/definitions/GetBlob"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"GetBlob": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"PutBlob": {
|
||||
"type": "object",
|
||||
"required": ["blob"],
|
||||
"properties": {
|
||||
"blob": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IBlobParameters =
|
||||
| {
|
||||
Put: PutBlob;
|
||||
}
|
||||
| {
|
||||
Get: GetBlob;
|
||||
};
|
||||
|
||||
export interface PutBlob {
|
||||
blob: number[];
|
||||
workspace_id?: string | null;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetBlob {
|
||||
id: string;
|
||||
workspace_id?: string | null;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IDocumentParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["YDocumentUpdate"],
|
||||
"properties": {
|
||||
"YDocumentUpdate": {
|
||||
"$ref": "#/definitions/YDocumentUpdate"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateDocumentParameter"],
|
||||
"properties": {
|
||||
"CreateDocumentParameter": {
|
||||
"$ref": "#/definitions/CreateDocumentParameter"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetDocumentParameter"],
|
||||
"properties": {
|
||||
"GetDocumentParameter": {
|
||||
"$ref": "#/definitions/GetDocumentParameter"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetDocumentResponse"],
|
||||
"properties": {
|
||||
"GetDocumentResponse": {
|
||||
"$ref": "#/definitions/GetDocumentResponse"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateDocumentParameter": {
|
||||
"type": "object",
|
||||
"required": ["workspace_id", "workspace_name"],
|
||||
"properties": {
|
||||
"workspace_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDocumentParameter": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDocumentResponse": {
|
||||
"type": "object",
|
||||
"required": ["updates"],
|
||||
"properties": {
|
||||
"updates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"YDocumentUpdate": {
|
||||
"type": "object",
|
||||
"required": ["id", "update"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"update": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IDocumentParameters =
|
||||
| {
|
||||
YDocumentUpdate: YDocumentUpdate;
|
||||
}
|
||||
| {
|
||||
CreateDocumentParameter: CreateDocumentParameter;
|
||||
}
|
||||
| {
|
||||
GetDocumentParameter: GetDocumentParameter;
|
||||
}
|
||||
| {
|
||||
GetDocumentResponse: GetDocumentResponse;
|
||||
};
|
||||
|
||||
export interface YDocumentUpdate {
|
||||
id: string;
|
||||
update: number[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CreateDocumentParameter {
|
||||
workspace_id: string;
|
||||
workspace_name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetDocumentParameter {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetDocumentResponse {
|
||||
updates: number[][];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IUserParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateUser"],
|
||||
"properties": {
|
||||
"CreateUser": {
|
||||
"$ref": "#/definitions/CreateUser"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["User"],
|
||||
"properties": {
|
||||
"User": {
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetUserParameters"],
|
||||
"properties": {
|
||||
"GetUserParameters": {
|
||||
"$ref": "#/definitions/GetUserParameters"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateUser": {
|
||||
"type": "object",
|
||||
"required": ["email", "name", "password"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetUserParameters": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "email", "id", "name"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IUserParameters =
|
||||
| {
|
||||
CreateUser: CreateUser;
|
||||
}
|
||||
| {
|
||||
User: User;
|
||||
}
|
||||
| {
|
||||
GetUserParameters: GetUserParameters;
|
||||
};
|
||||
|
||||
export interface CreateUser {
|
||||
avatar_url?: string | null;
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface User {
|
||||
avatar_url?: string | null;
|
||||
created_at: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetUserParameters {
|
||||
email: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IWorkspaceParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateWorkspace"],
|
||||
"properties": {
|
||||
"CreateWorkspace": {
|
||||
"$ref": "#/definitions/CreateWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspace"],
|
||||
"properties": {
|
||||
"GetWorkspace": {
|
||||
"$ref": "#/definitions/GetWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspaces"],
|
||||
"properties": {
|
||||
"GetWorkspaces": {
|
||||
"$ref": "#/definitions/GetWorkspaces"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspaceResult"],
|
||||
"properties": {
|
||||
"GetWorkspaceResult": {
|
||||
"$ref": "#/definitions/GetWorkspaceResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspacesResult"],
|
||||
"properties": {
|
||||
"GetWorkspacesResult": {
|
||||
"$ref": "#/definitions/GetWorkspacesResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["UpdateWorkspace"],
|
||||
"properties": {
|
||||
"UpdateWorkspace": {
|
||||
"$ref": "#/definitions/UpdateWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateWorkspaceResult"],
|
||||
"properties": {
|
||||
"CreateWorkspaceResult": {
|
||||
"$ref": "#/definitions/CreateWorkspaceResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["name", "user_id"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "only set name, avatar is update in datacenter to yDoc directly",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateWorkspaceResult": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspaceResult": {
|
||||
"type": "object",
|
||||
"required": ["workspace"],
|
||||
"properties": {
|
||||
"workspace": {
|
||||
"$ref": "#/definitions/WorkspaceDetail"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspaces": {
|
||||
"type": "object",
|
||||
"required": ["user_id"],
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspacesResult": {
|
||||
"type": "object",
|
||||
"required": ["workspaces"],
|
||||
"properties": {
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WorkspaceWithPermission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionType": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1, 10, 99]
|
||||
},
|
||||
"UpdateWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["id", "public"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "email", "id", "name"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkspaceDetail": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "id", "member_count", "public", "type"],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"member_count": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"owner": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/User"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/WorkspaceType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkspaceType": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1]
|
||||
},
|
||||
"WorkspaceWithPermission": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "id", "permission", "public", "type"],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permission": {
|
||||
"$ref": "#/definitions/PermissionType"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/WorkspaceType"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IWorkspaceParameters =
|
||||
| {
|
||||
CreateWorkspace: CreateWorkspace;
|
||||
}
|
||||
| {
|
||||
GetWorkspace: GetWorkspace;
|
||||
}
|
||||
| {
|
||||
GetWorkspaces: GetWorkspaces;
|
||||
}
|
||||
| {
|
||||
GetWorkspaceResult: GetWorkspaceResult;
|
||||
}
|
||||
| {
|
||||
GetWorkspacesResult: GetWorkspacesResult;
|
||||
}
|
||||
| {
|
||||
UpdateWorkspace: UpdateWorkspace;
|
||||
}
|
||||
| {
|
||||
CreateWorkspaceResult: CreateWorkspaceResult;
|
||||
};
|
||||
export type WorkspaceType = 0 | 1;
|
||||
export type PermissionType = 0 | 1 | 10 | 99;
|
||||
|
||||
export interface CreateWorkspace {
|
||||
/**
|
||||
* only set name, avatar is update in datacenter to yDoc directly
|
||||
*/
|
||||
name: string;
|
||||
user_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspace {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspaces {
|
||||
user_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspaceResult {
|
||||
workspace: WorkspaceDetail;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface WorkspaceDetail {
|
||||
created_at: number;
|
||||
id: string;
|
||||
member_count: number;
|
||||
owner?: User | null;
|
||||
public: boolean;
|
||||
type: WorkspaceType;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface User {
|
||||
avatar_url?: string | null;
|
||||
created_at: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspacesResult {
|
||||
workspaces: WorkspaceWithPermission[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface WorkspaceWithPermission {
|
||||
created_at: number;
|
||||
id: string;
|
||||
permission: PermissionType;
|
||||
public: boolean;
|
||||
type: WorkspaceType;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface UpdateWorkspace {
|
||||
id: number;
|
||||
public: boolean;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CreateWorkspaceResult {
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -11,6 +11,13 @@ import { getLogger } from '../logger';
|
||||
// provider: string;
|
||||
// };
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
__TAURI_IPC__: Function;
|
||||
}
|
||||
}
|
||||
|
||||
export type User = {
|
||||
name: string;
|
||||
id: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
packages:
|
||||
# all packages in direct subdirs of packages/
|
||||
- 'packages/*'
|
||||
- 'packages/*'
|
||||
# app folder is on top level because of its importance
|
||||
- 'client-app'
|
||||
|
||||