mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 09:04:56 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec1948f62 | ||
|
|
7e6ead4232 | ||
|
|
02dcfdcc40 | ||
|
|
5a2f508dac | ||
|
|
2bd9f1a353 | ||
|
|
9f6ea83ac1 | ||
|
|
d33df659f8 | ||
|
|
c9a4129a3e |
@@ -5,7 +5,14 @@ rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"]
|
||||
[target.'cfg(target_os = "macos")']
|
||||
rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains", "-C", "link-args=-all_load", "-C", "link-args=-weak_framework ScreenCaptureKit"]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains",
|
||||
"-C",
|
||||
"link-args=-all_load",
|
||||
"-C",
|
||||
"link-args=-weak_framework ScreenCaptureKit",
|
||||
]
|
||||
# https://sourceware.org/bugzilla/show_bug.cgi?id=21032
|
||||
# https://sourceware.org/bugzilla/show_bug.cgi?id=21031
|
||||
# https://github.com/rust-lang/rust/issues/134820
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -33,6 +33,9 @@ node_modules
|
||||
!.vscode/launch.template.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Kiro
|
||||
.kiro
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
|
||||
10
.taplo.toml
10
.taplo.toml
@@ -1,7 +1,7 @@
|
||||
include = ["./*.toml", "./packages/**/*.toml"]
|
||||
exclude = ["node_modules/**/*.toml", "target/**/*.toml"]
|
||||
|
||||
# https://taplo.tamasfe.dev/configuration/formatter-options.html
|
||||
[formatting]
|
||||
align_entries = true
|
||||
column_width = 180
|
||||
reorder_arrays = true
|
||||
reorder_keys = true
|
||||
align_entries = true
|
||||
indent_tables = true
|
||||
reorder_keys = true
|
||||
|
||||
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -40,6 +40,7 @@ dependencies = [
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"cc",
|
||||
"chrono",
|
||||
"criterion2",
|
||||
@@ -50,6 +51,7 @@ dependencies = [
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"readability",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha3",
|
||||
"strum_macros",
|
||||
@@ -69,6 +71,7 @@ dependencies = [
|
||||
"tree-sitter-scala",
|
||||
"tree-sitter-typescript",
|
||||
"url",
|
||||
"y-octo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -442,12 +445,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
||||
|
||||
[[package]]
|
||||
name = "auto_enums"
|
||||
version = "0.8.7"
|
||||
@@ -584,18 +581,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -1612,12 +1597,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@@ -1724,19 +1703,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@@ -1834,10 +1800,6 @@ name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -2255,16 +2217,6 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lasso"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3280,25 +3232,6 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -3320,16 +3253,6 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -3350,15 +3273,6 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -3387,15 +3301,6 @@ dependencies = [
|
||||
"rand 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.3.0"
|
||||
@@ -4477,12 +4382,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
@@ -5198,12 +5097,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@@ -5808,15 +5701,6 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml5ever"
|
||||
version = "0.17.0"
|
||||
@@ -5842,10 +5726,8 @@ dependencies = [
|
||||
"arbitrary",
|
||||
"assert-json-diff",
|
||||
"async-lock",
|
||||
"bitvec",
|
||||
"byteorder",
|
||||
"criterion",
|
||||
"lasso",
|
||||
"lib0",
|
||||
"log",
|
||||
"loom",
|
||||
@@ -5862,7 +5744,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol_str",
|
||||
"thiserror 2.0.12",
|
||||
"yrs 0.23.4",
|
||||
"yrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5892,18 +5774,7 @@ dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"regex",
|
||||
"y-octo",
|
||||
"y-sync",
|
||||
"yrs 0.23.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y-sync"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e3675a497cde881a71e7e5c2ae1d087dfc7733ddece9b24a9a61408e969d3b"
|
||||
dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
"yrs 0.17.4",
|
||||
"yrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5930,21 +5801,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yrs"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4830316bfee4bec0044fe34a001cda783506d5c4c0852f8433c6041dfbfce51"
|
||||
dependencies = [
|
||||
"atomic_refcell",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallstr",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yrs"
|
||||
version = "0.23.4"
|
||||
|
||||
223
Cargo.toml
223
Cargo.toml
@@ -13,109 +13,122 @@ members = [
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
affine_common = { path = "./packages/common/native" }
|
||||
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
|
||||
ahash = "0.8"
|
||||
anyhow = "1"
|
||||
arbitrary = { version = "1.3", features = ["derive"] }
|
||||
assert-json-diff = "2.0"
|
||||
async-lock = { version = "3.4.0", features = ["loom"] }
|
||||
base64-simd = "0.8"
|
||||
bitvec = "1.0"
|
||||
block2 = "0.6"
|
||||
byteorder = "1.5"
|
||||
cpal = "0.15"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
core-foundation = "0.10"
|
||||
coreaudio-rs = "0.12"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion2 = { version = "3", default-features = false }
|
||||
crossbeam-channel = "0.5"
|
||||
dispatch2 = "0.3"
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.28", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
infer = { version = "0.19.0" }
|
||||
lasso = { version = "0.7", features = ["multi-threaded"] }
|
||||
lib0 = { version = "0.16", features = ["lib0-serde"] }
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
objc2-foundation = "0.3"
|
||||
once_cell = "1"
|
||||
ordered-float = "5"
|
||||
parking_lot = "0.12"
|
||||
path-ext = "0.1.2"
|
||||
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
proptest = "1.3"
|
||||
proptest-derive = "0.5"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
rand_distr = "0.5"
|
||||
rayon = "1.10"
|
||||
readability = { version = "0.3.0", default-features = false }
|
||||
regex = "1.10"
|
||||
rubato = "0.16"
|
||||
screencapturekit = "0.3"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
smol_str = "0.3"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
strum_macros = "0.27.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.27"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
tree-sitter-java = { version = "0.23" }
|
||||
tree-sitter-javascript = { version = "0.23" }
|
||||
tree-sitter-kotlin-ng = { version = "1.1" }
|
||||
tree-sitter-python = { version = "0.23" }
|
||||
tree-sitter-rust = { version = "0.24" }
|
||||
tree-sitter-scala = { version = "0.24" }
|
||||
tree-sitter-typescript = { version = "0.23" }
|
||||
uniffi = "0.29"
|
||||
url = { version = "2.5" }
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Devices_FunctionDiscovery",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_Media_Audio",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
] }
|
||||
windows-core = { version = "0.61" }
|
||||
y-octo = { path = "./packages/common/y-octo/core" }
|
||||
y-sync = { version = "0.4" }
|
||||
yrs = "0.23.0"
|
||||
[workspace.dependencies]
|
||||
affine_common = { path = "./packages/common/native" }
|
||||
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
|
||||
ahash = "0.8"
|
||||
anyhow = "1"
|
||||
arbitrary = { version = "1.3", features = ["derive"] }
|
||||
assert-json-diff = "2.0"
|
||||
async-lock = { version = "3.4.0", features = ["loom"] }
|
||||
base64-simd = "0.8"
|
||||
bitvec = "1.0"
|
||||
block2 = "0.6"
|
||||
byteorder = "1.5"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
core-foundation = "0.10"
|
||||
coreaudio-rs = "0.12"
|
||||
cpal = "0.15"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion2 = { version = "3", default-features = false }
|
||||
crossbeam-channel = "0.5"
|
||||
dispatch2 = "0.3"
|
||||
docx-parser = { git = "https://github.com/toeverything/docx-parser" }
|
||||
dotenvy = "0.15"
|
||||
file-format = { version = "0.28", features = ["reader"] }
|
||||
homedir = "0.3"
|
||||
infer = { version = "0.19.0" }
|
||||
lasso = { version = "0.7", features = ["multi-threaded"] }
|
||||
lib0 = { version = "0.16", features = ["lib0-serde"] }
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-beta.3", features = [
|
||||
"async",
|
||||
"chrono_date",
|
||||
"error_anyhow",
|
||||
"napi9",
|
||||
"serde",
|
||||
] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
objc2-foundation = "0.3"
|
||||
once_cell = "1"
|
||||
ordered-float = "5"
|
||||
parking_lot = "0.12"
|
||||
path-ext = "0.1.2"
|
||||
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
proptest = "1.3"
|
||||
proptest-derive = "0.5"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
rand_distr = "0.5"
|
||||
rayon = "1.10"
|
||||
readability = { version = "0.3.0", default-features = false }
|
||||
regex = "1.10"
|
||||
rubato = "0.16"
|
||||
screencapturekit = "0.3"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
smol_str = "0.3"
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
strum_macros = "0.27.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.27"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
tree-sitter-java = { version = "0.23" }
|
||||
tree-sitter-javascript = { version = "0.23" }
|
||||
tree-sitter-kotlin-ng = { version = "1.1" }
|
||||
tree-sitter-python = { version = "0.23" }
|
||||
tree-sitter-rust = { version = "0.24" }
|
||||
tree-sitter-scala = { version = "0.24" }
|
||||
tree-sitter-typescript = { version = "0.23" }
|
||||
uniffi = "0.29"
|
||||
url = { version = "2.5" }
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Devices_FunctionDiscovery",
|
||||
"Win32_Foundation",
|
||||
"Win32_Media_Audio",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Variant",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
] }
|
||||
windows-core = { version = "0.61" }
|
||||
y-octo = { path = "./packages/common/y-octo/core" }
|
||||
y-sync = { version = "0.4" }
|
||||
yrs = "0.23.0"
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
@@ -126,6 +139,6 @@ lto = true
|
||||
opt-level = 3
|
||||
strip = "symbols"
|
||||
|
||||
# android uniffi bindgen requires symbols
|
||||
[profile.release.package.affine_mobile_native]
|
||||
strip = "none"
|
||||
# android uniffi bindgen requires symbols
|
||||
[profile.release.package.affine_mobile_native]
|
||||
strip = "none"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { focusBlockEnd } from '@blocksuite/affine-shared/commands';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { FontIcon } from '@blocksuite/icons/lit';
|
||||
@@ -18,10 +17,11 @@ export const calloutSlashMenuConfig: SlashMenuConfig = {
|
||||
},
|
||||
searchAlias: ['callout'],
|
||||
group: '0_Basic@9',
|
||||
when: ({ std, model }) => {
|
||||
return (
|
||||
std.get(FeatureFlagService).getFlag('enable_callout') &&
|
||||
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
|
||||
when: ({ model }) => {
|
||||
return !isInsideBlockByFlavour(
|
||||
model.store,
|
||||
model,
|
||||
'affine:edgeless-text'
|
||||
);
|
||||
},
|
||||
action: ({ model, std }) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface BlockSuiteFlags {
|
||||
enable_mobile_linked_doc_menu: boolean;
|
||||
enable_mobile_database_editing: boolean;
|
||||
enable_block_meta: boolean;
|
||||
enable_callout: boolean;
|
||||
enable_edgeless_scribbled_style: boolean;
|
||||
enable_table_virtual_scroll: boolean;
|
||||
enable_turbo_renderer: boolean;
|
||||
@@ -43,7 +42,6 @@ export class FeatureFlagService extends StoreExtension {
|
||||
enable_mobile_linked_doc_menu: false,
|
||||
enable_block_meta: true,
|
||||
enable_mobile_database_editing: false,
|
||||
enable_callout: false,
|
||||
enable_edgeless_scribbled_style: false,
|
||||
enable_table_virtual_scroll: false,
|
||||
enable_turbo_renderer: false,
|
||||
|
||||
@@ -47,10 +47,7 @@ import {
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
createDefaultDoc,
|
||||
@@ -290,10 +287,11 @@ const textToolActionItems: KeyboardToolbarActionItem[] = [
|
||||
{
|
||||
name: 'Callout',
|
||||
icon: FontIcon(),
|
||||
showWhen: ({ std, rootComponent: { model } }) => {
|
||||
return (
|
||||
std.get(FeatureFlagService).getFlag('enable_callout') &&
|
||||
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
|
||||
showWhen: ({ rootComponent: { model } }) => {
|
||||
return !isInsideBlockByFlavour(
|
||||
model.store,
|
||||
model,
|
||||
'affine:edgeless-text'
|
||||
);
|
||||
},
|
||||
action: ({ rootComponent: { model }, std }) => {
|
||||
|
||||
@@ -181,6 +181,10 @@ export class LinkedDocPopover extends SignalWatcher(
|
||||
target: eventSource,
|
||||
signal: keydownObserverAbortController.signal,
|
||||
interceptor: (event, next) => {
|
||||
if (event.key === 'GroupNext' || event.key === 'GroupPrevious') {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -4,8 +4,22 @@ name = "affine_common"
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
doc-loader = ["docx-parser", "infer", "path-ext", "pdf-extract", "readability", "serde_json", "strum_macros", "text-splitter", "thiserror", "tree-sitter", "url"]
|
||||
default = ["hashcash"]
|
||||
doc-loader = [
|
||||
"docx-parser",
|
||||
"infer",
|
||||
"path-ext",
|
||||
"pdf-extract",
|
||||
"readability",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum_macros",
|
||||
"text-splitter",
|
||||
"thiserror",
|
||||
"tree-sitter",
|
||||
"url",
|
||||
]
|
||||
hashcash = ["sha3", "rand"]
|
||||
tree-sitter = [
|
||||
"cc",
|
||||
"dep:tree-sitter",
|
||||
@@ -21,34 +35,41 @@ tree-sitter = [
|
||||
"dep:tree-sitter-scala",
|
||||
"dep:tree-sitter-typescript",
|
||||
]
|
||||
ydoc-loader = ["assert-json-diff", "y-octo"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
sha3 = { workspace = true, optional = true }
|
||||
|
||||
docx-parser = { workspace = true, optional = true }
|
||||
infer = { workspace = true, optional = true }
|
||||
path-ext = { workspace = true, optional = true }
|
||||
pdf-extract = { workspace = true, optional = true }
|
||||
readability = { workspace = true, optional = true, default-features = false }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
strum_macros = { workspace = true, optional = true }
|
||||
text-splitter = { workspace = true, features = ["markdown", "tiktoken-rs"], optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
tree-sitter = { workspace = true, optional = true }
|
||||
tree-sitter-c = { workspace = true, optional = true }
|
||||
tree-sitter-c-sharp = { workspace = true, optional = true }
|
||||
tree-sitter-cpp = { workspace = true, optional = true }
|
||||
tree-sitter-go = { workspace = true, optional = true }
|
||||
tree-sitter-java = { workspace = true, optional = true }
|
||||
assert-json-diff = { workspace = true, optional = true }
|
||||
docx-parser = { workspace = true, optional = true }
|
||||
infer = { workspace = true, optional = true }
|
||||
path-ext = { workspace = true, optional = true }
|
||||
pdf-extract = { workspace = true, optional = true }
|
||||
readability = { workspace = true, optional = true, default-features = false }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
strum_macros = { workspace = true, optional = true }
|
||||
text-splitter = { workspace = true, features = [
|
||||
"markdown",
|
||||
"tiktoken-rs",
|
||||
], optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
tree-sitter = { workspace = true, optional = true }
|
||||
tree-sitter-c = { workspace = true, optional = true }
|
||||
tree-sitter-c-sharp = { workspace = true, optional = true }
|
||||
tree-sitter-cpp = { workspace = true, optional = true }
|
||||
tree-sitter-go = { workspace = true, optional = true }
|
||||
tree-sitter-java = { workspace = true, optional = true }
|
||||
tree-sitter-javascript = { workspace = true, optional = true }
|
||||
tree-sitter-kotlin-ng = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-scala = { workspace = true, optional = true }
|
||||
tree-sitter-kotlin-ng = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-scala = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
y-octo = { workspace = true, optional = true }
|
||||
|
||||
tiktoken-rs = { workspace = true }
|
||||
|
||||
|
||||
BIN
packages/common/native/fixtures/demo.ydoc
Normal file
BIN
packages/common/native/fixtures/demo.ydoc
Normal file
Binary file not shown.
567
packages/common/native/fixtures/demo.ydoc.json
Normal file
567
packages/common/native/fixtures/demo.ydoc.json
Normal file
@@ -0,0 +1,567 @@
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"block_id": "F_zl1z0ex6dSxM25ZBUWk",
|
||||
"flavour": "affine:page",
|
||||
"content": [
|
||||
"Write, Draw, Plan all at Once."
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": null,
|
||||
"parent_block_id": null,
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "U8g2Edcy8rSmu_XUpYSTz",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. "
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "NxvF6M1Zo-TrUoJyFbMy4",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "VHVvola5BtXnC7FMaGzE7",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"You own your data, with no compromises"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "Hvbw1lSZ7Bl-LMiOc_xFb",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Local-first & Real-time collaborative"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "2N8pJ32byPEc7dlJv-0cC",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience."
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "rysuvULJQgFPRmNB_iTk3",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time."
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "zbvmiGhuxn0_tfJ_e4N2V",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "omG6fe87r60dPxCtv_Odb",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"omG6fe87r60dPxCtv_Odb\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "CQeQaaREQwGDXg9oAng4q",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Blocks that assemble your next docs, tasks kanban or whiteboard"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "QqLfbOIINoAxapnB23E5t",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"QqLfbOIINoAxapnB23E5t\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "RTtZAHZG4xoAXKKnEsRhB",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"There is a large overlap of their atomic \"building blocks\" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. "
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "Fpxp7rZCad7-W51xCWta1",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"Fpxp7rZCad7-W51xCWta1\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "c_KFpzTBHdioeoIsnLIjd",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too."
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "Fpxp7rZCad7-W51xCWta1",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"Fpxp7rZCad7-W51xCWta1\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "O6D7PxXwCbfCLp1_peft1",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"If you want to learn more about the product design of AFFiNE, here goes the concepts:"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "Fpxp7rZCad7-W51xCWta1",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"Fpxp7rZCad7-W51xCWta1\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "os84XIVFN6z6aN9tq16S5",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools."
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "Fpxp7rZCad7-W51xCWta1",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"Fpxp7rZCad7-W51xCWta1\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "8jk1eREgtKdhwtee0nym-",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"A true canvas for blocks in any form"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "kDyc-RntYCDWSSYFl4SSn",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. "
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "k0e4IqfQ2PRmsXjzDQ-3T",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "QR-4T-qYH3gTbbo_VKjU-",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"\"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "nyEEsW01cCQVIr7yG9ywg",
|
||||
"flavour": "affine:list",
|
||||
"content": [
|
||||
"Quip & Notion with their great concept of \"everything is a block\""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "ZmeTH3oq8ON10pqV1pQ-n",
|
||||
"flavour": "affine:list",
|
||||
"content": [
|
||||
"Trello with their Kanban"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "SbbdUF95MVoBTlxILHyUL",
|
||||
"flavour": "affine:list",
|
||||
"content": [
|
||||
"Airtable & Miro with their no-code programable datasheets"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "fIQp-yM2v8Iar7kXAooXh",
|
||||
"flavour": "affine:list",
|
||||
"content": [
|
||||
"Miro & Whimiscal with their edgeless visual whiteboard"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "2S9xQwALMBxbas1N6Eyf4",
|
||||
"flavour": "affine:list",
|
||||
"content": [
|
||||
"Remnote & Capacities with their object-based tag system"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "sfycWDfMpgKsE7s5qucWr",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"sfycWDfMpgKsE7s5qucWr\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "C5xu77eIYukdKCu7h0Cjp",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"For more details, please refer to our RoadMap"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "tPAicJ4WMxtBhys0Exlxq",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"tPAicJ4WMxtBhys0Exlxq\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "2goqe7zWXhtKX-pLcYN-h",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Self Host"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "tPAicJ4WMxtBhys0Exlxq",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"tPAicJ4WMxtBhys0Exlxq\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "cBud4or0twRvF36twhwqZ",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Self host AFFiNE"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "tPAicJ4WMxtBhys0Exlxq",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"tPAicJ4WMxtBhys0Exlxq\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"flavour": "affine:database",
|
||||
"content": [
|
||||
"Learning From",
|
||||
"Title",
|
||||
"Tag",
|
||||
"Reference",
|
||||
"Developers",
|
||||
"AFFiNE"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "j9pzdPaGIVH627CPh_mik",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "YJ13TsE8mv5mxy2DIpMOD",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Affine Development"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "T_BOz9f-9uD5QjKABiciE",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"For developers or installations guides, please go to AFFiNE Doc"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "yiMJW4ASGVUC7mly7ulce",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Quip & Notion with their great concept of \"everything is a block\""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "qug1UtELLsBg2XwU1IJYK",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Trello with their Kanban"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "p86Sceg7KYFe1OgUUswCM",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Airtable & Miro with their no-code programable datasheets"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "ahyiL8RAajMoIoDekk2Je",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Miro & Whimiscal with their edgeless visual whiteboard"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "pKh_LNdwWD-yLDh-TVYAz",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Remnote & Capacities with their object-based tag system"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:database",
|
||||
"parent_block_id": "FB07QqbHZhRqj43IEOzJP",
|
||||
"additional": "{\"databaseName\":\"Learning From\",\"displayMode\":\"page\",\"noteBlockId\":\"j9pzdPaGIVH627CPh_mik\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "b0ftuFAo7qDYRiI18TN6b",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"Affine Development"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "wyVFKrydUAthKQEML84vE",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"wyVFKrydUAthKQEML84vE\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "64rS_MtVI-f_MpE6qCbls",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
"For developer or installation guides, please go to AFFiNE Development"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "wyVFKrydUAthKQEML84vE",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"wyVFKrydUAthKQEML84vE\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "7dotDICZwPWNvFXZSh-oF",
|
||||
"flavour": "affine:paragraph",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:note",
|
||||
"parent_block_id": "wyVFKrydUAthKQEML84vE",
|
||||
"additional": "{\"displayMode\":\"page\",\"noteBlockId\":\"wyVFKrydUAthKQEML84vE\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"flavour": "affine:surface",
|
||||
"content": [
|
||||
"",
|
||||
" ",
|
||||
"AFFiNE ",
|
||||
"Database Reference",
|
||||
"Development",
|
||||
"Related Articles",
|
||||
"Self-host",
|
||||
"What is AFFiNE",
|
||||
"You can check these URLs to learn about AFFiNE"
|
||||
],
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:page",
|
||||
"parent_block_id": "F_zl1z0ex6dSxM25ZBUWk",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "YkZXI2iWRvQARfQPqspP_",
|
||||
"flavour": "affine:bookmark",
|
||||
"content": null,
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:surface",
|
||||
"parent_block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "6xwR75KDkxvygEFWZfmhZ",
|
||||
"flavour": "affine:bookmark",
|
||||
"content": null,
|
||||
"blob": null,
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:surface",
|
||||
"parent_block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "AjY4OYuyp2536pBZlf-vD",
|
||||
"flavour": "affine:image",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": [
|
||||
"BFZk3c2ERp-sliRvA7MQ_p3NdkdCLt2Ze0DQ9i21dpA="
|
||||
],
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:surface",
|
||||
"parent_block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "ErTXTP8VqP5fabuB5FZHH",
|
||||
"flavour": "affine:image",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": [
|
||||
"HWvCItS78DzPGbwcuaGcfkpVDUvL98IvH5SIK8-AcL8="
|
||||
],
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:surface",
|
||||
"parent_block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
},
|
||||
{
|
||||
"block_id": "hMg8RXNlkD7XJS9b27fGA",
|
||||
"flavour": "affine:image",
|
||||
"content": [
|
||||
""
|
||||
],
|
||||
"blob": [
|
||||
"ZRKpsBoC88qEMmeiXKXqywfA1rLvWoLa5rpEh9x9Oj0="
|
||||
],
|
||||
"ref_doc_id": null,
|
||||
"ref_info": null,
|
||||
"parent_flavour": "affine:surface",
|
||||
"parent_block_id": "Jn4N8YdsFnqjBn-ae5zeR",
|
||||
"additional": "{\"displayMode\":\"edgeless\"}"
|
||||
}
|
||||
],
|
||||
"title": "Write, Draw, Plan all at Once.",
|
||||
"summary": "AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. You own your data, with no compromisesLocal-first & Real-time collaborativeWe love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.Blocks that assemble your next docs, tasks kanban or whiteboardThere is a large overlap of their atomic \"building blocks\" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. "
|
||||
}
|
||||
528
packages/common/native/src/doc_parser.rs
Normal file
528
packages/common/native/src/doc_parser.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue};
|
||||
use thiserror::Error;
|
||||
use y_octo::{Any, DocOptions, JwstCodecError, Map, Value};
|
||||
|
||||
const SUMMARY_LIMIT: usize = 1000;
|
||||
const PAGE_FLAVOUR: &str = "affine:page";
|
||||
const NOTE_FLAVOUR: &str = "affine:note";
|
||||
|
||||
const BOOKMARK_FLAVOURS: [&str; 5] = [
|
||||
"affine:bookmark",
|
||||
"affine:embed-youtube",
|
||||
"affine:embed-figma",
|
||||
"affine:embed-github",
|
||||
"affine:embed-loom",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrawlDocInput {
|
||||
pub doc_bin: Vec<u8>,
|
||||
pub root_doc_bin: Option<Vec<u8>>,
|
||||
pub space_id: String,
|
||||
pub doc_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlockInfo {
|
||||
pub block_id: String,
|
||||
pub flavour: String,
|
||||
pub content: Option<Vec<String>>,
|
||||
pub blob: Option<Vec<String>>,
|
||||
pub ref_doc_id: Option<Vec<String>>,
|
||||
pub ref_info: Option<Vec<String>>,
|
||||
pub parent_flavour: Option<String>,
|
||||
pub parent_block_id: Option<String>,
|
||||
pub additional: Option<String>,
|
||||
}
|
||||
|
||||
impl BlockInfo {
|
||||
fn base(
|
||||
block_id: &str,
|
||||
flavour: &str,
|
||||
parent_flavour: Option<&String>,
|
||||
parent_block_id: Option<&String>,
|
||||
additional: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
block_id: block_id.to_string(),
|
||||
flavour: flavour.to_string(),
|
||||
content: None,
|
||||
blob: None,
|
||||
ref_doc_id: None,
|
||||
ref_info: None,
|
||||
parent_flavour: parent_flavour.cloned(),
|
||||
parent_block_id: parent_block_id.cloned(),
|
||||
additional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CrawlResult {
|
||||
pub blocks: Vec<BlockInfo>,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize)]
|
||||
pub enum ParseError {
|
||||
#[error("doc_not_found")]
|
||||
DocNotFound,
|
||||
#[error("invalid_binary")]
|
||||
InvalidBinary,
|
||||
#[error("sqlite_error: {0}")]
|
||||
SqliteError(String),
|
||||
#[error("parser_error: {0}")]
|
||||
ParserError(String),
|
||||
#[error("unknown: {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<JwstCodecError> for ParseError {
|
||||
fn from(value: JwstCodecError) -> Self {
|
||||
Self::ParserError(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_doc_from_binary(input: CrawlDocInput) -> Result<CrawlResult, ParseError> {
|
||||
let CrawlDocInput {
|
||||
doc_bin,
|
||||
root_doc_bin: _,
|
||||
space_id: _,
|
||||
doc_id,
|
||||
} = input;
|
||||
|
||||
if doc_bin.is_empty() {
|
||||
return Err(ParseError::InvalidBinary);
|
||||
}
|
||||
|
||||
let mut doc = DocOptions::new().with_guid(doc_id.clone()).build();
|
||||
doc
|
||||
.apply_update_from_binary_v1(&doc_bin)
|
||||
.map_err(|_| ParseError::InvalidBinary)?;
|
||||
|
||||
let blocks_map = doc.get_map("blocks")?;
|
||||
if blocks_map.is_empty() {
|
||||
return Err(ParseError::ParserError("blocks map is empty".into()));
|
||||
}
|
||||
|
||||
let mut block_pool: HashMap<String, Map> = HashMap::new();
|
||||
let mut parent_lookup: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for (_, value) in blocks_map.iter() {
|
||||
if let Some(block_map) = value.to_map() {
|
||||
if let Some(block_id) = get_block_id(&block_map) {
|
||||
for child_id in collect_child_ids(&block_map) {
|
||||
parent_lookup.insert(child_id, block_id.clone());
|
||||
}
|
||||
block_pool.insert(block_id, block_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let root_block_id = block_pool
|
||||
.iter()
|
||||
.find_map(|(id, block)| {
|
||||
get_flavour(block)
|
||||
.filter(|flavour| flavour == PAGE_FLAVOUR)
|
||||
.map(|_| id.clone())
|
||||
})
|
||||
.ok_or_else(|| ParseError::ParserError("root block not found".into()))?;
|
||||
|
||||
let mut queue: Vec<(Option<String>, String)> = vec![(None, root_block_id.clone())];
|
||||
let mut visited: HashSet<String> = HashSet::from([root_block_id.clone()]);
|
||||
let mut blocks: Vec<BlockInfo> = Vec::with_capacity(block_pool.len());
|
||||
let mut doc_title = String::new();
|
||||
let mut summary = String::new();
|
||||
let mut summary_remaining = SUMMARY_LIMIT as isize;
|
||||
|
||||
while let Some((parent_block_id, block_id)) = queue.pop() {
|
||||
let block = match block_pool.get(&block_id) {
|
||||
Some(block) => block,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let flavour = match get_flavour(block) {
|
||||
Some(flavour) => flavour,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let parent_block = parent_block_id.as_ref().and_then(|id| block_pool.get(id));
|
||||
let parent_flavour = parent_block.and_then(get_flavour);
|
||||
|
||||
let note_block = nearest_by_flavour(&block_id, NOTE_FLAVOUR, &parent_lookup, &block_pool);
|
||||
let note_block_id = note_block.as_ref().and_then(get_block_id);
|
||||
let display_mode = determine_display_mode(note_block.as_ref());
|
||||
|
||||
// enqueue children first to keep traversal order similar to JS implementation
|
||||
let mut child_ids = collect_child_ids(block);
|
||||
for child_id in child_ids.drain(..).rev() {
|
||||
if visited.insert(child_id.clone()) {
|
||||
queue.push((Some(block_id.clone()), child_id));
|
||||
}
|
||||
}
|
||||
|
||||
let build_block = |database_name: Option<&String>| {
|
||||
BlockInfo::base(
|
||||
&block_id,
|
||||
&flavour,
|
||||
parent_flavour.as_ref(),
|
||||
parent_block_id.as_ref(),
|
||||
compose_additional(&display_mode, note_block_id.as_ref(), database_name),
|
||||
)
|
||||
};
|
||||
|
||||
if flavour == PAGE_FLAVOUR {
|
||||
let title = get_string(block, "prop:title").unwrap_or_default();
|
||||
doc_title = title.clone();
|
||||
let mut info = build_block(None);
|
||||
info.content = Some(vec![title]);
|
||||
blocks.push(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
flavour.as_str(),
|
||||
"affine:paragraph" | "affine:list" | "affine:code"
|
||||
) {
|
||||
if let Some((text, text_len)) = text_content(block, "prop:text") {
|
||||
let database_name = if flavour == "affine:paragraph"
|
||||
&& parent_flavour.as_deref() == Some("affine:database")
|
||||
{
|
||||
parent_block.and_then(|map| get_string(map, "prop:title"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut info = build_block(database_name.as_ref());
|
||||
info.content = Some(vec![text.clone()]);
|
||||
blocks.push(info);
|
||||
append_summary(&mut summary, &mut summary_remaining, text_len, &text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
flavour.as_str(),
|
||||
"affine:embed-linked-doc" | "affine:embed-synced-doc"
|
||||
) {
|
||||
if let Some(page_id) = get_string(block, "prop:pageId") {
|
||||
let mut info = build_block(None);
|
||||
info.ref_doc_id = Some(vec![page_id.clone()]);
|
||||
if let Some(payload) = embed_ref_payload(block, &page_id) {
|
||||
info.ref_info = Some(vec![payload]);
|
||||
}
|
||||
blocks.push(info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:attachment" {
|
||||
if let Some(blob_id) = get_string(block, "prop:sourceId") {
|
||||
let mut info = build_block(None);
|
||||
info.blob = Some(vec![blob_id]);
|
||||
info.content = Some(vec![get_string(block, "prop:name").unwrap_or_default()]);
|
||||
blocks.push(info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:image" {
|
||||
if let Some(blob_id) = get_string(block, "prop:sourceId") {
|
||||
let mut info = build_block(None);
|
||||
info.blob = Some(vec![blob_id]);
|
||||
info.content = Some(vec![get_string(block, "prop:caption").unwrap_or_default()]);
|
||||
blocks.push(info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:surface" {
|
||||
let texts = gather_surface_texts(block);
|
||||
let mut info = build_block(None);
|
||||
info.content = Some(texts);
|
||||
blocks.push(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:database" {
|
||||
let (texts, database_name) = gather_database_texts(block);
|
||||
let mut info = BlockInfo::base(
|
||||
&block_id,
|
||||
&flavour,
|
||||
parent_flavour.as_ref(),
|
||||
parent_block_id.as_ref(),
|
||||
compose_additional(
|
||||
&display_mode,
|
||||
note_block_id.as_ref(),
|
||||
database_name.as_ref(),
|
||||
),
|
||||
);
|
||||
info.content = Some(texts);
|
||||
blocks.push(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:latex" {
|
||||
if let Some(content) = get_string(block, "prop:latex") {
|
||||
let mut info = build_block(None);
|
||||
info.content = Some(vec![content]);
|
||||
blocks.push(info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if flavour == "affine:table" {
|
||||
let contents = gather_table_contents(block);
|
||||
let mut info = build_block(None);
|
||||
info.content = Some(contents);
|
||||
blocks.push(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
if BOOKMARK_FLAVOURS.contains(&flavour.as_str()) {
|
||||
blocks.push(build_block(None));
|
||||
}
|
||||
}
|
||||
|
||||
if doc_title.is_empty() {
|
||||
doc_title = "Untitled".into();
|
||||
}
|
||||
|
||||
Ok(CrawlResult {
|
||||
blocks,
|
||||
title: doc_title,
|
||||
summary,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_child_ids(block: &Map) -> Vec<String> {
|
||||
block
|
||||
.get("sys:children")
|
||||
.and_then(|value| value.to_array())
|
||||
.map(|array| {
|
||||
array
|
||||
.iter()
|
||||
.filter_map(|value| value_to_string(&value))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_block_id(block: &Map) -> Option<String> {
|
||||
get_string(block, "sys:id")
|
||||
}
|
||||
|
||||
fn get_flavour(block: &Map) -> Option<String> {
|
||||
get_string(block, "sys:flavour")
|
||||
}
|
||||
|
||||
fn get_string(block: &Map, key: &str) -> Option<String> {
|
||||
block.get(key).and_then(|value| value_to_string(&value))
|
||||
}
|
||||
|
||||
fn text_content(block: &Map, key: &str) -> Option<(String, usize)> {
|
||||
block.get(key).and_then(|value| {
|
||||
value.to_text().map(|text| {
|
||||
let content = text.to_string();
|
||||
let len = text.len() as usize;
|
||||
(content, len)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn nearest_by_flavour(
|
||||
start: &str,
|
||||
flavour: &str,
|
||||
parent_lookup: &HashMap<String, String>,
|
||||
blocks: &HashMap<String, Map>,
|
||||
) -> Option<Map> {
|
||||
let mut cursor = Some(start.to_string());
|
||||
while let Some(node) = cursor {
|
||||
if let Some(block) = blocks.get(&node) {
|
||||
if get_flavour(block).as_deref() == Some(flavour) {
|
||||
return Some(block.clone());
|
||||
}
|
||||
}
|
||||
cursor = parent_lookup.get(&node).cloned();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn determine_display_mode(note_block: Option<&Map>) -> String {
|
||||
match note_block.and_then(|block| get_string(block, "prop:displayMode")) {
|
||||
Some(mode) if mode == "both" => "page".into(),
|
||||
Some(mode) => mode,
|
||||
None => "edgeless".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_additional(
|
||||
display_mode: &str,
|
||||
note_block_id: Option<&String>,
|
||||
database_name: Option<&String>,
|
||||
) -> Option<String> {
|
||||
let mut payload = JsonMap::new();
|
||||
payload.insert(
|
||||
"displayMode".into(),
|
||||
JsonValue::String(display_mode.to_string()),
|
||||
);
|
||||
if let Some(note_id) = note_block_id {
|
||||
payload.insert("noteBlockId".into(), JsonValue::String(note_id.clone()));
|
||||
}
|
||||
if let Some(name) = database_name {
|
||||
payload.insert("databaseName".into(), JsonValue::String(name.clone()));
|
||||
}
|
||||
Some(JsonValue::Object(payload).to_string())
|
||||
}
|
||||
|
||||
fn embed_ref_payload(block: &Map, page_id: &str) -> Option<String> {
|
||||
let mut payload = JsonMap::new();
|
||||
payload.insert("docId".into(), JsonValue::String(page_id.to_string()));
|
||||
|
||||
if let Some(params_value) = block.get("prop:params") {
|
||||
if let Ok(JsonValue::Object(params)) = serde_json::to_value(¶ms_value) {
|
||||
for (key, value) in params.into_iter() {
|
||||
payload.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(JsonValue::Object(payload).to_string())
|
||||
}
|
||||
|
||||
fn gather_surface_texts(block: &Map) -> Vec<String> {
|
||||
let mut texts = Vec::new();
|
||||
let elements = match block.get("prop:elements").and_then(|value| value.to_map()) {
|
||||
Some(map) => map,
|
||||
None => return texts,
|
||||
};
|
||||
|
||||
if elements
|
||||
.get("type")
|
||||
.and_then(|value| value_to_string(&value))
|
||||
.as_deref()
|
||||
!= Some("$blocksuite:internal:native$")
|
||||
{
|
||||
return texts;
|
||||
}
|
||||
|
||||
if let Some(value_map) = elements.get("value").and_then(|value| value.to_map()) {
|
||||
for value in value_map.values() {
|
||||
if let Some(element) = value.to_map() {
|
||||
if let Some(text) = element.get("text").and_then(|value| value.to_text()) {
|
||||
texts.push(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
texts.sort();
|
||||
texts
|
||||
}
|
||||
|
||||
fn gather_database_texts(block: &Map) -> (Vec<String>, Option<String>) {
|
||||
let mut texts = Vec::new();
|
||||
let database_title = get_string(block, "prop:title");
|
||||
if let Some(title) = &database_title {
|
||||
texts.push(title.clone());
|
||||
}
|
||||
|
||||
if let Some(columns) = block.get("prop:columns").and_then(|value| value.to_array()) {
|
||||
for column_value in columns.iter() {
|
||||
if let Some(column) = column_value.to_map() {
|
||||
if let Some(name) = get_string(&column, "name") {
|
||||
texts.push(name);
|
||||
}
|
||||
if let Some(data) = column.get("data").and_then(|value| value.to_map()) {
|
||||
if let Some(options) = data.get("options").and_then(|value| value.to_array()) {
|
||||
for option_value in options.iter() {
|
||||
if let Some(option) = option_value.to_map() {
|
||||
if let Some(value) = get_string(&option, "value") {
|
||||
texts.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(texts, database_title)
|
||||
}
|
||||
|
||||
fn gather_table_contents(block: &Map) -> Vec<String> {
|
||||
let mut contents = Vec::new();
|
||||
for key in block.keys() {
|
||||
if key.starts_with("prop:cells.") && key.ends_with(".text") {
|
||||
if let Some(value) = block.get(key).and_then(|value| value_to_string(&value)) {
|
||||
if !value.is_empty() {
|
||||
contents.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contents
|
||||
}
|
||||
|
||||
fn value_to_string(value: &Value) -> Option<String> {
|
||||
if let Some(text) = value.to_text() {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
|
||||
if let Some(any) = value.to_any() {
|
||||
return any_to_string(&any);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn any_to_string(any: &Any) -> Option<String> {
|
||||
match any {
|
||||
Any::String(value) => Some(value.to_string()),
|
||||
Any::Integer(value) => Some(value.to_string()),
|
||||
Any::Float32(value) => Some(value.0.to_string()),
|
||||
Any::Float64(value) => Some(value.0.to_string()),
|
||||
Any::BigInt64(value) => Some(value.to_string()),
|
||||
Any::True => Some("true".into()),
|
||||
Any::False => Some("false".into()),
|
||||
Any::Null | Any::Undefined => None,
|
||||
Any::Array(_) | Any::Object(_) | Any::Binary(_) => serde_json::to_string(any).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_summary(summary: &mut String, remaining: &mut isize, text_len: usize, text: &str) {
|
||||
if *remaining > 0 {
|
||||
summary.push_str(text);
|
||||
*remaining -= text_len as isize;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_doc_from_binary() {
|
||||
let json = include_bytes!("../fixtures/demo.ydoc.json");
|
||||
let input = CrawlDocInput {
|
||||
doc_bin: include_bytes!("../fixtures/demo.ydoc").to_vec(),
|
||||
root_doc_bin: None,
|
||||
space_id: "o9WCLGyxkLxdULZ-f2B9V".to_string(),
|
||||
doc_id: "dYpV7PPhk8amRkY5IAcVO".to_string(),
|
||||
};
|
||||
|
||||
let result = parse_doc_from_binary(input).unwrap();
|
||||
let config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict)
|
||||
.numeric_mode(assert_json_diff::NumericMode::AssumeFloat);
|
||||
assert_json_diff::assert_json_matches!(
|
||||
serde_json::from_slice::<serde_json::Value>(json).unwrap(),
|
||||
serde_json::json!(result),
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
#[cfg(feature = "doc-loader")]
|
||||
pub mod doc_loader;
|
||||
#[cfg(feature = "ydoc-loader")]
|
||||
pub mod doc_parser;
|
||||
#[cfg(feature = "hashcash")]
|
||||
pub mod hashcash;
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
description = "High-performance and thread-safe CRDT implementation compatible with Yjs"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/toeverything/y-octo"
|
||||
include = ["src/**/*", "benches/**/*", "bin/**/*", "LICENSE", "README.md"]
|
||||
keywords = ["collaboration", "crdt", "crdts", "yjs", "yata"]
|
||||
include = ["LICENSE", "README.md", "benches/**/*", "bin/**/*", "src/**/*"]
|
||||
keywords = ["collaboration", "crdt", "crdts", "yata", "yjs"]
|
||||
license = "MIT"
|
||||
name = "y-octo"
|
||||
readme = "README.md"
|
||||
@@ -20,9 +20,7 @@ version = "0.0.1"
|
||||
|
||||
[dependencies]
|
||||
ahash = { workspace = true }
|
||||
bitvec = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
lasso = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nanoid = { workspace = true }
|
||||
nom = { workspace = true }
|
||||
|
||||
@@ -22,7 +22,6 @@ rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
regex = { workspace = true, optional = true }
|
||||
y-octo = { workspace = true }
|
||||
y-sync = { workspace = true }
|
||||
yrs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
[versions]
|
||||
android-gradle-plugin = "8.10.0"
|
||||
androidx-activity-compose = "1.10.1"
|
||||
androidx-appcompat = "1.7.0"
|
||||
androidx-browser = "1.8.0"
|
||||
androidx-compose-bom = "2025.05.00"
|
||||
androidx-coordinatorlayout = "1.3.0"
|
||||
androidx-core-ktx = "1.16.0"
|
||||
androidx-core-splashscreen = "1.0.1"
|
||||
android-gradle-plugin = "8.10.0"
|
||||
androidx-activity-compose = "1.10.1"
|
||||
androidx-appcompat = "1.7.0"
|
||||
androidx-browser = "1.8.0"
|
||||
androidx-compose-bom = "2025.05.00"
|
||||
androidx-coordinatorlayout = "1.3.0"
|
||||
androidx-core-ktx = "1.16.0"
|
||||
androidx-core-splashscreen = "1.0.1"
|
||||
androidx-datastore-preferences = "1.2.0-alpha02"
|
||||
androidx-espresso-core = "3.6.1"
|
||||
androidx-junit = "1.2.1"
|
||||
androidx-lifecycle-compose = "2.9.0"
|
||||
androidx-material3 = "1.3.1"
|
||||
androidx-navigation = "2.9.0"
|
||||
apollo = "4.2.0"
|
||||
apollo-kotlin-adapters = "0.0.6"
|
||||
androidx-espresso-core = "3.6.1"
|
||||
androidx-junit = "1.2.1"
|
||||
androidx-lifecycle-compose = "2.9.0"
|
||||
androidx-material3 = "1.3.1"
|
||||
androidx-navigation = "2.9.0"
|
||||
apollo = "4.2.0"
|
||||
apollo-kotlin-adapters = "0.0.6"
|
||||
# @keep
|
||||
compileSdk = "35"
|
||||
firebase-bom = "33.13.0"
|
||||
firebase-crashlytics = "3.0.3"
|
||||
google-services = "4.4.2"
|
||||
gradle-versions = "0.52.0"
|
||||
hilt = "2.56.2"
|
||||
hilt-ext = "1.2.0"
|
||||
jna = "5.17.0"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.1.20"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
kotlinx-datetime = "0.6.2"
|
||||
compileSdk = "35"
|
||||
firebase-bom = "33.13.0"
|
||||
firebase-crashlytics = "3.0.3"
|
||||
google-services = "4.4.2"
|
||||
gradle-versions = "0.52.0"
|
||||
hilt = "2.56.2"
|
||||
hilt-ext = "1.2.0"
|
||||
jna = "5.17.0"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.1.20"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
kotlinx-datetime = "0.6.2"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
ksp = "2.1.20-2.0.1"
|
||||
ksp = "2.1.20-2.0.1"
|
||||
# @keep
|
||||
minSdk = "22"
|
||||
minSdk = "22"
|
||||
mozilla-rust-android = "0.9.6"
|
||||
okhttp-bom = "5.0.0-alpha.14"
|
||||
richtext = "1.0.0-alpha02"
|
||||
okhttp-bom = "5.0.0-alpha.14"
|
||||
richtext = "1.0.0-alpha02"
|
||||
# @keep
|
||||
targetSdk = "35"
|
||||
timber = "5.0.1"
|
||||
targetSdk = "35"
|
||||
timber = "5.0.1"
|
||||
version-catalog-update = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
|
||||
androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
|
||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
|
||||
androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
|
||||
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
|
||||
androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" }
|
||||
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||
androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" }
|
||||
androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinatorlayout" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore-preferences" }
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-compose" }
|
||||
androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
|
||||
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
|
||||
apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters" }
|
||||
apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters" }
|
||||
apollo-api = { module = "com.apollographql.apollo:apollo-api", version.ref = "apollo" }
|
||||
apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
|
||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||
firebase-storage = { module = "com.google.firebase:firebase-storage" }
|
||||
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
hilt-ext-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-ext" }
|
||||
hilt-ext-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-ext" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp-bom" }
|
||||
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse" }
|
||||
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
|
||||
androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" }
|
||||
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
|
||||
androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" }
|
||||
androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinatorlayout" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore-preferences" }
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-compose" }
|
||||
androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
|
||||
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
|
||||
apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters" }
|
||||
apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters" }
|
||||
apollo-api = { module = "com.apollographql.apollo:apollo-api", version.ref = "apollo" }
|
||||
apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
|
||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||
firebase-storage = { module = "com.google.firebase:firebase-storage" }
|
||||
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
hilt-ext-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-ext" }
|
||||
hilt-ext-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-ext" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp-bom" }
|
||||
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines" }
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse" }
|
||||
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
apollo-android = { id = "com.apollographql.apollo", version.ref = "apollo" }
|
||||
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
|
||||
google-service = { id = "com.google.gms.google-services", version.ref = "google-services" }
|
||||
gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "mozilla-rust-android" }
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
apollo-android = { id = "com.apollographql.apollo", version.ref = "apollo" }
|
||||
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" }
|
||||
google-service = { id = "com.google.gms.google-services", version.ref = "google-services" }
|
||||
gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "mozilla-rust-android" }
|
||||
version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" }
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { HelperToMain, MainToHelper } from '../shared/type';
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
import { openExternalSafely } from './security/open-external';
|
||||
|
||||
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
||||
|
||||
@@ -105,10 +106,10 @@ class HelperProcessManager {
|
||||
return dialog.showSaveDialog(window, opts);
|
||||
},
|
||||
};
|
||||
const shellMethods = pickAndBind(shell, [
|
||||
'openExternal',
|
||||
'showItemInFolder',
|
||||
]);
|
||||
const shellMethods = {
|
||||
openExternal: openExternalSafely as typeof shell.openExternal,
|
||||
showItemInFolder: shell.showItemInFolder.bind(shell),
|
||||
};
|
||||
const appMethods = pickAndBind(app, ['getPath']);
|
||||
|
||||
const mainToHelperServer: MainToHelper = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import path from 'node:path';
|
||||
import { shell } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { openExternalSafely } from '../security/open-external';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import {
|
||||
askForMeetingPermission,
|
||||
@@ -87,7 +88,9 @@ export const recordingHandlers = {
|
||||
microphone: 'Privacy_Microphone',
|
||||
};
|
||||
const url = `x-apple.systempreferences:com.apple.preference.security?${urlMap[type]}`;
|
||||
return shell.openExternal(url);
|
||||
return openExternalSafely(url, {
|
||||
additionalProtocols: ['x-apple.systempreferences:'],
|
||||
});
|
||||
}
|
||||
// this only available on MacOS
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,35 @@
|
||||
import { app, shell } from 'electron';
|
||||
import { app } from 'electron';
|
||||
|
||||
import { openExternalSafely } from './security/open-external';
|
||||
|
||||
const extractRedirectTarget = (rawUrl: string) => {
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
const redirectUri = parsed.searchParams.get('redirect_uri');
|
||||
if (redirectUri) {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
if (parsed.hash) {
|
||||
const hash = parsed.hash.startsWith('#')
|
||||
? parsed.hash.slice(1)
|
||||
: parsed.hash;
|
||||
|
||||
const queryIndex = hash.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
const hashParams = new URLSearchParams(hash.slice(queryIndex + 1));
|
||||
const hashRedirect = hashParams.get('redirect_uri');
|
||||
if (hashRedirect) {
|
||||
return hashRedirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
const isInternalUrl = (url: string) => {
|
||||
@@ -18,7 +49,9 @@ app.on('web-contents-created', (_, contents) => {
|
||||
}
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
shell.openExternal(url).catch(console.error);
|
||||
openExternalSafely(url).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -32,9 +65,22 @@ app.on('web-contents-created', (_, contents) => {
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||
*/
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (!isInternalUrl(url) || url.includes('/redirect-proxy')) {
|
||||
// Open default browser
|
||||
shell.openExternal(url).catch(console.error);
|
||||
if (!isInternalUrl(url)) {
|
||||
openExternalSafely(url).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
} else if (url.includes('/redirect-proxy')) {
|
||||
const redirectTarget = extractRedirectTarget(url);
|
||||
if (redirectTarget) {
|
||||
openExternalSafely(redirectTarget).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
'[security] Blocked redirect proxy with missing redirect target:',
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
// Prevent creating new window in application
|
||||
return { action: 'deny' };
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { shell } from 'electron';
|
||||
|
||||
const DEFAULT_ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
|
||||
|
||||
export interface OpenExternalOptions {
|
||||
additionalProtocols?: string[];
|
||||
}
|
||||
|
||||
export const isAllowedExternalUrl = (
|
||||
rawUrl: string,
|
||||
additionalProtocols: Iterable<string> = []
|
||||
) => {
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
const protocol = parsed.protocol.toLowerCase();
|
||||
if (DEFAULT_ALLOWED_PROTOCOLS.has(protocol)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const extra of additionalProtocols) {
|
||||
if (protocol === extra.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('[security] Failed to parse external URL', rawUrl, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const openExternalSafely = async (
|
||||
rawUrl: string,
|
||||
options: OpenExternalOptions = {}
|
||||
) => {
|
||||
const { additionalProtocols = [] } = options;
|
||||
|
||||
if (!isAllowedExternalUrl(rawUrl, additionalProtocols)) {
|
||||
console.warn('[security] Blocked attempt to open external URL:', rawUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shell.openExternal(rawUrl);
|
||||
} catch (error) {
|
||||
console.error('[security] Failed to open external URL:', rawUrl, error);
|
||||
}
|
||||
};
|
||||
export const ALLOWED_EXTERNAL_PROTOCOLS: ReadonlySet<string> = new Set(
|
||||
DEFAULT_ALLOWED_PROTOCOLS
|
||||
);
|
||||
@@ -58,6 +58,10 @@ export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
|
||||
export const MenubarStateKey = 'menubarState' as const;
|
||||
export const MenubarStateSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
openOnLeftClick: z.boolean().default(false),
|
||||
minimizeToTray: z.boolean().default(false),
|
||||
closeToTray: z.boolean().default(false),
|
||||
startMinimized: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type MenubarStateSchema = z.infer<typeof MenubarStateSchema>;
|
||||
|
||||
@@ -317,7 +317,14 @@ class TrayState implements Disposable {
|
||||
logger.debug('User clicked on tray icon');
|
||||
this.update();
|
||||
if (!isMacOS()) {
|
||||
this.tray?.popUpContextMenu();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.openOnLeftClick
|
||||
) {
|
||||
showMainWindow();
|
||||
} else {
|
||||
this.tray?.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
updateApplicationsPing$.next(Date.now());
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { app, clipboard, nativeImage, nativeTheme, shell } from 'electron';
|
||||
import { app, clipboard, nativeImage, nativeTheme } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
import { map, shareReplay } from 'rxjs';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { persistentConfig } from '../config-storage/persist';
|
||||
import { logger } from '../logger';
|
||||
import { openExternalSafely } from '../security/open-external';
|
||||
import type { WorkbenchViewMeta } from '../shared-state-schema';
|
||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import {
|
||||
activateView,
|
||||
@@ -33,6 +37,19 @@ import { getOrCreateCustomThemeWindow } from '../windows-manager/custom-theme-wi
|
||||
import { getChallengeResponse } from './challenge';
|
||||
import { uiSubjects } from './subject';
|
||||
|
||||
const TraySettingsState = {
|
||||
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
|
||||
map(v => MenubarStateSchema.parse(v ?? {})),
|
||||
shareReplay(1)
|
||||
),
|
||||
|
||||
get value() {
|
||||
return MenubarStateSchema.parse(
|
||||
globalStateStorage.get(MenubarStateKey) ?? {}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const uiHandlers = {
|
||||
isMaximized: async () => {
|
||||
const window = await getMainWindow();
|
||||
@@ -47,7 +64,14 @@ export const uiHandlers = {
|
||||
},
|
||||
handleMinimizeApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
window?.minimize();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.minimizeToTray
|
||||
) {
|
||||
window?.hide();
|
||||
} else {
|
||||
window?.minimize();
|
||||
}
|
||||
},
|
||||
handleMaximizeApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
@@ -68,7 +92,15 @@ export const uiHandlers = {
|
||||
await handleWebContentsResize(e.sender);
|
||||
},
|
||||
handleCloseApp: async () => {
|
||||
app.quit();
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.closeToTray
|
||||
) {
|
||||
const window = await getMainWindow();
|
||||
window?.hide();
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
handleHideApp: async () => {
|
||||
const window = await getMainWindow();
|
||||
@@ -151,7 +183,7 @@ export const uiHandlers = {
|
||||
}
|
||||
},
|
||||
openExternal(_, url: string) {
|
||||
return shell.openExternal(url);
|
||||
return openExternalSafely(url);
|
||||
},
|
||||
|
||||
// tab handlers
|
||||
|
||||
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, map, shareReplay } from 'rxjs';
|
||||
|
||||
import { isLinux, isMacOS, isWindows, resourcesPath } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
@@ -10,11 +10,26 @@ import { buildType } from '../config';
|
||||
import { mainWindowOrigin } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||
import { globalStateStorage } from '../shared-storage/storage';
|
||||
import { uiSubjects } from '../ui/subject';
|
||||
|
||||
const IS_DEV: boolean =
|
||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||
|
||||
const TraySettingsState = {
|
||||
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
|
||||
map(v => MenubarStateSchema.parse(v ?? {})),
|
||||
shareReplay(1)
|
||||
),
|
||||
|
||||
get value() {
|
||||
return MenubarStateSchema.parse(
|
||||
globalStateStorage.get(MenubarStateKey) ?? {}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function closeAllWindows() {
|
||||
BrowserWindow.getAllWindows().forEach(w => {
|
||||
if (!w.isDestroyed()) {
|
||||
@@ -125,9 +140,16 @@ export class MainWindowManager {
|
||||
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
|
||||
e.preventDefault();
|
||||
if (!isMacOS()) {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
if (
|
||||
TraySettingsState.value.enabled &&
|
||||
TraySettingsState.value.closeToTray
|
||||
) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
}
|
||||
} else {
|
||||
// hide window on macOS
|
||||
// application quit will be handled by closing the hidden window
|
||||
@@ -209,7 +231,10 @@ export class MainWindowManager {
|
||||
if (IS_DEV) {
|
||||
// do not gain focus in dev mode
|
||||
mainWindow.showInactive();
|
||||
} else {
|
||||
} else if (
|
||||
!TraySettingsState.value.enabled ||
|
||||
!TraySettingsState.value.startMinimized
|
||||
) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -94,6 +95,9 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@@ -145,6 +149,7 @@ export class AIChatPanelTitle extends SignalWatcher(
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
|
||||
@@ -3,7 +3,10 @@ import type {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
@@ -107,6 +110,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@@ -429,6 +435,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
@@ -456,6 +463,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
|
||||
@@ -5,7 +5,10 @@ import type {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
@@ -128,6 +131,9 @@ export class AIChatComposer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor portalContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@@ -197,6 +203,7 @@ export class AIChatComposer extends SignalWatcher(
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
@@ -151,6 +154,9 @@ export class AIChatContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@@ -472,6 +478,7 @@ export class AIChatContent extends SignalWatcher(
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.serverService=${this.serverService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
|
||||
@@ -3,7 +3,10 @@ import type {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
@@ -359,6 +362,9 @@ export class AIChatInput extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService: AIDraftService | undefined;
|
||||
|
||||
@@ -533,6 +539,7 @@ export class AIChatInput extends SignalWatcher(
|
||||
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
|
||||
.isNetworkActive=${this._isNetworkActive}
|
||||
.onNetworkActiveChange=${this._toggleNetworkSearch}
|
||||
.serverService=${this.serverService}
|
||||
.toolsConfigService=${this.aiToolsConfigService}
|
||||
.notificationService=${this.notificationService}
|
||||
.subscriptionService=${this.subscriptionService}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type CopilotChatHistoryFragment,
|
||||
ServerDeploymentType,
|
||||
SubscriptionStatus,
|
||||
} from '@affine/graphql';
|
||||
import {
|
||||
@@ -110,6 +114,9 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
| undefined;
|
||||
// --------- search props end ---------
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor toolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@@ -153,6 +160,9 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
options: {
|
||||
items: this.aiModelService.models.value.map(model => {
|
||||
const isSelected = model.id === this.model.value?.id;
|
||||
const isSelfHosted =
|
||||
this.serverService.server.config$.value?.type ===
|
||||
ServerDeploymentType.Selfhosted;
|
||||
const status =
|
||||
this.subscriptionService.subscription.ai$.value?.status;
|
||||
const isSubscribed = status === SubscriptionStatus.Active;
|
||||
@@ -172,7 +182,7 @@ export class ChatInputPreference extends SignalWatcher(
|
||||
</div>
|
||||
`,
|
||||
select: () => {
|
||||
if (model.isPro && !isSubscribed) {
|
||||
if (model.isPro && !isSelfHosted && !isSubscribed) {
|
||||
this.notificationService.toast(
|
||||
`Pro models require an AFFiNE AI subscription.`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -167,6 +168,9 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@@ -373,6 +377,7 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.playgroundConfig=${this.playgroundConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.serverService=${this.serverService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
@@ -84,6 +85,9 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@@ -348,6 +352,7 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
|
||||
@@ -3,7 +3,10 @@ import type {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type {
|
||||
@@ -624,6 +627,7 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
}}
|
||||
.portalContainer=${this.parentElement}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.serverService=${this.serverService}
|
||||
.subscriptionService=${this.subscriptionService}
|
||||
.aiModelService=${this.aiModelService}
|
||||
.onAISubscribe=${this.onAISubscribe}
|
||||
@@ -646,6 +650,9 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor reasoningConfig!: AIReasoningConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docDisplayConfig!: DocDisplayConfig;
|
||||
|
||||
@@ -708,6 +715,7 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
searchMenuConfig: SearchMenuConfig,
|
||||
networkSearchConfig: AINetworkSearchConfig,
|
||||
reasoningConfig: AIReasoningConfig,
|
||||
serverService: ServerService,
|
||||
affineFeatureFlagService: FeatureFlagService,
|
||||
affineWorkspaceDialogService: WorkspaceDialogService,
|
||||
aiDraftService: AIDraftService,
|
||||
@@ -723,6 +731,7 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
.docDisplayConfig=${docDisplayConfig}
|
||||
.searchMenuConfig=${searchMenuConfig}
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
.serverService=${serverService}
|
||||
.affineFeatureFlagService=${affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
|
||||
.aiDraftService=${aiDraftService}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const docIconPickerTrigger = style({
|
||||
width: 64,
|
||||
@@ -35,3 +35,11 @@ export const placeholderContentText = style({
|
||||
color: cssVarV2.text.secondary,
|
||||
fontSize: 12,
|
||||
});
|
||||
|
||||
globalStyle('.doc-icon-container[data-has-icon="false"]', {
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,10 +6,17 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import * as styles from './doc-icon-picker.css';
|
||||
|
||||
const TitleContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
const TitleContainer = ({
|
||||
children,
|
||||
hasIcon,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
hasIcon: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="doc-icon-container"
|
||||
data-has-icon={hasIcon ? 'true' : 'false'}
|
||||
style={{
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
@@ -45,7 +52,7 @@ export const DocIconPicker = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleContainer>
|
||||
<TitleContainer hasIcon={!isPlaceholder}>
|
||||
<IconEditor
|
||||
icon={icon?.icon}
|
||||
onIconChange={data => {
|
||||
|
||||
@@ -62,22 +62,92 @@ export const ThemeSettings = () => {
|
||||
const MenubarSetting = () => {
|
||||
const t = useI18n();
|
||||
const traySettingService = useService(TraySettingService);
|
||||
const { enabled } = useLiveData(traySettingService.setting$);
|
||||
const traySetting = useLiveData(traySettingService.settings$);
|
||||
|
||||
return (
|
||||
<SettingWrapper
|
||||
id="menubar"
|
||||
title={t['com.affine.appearanceSettings.menubar.title']()}
|
||||
>
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.menubar.toggle']()}
|
||||
desc={t['com.affine.appearanceSettings.menubar.description']()}
|
||||
<>
|
||||
<SettingWrapper
|
||||
id="menubar"
|
||||
title={t['com.affine.appearanceSettings.menubar.title']()}
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={checked => traySettingService.setEnabled(checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.menubar.toggle']()}
|
||||
desc={t['com.affine.appearanceSettings.menubar.description']()}
|
||||
>
|
||||
<Switch
|
||||
checked={traySetting.enabled}
|
||||
onChange={checked => traySettingService.setEnabled(checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
{traySetting.enabled && !environment.isMacOs ? (
|
||||
<SettingWrapper
|
||||
id="windowBehavior"
|
||||
title={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.title'
|
||||
]()}
|
||||
>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.toggle'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={traySetting.openOnLeftClick}
|
||||
onChange={checked =>
|
||||
traySettingService.setOpenOnLeftClick(checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.toggle'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={traySetting.minimizeToTray}
|
||||
onChange={checked =>
|
||||
traySettingService.setMinimizeToTray(checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.toggle'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={traySetting.closeToTray}
|
||||
onChange={checked => traySettingService.setCloseToTray(checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.toggle'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={traySetting.startMinimized}
|
||||
onChange={checked =>
|
||||
traySettingService.setStartMinimized(checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const trustedDomain = [
|
||||
];
|
||||
|
||||
const logger = new DebugLogger('redirect_proxy');
|
||||
const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
|
||||
|
||||
/**
|
||||
* /redirect-proxy page
|
||||
@@ -32,6 +33,11 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
try {
|
||||
const target = new URL(redirectUri);
|
||||
|
||||
if (!ALLOWED_PROTOCOLS.has(target.protocol)) {
|
||||
logger.warn('Blocked redirect with disallowed protocol', target.protocol);
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
if (
|
||||
target.hostname === window.location.hostname ||
|
||||
trustedDomain.some(domain =>
|
||||
@@ -46,7 +52,8 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
return { allow: true };
|
||||
logger.warn('Blocked redirect to untrusted domain', redirectUri);
|
||||
return { allow: false };
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
@@ -92,6 +92,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
chatPanelRef.current.reasoningConfig = reasoningConfig;
|
||||
chatPanelRef.current.playgroundConfig = playgroundConfig;
|
||||
chatPanelRef.current.extensions = specs;
|
||||
chatPanelRef.current.serverService = framework.get(ServerService);
|
||||
chatPanelRef.current.affineFeatureFlagService =
|
||||
framework.get(FeatureFlagService);
|
||||
chatPanelRef.current.affineWorkspaceDialogService = framework.get(
|
||||
|
||||
@@ -3,26 +3,73 @@ import type {
|
||||
MenubarStateSchema,
|
||||
} from '@affine/electron/main/shared-state-schema';
|
||||
import { LiveData, Service } from '@toeverything/infra';
|
||||
import { defaults } from 'lodash-es';
|
||||
|
||||
import type { GlobalStateService } from '../../storage';
|
||||
|
||||
const MENUBAR_SETTING_KEY: typeof MenubarStateKey = 'menubarState';
|
||||
|
||||
const defaultTraySetting: MenubarStateSchema = {
|
||||
enabled: true,
|
||||
minimizeToTray: false,
|
||||
closeToTray: false,
|
||||
startMinimized: false,
|
||||
openOnLeftClick: false,
|
||||
};
|
||||
|
||||
export class TraySettingService extends Service {
|
||||
constructor(private readonly globalStateService: GlobalStateService) {
|
||||
super();
|
||||
}
|
||||
|
||||
setting$ = LiveData.from(
|
||||
this.globalStateService.globalState.watch<MenubarStateSchema>(
|
||||
MENUBAR_SETTING_KEY
|
||||
),
|
||||
null
|
||||
).map(v => v ?? { enabled: true });
|
||||
readonly settings$ = LiveData.computed(get => {
|
||||
const value = get(
|
||||
LiveData.from(
|
||||
this.globalStateService.globalState.watch<MenubarStateSchema>(
|
||||
MENUBAR_SETTING_KEY
|
||||
),
|
||||
undefined
|
||||
)
|
||||
);
|
||||
return defaults(value, defaultTraySetting);
|
||||
});
|
||||
|
||||
get settings() {
|
||||
return this.settings$.value;
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||
enabled,
|
||||
...this.settings$.value,
|
||||
enabled: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
setMinimizeToTray(minimizeToTray: boolean) {
|
||||
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||
...this.settings$.value,
|
||||
minimizeToTray: minimizeToTray,
|
||||
});
|
||||
}
|
||||
|
||||
setCloseToTray(closeToTray: boolean) {
|
||||
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||
...this.settings$.value,
|
||||
closeToTray: closeToTray,
|
||||
});
|
||||
}
|
||||
|
||||
setStartMinimized(startMinimized: boolean) {
|
||||
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||
...this.settings$.value,
|
||||
startMinimized: startMinimized,
|
||||
});
|
||||
}
|
||||
|
||||
setOpenOnLeftClick(openOnLeftClick: boolean) {
|
||||
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||
...this.settings$.value,
|
||||
openOnLeftClick: openOnLeftClick,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +95,6 @@ export const AFFINE_FLAGS = {
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_callout: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_callout',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-callout.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-callout.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: isCanaryBuild,
|
||||
},
|
||||
|
||||
enable_emoji_folder_icon: {
|
||||
category: 'affine',
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -32,6 +32,7 @@ export const AIChatBlockPeekView = ({
|
||||
} = useAIChatConfig();
|
||||
|
||||
const framework = useFramework();
|
||||
const serverService = framework.get(ServerService);
|
||||
const affineFeatureFlagService = framework.get(FeatureFlagService);
|
||||
const affineWorkspaceDialogService = framework.get(WorkspaceDialogService);
|
||||
const aiDraftService = framework.get(AIDraftService);
|
||||
@@ -48,6 +49,7 @@ export const AIChatBlockPeekView = ({
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
serverService,
|
||||
affineFeatureFlagService,
|
||||
affineWorkspaceDialogService,
|
||||
aiDraftService,
|
||||
@@ -64,6 +66,7 @@ export const AIChatBlockPeekView = ({
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
serverService,
|
||||
affineFeatureFlagService,
|
||||
affineWorkspaceDialogService,
|
||||
aiDraftService,
|
||||
|
||||
@@ -39,6 +39,16 @@ export class UrlService extends Service {
|
||||
* @param url only full url with http/https protocol is supported
|
||||
*/
|
||||
openExternal(url: string) {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
throw new Error(`Invalid external URL: ${url}`);
|
||||
}
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
throw new Error('only http/https URLs are supported');
|
||||
}
|
||||
if (BUILD_CONFIG.isWeb || BUILD_CONFIG.isMobileWeb) {
|
||||
location.href = url;
|
||||
} else {
|
||||
|
||||
@@ -991,6 +991,42 @@ export function useAFFiNEI18N(): {
|
||||
* `Display the menubar app in the tray for quick access to AFFiNE or meeting recordings.`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.description"](): string;
|
||||
/**
|
||||
* `Window behavior`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.title"](): string;
|
||||
/**
|
||||
* `Quick open from tray icon`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.toggle"](): string;
|
||||
/**
|
||||
* `Open AFFiNE when left‑clicking the tray icon.`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.description"](): string;
|
||||
/**
|
||||
* `Minimize to tray`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.toggle"](): string;
|
||||
/**
|
||||
* `Minimize AFFiNE to the system tray.`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.description"](): string;
|
||||
/**
|
||||
* `Close to tray`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.toggle"](): string;
|
||||
/**
|
||||
* `Close AFFiNE to the system tray.`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.description"](): string;
|
||||
/**
|
||||
* `Start minimized`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.toggle"](): string;
|
||||
/**
|
||||
* `Start AFFiNE minimized to the system tray.`
|
||||
*/
|
||||
["com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.description"](): string;
|
||||
/**
|
||||
* `Theme`
|
||||
*/
|
||||
|
||||
@@ -237,6 +237,15 @@
|
||||
"com.affine.appearanceSettings.menubar.title": "Menubar",
|
||||
"com.affine.appearanceSettings.menubar.toggle": "Enable menubar app",
|
||||
"com.affine.appearanceSettings.menubar.description": "Display the menubar app in the tray for quick access to AFFiNE or meeting recordings.",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.title": "Window behavior",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.toggle": "Quick open from tray icon",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.openOnLeftClick.description": "Open AFFiNE when left‑clicking the tray icon.",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.toggle": "Minimize to tray",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.minimizeToTray.description": "Minimize AFFiNE to the system tray.",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.toggle": "Close to tray",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.closeToTray.description": "Close AFFiNE to the system tray.",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.toggle": "Start minimized",
|
||||
"com.affine.appearanceSettings.menubar.windowBehavior.startMinimized.description": "Start AFFiNE minimized to the system tray.",
|
||||
"com.affine.appearanceSettings.theme.title": "Theme",
|
||||
"com.affine.appearanceSettings.title": "Appearance settings",
|
||||
"com.affine.appearanceSettings.translucentUI.description": "Use transparency effect on the sidebar.",
|
||||
|
||||
@@ -25,8 +25,14 @@ thiserror = { workspace = true }
|
||||
uniffi = { workspace = true, features = ["cli", "tokio"] }
|
||||
|
||||
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
|
||||
objc2 = { workspace = true }
|
||||
objc2-foundation = { workspace = true, features = ["NSArray", "NSFileManager", "NSPathUtilities", "NSString", "NSURL"] }
|
||||
objc2 = { workspace = true }
|
||||
objc2-foundation = { workspace = true, features = [
|
||||
"NSArray",
|
||||
"NSFileManager",
|
||||
"NSPathUtilities",
|
||||
"NSString",
|
||||
"NSURL",
|
||||
] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "ios", target_os = "macos")))'.dependencies]
|
||||
homedir = { workspace = true }
|
||||
|
||||
@@ -7,17 +7,32 @@ version = "0.0.0"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
affine_common = { workspace = true }
|
||||
affine_common = { workspace = true }
|
||||
affine_media_capture = { path = "./media_capture" }
|
||||
affine_nbstore = { path = "./nbstore" }
|
||||
affine_sqlite_v1 = { path = "./sqlite_v1" }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
affine_nbstore = { path = "./nbstore" }
|
||||
affine_sqlite_v1 = { path = "./sqlite_v1" }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"json",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -11,20 +11,34 @@ use-as-lib = ["napi-derive/noop", "napi/noop"]
|
||||
|
||||
[dependencies]
|
||||
affine_schema = { path = "../schema" }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[target.'cfg(any(target_os = "ios", target_os = "android"))'.dependencies]
|
||||
uniffi = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
affine_schema = { path = "../schema" }
|
||||
dotenvy = { workspace = true }
|
||||
napi-build = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dotenvy = { workspace = true }
|
||||
napi-build = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -8,16 +8,30 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
affine_schema = { path = "../schema" }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[build-dependencies]
|
||||
affine_schema = { path = "../schema" }
|
||||
dotenvy = { workspace = true }
|
||||
napi-build = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dotenvy = { workspace = true }
|
||||
napi-build = { workspace = true }
|
||||
sqlx = { workspace = true, default-features = false, features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -577,9 +577,10 @@ test.describe('slash search', () => {
|
||||
|
||||
// search should active the first item
|
||||
await type(page, 'co');
|
||||
await expect(slashItems).toHaveCount(3);
|
||||
await expect(slashItems).toHaveCount(4);
|
||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
||||
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']);
|
||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']);
|
||||
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
||||
|
||||
await type(page, 'p');
|
||||
@@ -588,9 +589,10 @@ test.describe('slash search', () => {
|
||||
|
||||
// assert backspace works
|
||||
await pressBackspace(page);
|
||||
await expect(slashItems).toHaveCount(3);
|
||||
await expect(slashItems).toHaveCount(4);
|
||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
||||
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']);
|
||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']);
|
||||
await expect(slashItems.nth(0)).toHaveAttribute('hover', 'true');
|
||||
});
|
||||
|
||||
@@ -606,14 +608,15 @@ test.describe('slash search', () => {
|
||||
await expect(slashMenu).toBeVisible();
|
||||
|
||||
await type(page, 'c');
|
||||
await expect(slashItems).toHaveCount(9);
|
||||
await expect(slashItems).toHaveCount(10);
|
||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
||||
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']);
|
||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']);
|
||||
await expect(slashItems.nth(3).locator('.text')).toHaveText(['Duplicate']);
|
||||
await expect(slashItems.nth(4).locator('.text')).toHaveText(['Code Block']);
|
||||
await expect(slashItems.nth(5).locator('.text')).toHaveText(['Linked Doc']);
|
||||
await expect(slashItems.nth(6).locator('.text')).toHaveText(['Attachment']);
|
||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']);
|
||||
await expect(slashItems.nth(3).locator('.text')).toHaveText(['New Doc']);
|
||||
await expect(slashItems.nth(4).locator('.text')).toHaveText(['Duplicate']);
|
||||
await expect(slashItems.nth(5).locator('.text')).toHaveText(['Code Block']);
|
||||
await expect(slashItems.nth(6).locator('.text')).toHaveText(['Linked Doc']);
|
||||
await expect(slashItems.nth(7).locator('.text')).toHaveText(['Attachment']);
|
||||
await type(page, 'b');
|
||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Code Block']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user