From b4279f80041455d96159e0cf5f0624b1586dbeec Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 7 May 2024 17:37:11 +0800 Subject: [PATCH 1/6] fix: ws connect with invalid token (#5282) * chore: bump client api * chore: fix potentail ws connect with invalid token * fix: cargo clippy --------- Co-authored-by: Lucas.Xu --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 22 ++--- frontend/appflowy_tauri/src-tauri/Cargo.toml | 15 ++- frontend/appflowy_web/wasm-libs/Cargo.lock | 22 ++--- frontend/appflowy_web/wasm-libs/Cargo.toml | 16 ++-- .../appflowy_web_app/src-tauri/Cargo.lock | 22 ++--- .../appflowy_web_app/src-tauri/Cargo.toml | 14 +-- frontend/rust-lib/Cargo.lock | 44 ++++++--- frontend/rust-lib/Cargo.toml | 14 +-- frontend/rust-lib/flowy-core/Cargo.toml | 2 +- frontend/rust-lib/flowy-error/Cargo.toml | 2 +- frontend/rust-lib/flowy-server/Cargo.toml | 3 +- .../flowy-server/src/af_cloud/server.rs | 93 ++++++++++++------- 12 files changed, 152 insertions(+), 117 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 79c5630583..1da9cd7027 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -739,8 +739,8 @@ dependencies = [ [[package]] name = "client-api" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "again", "anyhow", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "futures-channel", "futures-util", @@ -1025,7 +1025,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "async-trait", @@ -1407,7 +1407,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -2777,7 +2777,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "futures-util", @@ -2794,7 +2794,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -3226,7 +3226,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "reqwest", @@ -5714,7 +5714,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 734400b5fa..a46ae6084e 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -47,6 +47,13 @@ collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" } + [dependencies] serde_json.workspace = true serde.workspace = true @@ -95,11 +102,3 @@ default = ["custom-protocol"] # this feature is used used for production builds where `devPath` points to the filesystem # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] - -[patch.crates-io] -# Please using the following command to update the revision id -# Current directory: frontend -# Run the script: -# scripts/tool/update_client_api_rev.sh new_rev_id -# ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" } diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index d737ee1abb..c85cb2ae63 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -216,7 +216,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -547,8 +547,8 @@ dependencies = [ [[package]] name = "client-api" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "again", "anyhow", @@ -594,7 +594,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "futures-channel", "futures-util", @@ -772,7 +772,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "async-trait", @@ -1011,7 +1011,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -1788,7 +1788,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "futures-util", @@ -1805,7 +1805,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -2106,7 +2106,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "reqwest", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 7fd76e8015..bd2427c83d 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -50,6 +50,13 @@ collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlo collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } yrs = "0.18.7" +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" } + [profile.dev] @@ -61,12 +68,3 @@ codegen-units = 16 lto = true opt-level = 3 codegen-units = 1 - - -[patch.crates-io] -# Please using the following command to update the revision id -# Current directory: frontend -# Run the script: -# scripts/tool/update_client_api_rev.sh new_rev_id -# ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index c0acda607a..f669732bdc 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -153,7 +153,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -713,8 +713,8 @@ dependencies = [ [[package]] name = "client-api" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "again", "anyhow", @@ -760,7 +760,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "futures-channel", "futures-util", @@ -1008,7 +1008,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "async-trait", @@ -1394,7 +1394,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "futures-util", @@ -2868,7 +2868,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -3305,7 +3305,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "reqwest", @@ -5809,7 +5809,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index a67075046c..7c85ebbed6 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -47,6 +47,13 @@ collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" } + [dependencies] serde_json.workspace = true serde.workspace = true @@ -95,10 +102,3 @@ default = ["custom-protocol"] # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] -[patch.crates-io] -# Please using the following command to update the revision id -# Current directory: frontend -# Run the script: -# scripts/tool/update_client_api_rev.sh new_rev_id -# ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a68a49b20a..046c52c854 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -649,8 +649,8 @@ dependencies = [ [[package]] name = "client-api" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "again", "anyhow", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "futures-channel", "futures-util", @@ -904,7 +904,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "bincode", @@ -929,7 +929,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "async-trait", @@ -1149,7 +1149,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1248,7 +1248,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -2461,7 +2461,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "futures-util", @@ -2478,7 +2478,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", @@ -2843,7 +2843,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "reqwest", @@ -3718,7 +3718,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3738,6 +3738,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3805,6 +3806,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4008,7 +4022,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4029,7 +4043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -4926,7 +4940,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=47e6f1e8#47e6f1e8b1d3d037401f094aa11459b6c75cfa1a" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef8e6f3#ef8e6f360f9c1d4daf2283e8475894d2ca5ef2fc" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b4c507dd7f..26de92e4ce 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -94,6 +94,13 @@ collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlo collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "870cd70" } yrs = "0.18.7" +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script.add_workspace_members: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ef8e6f3" } + [profile.dev] opt-level = 1 lto = false @@ -122,10 +129,3 @@ incremental = false # TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged. # Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0. rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" } - -# Please using the following command to update the revision id -# Current directory: frontend -# Run the script.add_workspace_members: -# scripts/tool/update_client_api_rev.sh new_rev_id -# ⚠️⚠️⚠️️ -client-api = { git = " https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "47e6f1e8" } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 7c67351f6b..289b4751b0 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -30,7 +30,7 @@ collab = { workspace = true } diesel.workspace = true uuid.workspace = true flowy-storage = { workspace = true } -client-api = { version = "0.1.0" } +client-api.workspace = true tracing.workspace = true futures-core = { version = "0.3", default-features = false } diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index bf40eac0ec..e671be811f 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -31,7 +31,7 @@ collab-database = { workspace = true, optional = true } collab-document = { workspace = true, optional = true } collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } -client-api = { version = "0.1.0", optional = true } +client-api = { workspace = true, optional = true } tantivy = { version = "0.21.1", optional = true } diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index eb8cfde5b2..b64b30fa49 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -49,9 +49,8 @@ lib-dispatch = { workspace = true } yrs.workspace = true rand = "0.8.5" - [dependencies.client-api] -version = "0.1.0" +workspace = true features = [ "collab-sync", "test_util", diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 260e571854..1fd6a5b03f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -12,8 +12,10 @@ use client_api::ws::{ use client_api::{Client, ClientConfiguration}; use flowy_storage::ObjectStorageService; use rand::Rng; -use tokio::sync::watch; +use tokio::select; +use tokio::sync::{watch, Mutex}; use tokio_stream::wrappers::WatchStream; +use tokio_util::sync::CancellationToken; use tracing::{error, event, info, warn}; use uuid::Uuid; @@ -74,11 +76,22 @@ impl AppFlowyCloudServer { let enable_sync = Arc::new(AtomicBool::new(enable_sync)); let network_reachable = Arc::new(AtomicBool::new(true)); - let ws_client = WSClient::new(WSClientConfig::default(), api_client.clone()); + let ws_client = WSClient::new( + WSClientConfig::default(), + api_client.clone(), + api_client.clone(), + ); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); + let ws_connect_cancellation_token = Arc::new(Mutex::new(CancellationToken::new())); - spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); + spawn_ws_conn( + token_state_rx, + &ws_client, + ws_connect_cancellation_token, + &api_client, + &enable_sync, + ); Self { config, client: api_client, @@ -241,12 +254,14 @@ impl AppFlowyServer for AppFlowyCloudServer { fn spawn_ws_conn( mut token_state_rx: TokenStateReceiver, ws_client: &Arc, + conn_cancellation_token: Arc>, api_client: &Arc, enable_sync: &Arc, ) { let weak_ws_client = Arc::downgrade(ws_client); let weak_api_client = Arc::downgrade(api_client); let enable_sync = enable_sync.clone(); + let cloned_conn_cancellation_token = conn_cancellation_token.clone(); af_spawn(async move { if let Some(ws_client) = weak_ws_client.upgrade() { @@ -256,15 +271,16 @@ fn spawn_ws_conn( match state { ConnectState::PingTimeout | ConnectState::Lost => { // Try to reconnect if the connection is timed out. - if let Some(api_client) = weak_api_client.upgrade() { - if enable_sync.load(Ordering::SeqCst) { - attempt_reconnect(&ws_client, &api_client, 2).await; - } + if weak_api_client.upgrade().is_some() && enable_sync.load(Ordering::SeqCst) { + attempt_reconnect(&ws_client, 2, &cloned_conn_cancellation_token).await; } }, ConnectState::Unauthorized => { if let Some(api_client) = weak_api_client.upgrade() { - if let Err(err) = api_client.refresh_token().await { + if let Err(err) = api_client + .refresh_token("websocket connect unauthorized") + .await + { error!("Failed to refresh token: {}", err); } } @@ -276,21 +292,13 @@ fn spawn_ws_conn( }); let weak_ws_client = Arc::downgrade(ws_client); - let weak_api_client = Arc::downgrade(api_client); af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { info!("🟢token state: {:?}", token_state); match token_state { TokenState::Refresh => { - if let (Some(api_client), Some(ws_client)) = - (weak_api_client.upgrade(), weak_ws_client.upgrade()) - { - match api_client.ws_connect_info().await { - Ok(conn_info) => { - let _ = ws_client.connect(api_client.ws_addr(), conn_info).await; - }, - Err(err) => error!("Failed to get ws url: {}", err), - } + if let Some(ws_client) = weak_ws_client.upgrade() { + attempt_reconnect(&ws_client, 5, &conn_cancellation_token).await; } }, TokenState::Invalid => { @@ -304,26 +312,43 @@ fn spawn_ws_conn( }); } +/// Attempts to reconnect a WebSocket client with a randomized delay to mitigate the thundering herd problem. +/// +/// This function cancels any existing reconnection attempt, sets up a new cancellation token, and then +/// attempts to reconnect after a randomized delay. The delay is set between a specified minimum and +/// that minimum plus 10 seconds. +/// async fn attempt_reconnect( ws_client: &Arc, - api_client: &Arc, - minimum_delay: u64, + minimum_delay_in_secs: u64, + conn_cancellation_token: &Arc>, ) { - // Introduce randomness in the reconnection attempts to avoid thundering herd problem - let delay_seconds = rand::thread_rng().gen_range(minimum_delay..8); - tokio::time::sleep(Duration::from_secs(delay_seconds)).await; - event!( - tracing::Level::INFO, - "🟢 Attempting to reconnect websocket." - ); - match api_client.ws_connect_info().await { - Ok(conn_info) => { - if let Err(e) = ws_client.connect(api_client.ws_addr(), conn_info).await { - error!("Failed to reconnect websocket: {}", e); + // Cancel the previous reconnection attempt + let mut cancel_token_lock = conn_cancellation_token.lock().await; + cancel_token_lock.cancel(); + + let new_cancel_token = CancellationToken::new(); + *cancel_token_lock = new_cancel_token.clone(); + drop(cancel_token_lock); + + // randomness in the reconnection attempts to avoid thundering herd problem + let delay_seconds = rand::thread_rng().gen_range(minimum_delay_in_secs..10); + let ws_client = ws_client.clone(); + tokio::spawn(async move { + select! { + _ = new_cancel_token.cancelled() => { + event!( + tracing::Level::TRACE, + "🟢websocket reconnection attempt cancelled." + ); + }, + _ = tokio::time::sleep(Duration::from_secs(delay_seconds)) => { + if let Err(e) = ws_client.connect().await { + error!("Failed to reconnect websocket: {}", e); + } } - }, - Err(err) => error!("Failed to get websocket URL: {}", err), - } + } + }); } pub trait AFServer: Send + Sync + 'static { From 6220680ce093da2d6e83c00dae0a43eb64fc3b18 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 May 2024 19:44:00 +0800 Subject: [PATCH 2/6] feat: support system default font family on desktop (#5279) * fix: add permission check before selecting image in image block * feat: use system default font on desktop * fix: set appbar icon size to 30 * feat: add default font family on desktop --- .../appearance_settings_test.dart | 7 +- .../page_style/document_page_style_bloc.dart | 5 +- .../presentation/base/mobile_view_page.dart | 95 +++++++++++-------- .../setting/font/font_picker_screen.dart | 14 +-- .../setting/font/font_setting.dart | 6 +- .../document_appearance_cubit.dart | 6 +- .../cover/document_immersive_cover.dart | 2 +- .../image/upload_image_file_widget.dart | 38 +++++--- .../mobile_toolbar_v3/aa_menu/_font_item.dart | 2 +- .../page_style/_page_style_cover_image.dart | 60 +----------- .../page_style/_page_style_layout.dart | 12 +-- .../document/presentation/editor_style.dart | 9 +- .../shared/permission/permission_checker.dart | 64 +++++++++++++ .../lib/util/font_family_extension.dart | 15 +++ .../util/google_font_family_extension.dart | 6 -- .../application/appearance_defaults.dart | 3 +- .../settings/appearance/base_appearance.dart | 33 +++---- .../appearance/desktop_appearance.dart | 4 +- .../font_family_setting.dart | 27 +++--- .../app_setting_test/appearance_test.dart | 2 +- .../document_appearance_test.dart | 2 +- .../theme_font_family_setting_test.dart | 11 ++- .../flowy-user/src/entities/user_setting.rs | 2 +- 23 files changed, 234 insertions(+), 191 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart create mode 100644 frontend/appflowy_flutter/lib/util/font_family_extension.dart delete mode 100644 frontend/appflowy_flutter/lib/util/google_font_family_extension.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart index b968ccfd8d..4b7848fd08 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart'; @@ -82,8 +83,10 @@ void main() { await tester.openSettingsPage(SettingsPage.appearance); expect( - find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily), - findsOneWidget, + find.textContaining( + DefaultAppearanceSettings.kDefaultFontFamily.fontFamilyDisplayName, + ), + findsNWidgets(2), ); }); }); diff --git a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart index b08d62c9be..52552fce3b 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -179,8 +178,8 @@ class DocumentPageStyleBloc ); } - String _getSelectedFontFamily(Map layoutObject) { - return layoutObject[ViewExtKeys.fontKey] ?? builtInFontFamily(); + String? _getSelectedFontFamily(Map layoutObject) { + return layoutObject[ViewExtKeys.fontKey]; } (PageStyleCoverImageType, String colorValue) _getSelectedCover( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 57be077b01..35b3ece7c5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -216,7 +216,11 @@ class _MobileViewPageState extends State { child: AppBarButton( padding: EdgeInsets.zero, onTap: (context) => context.pop(), - child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_back_s), + child: _buildImmersiveAppBarIcon( + FlowySvgs.m_app_bar_back_s, + 30.0, + iconPadding: 6.0, + ), ), ), actions: actions, @@ -274,13 +278,13 @@ class _MobileViewPageState extends State { ), ); }, - child: _buildImmersiveAppBarIcon(FlowySvgs.m_layout_s), + child: _buildImmersiveAppBarIcon(FlowySvgs.m_layout_s, 30.0), ); } Widget _buildAppBarMoreButton(ViewPB view) { return AppBarButton( - padding: const EdgeInsets.only(left: 8, right: 16, top: 2, bottom: 2), + padding: const EdgeInsets.only(left: 8, right: 16), onTap: (context) { EditorNotification.exitEditing().post(); @@ -292,49 +296,62 @@ class _MobileViewPageState extends State { builder: (_) => _buildAppBarMoreBottomSheet(context), ); }, - child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s), + child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s, 30.0), ); } - Widget _buildImmersiveAppBarIcon(FlowySvgData icon) { - return ValueListenableBuilder( - valueListenable: _isImmersiveMode, - builder: (context, isImmersiveMode, child) { - return ValueListenableBuilder( - valueListenable: _appBarOpacity, - builder: (context, appBarOpacity, child) { - Color? color; + Widget _buildImmersiveAppBarIcon( + FlowySvgData icon, + double dimension, { + double iconPadding = 5.0, + }) { + assert( + dimension > 0.0 && dimension <= kToolbarHeight, + 'dimension must be greater than 0, and less than or equal to kToolbarHeight', + ); + return UnconstrainedBox( + child: SizedBox.square( + dimension: dimension, + child: ValueListenableBuilder( + valueListenable: _isImmersiveMode, + builder: (context, isImmersiveMode, child) { + return ValueListenableBuilder( + valueListenable: _appBarOpacity, + builder: (context, appBarOpacity, child) { + Color? color; - // if there's no cover or the cover is not immersive, - // make sure the app bar is always visible - if (!isImmersiveMode) { - color = null; - } else if (appBarOpacity < 0.99) { - color = Colors.white; - } + // if there's no cover or the cover is not immersive, + // make sure the app bar is always visible + if (!isImmersiveMode) { + color = null; + } else if (appBarOpacity < 0.99) { + color = Colors.white; + } - Widget child = Container( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: FlowySvg( - icon, - color: color, - ), + Widget child = Container( + margin: EdgeInsets.all(iconPadding), + child: FlowySvg( + icon, + color: color, + ), + ); + + if (isImmersiveMode && appBarOpacity <= 0.99) { + child = DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(dimension / 2.0), + color: Colors.black.withOpacity(0.2), + ), + child: child, + ); + } + + return child; + }, ); - - if (isImmersiveMode && appBarOpacity <= 0.99) { - child = DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(22), - color: Colors.black.withOpacity(0.2), - ), - child: child, - ); - } - - return child; }, - ); - }, + ), + ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart index 93e89d7c29..64dd62729c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart @@ -3,7 +3,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/util/google_font_family_extension.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -13,7 +13,7 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; final List _availableFonts = [ - builtInFontFamily(), + defaultFontFamily, ...GoogleFonts.asMap().keys, ]; @@ -106,16 +106,12 @@ class _FontSelectorState extends State { } final fontFamilyName = availableFonts[index - 1]; - final usingDefaultFontFamily = fontFamilyName == builtInFontFamily(); + final usingDefaultFontFamily = fontFamilyName == defaultFontFamily; final fontFamily = !usingDefaultFontFamily ? getGoogleFontSafely(fontFamilyName).fontFamily - : TextStyle(fontFamily: builtInFontFamily()).fontFamily; + : defaultFontFamily; return FlowyOptionTile.checkbox( - // display the default font name if the font family name is empty - // or using the default font family - text: fontFamilyName.isNotEmpty && !usingDefaultFontFamily - ? fontFamilyName.parseFontFamilyName() - : LocaleKeys.settings_appearance_fontFamily_defaultFont.tr(), + text: fontFamilyName.fontFamilyDisplayName, isSelected: widget.selectedFontFamilyName == fontFamilyName, showTopBorder: false, onTap: () => widget.onFontFamilySelected(fontFamilyName), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart index 8e3827bf94..050bf4b594 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -22,9 +22,7 @@ class FontSetting extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final selectedFont = context.watch().state.font; - final name = selectedFont == builtInFontFamily() - ? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr() - : selectedFont; + final name = selectedFont.fontFamilyDisplayName; return MobileSettingItem( name: LocaleKeys.settings_appearance_fontFamily_label.tr(), trailing: Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart index 3a39f0ed56..1a39c519a3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart @@ -57,9 +57,9 @@ class DocumentAppearance { class DocumentAppearanceCubit extends Cubit { DocumentAppearanceCubit() : super( - DocumentAppearance( + const DocumentAppearance( fontSize: 16.0, - fontFamily: builtInFontFamily(), + fontFamily: defaultFontFamily, codeFontFamily: builtInCodeFontFamily, ), ); @@ -69,7 +69,7 @@ class DocumentAppearanceCubit extends Cubit { final fontSize = prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0; final fontFamily = prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? - builtInFontFamily(); + defaultFontFamily; final defaultTextDirection = prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index fd0a5a4885..e25f32f55f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -127,7 +127,7 @@ class _DocumentImmersiveCoverState extends State { BuildContext context, DocumentImmersiveCoverState state, ) { - String? fontFamily = builtInFontFamily(); + String? fontFamily = defaultFontFamily; final documentFontFamily = context.read().state.fontFamily; if (documentFontFamily != null && fontFamily != documentFontFamily) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart index 8c727ac4d5..d4d94be091 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/permission/permission_checker.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -20,22 +22,28 @@ class UploadImageFileWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyHover( - child: FlowyButton( - showDefaultBoxDecorationOnMobile: true, - text: Container( - margin: const EdgeInsets.all(4.0), - alignment: Alignment.center, - child: FlowyText( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ), + final child = FlowyButton( + showDefaultBoxDecorationOnMobile: true, + text: Container( + margin: const EdgeInsets.all(4.0), + alignment: Alignment.center, + child: FlowyText( + LocaleKeys.document_imageBlock_upload_placeholder.tr(), ), - onTap: _uploadImage, ), + onTap: () => _uploadImage(context), ); + + if (PlatformExtension.isDesktopOrWeb) { + return FlowyHover( + child: child, + ); + } + + return child; } - Future _uploadImage() async { + Future _uploadImage(BuildContext context) async { if (PlatformExtension.isDesktopOrWeb) { // on desktop, the users can pick a image file from folder final result = await getIt().pickFiles( @@ -45,6 +53,12 @@ class UploadImageFileWidget extends StatelessWidget { ); onPickFile(result?.files.firstOrNull?.path); } else { + final photoPermission = + await PermissionChecker.checkPhotoPermission(context); + if (!photoPermission) { + Log.error('Has no permission to access the photo library'); + return; + } // on mobile, the users can pick a image file from camera or image library final result = await ImagePicker().pickImage(source: ImageSource.gallery); onPickFile(result?.path); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart index 8fddc4edbe..b1004a3eae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart @@ -5,7 +5,7 @@ import 'package:appflowy/plugins/document/application/document_appearance_cubit. import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/util/google_font_family_extension.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 0b72fd60f0..1d6b100534 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -4,13 +4,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; import 'package:appflowy/shared/feedback_gesture_detector.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/shared/permission/permission_checker.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -18,11 +17,9 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:permission_handler/permission_handler.dart'; class PageStyleCoverImage extends StatelessWidget { PageStyleCoverImage({ @@ -121,7 +118,8 @@ class PageStyleCoverImage extends StatelessWidget { } Future _pickImage(BuildContext context) async { - final photoPermission = await _checkPhotoPermission(context); + final photoPermission = + await PermissionChecker.checkPhotoPermission(context); if (!photoPermission) { Log.error('Has no permission to access the photo library'); return; @@ -129,9 +127,7 @@ class PageStyleCoverImage extends StatelessWidget { XFile? result; try { - result = await _imagePicker.pickImage( - source: ImageSource.gallery, - ); + result = await _imagePicker.pickImage(source: ImageSource.gallery); } catch (e) { Log.error('Error while picking image: $e'); return; @@ -224,54 +220,6 @@ class PageStyleCoverImage extends StatelessWidget { }, ); } - - Future _checkPhotoPermission(BuildContext context) async { - // check the permission first - final status = await Permission.photos.status; - // if the permission is permanently denied, we should open the app settings - if (status.isPermanentlyDenied && context.mounted) { - unawaited( - showFlowyMobileConfirmDialog( - context, - title: FlowyText.semibold( - LocaleKeys.pageStyle_photoPermissionTitle.tr(), - maxLines: 3, - textAlign: TextAlign.center, - ), - content: FlowyText( - LocaleKeys.pageStyle_photoPermissionDescription.tr(), - maxLines: 5, - textAlign: TextAlign.center, - fontSize: 12.0, - ), - actionAlignment: ConfirmDialogActionAlignment.vertical, - actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(), - actionButtonColor: Colors.blue, - cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(), - cancelButtonColor: Colors.blue, - onActionButtonPressed: () { - openAppSettings(); - }, - ), - ); - - return false; - } else if (status.isDenied) { - // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937 - Permission permission = Permission.photos; - if (defaultTargetPlatform == TargetPlatform.android && - ApplicationInfo.androidSDKVersion <= 32) { - permission = Permission.storage; - } - // if the permission is denied, we should request the permission - final newStatus = await permission.request(); - if (newStatus.isDenied) { - return false; - } - } - - return true; - } } class _UnsplashCover extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart index c5cebd7ed3..bbe0fda27c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart @@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_she import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; import 'package:appflowy/shared/feedback_gesture_detector.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -163,11 +164,8 @@ class _FontButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - String fontFamily = state.fontFamily ?? builtInFontFamily(); - if (fontFamily == builtInFontFamily()) { - fontFamily = - LocaleKeys.settings_appearance_fontFamily_defaultFont.tr(); - } + final fontFamilyDisplayName = + (state.fontFamily ?? defaultFontFamily).fontFamilyDisplayName; return GestureDetector( onTap: () => _showFontSelector(context), behavior: HitTestBehavior.opaque, @@ -182,7 +180,7 @@ class _FontButton extends StatelessWidget { const HSpace(16.0), FlowyText(LocaleKeys.titleBar_font.tr()), const Spacer(), - FlowyText(fontFamily), + FlowyText(fontFamilyDisplayName), const HSpace(6.0), const FlowySvg(FlowySvgs.m_page_style_arrow_right_s), const HSpace(12.0), @@ -219,7 +217,7 @@ class _FontButton extends StatelessWidget { child: FontSelector( scrollController: controller, selectedFontFamilyName: - state.fontFamily ?? builtInFontFamily(), + state.fontFamily ?? defaultFontFamily, onFontFamilySelected: (fontFamilyName) { context.read().add( DocumentPageStyleEvent.updateFontFamily( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 02eee6213d..cdf2dcfffd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -8,7 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/util/google_font_family_extension.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; @@ -92,7 +92,7 @@ class EditorStyleCustomizer { final theme = Theme.of(context); final fontSize = pageStyle.fontLayout.fontSize; final lineHeight = pageStyle.lineHeightLayout.lineHeight; - final fontFamily = pageStyle.fontFamily ?? builtInFontFamily(); + final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; final defaultTextDirection = context.read().state.defaultTextDirection; final baseTextStyle = this.baseTextStyle(fontFamily); @@ -178,7 +178,7 @@ class EditorStyleCustomizer { TextStyle outlineBlockPlaceholderStyleBuilder() { final fontSize = context.read().state.fontSize; return TextStyle( - fontFamily: builtInFontFamily(), + fontFamily: defaultFontFamily, fontSize: fontSize, height: 1.5, color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6), @@ -219,7 +219,8 @@ class EditorStyleCustomizer { try { return getGoogleFontSafely(fontFamily, fontWeight: fontWeight); } on Exception { - if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) { + if ([defaultFontFamily, fallbackFontFamily, builtInCodeFontFamily] + .contains(fontFamily)) { return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight); } diff --git a/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart b/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart new file mode 100644 index 0000000000..7ae91fc73e --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart @@ -0,0 +1,64 @@ +// Check if the user has the required permission to access the device's +// - camera +// - storage +// - ... +import 'dart:async'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class PermissionChecker { + static Future checkPhotoPermission(BuildContext context) async { + // check the permission first + final status = await Permission.photos.status; + // if the permission is permanently denied, we should open the app settings + if (status.isPermanentlyDenied && context.mounted) { + unawaited( + showFlowyMobileConfirmDialog( + context, + title: FlowyText.semibold( + LocaleKeys.pageStyle_photoPermissionTitle.tr(), + maxLines: 3, + textAlign: TextAlign.center, + ), + content: FlowyText( + LocaleKeys.pageStyle_photoPermissionDescription.tr(), + maxLines: 5, + textAlign: TextAlign.center, + fontSize: 12.0, + ), + actionAlignment: ConfirmDialogActionAlignment.vertical, + actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(), + actionButtonColor: Colors.blue, + cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(), + cancelButtonColor: Colors.blue, + onActionButtonPressed: () { + openAppSettings(); + }, + ), + ); + + return false; + } else if (status.isDenied) { + // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937 + Permission permission = Permission.photos; + if (defaultTargetPlatform == TargetPlatform.android && + ApplicationInfo.androidSDKVersion <= 32) { + permission = Permission.storage; + } + // if the permission is denied, we should request the permission + final newStatus = await permission.request(); + if (newStatus.isDenied) { + return false; + } + } + + return true; + } +} diff --git a/frontend/appflowy_flutter/lib/util/font_family_extension.dart b/frontend/appflowy_flutter/lib/util/font_family_extension.dart new file mode 100644 index 0000000000..12fb5aaad0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/font_family_extension.dart @@ -0,0 +1,15 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/patterns/common_patterns.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension FontFamilyExtension on String { + String parseFontFamilyName() => replaceAll('_regular', '') + .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}'); + + // display the default font name if the font family name is empty + // or using the default font family + String get fontFamilyDisplayName => isEmpty || this == defaultFontFamily + ? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr() + : parseFontFamilyName(); +} diff --git a/frontend/appflowy_flutter/lib/util/google_font_family_extension.dart b/frontend/appflowy_flutter/lib/util/google_font_family_extension.dart deleted file mode 100644 index 30a3229085..0000000000 --- a/frontend/appflowy_flutter/lib/util/google_font_family_extension.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:appflowy/shared/patterns/common_patterns.dart'; - -extension GoogleFontsParser on String { - String parseFontFamilyName() => replaceAll('_regular', '') - .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}'); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart index 5faed08e79..b3cb390e8e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart @@ -1,9 +1,10 @@ +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; /// A class for the default appearance settings for the app class DefaultAppearanceSettings { - static const kDefaultFontFamily = 'Poppins'; + static const kDefaultFontFamily = defaultFontFamily; static const kDefaultThemeMode = ThemeMode.system; static const kDefaultThemeName = "Default"; static const kDefaultTheme = BuiltInTheme.defaultTheme; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart index 6fe42e6273..f2c6141407 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -1,28 +1,19 @@ -import 'dart:io'; - import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; -String builtInFontFamily() { - if (PlatformExtension.isDesktopOrWeb) { - return 'Poppins'; - } +// the default font family is empty, so we can use the default font family of the platform +// the system will choose the default font family of the platform +// iOS: San Francisco +// Android: Roboto +// Desktop: Based on the OS +const defaultFontFamily = ''; - if (Platform.isIOS) { - return 'San Francisco'; - } - - if (Platform.isAndroid) { - return 'Roboto'; - } - - return 'Roboto'; -} - -// 'Poppins'; +// the Poppins font is embedded in the app, so we can use it without GoogleFonts +// TODO(Lucas): after releasing version 0.5.6, remove it. +const fallbackFontFamily = 'Poppins'; const builtInCodeFontFamily = 'RobotoMono'; abstract class BaseAppearance { @@ -48,17 +39,15 @@ abstract class BaseAppearance { letterSpacing = fontSize * (letterSpacing ?? 0.005); final textStyle = TextStyle( - fontFamily: fontFamily, + fontFamily: fontFamily.isEmpty ? null : fontFamily, fontSize: fontSize, color: fontColor, fontWeight: fontWeight, - fontFamilyFallback: [builtInFontFamily()], letterSpacing: letterSpacing, height: lineHeight, ); - // we embed Poppins font in the app, so we can use it without GoogleFonts - if (fontFamily == builtInFontFamily()) { + if (fontFamily == defaultFontFamily || fontFamily == fallbackFontFamily) { return textStyle; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 8925fc9bda..001de8af4e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @override @@ -13,7 +12,6 @@ class DesktopAppearance extends BaseAppearance { String fontFamily, String codeFontFamily, ) { - assert(fontFamily.isNotEmpty); assert(codeFontFamily.isNotEmpty); final theme = brightness == Brightness.light diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart index 769071d55d..48a761da75 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart @@ -1,17 +1,17 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/util/google_font_family_extension.dart'; +import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -83,7 +83,10 @@ class FontFamilyDropDown extends StatefulWidget { } class _FontFamilyDropDownState extends State { - final List availableFonts = GoogleFonts.asMap().keys.toList(); + final List availableFonts = [ + defaultFontFamily, + ...GoogleFonts.asMap().keys, + ]; final ValueNotifier query = ValueNotifier(''); @override @@ -94,10 +97,11 @@ class _FontFamilyDropDownState extends State { @override Widget build(BuildContext context) { + final currentValue = widget.currentFontFamily.fontFamilyDisplayName; return FlowySettingValueDropDown( popoverKey: ThemeFontFamilySetting.popoverKey, popoverController: widget.popoverController, - currentValue: widget.currentFontFamily.parseFontFamilyName(), + currentValue: currentValue, onClose: () { query.value = ''; widget.onClose?.call(); @@ -168,8 +172,8 @@ class _FontFamilyDropDownState extends State { BuildContext context, TextStyle style, ) { - final buttonFontFamily = style.fontFamily!.parseFontFamilyName(); - + final buttonFontFamily = + style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily; return Tooltip( message: buttonFontFamily, waitDuration: const Duration(milliseconds: 150), @@ -179,8 +183,8 @@ class _FontFamilyDropDownState extends State { child: FlowyButton( onHover: (_) => FocusScope.of(context).unfocus(), text: FlowyText.medium( - buttonFontFamily, - fontFamily: style.fontFamily!, + buttonFontFamily.fontFamilyDisplayName, + fontFamily: buttonFontFamily, ), rightIcon: buttonFontFamily == widget.currentFontFamily.parseFontFamilyName() @@ -190,15 +194,14 @@ class _FontFamilyDropDownState extends State { if (widget.onFontFamilyChanged != null) { widget.onFontFamilyChanged!(buttonFontFamily); } else { - final fontFamily = style.fontFamily!.parseFontFamilyName(); if (widget.currentFontFamily.parseFontFamilyName() != buttonFontFamily) { context .read() - .setFontFamily(fontFamily); + .setFontFamily(buttonFontFamily); context .read() - .syncFontFamily(fontFamily); + .syncFontFamily(buttonFontFamily); } } PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart index d3faf00336..4f8f4b786a 100644 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart @@ -36,7 +36,7 @@ void main() { AppTheme.fallback, ), verify: (bloc) { - expect(bloc.state.font, builtInFontFamily()); + expect(bloc.state.font, defaultFontFamily); expect(bloc.state.monospaceFont, 'SF Mono'); expect(bloc.state.themeMode, ThemeMode.system); }, diff --git a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart index d2c7102b0d..2f369cd0cf 100644 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart @@ -27,7 +27,7 @@ void main() { test('Initial state', () { expect(cubit.state.fontSize, 16.0); - expect(cubit.state.fontFamily, builtInFontFamily()); + expect(cubit.state.fontFamily, defaultFontFamily); }); test('Fetch document appearance from SharedPreferences', () async { diff --git a/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart b/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart index 26fac55ecf..d8639aa600 100644 --- a/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart @@ -1,7 +1,9 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -56,9 +58,9 @@ void main() { value: documentAppearanceCubit, ), ], - child: Scaffold( + child: const Scaffold( body: ThemeFontFamilySetting( - currentFontFamily: builtInFontFamily(), + currentFontFamily: defaultFontFamily, ), ), ), @@ -71,7 +73,10 @@ void main() { await tester.pumpAndSettle(); // Verify the initial font family - expect(find.text(builtInFontFamily()), findsAtLeastNWidgets(1)); + expect( + find.text(LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()), + findsAtLeastNWidgets(1), + ); when(() => appearanceSettingsCubit.setFontFamily(any())) .thenAnswer((_) async {}); verifyNever(() => appearanceSettingsCubit.setFontFamily(any())); diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index 6aa421bdbc..1a4e8f288c 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -128,7 +128,7 @@ pub struct DocumentSettingsPB { } pub const APPEARANCE_DEFAULT_THEME: &str = "Default"; -pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins"; +pub const APPEARANCE_DEFAULT_FONT: &str = ""; // Use system default font pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono"; const APPEARANCE_RESET_AS_DEFAULT: bool = true; const APPEARANCE_DEFAULT_IS_MENU_COLLAPSED: bool = false; From aaa72fc143de5a36c6d9b5fda749c9048f4c9a7a Mon Sep 17 00:00:00 2001 From: Dekotale <33232965+dekotale@users.noreply.github.com> Date: Wed, 8 May 2024 04:37:36 +0200 Subject: [PATCH 3/6] =?UTF-8?q?chore:=20update=20translations=20with=20Fin?= =?UTF-8?q?k=20=F0=9F=90=A6=20(#5249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lucas.Xu --- frontend/resources/translations/de-DE.json | 4 +- frontend/resources/translations/es-VE.json | 494 +++++++++++++++++++-- 2 files changed, 466 insertions(+), 32 deletions(-) diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 6534abb8aa..623d585d8b 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -622,14 +622,14 @@ "typeAValue": "Einen Wert eingeben...", "layout": "Layout", "databaseLayout": "Layout", - "viewList": "Datenbank-Ansichten", "editView": "Ansicht editieren", "boardSettings": "Board-Einstellungen", "calendarSettings": "Kalender-Einstellungen", "createView": "New Ansicht", "duplicateView": "Ansicht duplizieren", "deleteView": "Anslicht löschen", - "numberOfVisibleFields": "{} angezeigt" + "numberOfVisibleFields": "{} angezeigt", + "viewList": "Datenbank-Ansichten" }, "textFilter": { "contains": "Enthält", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index ff651f5ccd..d8de0b594f 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -42,15 +42,29 @@ "emailHint": "Correo", "passwordHint": "Contraseña", "dontHaveAnAccount": "¿No posee credenciales?", + "createAccount": "Crear cuenta", "repeatPasswordEmptyError": "La contraseña no puede estar en blanco", "unmatchedPasswordError": "Las contraseñas no coinciden", "syncPromptMessage": "La sincronización de los datos puede tardar un poco. Por favor no cierres esta página", "or": "O", + "signInWithGoogle": "Iniciar sesión con Google", + "signInWithGithub": "Iniciar sesión con Github", + "signInWithDiscord": "Iniciar sesión con Discord", + "signUpWithGoogle": "Registrarse con Google", + "signUpWithGithub": "Registrarse con Github", + "signUpWithDiscord": "Registrarse con Discord", "signInWith": "Inicia sesión con:", "signInWithEmail": "Iniciar sesión con correo electrónico", + "signInWithMagicLink": "Iniciar sesión con enlace mágico", + "signUpWithMagicLink": "Registrarse con enlace mágico", "pleaseInputYourEmail": "Por favor, introduzca su dirección de correo electrónico", + "settings": "Configuración", "magicLinkSent": "Enlace mágico enviado a tu correo electrónico, por favor revisa tu bandeja de entrada", "invalidEmail": "Por favor, introduce una dirección de correo electrónico válida", + "alreadyHaveAnAccount": "¿Ya tienes cuenta?", + "logIn": "Iniciar sesión", + "generalError": "Algo ha salido mal. Por favor, inténtalo más tarde", + "limitRateError": "Por razones de seguridad, solo puedes solicitar un enlace mágico cada 60 segundos", "LogInWithGoogle": "Iniciar sesión con Google", "LogInWithGithub": "Iniciar sesión con Github", "LogInWithDiscord": "Iniciar sesión con Discord", @@ -78,11 +92,14 @@ "createLimitExceeded": "Has alcanzado el límite máximo de espacio de trabajo permitido para su cuenta. Si necesita espacios de trabajo adicionales para continuar su trabajo, solicítelos en Github", "deleteSuccess": "Espacio de trabajo eliminado correctamente", "deleteFailed": "No se pudo eliminar el espacio de trabajo", + "openSuccess": "Espacio de trabajo abierto correctamente", "openFailed": "No se pudo abrir el espacio de trabajo", "renameSuccess": "Espacio de trabajo renombrado exitosamente", "renameFailed": "No se pudo cambiar el nombre del espacio de trabajo", "updateIconSuccess": "Icono de espacio de trabajo actualizado correctamente", + "updateIconFailed": "Fallo actualizando el icono del espacio de trabajo", "cannotDeleteTheOnlyWorkspace": "No se puede eliminar el único espacio de trabajo", + "fetchWorkspacesFailed": "No se pudieron recuperar los espacios de trabajo", "leaveCurrentWorkspace": "Salir del espacio de trabajo", "leaveCurrentWorkspacePrompt": "¿Está seguro de que desea abandonar el espacio de trabajo actual?" }, @@ -219,6 +236,8 @@ "private": "Privado", "workspace": "Espacio de trabajo", "favorites": "Favoritos", + "clickToHidePrivate": "Haz clic para ocultar el espacio privado\nLas páginas que creaste aquí solo son visibles para ti", + "clickToHideWorkspace": "Haga clic para ocultar el espacio de trabajo\nLas páginas que creaste aquí son visibles para todos los miembros", "clickToHidePersonal": "Haga clic para ocultar la sección personal", "clickToHideFavorites": "Haga clic para ocultar la sección de favoritos", "addAPage": "Añadir una página", @@ -240,6 +259,7 @@ }, "button": { "ok": "OK", + "confirm": "Confirmar", "done": "Hecho", "cancel": "Cancelar", "signIn": "Ingresar", @@ -267,6 +287,7 @@ "helpCenter": "Centro de ayuda", "add": "Añadir", "yes": "Si", + "clear": "Limpiar", "remove": "Eliminar", "dontRemove": "no quitar", "copyLink": "Copiar enlace", @@ -301,6 +322,35 @@ }, "settings": { "title": "Ajustes", + "accountPage": { + "menuLabel": "Mi cuenta", + "title": "Mi cuenta", + "description": "Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta.", + "general": { + "title": "Nombre de cuenta e imagen de perfil", + "changeProfilePicture": "Cambiar" + }, + "email": { + "title": "Email", + "actions": { + "change": "Cambiar email" + } + }, + "keys": { + "title": "Claves API de IA", + "openAILabel": "Clave API de OpenAI", + "openAITooltip": "La clave API de OpenAI para usar en los modelos de IA", + "openAIHint": "Ingresa tu clave API de OpenAI", + "stabilityAILabel": "Clave API de Stability", + "stabilityAITooltip": "La clave API de Stability que se utilizará en los modelos de IA", + "stabilityAIHint": "Ingresa tu clave API de Stability" + }, + "login": { + "title": "Inicio de sesión en la cuenta", + "loginLabel": "Inicio de sesión", + "logoutLabel": "Cerrar sesión" + } + }, "menu": { "appearance": "Apariencia", "language": "Lenguaje", @@ -322,10 +372,12 @@ "cloudLocal": "Local", "cloudSupabase": "Supabase", "cloudSupabaseUrl": "URL de la base de datos", + "cloudSupabaseUrlCanNotBeEmpty": "La URL de supabase no puede estar vacía.", "cloudSupabaseAnonKey": "Supabase clave anon", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clave anon no puede estar vacía si la URL de supabase no está vacía", "cloudAppFlowy": "Nube AppFlowy", "cloudAppFlowySelfHost": "AppFlowy Cloud autohospedado", + "appFlowyCloudUrlCanNotBeEmpty": "La URL de la nube no puede estar vacía", "clickToCopy": "Haga clic para copiar", "selfHostStart": "Si no tiene un servidor, consulte la", "selfHostContent": "documento", @@ -335,14 +387,20 @@ "cloudWSURLHint": "Ingrese la dirección websocket de su servidor", "restartApp": "Reiniciar", "restartAppTip": "Reinicie la aplicación para que se apliquen los cambios. Tenga en cuenta que esto podría cerrar la sesión de su cuenta actual.", + "changeServerTip": "Después de cambiar el servidor, debes hacer clic en el botón reiniciar para que los cambios surtan efecto", + "enableEncryptPrompt": "Activa el cifrado para proteger tus datos con esta clave. Guárdalo de forma segura; una vez habilitado, no se puede desactivar. Si se pierden, tus datos se vuelven irrecuperables. Haz clic para copiar", "inputEncryptPrompt": "Introduzca su secreto de cifrado para", "clickToCopySecret": "Haga clic para copiar el código secreto", "configServerSetting": "Configure los ajustes de su servidor", + "configServerGuide": "Después de seleccionar \"Inicio rápido\", navega hasta \"Configuración\" y luego \"Configuración de la nube\" para configurar tu servidor autoalojado.", "inputTextFieldHint": "Su código secreto", "historicalUserList": "Historial de inicio de sesión del usuario", + "historicalUserListTooltip": "Esta lista muestra tus cuentas anónimas. Puedes hacer clic en una cuenta para ver sus detalles. Las cuentas anónimas se crean haciendo clic en el botón \"Comenzar\".", "openHistoricalUser": "Haga clic para abrir la cuenta anónima", + "customPathPrompt": "Almacenar la carpeta de datos de AppFlowy en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos", "importAppFlowyData": "Importar datos desde una carpeta externa de AppFlowy", "importingAppFlowyDataTip": "La importación de datos está en curso. Por favor no cierres la aplicación.", + "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de AppFlowy e impórtalos a la carpeta de datos actual de AppFlowy", "importSuccess": "Importó exitosamente la carpeta de datos de AppFlowy", "importFailed": "Error al importar la carpeta de datos de AppFlowy", "importGuide": "Para obtener más detalles, consulte el documento de referencia.", @@ -358,7 +416,8 @@ "resetSetting": "restaurar", "fontFamily": { "label": "Familia tipográfica", - "search": "Buscar" + "search": "Buscar", + "defaultFont": "Fuente predeterminada" }, "themeMode": { "label": "Theme Mode", @@ -366,6 +425,7 @@ "dark": "Modo Oscuro", "system": "Adapt to System" }, + "fontScaleFactor": "Escala de la fuente", "documentSettings": { "cursorColor": "Color del cursor del documento", "selectionColor": "Color de selección de documento", @@ -386,6 +446,7 @@ }, "textDirection": { "label": "Dirección de texto predeterminada", + "hint": "Especifica si el texto debe comenzar desde la izquierda o desde la derecha de forma predeterminada.", "ltr": "LTR (de izquierda hacia derecha)", "rtl": "RTL (de derecha hacia izquierda)", "auto": "AUTO", @@ -418,10 +479,37 @@ "twelveHour": "doce horas", "twentyFourHour": "veinticuatro horas" }, + "showNamingDialogWhenCreatingPage": "Mostrar diálogo de nombres al crear una página", + "enableRTLToolbarItems": "Habilitar elementos de la barra de herramientas RTL", "members": { "title": "Configuración de miembros", + "inviteMembers": "Invitar miembros", "sendInvite": "Enviar invitación", - "user": "Usuario" + "copyInviteLink": "Copiar enlace de invitación", + "label": "Miembros", + "user": "Usuario", + "role": "Rol", + "removeFromWorkspace": "Quitar del espacio de trabajo", + "owner": "Dueño", + "guest": "Invitado", + "member": "Miembro", + "memberHintText": "Un miembro puede leer, comentar y editar páginas. Invitar a miembros e invitados.", + "guestHintText": "Un Invitado puede leer, reaccionar, comentar y editar ciertas páginas con permiso.", + "emailInvalidError": "Email no válido, compruébalo y vuelve a intentarlo.", + "emailSent": "Email enviado, por favor revisa la bandeja de entrada", + "members": "miembros", + "membersCount": { + "zero": "{} miembros", + "one": "{} miembro", + "other": "{} miembros" + }, + "memberLimitExceeded": "Has alcanzado el límite máximo de miembros permitidos para tu cuenta. Si deseas agregar más miembros adicionales para continuar con tu trabajo, solicítalo en Github.", + "failedToAddMember": "No se pudo agregar el miembro", + "addMemberSuccess": "Miembro agregado con éxito", + "removeMember": "Eliminar miembro", + "areYouSureToRemoveMember": "¿Estás seguro de que deseas eliminar a este miembro?", + "inviteMemberSuccess": "La invitación ha sido enviada con éxito", + "failedToInviteMember": "No se pudo invitar al miembro" } }, "files": { @@ -458,7 +546,11 @@ "recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de AppFlowy", "exportFileSuccess": "¡Exportar archivo con éxito!", "exportFileFail": "¡Error en la exportación del archivo!", - "export": "Exportar" + "export": "Exportar", + "clearCache": "Limpiar caché", + "clearCacheDesc": "Si tienes problemas con las imágenes que no cargan o las fuentes no se muestran correctamente, intenta limpiar la caché. Esta acción no eliminará tus datos de usuario.", + "areYouSureToClearCache": "¿Estás seguro de limpiar el caché?", + "clearCacheSuccess": "¡Caché limpiada exitosamente!" }, "user": { "name": "Nombre", @@ -472,10 +564,24 @@ "shortcuts": { "shortcutsLabel": "Atajos", "command": "Commando", + "keyBinding": "Atajos", "addNewCommand": "Añadir nuevo comando", "updateShortcutStep": "Presione la combinación de teclas deseada y presione ENTER", + "shortcutIsAlreadyUsed": "Este atajo ya se utiliza para: {conflict}", + "resetToDefault": "Restablecer los atajos predeterminados", "couldNotLoadErrorMsg": "No se pudieron cargar los atajos. Inténtalo de nuevo.", - "couldNotSaveErrorMsg": "No se pudieron guardar los atajos. Inténtalo de nuevo." + "couldNotSaveErrorMsg": "No se pudieron guardar los atajos. Inténtalo de nuevo.", + "commands": { + "codeBlockNewParagraph": "Insertar un nuevo párrafo al lado del bloque de código", + "codeBlockIndentLines": "Insertar dos espacios al inicio de la línea en el bloque de código", + "codeBlockOutdentLines": "Eliminar dos espacios al inicio de la línea en el bloque de código", + "codeBlockAddTwoSpaces": "Insertar dos espacios en la posición del cursor en el bloque de código", + "codeBlockSelectAll": "Seleccionar todo el contenido dentro de un bloque de código", + "codeBlockPasteText": "Pegar texto en bloque de código", + "textAlignLeft": "Alinear texto a la izquierda", + "textAlignCenter": "Alinear el texto al centro", + "textAlignRight": "Alinear el texto a la derecha" + } }, "mobile": { "personalInfo": "Informacion personal", @@ -489,6 +595,7 @@ "userAgreement": "Acuerdo del Usuario", "termsAndConditions": "Términos y condiciones", "userprofileError": "No se pudo cargar el perfil de usuario", + "userprofileErrorDescription": "Intenta cerrar sesión y volver a entrar para comprobar si el problema persiste.", "selectLayout": "Seleccionar diseño", "selectStartingDay": "Seleccione el día de inicio", "version": "Versión" @@ -513,14 +620,15 @@ "typeAValue": "Escriba un valor...", "layout": "Disposición", "databaseLayout": "Disposición", + "viewList": "Vistas de base de datos", "editView": "Editar vista", "boardSettings": "Configuración del tablero", "calendarSettings": "Configuración del calendario", "createView": "Nueva vista", "duplicateView": "Duplicar vista", "deleteView": "Eliminar vista", - "Properties": "Propiedades", - "viewList": "Vistas de base de datos" + "numberOfVisibleFields": "{} mostrado", + "Properties": "Propiedades" }, "textFilter": { "contains": "Contiene", @@ -566,7 +674,25 @@ "onOrAfter": "Es en o después", "between": "Está entre", "empty": "Esta vacio", - "notEmpty": "No está vacío" + "notEmpty": "No está vacío", + "choicechipPrefix": { + "before": "Antes", + "after": "Después", + "onOrBefore": "En o antes de", + "onOrAfter": "En o después de", + "isEmpty": "Está vacio", + "isNotEmpty": "No está vacío" + } + }, + "numberFilter": { + "equal": "Es igual", + "notEqual": "No es igual", + "lessThan": "Es menor que", + "greaterThan": "Es mayor que", + "lessThanOrEqualTo": "Es menor o igual que", + "greaterThanOrEqualTo": "Es mayor o igual que", + "isEmpty": "Está vacío", + "isNotEmpty": "No está vacío" }, "field": { "hide": "Ocultar", @@ -575,6 +701,8 @@ "insertRight": "Insertar a la Derecha", "duplicate": "Duplicar", "delete": "Eliminar", + "wrapCellContent": "Ajustar texto", + "clear": "Borrar celdas", "textFieldName": "Texto", "checkboxFieldName": "Casilla de verificación", "dateFieldName": "Fecha", @@ -585,6 +713,7 @@ "multiSelectFieldName": "Selección múltiple", "urlFieldName": "URL", "checklistFieldName": "Lista de Verificación", + "relationFieldName": "Relación", "numberFormat": "Formato numérico", "dateFormat": "Formato de fecha", "includeTime": "Incluir tiempo", @@ -614,18 +743,36 @@ "editProperty": "Editar propiedad", "newProperty": "Nueva propiedad", "deleteFieldPromptMessage": "¿Está seguro? Esta propiedad será eliminada", + "clearFieldPromptMessage": "¿Estás seguro? Se vaciarán todas las celdas de esta columna.", "newColumn": "Nueva columna", - "format": "Formato" + "format": "Formato", + "reminderOnDateTooltip": "Esta celda tiene un recordatorio programado", + "optionAlreadyExist": "La opción ya existe" }, "rowPage": { "newField": "Agregar un nuevo campo", - "fieldDragElementTooltip": "Haga clic para abrir el menú" + "fieldDragElementTooltip": "Haga clic para abrir el menú", + "showHiddenFields": { + "one": "Mostrar {count} campo oculto", + "many": "Mostrar {count} campos ocultos", + "other": "Mostrar {count} campos ocultos" + }, + "hideHiddenFields": { + "one": "Ocultar {count} campo oculto", + "many": "Ocultar {count} campos ocultos", + "other": "Ocultar {count} campos ocultos" + } }, "sort": { "ascending": "ascendente", "descending": "Descendente", + "by": "Por", + "empty": "Sin ordenamiento activo", + "cannotFindCreatableField": "No se encuentra un campo adecuado para ordenar", "deleteAllSorts": "Eliminar todos filtros", "addSort": "Agregar clasificación", + "removeSorting": "¿Le gustaría eliminar la ordenación?", + "fieldInUse": "Ya estás ordenando por este campo", "deleteSort": "Borrar ordenar" }, "row": { @@ -673,10 +820,36 @@ }, "url": { "launch": "Abrir en el navegador", - "copy": "Copiar URL" + "copy": "Copiar URL", + "textFieldHint": "Introduce una URL", + "copiedNotification": "¡Copiado al portapapeles!" + }, + "relation": { + "relatedDatabasePlaceLabel": "Base de datos relacionada", + "relatedDatabasePlaceholder": "Ninguno", + "inRelatedDatabase": "En", + "rowSearchTextFieldPlaceholder": "Buscar", + "noDatabaseSelected": "No se seleccionó ninguna base de datos, seleccione una primero de la lista a continuación:", + "emptySearchResult": "No se encontraron registros", + "linkedRowListLabel": "{count} filas vinculadas", + "unlinkedRowListLabel": "Vincular otra fila" }, "menuName": "Cuadrícula", - "referencedGridPrefix": "Vista de" + "referencedGridPrefix": "Vista de", + "calculate": "Calcular", + "calculationTypeLabel": { + "none": "Ninguno", + "average": "Promedio", + "max": "Max", + "median": "Media", + "min": "Min", + "sum": "Suma", + "count": "Contar", + "countEmpty": "Contar vacío", + "countEmptyShort": "VACÍO", + "countNonEmpty": "Contar no vacíos", + "countNonEmptyShort": "RELLENO" + } }, "document": { "menuName": "Documento", @@ -730,10 +903,14 @@ "discardResponse": "¿Quieres descartar las respuestas de IA?", "createInlineMathEquation": "Crear ecuación", "fonts": "Tipo de letra", + "insertDate": "Insertar fecha", + "emoji": "Emoji", "toggleList": "Alternar lista", + "quoteList": "Lista de citas", "numberedList": "lista numerada", "bulletedList": "Lista con viñetas", "todoList": "Lista de tareas", + "callout": "Callout", "cover": { "changeCover": "Cubierta de cambio", "colors": "Colores", @@ -777,17 +954,22 @@ "left": "Izquierda", "center": "Centro", "right": "Bien", - "defaultColor": "Por defecto" + "defaultColor": "Por defecto", + "depth": "Profundidad" }, "image": { "copiedToPasteBoard": "El enlace de la imagen se ha copiado en el portapapeles.", - "addAnImage": "Añadir una imagen" + "addAnImage": "Añadir una imagen", + "imageUploadFailed": "Error al subir la imagen", + "errorCode": "Código de error" }, "urlPreview": { - "copiedToPasteBoard": "El enlace ha sido copiado al portapapeles." + "copiedToPasteBoard": "El enlace ha sido copiado al portapapeles.", + "convertToLink": "Convertir en enlace incrustado" }, "outline": { - "addHeadingToCreateOutline": "Agregue encabezados para crear una tabla de contenido." + "addHeadingToCreateOutline": "Agregue encabezados para crear una tabla de contenido.", + "noMatchHeadings": "No se han encontrado títulos coincidentes." }, "table": { "addAfter": "Agregar después", @@ -810,7 +992,11 @@ "toContinue": "continuar", "newDatabase": "Nueva base de datos", "linkToDatabase": "Enlace a la base de datos" - } + }, + "date": "Fecha" + }, + "outlineBlock": { + "placeholder": "Tabla de contenidos" }, "textBlock": { "placeholder": "Escriba '/' para comandos" @@ -829,27 +1015,48 @@ "placeholder": "Introduce la URL de la imagen" }, "ai": { - "label": "Generar imagen desde OpenAI" + "label": "Generar imagen desde OpenAI", + "placeholder": "Ingrese el prompt para que OpenAI genere una imagen" + }, + "stability_ai": { + "label": "Generar imagen desde Stability AI", + "placeholder": "Ingrese el prompt para que Stability AI genere una imagen" }, "support": "El límite de tamaño de la imagen es de 5 MB. Formatos admitidos: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "Imagen inválida", "invalidImageSize": "El tamaño de la imagen debe ser inferior a 5 MB", "invalidImageFormat": "El formato de imagen no es compatible. Formatos admitidos: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "URL de imagen no válida" + "invalidImageUrl": "URL de imagen no válida", + "noImage": "El fichero o directorio no existe" }, "embedLink": { "label": "Insertar enlace", "placeholder": "Pega o escribe el enlace de una imagen" }, + "unsplash": { + "label": "Desempaquetar" + }, "searchForAnImage": "Buscar una imagen", - "saveImageToGallery": "Guardar imagen" + "pleaseInputYourOpenAIKey": "ingresa tu clave OpenAI en la página de Configuración", + "pleaseInputYourStabilityAIKey": "ingresa tu clave de Stability AI en la página de configuración", + "saveImageToGallery": "Guardar imagen", + "failedToAddImageToGallery": "No se pudo agregar la imagen a la galería", + "successToAddImageToGallery": "Imagen agregada a la galería con éxito", + "unableToLoadImage": "No se puede cargar la imagen", + "maximumImageSize": "El tamaño máximo de imagen es de 10 MB", + "uploadImageErrorImageSizeTooBig": "El tamaño de la imagen debe ser inferior a 10 MB.", + "imageIsUploading": "La imagen se está subiendo" }, "codeBlock": { "language": { "label": "Idioma", - "placeholder": "Seleccione el idioma" - } + "placeholder": "Seleccione el idioma", + "auto": "Auto" + }, + "copyTooltip": "Copiar el contenido del bloque de código.", + "searchLanguageHint": "Buscar un idioma", + "codeCopiedSnackbar": "¡Código copiado al portapapeles!" }, "inlineLink": { "placeholder": "Pegar o escribir un enlace", @@ -866,13 +1073,25 @@ } }, "mention": { + "placeholder": "Menciona una persona, una página o fecha...", "page": { "label": "Enlace a la página", "tooltip": "Haga clic para abrir la página" - } + }, + "deleted": "Eliminado", + "deletedContent": "Este contenido no existe o ha sido eliminado." }, "toolbar": { "resetToDefaultFont": "Restablecer a los predeterminados" + }, + "errorBlock": { + "theBlockIsNotSupported": "La versión actual no admite este bloque.", + "blockContentHasBeenCopied": "El contenido del bloque ha sido copiado." + }, + "mobilePageSelector": { + "title": "Seleccionar página", + "failedToLoad": "No se pudo cargar la lista de páginas", + "noPagesFound": "No se encontraron páginas" } }, "board": { @@ -880,14 +1099,19 @@ "createNewCard": "Nuevo", "renameGroupTooltip": "Presione para cambiar el nombre del grupo", "createNewColumn": "Agregar un nuevo grupo", + "addToColumnTopTooltip": "Añade una nueva tarjeta en la parte superior", + "addToColumnBottomTooltip": "Añade una nueva tarjeta en la parte inferior.", "renameColumn": "Renombrar", "hideColumn": "Ocultar", "newGroup": "Nuevo grupo", "deleteColumn": "Borrar", + "deleteColumnConfirmation": "Esto eliminará este grupo y todas las tarjetas que contiene.\n¿Estás seguro de que quieres continuar?", "groupActions": "Acciones grupales" }, "hiddenGroupSection": { - "sectionTitle": "Grupos ocultos" + "sectionTitle": "Grupos ocultos", + "collapseTooltip": "Ocultar los grupos ocultos", + "expandTooltip": "Ver los grupos ocultos" }, "cardDetail": "Detalle de la tarjeta", "cardActions": "Acciones de tarjeta", @@ -921,6 +1145,10 @@ "previousMonth": "Mes anterior", "nextMonth": "Próximo mes" }, + "mobileEventScreen": { + "emptyTitle": "No hay eventos", + "emptyBody": "Presiona el botón más para crear un evento en este día." + }, "settings": { "showWeekNumbers": "Mostrar números de semana", "showWeekends": "Mostrar fines de semana", @@ -928,12 +1156,14 @@ "layoutDateField": "Diseño de calendario por", "changeLayoutDateField": "Cambiar campo de diseño", "noDateTitle": "Sin cita", + "noDateHint": "Los eventos no programados se mostrarán aquí", + "unscheduledEventsTitle": "Eventos no programados", "clickToAdd": "Haga clic para agregar al calendario", - "name": "Diseño de calendario", - "noDateHint": "Los eventos no programados se mostrarán aquí" + "name": "Diseño de calendario" }, "referencedCalendarPrefix": "Vista de", - "quickJumpYear": "Ir a" + "quickJumpYear": "Ir a", + "duplicateEvent": "duplicar evento" }, "errorDialog": { "title": "Error de flujo de aplicación", @@ -1003,6 +1233,7 @@ }, "inlineActions": { "noResults": "No hay resultados", + "recentPages": "Paginas recientes", "pageReference": "Referencia de página", "docReference": "Referencia de documento", "boardReference": "Referencia del tablero", @@ -1020,7 +1251,24 @@ "includeTime": "incluir tiempo", "isRange": "Fecha final", "timeFormat": "Formato de tiempo", - "clearDate": "Borrar fecha" + "clearDate": "Borrar fecha", + "reminderLabel": "Recordatorio", + "selectReminder": "Seleccionar recordatorio", + "reminderOptions": { + "none": "Ninguno", + "atTimeOfEvent": "Hora del evento", + "fiveMinsBefore": "5 minutos antes", + "tenMinsBefore": "10 minutos antes", + "fifteenMinsBefore": "15 minutos antes", + "thirtyMinsBefore": "30 minutos antes", + "oneHourBefore": "1 hora antes", + "twoHoursBefore": "2 horas antes", + "onDayOfEvent": "El día del evento", + "oneDayBefore": "1 dia antes", + "twoDaysBefore": "2 dias antes", + "oneWeekBefore": "1 semana antes", + "custom": "Personalizado" + } }, "relativeDates": { "yesterday": "Ayer", @@ -1033,6 +1281,7 @@ "mobile": { "title": "Actualizaciones" }, + "emptyTitle": "¡Todo al día!", "emptyBody": "No hay notificaciones ni acciones pendientes. Disfruta de la calma.", "tabs": { "inbox": "Bandeja de entrada", @@ -1066,14 +1315,18 @@ "replace": "Reemplazar", "replaceAll": "Reemplaza todo", "noResult": "No hay resultados", - "caseSensitive": "Distingue mayúsculas y minúsculas" + "caseSensitive": "Distingue mayúsculas y minúsculas", + "searchMore": "Busca para encontrar más resultados" }, "error": { - "weAreSorry": "Lo lamentamos" + "weAreSorry": "Lo lamentamos", + "loadingViewError": "Estamos teniendo problemas para cargar esta vista. Verifica tu conexión a Internet, actualiza la aplicación y no dudes en comunicarte con el equipo si el problema continúa." }, "editor": { "bold": "Negrita", "bulletedList": "Lista con viñetas", + "bulletedListShortForm": "Con viñetas", + "checkbox": "Checkbox", "embedCode": "Código de inserción", "heading1": "H1", "heading2": "H2", @@ -1081,9 +1334,12 @@ "highlight": "Destacar", "color": "Color", "image": "Imagen", + "date": "Fecha", + "page": "Página", "italic": "Itálico", "link": "Enlace", "numberedList": "Lista numerada", + "numberedListShortForm": "Numerado", "quote": "Cita", "strikethrough": "Tachado", "text": "Texto", @@ -1107,6 +1363,184 @@ "backgroundColorBlue": "Fondo azul", "backgroundColorPurple": "fondo morado", "backgroundColorPink": "fondo rosa", - "backgroundColorRed": "fondo rojo" + "backgroundColorRed": "fondo rojo", + "backgroundColorLime": "Fondo lima", + "backgroundColorAqua": "Fondo aguamarina", + "done": "Hecho", + "cancel": "Cancelar", + "tint1": "Tono 1", + "tint2": "Tono 2", + "tint3": "Tono 3", + "tint4": "Tono 4", + "tint5": "Tono 5", + "tint6": "Tono 6", + "tint7": "Tono 7", + "tint8": "Tono 8", + "tint9": "Tono 9", + "lightLightTint1": "Morado", + "lightLightTint2": "Rosa", + "lightLightTint3": "Rosa claro", + "lightLightTint4": "Naranja", + "lightLightTint5": "Amarillo", + "lightLightTint6": "Lima", + "lightLightTint7": "Verde", + "lightLightTint8": "Aqua", + "lightLightTint9": "Azul", + "urlHint": "URL", + "mobileHeading1": "Encabezado 1", + "mobileHeading2": "Encabezado 2", + "mobileHeading3": "Encabezado 3", + "textColor": "Color de texto", + "backgroundColor": "Color de fondo", + "addYourLink": "Añadir enlace", + "openLink": "Abrir enlace", + "copyLink": "Copiar enlace", + "removeLink": "Quitar enlace", + "editLink": "Editar enlace", + "linkText": "Texto", + "linkTextHint": "Introduce un texto", + "linkAddressHint": "Introduce una URL", + "highlightColor": "Color de resaltado", + "clearHighlightColor": "Quitar color de resaltado", + "customColor": "Color personalizado", + "hexValue": "Valor Hex", + "opacity": "Transparencia", + "resetToDefaultColor": "Reestablecer color predeterminado", + "ltr": "LTR", + "rtl": "RTL", + "auto": "Auto", + "cut": "Cortar", + "copy": "Copiar", + "paste": "Pegar", + "find": "Buscar", + "select": "Seleccionar", + "selectAll": "Seleccionar todo", + "previousMatch": "Resultado anterior", + "nextMatch": "Siguiente resultado", + "closeFind": "Cerrar", + "replace": "Reemplazar", + "replaceAll": "Reemplazar todo", + "regex": "Expresión regular", + "caseSensitive": "Distingue mayúsculas y minúsculas", + "uploadImage": "Subir imagen", + "urlImage": "URL de la Imagen", + "incorrectLink": "Enlace incorrecto", + "upload": "Subir", + "chooseImage": "Elige una imagen", + "loading": "Cargando", + "imageLoadFailed": "Error al subir la imagen", + "divider": "Divisor", + "table": "Tabla", + "colAddBefore": "Añadir antes", + "rowAddBefore": "Añadir antes", + "colAddAfter": "Añadir después", + "rowAddAfter": "Añadir después", + "colRemove": "Quitar", + "rowRemove": "Quitar", + "colDuplicate": "Duplicar", + "rowDuplicate": "Duplicar", + "colClear": "Borrar contenido", + "rowClear": "Borrar contenido", + "slashPlaceHolder": "Escribe '/' para insertar un bloque o comienza a escribir", + "typeSomething": "Escribe algo...", + "toggleListShortForm": "Alternar", + "quoteListShortForm": "Cita", + "mathEquationShortForm": "Fórmula", + "codeBlockShortForm": "Código" + }, + "favorite": { + "noFavorite": "Ninguna página favorita", + "noFavoriteHintText": "Desliza la página hacia la izquierda para agregarla a tus favoritos" + }, + "cardDetails": { + "notesPlaceholder": "Escribe una / para insertar un bloque o comienza a escribir" + }, + "blockPlaceholders": { + "todoList": "Por hacer", + "bulletList": "Lista", + "numberList": "Lista", + "quote": "Cita", + "heading": "Título {}" + }, + "titleBar": { + "pageIcon": "Icono de página", + "language": "Idioma", + "font": "Fuente", + "actions": "Acciones", + "date": "Fecha", + "addField": "Añadir campo", + "userIcon": "Icono de usuario" + }, + "noLogFiles": "No hay archivos de registro", + "newSettings": { + "myAccount": { + "title": "Mi cuenta", + "subtitle": "Personaliza tu perfil, administra la seguridad de la cuenta, abre claves IA o inicia sesión en tu cuenta.", + "profileLabel": "Nombre de cuenta e imagen de perfil", + "profileNamePlaceholder": "Introduce tu nombre", + "accountSecurity": "Seguridad de la cuenta", + "2FA": "Autenticación de 2 pasos", + "aiKeys": "Claves IA", + "accountLogin": "Inicio de sesión de la cuenta", + "updateNameError": "No se pudo actualizar el nombre", + "updateIconError": "No se pudo actualizar el ícono", + "deleteAccount": { + "title": "Borrar cuenta", + "subtitle": "Elimina permanentemente tu cuenta y todos tus datos.", + "deleteMyAccount": "Borrar mi cuenta", + "dialogTitle": "Borrar cuenta", + "dialogContent1": "¿Estás seguro de que deseas eliminar permanentemente tu cuenta?", + "dialogContent2": "Esta acción no se puede deshacer y eliminará el acceso a todos los espacios de equipo, borrará toda tu cuenta, incluidos los espacios de trabajo privados, y lo eliminará de todos los espacios de trabajo compartidos." + } + }, + "workplace": { + "name": "Espacio de trabajo", + "title": "Configuración del espacio de trabajo", + "subtitle": "Personaliza la apariencia, el tema, la fuente, el diseño del texto, la fecha, la hora y el idioma de tu espacio de trabajo.", + "workplaceName": "Nombre del espacio de trabajo", + "workplaceNamePlaceholder": "Introduce el nombre del espacio de trabajo", + "workplaceIcon": "Icono del espacio de trabajo", + "workplaceIconSubtitle": "Sube una imagen o usa un emoji para tu espacio de trabajo. El icono se mostrará en la barra lateral y en las notificaciones.", + "renameError": "Error al renombrar el espacio de trabajo", + "updateIconError": "Error al actualizar el ícono", + "appearance": { + "name": "Apariencia", + "themeMode": { + "auto": "Auto", + "light": "Claro", + "dark": "Oscuro" + }, + "language": "Idioma" + } + }, + "syncState": { + "syncing": "Sincronización", + "synced": "Sincronizado", + "noNetworkConnected": "Ninguna red conectada" + } + }, + "pageStyle": { + "title": "Estilo de página", + "layout": "Disposición", + "coverImage": "Imagen de portada", + "pageIcon": "Icono de página", + "colors": "Colores", + "gradient": "Degradado", + "backgroundImage": "Imagen de fondo", + "presets": "Preajustes", + "photo": "Foto", + "unsplash": "Desempaquetar", + "pageCover": "Portada de página", + "none": "Ninguno" + }, + "commandPalette": { + "placeholder": "Escribe para buscar vistas...", + "bestMatches": "Mejores resultados", + "recentHistory": "Historial reciente", + "navigateHint": "para navegar", + "loadingTooltip": "Buscando resultados...", + "betaLabel": "BETA", + "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", + "fromTrashHint": "De la papelera" } } From eeddf341e189c40f39ef53b3d4a781f8bd153533 Mon Sep 17 00:00:00 2001 From: TyrantRey <48596994+LostALice@users.noreply.github.com> Date: Wed, 8 May 2024 10:37:48 +0800 Subject: [PATCH 4/6] chore: translate english to traditional chinese (#5278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * chore: remove unused translations --------- Co-authored-by: Lucas.Xu --- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/ca-ES.json | 2 +- frontend/resources/translations/ckb-KU.json | 2 +- frontend/resources/translations/cs-CZ.json | 2 +- frontend/resources/translations/de-DE.json | 2 +- frontend/resources/translations/es-VE.json | 2 +- frontend/resources/translations/eu-ES.json | 2 +- frontend/resources/translations/fa.json | 2 +- frontend/resources/translations/fr-CA.json | 2 +- frontend/resources/translations/fr-FR.json | 2 +- frontend/resources/translations/hu-HU.json | 2 +- frontend/resources/translations/id-ID.json | 2 +- frontend/resources/translations/it-IT.json | 2 +- frontend/resources/translations/ja-JP.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- frontend/resources/translations/pl-PL.json | 2 +- frontend/resources/translations/pt-BR.json | 2 +- frontend/resources/translations/pt-PT.json | 2 +- frontend/resources/translations/ru-RU.json | 2 +- frontend/resources/translations/sv-SE.json | 2 +- frontend/resources/translations/tr-TR.json | 2 +- frontend/resources/translations/vi-VN.json | 2 +- frontend/resources/translations/vi.json | 2 +- frontend/resources/translations/zh-CN.json | 2 +- frontend/resources/translations/zh-TW.json | 270 ++++++++++++++++++-- 25 files changed, 270 insertions(+), 48 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 84d0f64985..409cdfdc28 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -1169,4 +1169,4 @@ "addField": "إضافة حقل", "userIcon": "رمز المستخدم" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index dccb18dcf8..2e98070d46 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -812,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 28535be2c8..1260ecf330 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -946,4 +946,4 @@ "frequentlyUsed": "زۆرجار بەکارت هێناوە" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index a5887f16b9..b6e1282376 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -1094,4 +1094,4 @@ "font": "Písmo", "actions": "Příkazy" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 623d585d8b..b5e51ff804 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -1541,4 +1541,4 @@ "betaTooltip": "Wir unterstützen derzeit nur die Suche nach Seiten", "fromTrashHint": "Aus dem Mülleimer" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index d8de0b594f..5828870ca0 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -1543,4 +1543,4 @@ "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", "fromTrashHint": "De la papelera" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index d3c25fa3aa..496a262596 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -601,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 0bb112d168..56e97333d7 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -674,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 958762094e..a6799575f1 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -1262,4 +1262,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index db1a933b28..dc4e2348fb 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -1468,4 +1468,4 @@ "loadingTooltip": "Nous recherchons des résultats...", "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 3a0d464d5a..bfecc2756f 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -599,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 4aa1e71038..f1f5c6a1f1 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -1022,4 +1022,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 47e90eaa87..4093c824ff 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -1262,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 4388f9c3a9..704552b55d 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -686,4 +686,4 @@ "deleteContentTitle": "{pageType} を削除してもよろしいですか?", "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 81f7be0185..03174e2ed2 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -598,4 +598,4 @@ "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 5b11fec3c9..43d17c717e 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -1077,4 +1077,4 @@ "language": "Język", "font": "Czcionka" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 1341f735b7..046eee783e 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -1219,4 +1219,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 8a9745650a..ab4bd7d438 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -857,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index c138c87bd1..72e7a7424e 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -1311,4 +1311,4 @@ "userIcon": "Пользовательская иконка" }, "noLogFiles": "Нет файлов журналов" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 1d125b1402..f35966d390 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -668,4 +668,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 8f82fedb62..acfacef891 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1477,4 +1477,4 @@ "betaTooltip": "Şu anda yalnızca sayfaları aramayı destekliyoruz", "fromTrashHint": "Çöp kutusundan" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 09b73db901..9b89f50dbd 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -868,4 +868,4 @@ "font": "Phông chữ", "date": "Ngày" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/vi.json b/frontend/resources/translations/vi.json index b921c1844e..4d1716447a 100644 --- a/frontend/resources/translations/vi.json +++ b/frontend/resources/translations/vi.json @@ -6,4 +6,4 @@ "failedToLoad": "Không tải được chế độ xem bảng" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 8d342913c2..a1f9d3807b 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1357,4 +1357,4 @@ "addField": "添加字段", "userIcon": "用户图标" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index aea8585650..a7f81d0205 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -45,10 +45,28 @@ "unmatchedPasswordError": "密碼重複輸入不一致", "syncPromptMessage": "同步資料可能需要一些時間。請不要關閉此頁面", "or": "或", + "signInWithGoogle": "使用Google 登入", + "signInWithGithub": "使用Github 登入", + "signInWithDiscord": "使用Discord 登入", + "signUpWithGoogle": "使用Google 註冊", + "signUpWithGithub": "使用Github 註冊", + "signUpWithDiscord": "使用Discord 註冊", "signInWith": "透過以下方式登入:", + "signInWithEmail": "使用電子郵件登入", + "signInWithMagicLink": "使用Magic Link 登入", + "signUpWithMagicLink": "使用Magic Link 註冊", + "pleaseInputYourEmail": "請輸入您的電郵地址", + "settings": "設定", + "magicLinkSent": "我們己發送Magic Link 到您的電子郵件,點擊連結登入", + "invalidEmail": "請輸入有效的電郵地址", + "alreadyHaveAnAccount": "已經有帳戶?", + "logIn": "登入", + "generalError": "出了些問題。請稍後再試", + "limitRateError": "出於安全原因,您只能每60 秒申請一次Magic Link", "LogInWithGoogle": "使用 Google 登入", "LogInWithGithub": "使用 Github 登入", - "LogInWithDiscord": "使用 Discord 登入" + "LogInWithDiscord": "使用 Discord 登入", + "loginAsGuestButtonText": "以訪客身分登入" }, "workspace": { "chooseWorkspace": "選擇你的工作區", @@ -63,12 +81,30 @@ "reportIssueOnGithub": "在 Github 提交 issue", "exportLogFiles": "匯出日誌記錄檔案", "reachOut": "在 Discord 上聯絡我們" - } + }, + "menuTitle": "工作區", + "deleteWorkspaceHintText": "您確定要刪除工作區嗎?此操作無法撤銷", + "createSuccess": "成功創建工作區", + "createFailed": "無法創建工作區", + "deleteSuccess": "工作區刪除成功", + "deleteFailed": "工作區刪除失敗", + "openSuccess": "成功開啟工作區", + "openFailed": "無法開啟工作區", + "renameSuccess": "工作區重命名成功", + "renameFailed": "無法重命名工作區", + "updateIconSuccess": "更新工作區圖標成功", + "updateIconFailed": "無法更新工作區圖標", + "cannotDeleteTheOnlyWorkspace": "無法刪除唯一的工作區", + "fetchWorkspacesFailed": "無法取得工作區", + "leaveCurrentWorkspace": "離開工作區", + "leaveCurrentWorkspacePrompt": "您確定要離開當前工作區嗎?" }, "shareAction": { "buttonText": "分享", "workInProgress": "即將推出", "markdown": "Markdown", + "html": "HTML", + "clipboard": "複製到剪貼簿", "csv": "CSV", "copyLink": "複製連結" }, @@ -127,7 +163,8 @@ "emptyDescription": "您沒有任何已刪除的檔案", "isDeleted": "已刪除", "isRestored": "已還原" - } + }, + "confirmDeleteTitle": "確定永久刪除此頁面" }, "deletePagePrompt": { "text": "此頁面在垃圾桶中", @@ -181,16 +218,23 @@ "dragRow": "長按以重新排序列", "viewDataBase": "檢視資料庫", "referencePage": "這個 {name} 已被引用", - "addBlockBelow": "在下方新增一個區塊" + "addBlockBelow": "在下方新增一個區塊", + "genSummary": "產成摘要" }, "sideBar": { "closeSidebar": "關閉側欄", "openSidebar": "開啟側欄", "personal": "個人", + "private": "私人", + "workspace": "工作區", "favorites": "最愛", + "clickToHidePrivate": "點擊以隱藏私人空間\n您在此處建立的頁面只有您自己可見", + "clickToHideWorkspace": "點擊以隱藏工作區\n您在此處建立的頁面對每個成員都可見", "clickToHidePersonal": "點選以隱藏個人區塊", "clickToHideFavorites": "點選以隱藏最愛區塊", "addAPage": "新增頁面", + "addAPageToPrivate": "新增頁面到私人空間", + "addAPageToWorkspace": "將頁面新增至工作區", "recent": "最近" }, "notifications": { @@ -207,6 +251,7 @@ }, "button": { "ok": "確定", + "confirm": "確認", "done": "完成", "cancel": "取消", "signIn": "登入", @@ -233,7 +278,20 @@ "rename": "重新命名", "helpCenter": "支援中心", "add": "新增", - "yes": "是" + "yes": "是", + "clear": "清除", + "remove": "刪除", + "dontRemove": "不要刪除", + "copyLink": "複製連結", + "align": "對齊", + "login": "登入", + "logout": "登出", + "deleteAccount": "刪除帳號", + "back": "返回", + "signInGoogle": "使用Google 登入", + "signInGithub": "使用Github 登入", + "signInDiscord": "使用Discord 登入", + "tryAGain": "再試一次" }, "label": { "welcome": "歡迎!", @@ -257,6 +315,35 @@ }, "settings": { "title": "設定", + "accountPage": { + "menuLabel": "我的帳號", + "title": "我的帳號", + "description": "自訂您的個人資料、管理帳戶安全性和 AI API 金鑰,或登入您的帳號", + "general": { + "title": "帳號名稱和個人資料圖片", + "changeProfilePicture": "更改個人資料圖片" + }, + "email": { + "title": "電子郵件", + "actions": { + "change": "更改電子郵件" + } + }, + "keys": { + "title": "AI API 金鑰", + "openAILabel": "Open AI API 金鑰", + "openAITooltip": "以OpenAI API 金鑰使用AI 模型", + "openAIHint": "輸入您的 OpenAI API 金鑰", + "stabilityAILabel": "Stability API 金鑰", + "stabilityAITooltip": "以Stability API 金鑰使用AI 模型", + "stabilityAIHint": "輸入您的Stability API 金鑰" + }, + "login": { + "title": "帳號登入", + "loginLabel": "登入", + "logoutLabel": "登出" + } + }, "menu": { "appearance": "外觀", "language": "語言", @@ -309,7 +396,8 @@ "importAppFlowyDataDescription": "從外部 AppFlowy 資料夾複製資料並匯入到目前的 AppFlowy 資料夾", "importSuccess": "成功匯入 AppFlowy 資料夾", "importFailed": "匯入 AppFlowy 資料夾失敗", - "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件" + "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件", + "supabaseSetting": "supabase 設定" }, "notifications": { "enableNotifications": { @@ -321,7 +409,8 @@ "resetSetting": "重設", "fontFamily": { "label": "字型", - "search": "搜尋" + "search": "搜尋", + "defaultFont": "系統預設" }, "themeMode": { "label": "主題模式", @@ -329,6 +418,7 @@ "dark": "深色模式", "system": "依照系統設定" }, + "fontScaleFactor": "字體比例", "documentSettings": { "cursorColor": "文件游標顏色", "selectionColor": "文件選取顏色", @@ -381,7 +471,31 @@ "twelveHour": "12 小時制", "twentyFourHour": "24 小時制" }, - "showNamingDialogWhenCreatingPage": "建立頁面時顯示命名對話框" + "showNamingDialogWhenCreatingPage": "建立頁面時顯示命名對話框", + "members": { + "title": "成員設定", + "inviteMembers": "邀請成員", + "sendInvite": "發送邀請", + "copyInviteLink": "複製邀請連結", + "label": "成員", + "user": "使用者", + "role": "身分組", + "removeFromWorkspace": "從工作區中刪除", + "owner": "擁有者", + "guest": "訪客", + "member": "成員", + "memberHintText": "成員可以閱讀、評論和編輯頁面。邀請其他成員和訪客", + "guestHintText": "訪客可以閱讀、做出回應、發表評論,並且可以在獲得許可的情況下編輯頁面", + "emailInvalidError": "電郵無效,請檢查並重試", + "emailSent": "郵件已發送,請查看您的收件匣", + "members": "成員", + "failedToAddMember": "新增成員失敗", + "addMemberSuccess": "成員新增成功", + "removeMember": "刪除成員", + "areYouSureToRemoveMember": "確定要刪除該成員?", + "inviteMemberSuccess": "邀請已成功發送", + "failedToInviteMember": "邀請成員失敗" + } }, "files": { "copy": "複製", @@ -417,7 +531,11 @@ "recoverLocationTooltips": "重設為 AppFlowy 的預設資料目錄", "exportFileSuccess": "匯出檔案成功!", "exportFileFail": "匯出檔案失敗!", - "export": "匯出" + "export": "匯出", + "clearCache": "清除快取", + "clearCacheDesc": "如果您遇到圖像無法載入或字體無法正確顯示等問題,請嘗試清除快取。此操作不會刪除您的使用者資料", + "areYouSureToClearCache": "確定清除快取?", + "clearCacheSuccess": "快取清除成功" }, "user": { "name": "名稱", @@ -437,7 +555,10 @@ "shortcutIsAlreadyUsed": "此快捷鍵已被使用於:{conflict}", "resetToDefault": "重設為預設鍵盤綁定", "couldNotLoadErrorMsg": "無法載入快捷鍵,請再試一次", - "couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次" + "couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次", + "commands": { + "textAlignRight": "向右對齊文字" + } }, "mobile": { "personalInfo": "個人資料", @@ -548,6 +669,7 @@ "multiSelectFieldName": "多選", "urlFieldName": "網址", "checklistFieldName": "核取清單", + "summaryFieldName": "AI 總結", "numberFormat": "數字格式", "dateFormat": "日期格式", "includeTime": "包含時間", @@ -577,9 +699,11 @@ "editProperty": "編輯屬性", "newProperty": "新增屬性", "deleteFieldPromptMessage": "您確定嗎?這個屬性將被刪除", + "clearFieldPromptMessage": "確定操作,該列中的所有單元格都將被清空", "newColumn": "新增欄位", "format": "格式", - "reminderOnDateTooltip": "此欄位設有預定提醒" + "reminderOnDateTooltip": "此欄位設有預定提醒", + "optionAlreadyExist": "選項已存在" }, "rowPage": { "newField": "新增欄位", @@ -593,7 +717,8 @@ "one": "隱藏 {count} 個隱藏欄位", "many": "隱藏 {count} 個隱藏欄位", "other": "隱藏 {count} 個隱藏欄位" - } + }, + "openAsFullPage": "以整頁形式打開" }, "sort": { "ascending": "升冪", @@ -614,7 +739,8 @@ "drag": "拖曳以移動", "dragAndClick": "拖曳以移動,點選以開啟選單", "insertRecordAbove": "在上方插入記錄", - "insertRecordBelow": "在下方插入記錄" + "insertRecordBelow": "在下方插入記錄", + "noContent": "無內容" }, "selectOption": { "create": "建立", @@ -646,7 +772,9 @@ }, "url": { "launch": "在瀏覽器中開啟", - "copy": "複製網址" + "copy": "複製網址", + "textFieldHint": "輸入網址", + "copiedNotification": "已複製到剪貼簿" }, "menuName": "網格", "referencedGridPrefix": "檢視", @@ -712,6 +840,7 @@ "discardResponse": "確定捨棄 AI 的回覆?", "createInlineMathEquation": "建立公式", "fonts": "字型", + "insertDate": "插入日期", "emoji": "表情符號", "toggleList": "切換列表", "quoteList": "引述列表", @@ -766,13 +895,17 @@ }, "image": { "copiedToPasteBoard": "圖片連結已複製到剪貼簿", - "addAnImage": "新增圖片" + "addAnImage": "新增圖片", + "imageUploadFailed": "圖片上傳失敗", + "errorCode": "錯誤代碼" }, "urlPreview": { - "copiedToPasteBoard": "連結已複製到剪貼簿" + "copiedToPasteBoard": "連結已複製到剪貼簿", + "convertToLink": "轉換為嵌入鏈接" }, "outline": { - "addHeadingToCreateOutline": "新增標題以建立目錄。" + "addHeadingToCreateOutline": "新增標題以建立目錄。", + "noMatchHeadings": "未找到匹配的標題" }, "table": { "addAfter": "在後方新增", @@ -798,6 +931,9 @@ }, "date": "日期" }, + "outlineBlock": { + "placeholder": "目錄" + }, "textBlock": { "placeholder": "輸入“/”作為命令" }, @@ -827,7 +963,8 @@ "invalidImage": "無效的圖片", "invalidImageSize": "圖片大小必須小於 5MB", "invalidImageFormat": "不支援的圖片格式。支援的格式:JPEG、PNG、GIF、SVG", - "invalidImageUrl": "無效的圖片網址" + "invalidImageUrl": "無效的圖片網址", + "noImage": "沒有該檔案或目錄" }, "embedLink": { "label": "嵌入連結", @@ -844,13 +981,18 @@ "successToAddImageToGallery": "圖片已成功新增到相簿", "unableToLoadImage": "無法載入圖片", "maximumImageSize": "支援的最大上傳圖片大小為 10MB", - "uploadImageErrorImageSizeTooBig": "圖片大小必須小於 10MB" + "uploadImageErrorImageSizeTooBig": "圖片大小必須小於 10MB", + "imageIsUploading": "圖片上傳中" }, "codeBlock": { "language": { "label": "語言", - "placeholder": "選擇語言" - } + "placeholder": "選擇語言", + "auto": "自動" + }, + "copyTooltip": "複製區塊的內容", + "searchLanguageHint": "搜尋語言", + "codeCopiedSnackbar": "程式碼已複製到剪貼簿" }, "inlineLink": { "placeholder": "貼上或輸入連結", @@ -881,6 +1023,11 @@ "errorBlock": { "theBlockIsNotSupported": "目前版本不支援此區塊。", "blockContentHasBeenCopied": "區塊內容已被複製。" + }, + "mobilePageSelector": { + "title": "選擇頁面", + "failedToLoad": "載入頁面清單失敗", + "noPagesFound": "沒有找到該頁面" } }, "board": { @@ -1025,6 +1172,7 @@ }, "inlineActions": { "noResults": "無結果", + "recentPages": "最近的頁面", "pageReference": "頁面參照", "docReference": "文件參照", "boardReference": "看板參照", @@ -1106,7 +1254,8 @@ "replace": "取代", "replaceAll": "全部取代", "noResult": "無結果", - "caseSensitive": "區分大小寫" + "caseSensitive": "區分大小寫", + "searchMore": "搜尋以查找更多結果" }, "error": { "weAreSorry": "我們很抱歉", @@ -1125,6 +1274,7 @@ "color": "顏色", "image": "圖片", "date": "日期", + "page": "頁面", "italic": "斜體", "link": "連結", "numberedList": "編號清單", @@ -1200,6 +1350,8 @@ "copy": "複製", "paste": "貼上", "find": "尋找", + "select": "選取", + "selectAll": "選取所有", "previousMatch": "上一個符合", "nextMatch": "下一個符合", "closeFind": "關閉", @@ -1256,5 +1408,75 @@ "addField": "新增欄位", "userIcon": "使用者圖示" }, - "noLogFiles": "這裡沒有日誌記錄檔案" -} + "noLogFiles": "這裡沒有日誌記錄檔案", + "newSettings": { + "myAccount": { + "title": "我的帳戶", + "subtitle": "自訂您的個人資料、管理帳戶安全性、Open AI 金鑰或登入您的帳戶", + "profileLabel": "帳號名稱和個人資料圖片", + "profileNamePlaceholder": "輸入你的名字", + "accountSecurity": "帳戶安全性", + "2FA": "兩步驟驗證", + "accountLogin": "帳號登入", + "updateNameError": "名稱更新失敗", + "updateIconError": "個人頭像更新失敗", + "deleteAccount": { + "title": "刪除帳號", + "subtitle": "永久刪除您的帳號和所有資料", + "deleteMyAccount": "刪除我的帳號", + "dialogTitle": "刪除帳號", + "dialogContent1": "確定要永久刪除您的帳號", + "dialogContent2": "此操作無法撤銷,此操作將刪除所有團隊空間的存取權限,刪除您的整個帳戶(包括私人工作區),並將您從所有共用工作區中刪除" + } + }, + "workplace": { + "name": "工作區", + "title": "工作區設定", + "subtitle": "自訂您的工作區外觀、主題、字體、文字佈局、日期、時間和語言", + "workplaceName": "工作區名稱", + "workplaceNamePlaceholder": "輸入工作區名稱", + "workplaceIcon": "工作區圖標", + "workplaceIconSubtitle": "為您的工作區上傳圖像或表情符號。圖示將顯示在您的側邊欄和通知中", + "renameError": "工作區重新命名失敗", + "updateIconError": "更新圖像失敗", + "appearance": { + "name": "外觀", + "themeMode": { + "auto": "自動", + "light": "亮白", + "dark": "黑暗" + }, + "language": "語言" + } + }, + "syncState": { + "syncing": "同步中", + "synced": "已同步", + "noNetworkConnected": "沒有連線網絡" + } + }, + "pageStyle": { + "title": "頁面樣式", + "layout": "佈局", + "coverImage": "封面圖片", + "pageIcon": "頁面圖片", + "colors": "顏色", + "gradient": "漸變", + "backgroundImage": "背景圖片", + "presets": "預設", + "photo": "圖片", + "pageCover": "封面", + "none": "無", + "photoPermissionDescription": "允許存取圖片庫以上傳圖片", + "openSettings": "打開設定", + "photoPermissionTitle": "AppFlowy 希望存取您的圖片庫", + "doNotAllow": "不允許" + }, + "commandPalette": { + "bestMatches": "最佳匹配", + "recentHistory": "最近歷史", + "loadingTooltip": "我們正在尋找結果...", + "betaLabel": "BETA", + "betaTooltip": "目前我們只支援搜尋頁面" + } +} \ No newline at end of file From dbbdc13d969c31c6e4a80e9f70dd2939b7b24858 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 8 May 2024 22:05:50 +0800 Subject: [PATCH 5/6] fix: database row page breadcrumbs on smaller screens (#5289) --- .../presentation/database_document_title.dart | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 47cf99b293..37905ac88b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -54,12 +54,7 @@ class ViewTitleBarWithRow extends StatelessWidget { return Visibility( visible: maxWidth < constraints.maxWidth, // if the width is too small, only show one view title bar without the ancestors - replacement: _ViewTitle( - key: ValueKey(state.ancestors.last), - view: state.ancestors.last, - maxTitleWidth: constraints.maxWidth - 50.0, - onUpdated: () {}, - ), + replacement: _buildRowName(), child: Row( // refresh the view title bar when the ancestors changed key: ValueKey(state.ancestors.hashCode), @@ -104,42 +99,39 @@ class ViewTitleBarWithRow extends StatelessWidget { } Widget _buildRowName() { - return BlocBuilder( - builder: (context, state) { - if (state.databaseController == null) { - return const SizedBox.shrink(); - } - return _RowName( - cellBuilder: EditableCellBuilder( - databaseController: state.databaseController!, - ), - primaryFieldId: state.fieldId!, - rowId: rowId, - ); - }, + return _RowName( + rowId: rowId, ); } } class _RowName extends StatelessWidget { const _RowName({ - required this.cellBuilder, - required this.primaryFieldId, required this.rowId, }); - final EditableCellBuilder cellBuilder; - final String primaryFieldId; final String rowId; @override Widget build(BuildContext context) { - return cellBuilder.buildCustom( - CellContext( - fieldId: primaryFieldId, - rowId: rowId, - ), - skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), + return BlocBuilder( + builder: (context, state) { + if (state.databaseController == null) { + return const SizedBox.shrink(); + } + + final cellBuilder = EditableCellBuilder( + databaseController: state.databaseController!, + ); + + return cellBuilder.buildCustom( + CellContext( + fieldId: state.fieldId!, + rowId: rowId, + ), + skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), + ); + }, ); } } @@ -220,12 +212,10 @@ enum _ViewTitleBehavior { class _ViewTitle extends StatefulWidget { const _ViewTitle({ - super.key, required this.view, this.behavior = _ViewTitleBehavior.editable, - this.maxTitleWidth = 180, required this.onUpdated, - }); + }) : maxTitleWidth = 180; final ViewPB view; final _ViewTitleBehavior behavior; From 6edb184bfbaf2f6b1820a5522308fdf43c3b8183 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 9 May 2024 13:32:35 +0800 Subject: [PATCH 6/6] refactor: mobile view page (#5288) * refactor: mobile view page * fix: provider not found * chore: add page style integration tests * fix: android title bar padding * fix: unable to click mentioned page on Android * fix: font family not available log --- .github/workflows/ios_ci.yaml | 17 +- .../page_style/document_page_style_test.dart | 139 +++++ .../integration_test/mobile_runner.dart | 2 + .../shared/common_operations.dart | 33 +- .../integration_test/shared/expectation.dart | 47 +- .../base/mobile_view_page_bloc.dart | 99 ++++ .../presentation/base/mobile_view_page.dart | 478 ++++++------------ .../base/view_page/app_bar_buttons.dart | 234 +++++++++ .../base/view_page/more_bottom_sheet.dart | 67 +++ .../mention/mention_page_block.dart | 69 +-- .../document/presentation/editor_style.dart | 7 +- .../workspace/application/view/view_ext.dart | 9 + 12 files changed, 818 insertions(+), 383 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index e6b6b741fd..c32a7f93c7 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -88,7 +88,16 @@ jobs: model: 'iPhone 15' shutdown_after_job: false - # enable it again if the 12 mins timeout is fixed - # - name: Run integration tests - # working-directory: frontend/appflowy_flutter - # run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} + # - name: Run AppFlowy on simulator + # working-directory: frontend/appflowy_flutter + # run: | + # flutter run -d ${{ steps.simulator-action.outputs.udid }} & + # pid=$! + # sleep 500 + # kill $pid + # continue-on-error: true + + # enable it again if the 12 mins timeout is fixed + # - name: Run integration tests + # working-directory: frontend/appflowy_flutter + # run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} diff --git a/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart b/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart new file mode 100644 index 0000000000..c915ebadfd --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart @@ -0,0 +1,139 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('document page style', () { + double getCurrentEditorFontSize() { + final editorPage = find + .byType(AppFlowyEditorPage) + .evaluate() + .single + .widget as AppFlowyEditorPage; + return editorPage.styleCustomizer + .style() + .textStyleConfiguration + .text + .fontSize!; + } + + double getCurrentEditorLineHeight() { + final editorPage = find + .byType(AppFlowyEditorPage) + .evaluate() + .single + .widget as AppFlowyEditorPage; + return editorPage.styleCustomizer + .style() + .textStyleConfiguration + .text + .height!; + } + + testWidgets('change font size in page style settings', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize); + // change font size from normal to large + await tester.tapSvgButton(FlowySvgs.m_font_size_large_s); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize); + // change font size from large to small + await tester.tapSvgButton(FlowySvgs.m_font_size_small_s); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize); + }); + + testWidgets('change line height in page style settings', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.normal.lineHeight, + ); + // change line height from normal to large + await tester.tapSvgButton(FlowySvgs.m_layout_large_s); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.large.lineHeight, + ); + // change line height from large to small + await tester.tapSvgButton(FlowySvgs.m_layout_small_s); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.small.lineHeight, + ); + }); + + testWidgets('use built-in image as cover', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + // toggle the preset button + await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m); + + // select the first preset + final firstBuiltInImage = find.byWidgetPredicate( + (widget) => + widget is Image && + widget.image is AssetImage && + (widget.image as AssetImage).assetName == + PageStyleCoverImageType.builtInImagePath('1'), + ); + await tester.tap(firstBuiltInImage); + + // click done button to exit the page style settings + await tester.tapButton(find.byType(BottomSheetDoneButton).first); + await tester.tapButton(find.byType(BottomSheetDoneButton).first); + + // check the cover + final builtInCover = find.descendant( + of: find.byType(DocumentImmersiveCover), + matching: firstBuiltInImage, + ); + expect(builtInCover, findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner.dart b/frontend/appflowy_flutter/integration_test/mobile_runner.dart index ca1a7ae0d3..9ebc2dcd97 100644 --- a/frontend/appflowy_flutter/integration_test/mobile_runner.dart +++ b/frontend/appflowy_flutter/integration_test/mobile_runner.dart @@ -1,8 +1,10 @@ import 'package:integration_test/integration_test.dart'; +import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test; import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; Future runIntegrationOnMobile() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); anonymous_sign_in_test.main(); + create_new_page_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 0a0858beec..af3cb7d44e 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,13 +1,10 @@ import 'dart:io'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/shared/feature_flags.dart'; @@ -31,6 +28,10 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -537,6 +538,30 @@ extension CommonOperations on WidgetTester { await tapButtonWithName(LocaleKeys.button_ok.tr()); } + + // For mobile platform to launch the app in anonymous mode + Future launchInAnonymousMode() async { + assert( + [TargetPlatform.android, TargetPlatform.iOS] + .contains(defaultTargetPlatform), + 'This method is only supported on mobile platforms', + ); + + await initializeAppFlowy(); + + final anonymousSignInButton = find.byType(SignInAnonymousButtonV2); + expect(anonymousSignInButton, findsOneWidget); + await tapButton(anonymousSignInButton); + + await pumpUntilFound(find.byType(MobileHomeScreen)); + } + + Future tapSvgButton(FlowySvgData svg) async { + final button = find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg.path == svg.path, + ); + await tapButton(button); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index 5f831f3d28..aeb3f04cb8 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; @@ -12,8 +11,10 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; @@ -183,25 +184,35 @@ extension Expectation on WidgetTester { String? parentName, ViewLayoutPB parentLayout = ViewLayoutPB.Document, }) { - if (parentName == null) { - return find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.name == name && - widget.view.layout == layout, - skipOffstage: false, + if (PlatformExtension.isDesktop) { + if (parentName == null) { + return find.byWidgetPredicate( + (widget) => + widget is SingleInnerViewItem && + widget.view.name == name && + widget.view.layout == layout, + skipOffstage: false, + ); + } + + return find.descendant( + of: find.byWidgetPredicate( + (widget) => + widget is InnerViewItem && + widget.view.name == parentName && + widget.view.layout == parentLayout, + skipOffstage: false, + ), + matching: findPageName(name, layout: layout), ); } - return find.descendant( - of: find.byWidgetPredicate( - (widget) => - widget is InnerViewItem && - widget.view.name == parentName && - widget.view.layout == parentLayout, - skipOffstage: false, - ), - matching: findPageName(name, layout: layout), + return find.byWidgetPredicate( + (widget) => + widget is SingleMobileInnerViewItem && + widget.view.name == name && + widget.view.layout == layout, + skipOffstage: false, ); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart new file mode 100644 index 0000000000..c9a6b8a8f0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart @@ -0,0 +1,99 @@ +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'mobile_view_page_bloc.freezed.dart'; + +class MobileViewPageBloc + extends Bloc { + MobileViewPageBloc({ + required this.viewId, + }) : _viewListener = ViewListener(viewId: viewId), + super(MobileViewPageState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _registerListeners(); + + final result = await ViewBackendService.getView(viewId); + final isImmersiveMode = + _isImmersiveMode(result.fold((s) => s, (f) => null)); + emit( + state.copyWith( + isLoading: false, + result: result, + isImmersiveMode: isImmersiveMode, + ), + ); + }, + updateImmersionMode: (isImmersiveMode) { + emit( + state.copyWith( + isImmersiveMode: isImmersiveMode, + ), + ); + }, + ); + }, + ); + } + + final String viewId; + final ViewListener _viewListener; + + @override + Future close() { + _viewListener.stop(); + return super.close(); + } + + void _registerListeners() { + _viewListener.start( + onViewUpdated: (view) { + final isImmersiveMode = _isImmersiveMode(view); + add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode)); + }, + ); + } + + // only the document page supports immersive mode (version 0.5.6) + bool _isImmersiveMode(ViewPB? view) { + if (view == null) { + return false; + } + + final cover = view.cover; + if (cover == null || cover.type == PageStyleCoverImageType.none) { + return false; + } else if (view.layout == ViewLayoutPB.Document) { + // only support immersive mode for document layout + return true; + } + + return false; + } +} + +@freezed +class MobileViewPageEvent with _$MobileViewPageEvent { + const factory MobileViewPageEvent.initial() = Initial; + const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) = + UpdateImmersionMode; +} + +@freezed +class MobileViewPageState with _$MobileViewPageState { + const factory MobileViewPageState({ + @Default(true) bool isLoading, + @Default(null) FlowyResult? result, + @Default(false) bool isImmersiveMode, + }) = _MobileViewPageState; + + factory MobileViewPageState.initial() => const MobileViewPageState(); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 35b3ece7c5..14073e6dc8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -1,14 +1,10 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart'; import 'package:appflowy/plugins/shared/sync_indicator.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; @@ -16,16 +12,12 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; class MobileViewPage extends StatefulWidget { const MobileViewPage({ @@ -47,94 +39,33 @@ class MobileViewPage extends StatefulWidget { } class _MobileViewPageState extends State { - late final Future> future; - // used to determine if the user has scrolled down and show the app bar in immersive mode ScrollNotificationObserverState? _scrollNotificationObserver; // control the app bar opacity when in immersive mode - final ValueNotifier _appBarOpacity = ValueNotifier(0.0); - - // only enable immersive mode for document layout - final ValueNotifier _isImmersiveMode = ValueNotifier(false); - ViewListener? viewListener; - - @override - void initState() { - super.initState(); - future = ViewBackendService.getView(widget.id); - } + final ValueNotifier _appBarOpacity = ValueNotifier(1.0); @override void dispose() { _appBarOpacity.dispose(); - _isImmersiveMode.dispose(); - viewListener?.stop(); _scrollNotificationObserver = null; super.dispose(); } @override Widget build(BuildContext context) { - final child = FutureBuilder( - future: future, - builder: (context, state) { - Widget body; - ViewPB? viewPB; - final actions = []; - if (state.connectionState != ConnectionState.done) { - body = const Center( - child: CircularProgressIndicator(), - ); - } else if (!state.hasData) { - body = FlowyMobileStateContainer.error( - emoji: '😔', - title: LocaleKeys.error_weAreSorry.tr(), - description: LocaleKeys.error_loadingViewError.tr(), - errorMsg: state.error.toString(), - ); - } else { - body = state.data!.fold((view) { - viewPB = view; - _updateImmersiveMode(view); - viewListener?.stop(); - viewListener = ViewListener(viewId: view.id) - ..start( - onViewUpdated: _updateImmersiveMode, - ); + return BlocProvider( + create: (_) => MobileViewPageBloc(viewId: widget.id) + ..add(const MobileViewPageEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final view = state.result?.fold((s) => s, (f) => null); + final body = _buildBody(context, state); - actions.addAll([ - if (FeatureFlag.syncDocument.isOn) ...[ - DocumentCollaborators( - width: 60, - height: 44, - fontSize: 14, - padding: const EdgeInsets.symmetric(vertical: 8), - view: view, - ), - const HSpace(16.0), - view.layout == ViewLayoutPB.Document - ? DocumentSyncIndicator(view: view) - : DatabaseSyncIndicator(view: view), - const HSpace(8.0), - ], - _buildAppBarLayoutButton(view), - _buildAppBarMoreButton(view), - ]); - final plugin = view.plugin(arguments: widget.arguments ?? const {}) - ..init(); - return plugin.widgetBuilder.buildWidget(shrinkWrap: false); - }, (error) { - return FlowyMobileStateContainer.error( - emoji: '😔', - title: LocaleKeys.error_weAreSorry.tr(), - description: LocaleKeys.error_loadingViewError.tr(), - errorMsg: error.toString(), - ); - }); - } + if (view == null) { + return _buildApp(context, null, body); + } - if (viewPB != null) { return MultiBlocProvider( providers: [ BlocProvider( @@ -143,47 +74,165 @@ class _MobileViewPageState extends State { ), BlocProvider( create: (_) => - ViewBloc(view: viewPB!)..add(const ViewEvent.initial()), + ViewBloc(view: view)..add(const ViewEvent.initial()), ), BlocProvider.value( value: getIt() ..add(const ReminderEvent.started()), ), - if (viewPB!.layout == ViewLayoutPB.Document) + if (view.layout.isDocumentView) BlocProvider( - create: (_) => DocumentPageStyleBloc(view: viewPB!) - ..add( - const DocumentPageStyleEvent.initial(), - ), + create: (_) => DocumentPageStyleBloc(view: view) + ..add(const DocumentPageStyleEvent.initial()), ), ], child: Builder( builder: (context) { final view = context.watch().state.view; - return _buildApp(view, actions, body); + return _buildApp(context, view, body); }, ), ); - } else { - return _buildApp(null, [], body); - } - }, + }, + ), ); - - return child; } - Widget _buildApp(ViewPB? view, List actions, Widget child) { - // only enable immersive mode for document layout - final isImmersive = view?.layout == ViewLayoutPB.Document; + Widget _buildApp( + BuildContext context, + ViewPB? view, + Widget child, + ) { + final isImmersiveMode = view?.layout.isDocumentView ?? false; + final title = _buildTitle(context, view); + final appBar = MobileViewPageImmersiveAppBar( + preferredSize: Size( + double.infinity, + AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight, + ), + title: title, + isImmersiveMode: isImmersiveMode, + appBarOpacity: _appBarOpacity, + actions: _buildAppBarActions(context, view), + ); + final body = isImmersiveMode + ? Builder( + builder: (context) { + _rebuildScrollNotificationObserver(context); + return child; + }, + ) + : child; + return Scaffold( + extendBodyBehindAppBar: isImmersiveMode, + appBar: appBar, + body: body, + ); + } + + Widget _buildBody(BuildContext context, MobileViewPageState state) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final result = state.result; + if (result == null) { + return FlowyMobileStateContainer.error( + emoji: '😔', + title: LocaleKeys.error_weAreSorry.tr(), + description: LocaleKeys.error_loadingViewError.tr(), + errorMsg: '', + ); + } + + return result.fold( + (view) { + final plugin = view.plugin(arguments: widget.arguments ?? const {}) + ..init(); + return plugin.widgetBuilder.buildWidget(shrinkWrap: false); + }, + (error) { + return FlowyMobileStateContainer.error( + emoji: '😔', + title: LocaleKeys.error_weAreSorry.tr(), + description: LocaleKeys.error_loadingViewError.tr(), + errorMsg: error.toString(), + ); + }, + ); + } + + // Document: + // - [ collaborators, sync_indicator, layout_button, more_button] + // Database: + // - [ sync_indicator, more_button] + List _buildAppBarActions(BuildContext context, ViewPB? view) { + if (view == null) { + return []; + } + + final isImmersiveMode = + context.read().state.isImmersiveMode; + final actions = []; + + if (FeatureFlag.syncDocument.isOn) { + // only document supports displaying collaborators. + if (view.layout.isDocumentView) { + actions.addAll([ + DocumentCollaborators( + width: 60, + height: 44, + fontSize: 14, + padding: const EdgeInsets.symmetric(vertical: 8), + view: view, + ), + const HSpace(16.0), + DocumentSyncIndicator(view: view), + const HSpace(8.0), + ]); + } else { + actions.addAll([ + DatabaseSyncIndicator(view: view), + const HSpace(8.0), + ]); + } + } + + if (view.layout.isDocumentView) { + actions.addAll([ + MobileViewPageLayoutButton( + view: view, + isImmersiveMode: isImmersiveMode, + appBarOpacity: _appBarOpacity, + ), + ]); + } + + actions.addAll([ + MobileViewPageMoreButton( + view: view, + isImmersiveMode: isImmersiveMode, + appBarOpacity: _appBarOpacity, + ), + ]); + + return actions; + } + + Widget _buildTitle(BuildContext context, ViewPB? view) { final icon = view?.icon.value; - final title = Row( + return Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null && icon.isNotEmpty) - EmojiText( - emoji: '$icon ', - fontSize: 22.0, + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 34.0), + child: EmojiText( + emoji: '$icon ', + fontSize: 22.0, + ), ), Expanded( child: FlowyText.medium( @@ -194,55 +243,6 @@ class _MobileViewPageState extends State { ), ], ); - - if (isImmersive) { - return Scaffold( - extendBodyBehindAppBar: true, - appBar: PreferredSize( - preferredSize: Size( - double.infinity, - AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight, - ), - child: ValueListenableBuilder( - valueListenable: _appBarOpacity, - builder: (_, opacity, __) => FlowyAppBar( - backgroundColor: - AppBarTheme.of(context).backgroundColor?.withOpacity(opacity), - showDivider: false, - title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title), - leading: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0), - child: AppBarButton( - padding: EdgeInsets.zero, - onTap: (context) => context.pop(), - child: _buildImmersiveAppBarIcon( - FlowySvgs.m_app_bar_back_s, - 30.0, - iconPadding: 6.0, - ), - ), - ), - actions: actions, - ), - ), - ), - body: Builder( - builder: (context) { - _rebuildScrollNotificationObserver(context); - return child; - }, - ), - ); - } - - return Scaffold( - appBar: FlowyAppBar( - title: title, - actions: actions, - ), - body: child, - ); } void _rebuildScrollNotificationObserver(BuildContext context) { @@ -251,163 +251,6 @@ class _MobileViewPageState extends State { _scrollNotificationObserver?.addListener(_onScrollNotification); } - Widget _buildAppBarLayoutButton(ViewPB view) { - // only display the layout button if the view is a document - if (view.layout != ViewLayoutPB.Document) { - return const SizedBox.shrink(); - } - - return AppBarButton( - padding: const EdgeInsets.symmetric(vertical: 2.0), - onTap: (context) { - EditorNotification.exitEditing().post(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showDoneButton: true, - showHeader: true, - title: LocaleKeys.pageStyle_title.tr(), - backgroundColor: Theme.of(context).colorScheme.background, - builder: (_) => BlocProvider.value( - value: context.read(), - child: PageStyleBottomSheet( - view: context.read().state.view, - ), - ), - ); - }, - child: _buildImmersiveAppBarIcon(FlowySvgs.m_layout_s, 30.0), - ); - } - - Widget _buildAppBarMoreButton(ViewPB view) { - return AppBarButton( - padding: const EdgeInsets.only(left: 8, right: 16), - onTap: (context) { - EditorNotification.exitEditing().post(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - backgroundColor: Theme.of(context).colorScheme.background, - builder: (_) => _buildAppBarMoreBottomSheet(context), - ); - }, - child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s, 30.0), - ); - } - - Widget _buildImmersiveAppBarIcon( - FlowySvgData icon, - double dimension, { - double iconPadding = 5.0, - }) { - assert( - dimension > 0.0 && dimension <= kToolbarHeight, - 'dimension must be greater than 0, and less than or equal to kToolbarHeight', - ); - return UnconstrainedBox( - child: SizedBox.square( - dimension: dimension, - child: ValueListenableBuilder( - valueListenable: _isImmersiveMode, - builder: (context, isImmersiveMode, child) { - return ValueListenableBuilder( - valueListenable: _appBarOpacity, - builder: (context, appBarOpacity, child) { - Color? color; - - // if there's no cover or the cover is not immersive, - // make sure the app bar is always visible - if (!isImmersiveMode) { - color = null; - } else if (appBarOpacity < 0.99) { - color = Colors.white; - } - - Widget child = Container( - margin: EdgeInsets.all(iconPadding), - child: FlowySvg( - icon, - color: color, - ), - ); - - if (isImmersiveMode && appBarOpacity <= 0.99) { - child = DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(dimension / 2.0), - color: Colors.black.withOpacity(0.2), - ), - child: child, - ); - } - - return child; - }, - ); - }, - ), - ), - ); - } - - Widget _buildAppBarMoreBottomSheet(BuildContext context) { - final view = context.read().state.view; - return ViewPageBottomSheet( - view: view, - onAction: (action) { - switch (action) { - case MobileViewBottomSheetBodyAction.duplicate: - context.pop(); - context.read().add(const ViewEvent.duplicate()); - // show toast - break; - case MobileViewBottomSheetBodyAction.share: - // unimplemented - context.pop(); - break; - case MobileViewBottomSheetBodyAction.delete: - // pop to home page - context - ..pop() - ..pop(); - context.read().add(const ViewEvent.delete()); - break; - case MobileViewBottomSheetBodyAction.addToFavorites: - case MobileViewBottomSheetBodyAction.removeFromFavorites: - context.pop(); - context.read().add(FavoriteEvent.toggle(view)); - break; - case MobileViewBottomSheetBodyAction.undo: - EditorNotification.undo().post(); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.redo: - EditorNotification.redo().post(); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.helpCenter: - // unimplemented - context.pop(); - break; - case MobileViewBottomSheetBodyAction.rename: - // no need to implement, rename is handled by the onRename callback. - throw UnimplementedError(); - } - }, - onRename: (name) { - if (name != view.name) { - context.read().add(ViewEvent.rename(name)); - } - context.pop(); - }, - ); - } - // immersive mode related // auto show or hide the app bar based on the scroll position void _onScrollNotification(ScrollNotification notification) { @@ -418,7 +261,10 @@ class _MobileViewPageState extends State { if (notification is ScrollUpdateNotification && defaultScrollNotificationPredicate(notification)) { final ScrollMetrics metrics = notification.metrics; - final height = MediaQuery.of(context).padding.top; + double height = MediaQuery.of(context).padding.top; + if (defaultTargetPlatform == TargetPlatform.android) { + height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight; + } final progress = (metrics.pixels / height).clamp(0.0, 1.0); // reduce the sensitivity of the app bar opacity change if ((progress - _appBarOpacity.value).abs() >= 0.1 || @@ -428,16 +274,4 @@ class _MobileViewPageState extends State { } } } - - void _updateImmersiveMode(ViewPB view) { - final cover = view.cover; - if (cover == null || cover.type == PageStyleCoverImageType.none) { - _isImmersiveMode.value = false; - } else if (view.layout != ViewLayoutPB.Document) { - // only support immersive mode for document layout - _isImmersiveMode.value = false; - } else { - _isImmersiveMode.value = true; - } - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart new file mode 100644 index 0000000000..72bb36ad3f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -0,0 +1,234 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class MobileViewPageImmersiveAppBar extends StatelessWidget + implements PreferredSizeWidget { + const MobileViewPageImmersiveAppBar({ + super.key, + required this.preferredSize, + required this.isImmersiveMode, + required this.appBarOpacity, + required this.title, + required this.actions, + }); + + final bool isImmersiveMode; + final ValueListenable appBarOpacity; + final Widget title; + final List actions; + + @override + final Size preferredSize; + + @override + Widget build(BuildContext context) { + if (!isImmersiveMode) { + FlowyAppBar( + title: title, + actions: actions, + ); + } + + return ValueListenableBuilder( + valueListenable: appBarOpacity, + builder: (_, opacity, __) => FlowyAppBar( + backgroundColor: + AppBarTheme.of(context).backgroundColor?.withOpacity(opacity), + showDivider: false, + title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title), + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0), + child: _buildAppBarBackButton(context), + ), + actions: actions, + ), + ); + } + + Widget _buildAppBarBackButton(BuildContext context) { + return AppBarButton( + padding: EdgeInsets.zero, + onTap: (context) => context.pop(), + child: _ImmersiveAppBarButton( + icon: FlowySvgs.m_app_bar_back_s, + dimension: 30.0, + iconPadding: 6.0, + isImmersiveMode: isImmersiveMode, + appBarOpacity: appBarOpacity, + ), + ); + } +} + +class MobileViewPageMoreButton extends StatelessWidget { + const MobileViewPageMoreButton({ + super.key, + required this.view, + required this.isImmersiveMode, + required this.appBarOpacity, + }); + + final ViewPB view; + final bool isImmersiveMode; + final ValueListenable appBarOpacity; + + @override + Widget build(BuildContext context) { + return AppBarButton( + padding: const EdgeInsets.only(left: 8, right: 16), + onTap: (context) { + EditorNotification.exitEditing().post(); + + showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value(value: context.read()), + ], + child: MobileViewPageMoreBottomSheet(view: view), + ), + ); + }, + child: _ImmersiveAppBarButton( + icon: FlowySvgs.m_app_bar_more_s, + dimension: 30.0, + iconPadding: 5.0, + isImmersiveMode: isImmersiveMode, + appBarOpacity: appBarOpacity, + ), + ); + } +} + +class MobileViewPageLayoutButton extends StatelessWidget { + const MobileViewPageLayoutButton({ + super.key, + required this.view, + required this.isImmersiveMode, + required this.appBarOpacity, + }); + + final ViewPB view; + final bool isImmersiveMode; + final ValueListenable appBarOpacity; + + @override + Widget build(BuildContext context) { + // only display the layout button if the view is a document + if (view.layout != ViewLayoutPB.Document) { + return const SizedBox.shrink(); + } + + return AppBarButton( + padding: const EdgeInsets.symmetric(vertical: 2.0), + onTap: (context) { + EditorNotification.exitEditing().post(); + + showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + showDoneButton: true, + showHeader: true, + title: LocaleKeys.pageStyle_title.tr(), + backgroundColor: Theme.of(context).colorScheme.background, + builder: (_) => BlocProvider.value( + value: context.read(), + child: PageStyleBottomSheet( + view: context.read().state.view, + ), + ), + ); + }, + child: _ImmersiveAppBarButton( + icon: FlowySvgs.m_layout_s, + dimension: 30.0, + iconPadding: 5.0, + isImmersiveMode: isImmersiveMode, + appBarOpacity: appBarOpacity, + ), + ); + } +} + +class _ImmersiveAppBarButton extends StatelessWidget { + const _ImmersiveAppBarButton({ + required this.icon, + required this.dimension, + required this.iconPadding, + required this.isImmersiveMode, + required this.appBarOpacity, + }); + + final FlowySvgData icon; + final double dimension; + final double iconPadding; + final bool isImmersiveMode; + final ValueListenable appBarOpacity; + + @override + Widget build(BuildContext context) { + assert( + dimension > 0.0 && dimension <= kToolbarHeight, + 'dimension must be greater than 0, and less than or equal to kToolbarHeight', + ); + + // if the immersive mode is on, the icon should be white and add a black background + // also, the icon opacity will change based on the app bar opacity + return UnconstrainedBox( + child: SizedBox.square( + dimension: dimension, + child: ValueListenableBuilder( + valueListenable: appBarOpacity, + builder: (context, appBarOpacity, child) { + Color? color; + + // if there's no cover or the cover is not immersive, + // make sure the app bar is always visible + if (!isImmersiveMode) { + color = null; + } else if (appBarOpacity < 0.99) { + color = Colors.white; + } + + Widget child = Container( + margin: EdgeInsets.all(iconPadding), + child: FlowySvg(icon, color: color), + ); + + if (isImmersiveMode && appBarOpacity <= 0.99) { + child = DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(dimension / 2.0), + color: Colors.black.withOpacity(0.2), + ), + child: child, + ); + } + + return child; + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart new file mode 100644 index 0000000000..b94242eceb --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class MobileViewPageMoreBottomSheet extends StatelessWidget { + const MobileViewPageMoreBottomSheet({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return ViewPageBottomSheet( + view: view, + onAction: (action) { + switch (action) { + case MobileViewBottomSheetBodyAction.duplicate: + context.pop(); + context.read().add(const ViewEvent.duplicate()); + // show toast + break; + case MobileViewBottomSheetBodyAction.share: + // unimplemented + context.pop(); + break; + case MobileViewBottomSheetBodyAction.delete: + // pop to home page + context + ..pop() + ..pop(); + context.read().add(const ViewEvent.delete()); + break; + case MobileViewBottomSheetBodyAction.addToFavorites: + case MobileViewBottomSheetBodyAction.removeFromFavorites: + context.pop(); + context.read().add(FavoriteEvent.toggle(view)); + break; + case MobileViewBottomSheetBodyAction.undo: + EditorNotification.undo().post(); + context.pop(); + break; + case MobileViewBottomSheetBodyAction.redo: + EditorNotification.redo().post(); + context.pop(); + break; + case MobileViewBottomSheetBodyAction.helpCenter: + // unimplemented + context.pop(); + break; + case MobileViewBottomSheetBodyAction.rename: + // no need to implement, rename is handled by the onRename callback. + throw UnimplementedError(); + } + }, + onRename: (name) { + if (name != view.name) { + context.read().add(ViewEvent.rename(name)); + } + context.pop(); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index af58ce48af..6755be9690 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; @@ -25,6 +23,7 @@ import 'package:appflowy_editor/appflowy_editor.dart' import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; @@ -110,40 +109,46 @@ class _MentionPageBlockState extends State { } final iconSize = widget.textStyle?.fontSize ?? 16.0; + final child = GestureDetector( + onTap: handleTap, + onDoubleTap: handleDoubleTap, + behavior: HitTestBehavior.translucent, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(4), + view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 12, + textAlign: TextAlign.center, + lineHeight: 1.3, + ) + : FlowySvg( + view.layout.icon, + size: Size.square(iconSize + 2.0), + ), + const HSpace(2), + FlowyText( + view.name, + decoration: TextDecoration.underline, + fontSize: widget.textStyle?.fontSize, + fontWeight: widget.textStyle?.fontWeight, + ), + const HSpace(2), + ], + ), + ); + + if (PlatformExtension.isMobile) { + return child; + } + return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: FlowyHover( cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: handleTap, - onDoubleTap: handleDoubleTap, - behavior: HitTestBehavior.translucent, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(4), - view.icon.value.isNotEmpty - ? EmojiText( - emoji: view.icon.value, - fontSize: 12, - textAlign: TextAlign.center, - lineHeight: 1.3, - ) - : FlowySvg( - view.layout.icon, - size: Size.square(iconSize + 2.0), - ), - const HSpace(2), - FlowyText( - view.name, - decoration: TextDecoration.underline, - fontSize: widget.textStyle?.fontSize, - fontWeight: widget.textStyle?.fontWeight, - ), - const HSpace(2), - ], - ), - ), + child: child, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index cdf2dcfffd..4aba56c15a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -95,6 +95,8 @@ class EditorStyleCustomizer { final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; final defaultTextDirection = context.read().state.defaultTextDirection; + final textScaleFactor = + context.read().state.textScaleFactor; final baseTextStyle = this.baseTextStyle(fontFamily); final codeFontSize = max(0.0, fontSize - 2); return EditorStyle.mobile( @@ -131,8 +133,7 @@ class EditorStyleCustomizer { textSpanDecorator: customizeAttributeDecorator, mobileDragHandleBallSize: const Size.square(12.0), magnifierSize: const Size(144, 96), - textScaleFactor: - context.watch().state.textScaleFactor, + textScaleFactor: textScaleFactor, ); } @@ -213,7 +214,7 @@ class EditorStyleCustomizer { ); TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { - if (fontFamily == null) { + if (fontFamily == null || fontFamily == defaultFontFamily) { return TextStyle(fontWeight: fontWeight); } try { diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 49ba3cc4c2..ca3bb62fbb 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -151,6 +151,15 @@ extension ViewLayoutExtension on ViewLayoutPB { _ => throw Exception('Unknown layout type'), }; + bool get isDocumentView => switch (this) { + ViewLayoutPB.Document => true, + ViewLayoutPB.Grid || + ViewLayoutPB.Board || + ViewLayoutPB.Calendar => + false, + _ => throw Exception('Unknown layout type'), + }; + bool get isDatabaseView => switch (this) { ViewLayoutPB.Grid || ViewLayoutPB.Board ||