diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index ffb81a85..e24bd536 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -191,6 +191,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -202,7 +203,8 @@ jobs: RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ @@ -224,6 +226,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -235,7 +238,8 @@ jobs: RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ @@ -265,6 +269,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Install Ninja run: choco install ninja -y @@ -278,7 +283,8 @@ jobs: run: | ## Note: we can't use the profile here because cargo-makepad doesn't support custom profiles. # cargo makepad android build -p rinx --profile fast - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d153945..8b0df3cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,7 @@ jobs: toolchain: 1.98.0 - uses: Swatinem/rust-cache@v2 - run: brew install cmake + - run: python3 tools/test_package_octos.py - run: cargo test --locked --features agent_chat --lib - run: cargo build --locked --features agent_chat - run: cargo check --locked --no-default-features --features agent_chat diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30df53a6..8138c3ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -565,6 +565,18 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable + - name: Setup pinned cargo-makepad + uses: ./.github/actions/setup-cargo-makepad + + - name: Package the standalone assistant runtime + env: + AWS_LC_SYS_CMAKE_BUILDER: 1 + run: | + cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android + python3 tools/package-octos.py android + echo "MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=$PWD/dist/runtime/octos-aarch64-linux-android" >> "$GITHUB_ENV" + - name: Package (android) uses: project-robius/makepad-packaging-action@v1.8.0 env: diff --git a/Cargo.lock b/Cargo.lock index 23a08998..ffb11747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7908,11 +7908,11 @@ dependencies = [ [[package]] name = "octosense-app-peers" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=b803dba9750eee55fe7657dc0205026e11931005#b803dba9750eee55fe7657dc0205026e11931005" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" dependencies = [ "futures-util", "log", - "octosense-octos-core", + "octosense-kernel", "serde_json", "tokio", "tokio-tungstenite 0.24.0", @@ -7931,10 +7931,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "octosense-kernel" +version = "0.1.0" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" +dependencies = [ + "log", + "octos-cli", + "octosense-llm-config", + "serde_json", + "tokio", + "tracing-subscriber", +] + [[package]] name = "octosense-llm-config" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=b803dba9750eee55fe7657dc0205026e11931005#b803dba9750eee55fe7657dc0205026e11931005" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" dependencies = [ "argon2", "chacha20poly1305 0.10.1", @@ -7947,19 +7960,6 @@ dependencies = [ "url", ] -[[package]] -name = "octosense-octos-core" -version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=b803dba9750eee55fe7657dc0205026e11931005#b803dba9750eee55fe7657dc0205026e11931005" -dependencies = [ - "log", - "octos-cli", - "octosense-llm-config", - "serde_json", - "tokio", - "tracing-subscriber", -] - [[package]] name = "ohos-sys" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 3e4206e2..9512508a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ uuid = { version = "1", features = ["v4"] } ## The assistant (ADR 0007): the scoped service contract a host injects. The ## local-runtime and remote connectors are optional (`octos-local`, ## `octos-remote`), so a hosted build links no runtime of its own. -octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "b803dba9750eee55fe7657dc0205026e11931005" } +octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "35d9d1218e1b8b165977a4f019fdea9f2675dc90" } octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "68f6a9df55692b5d8ef8873a12721e279a3f40d6" } octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } octoscript-widgets = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } @@ -328,6 +328,8 @@ icons = [ "./resources/icon.ico", ] out_dir = "./dist" +# One pinned local assistant runtime; installed beside Rinx by the packager. +external-binaries = ["./dist/runtime/octos"] ## Here, we define the list of resource directories for both Makepad and Rinx. ## The `src` directory is based on `./dist/` as the `out_dir` (above), which is where the @@ -345,6 +347,7 @@ resources = [ { src = "./LICENSE", target = "licenses/LICENSE" }, { src = "./LICENSE-MIT", target = "licenses/LICENSE-MIT" }, { src = "./NOTICE", target = "licenses/NOTICE" }, + { src = "./dist/runtime/licenses/octos-LICENSE", target = "licenses/octos-LICENSE" }, { src = "./dist/resources/rinx", target = "rinx" }, ] @@ -358,7 +361,8 @@ robius-packaging-commands before-each-package \ --force-makepad \ --binary-name rinx \ --path-to-binary ./target/release/rinx \ - --add-deb-dep geoclue-2.0 + --add-deb-dep geoclue-2.0 && +python3 tools/package-octos.py desktop """ ## This is not yet implemented, so remove it. diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index cfffd1bd..49f28b17 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -499,7 +499,7 @@ fixtures. No provider key or Matrix credential is used anywhere. | 5. Workspace, transcript, memory and tool isolation, incl. automatic injection and simultaneous mini-app requests | octos `should_isolate_app_peer_workspace_and_memory_from_the_system_and_each_other`, `should_open_isolated_request_contexts_and_refuse_them_after_close`, `should_refuse_a_binding_that_shares_state_with_another_app_peer`, `should_never_extract_an_app_bound_session_into_the_profile_memory` | Met (kernel tests) | | 6. Model selection affects only the intended peer, keeps credentials with the host, reports fallback, leaves the default | octos `should_select_a_configured_model_for_one_peer_without_touching_the_profile_default`; a request context runs on its peer's lane | Met (kernel tests) | | 7. Standalone local and remote work without OctoSense; hosted cannot spawn a kernel; closing leaves others usable | Rinx `tests/standalone_local.rs` and `tests/standalone_remote.rs`. The shell tests `rinx_is_hosted_with_the_shells_service_and_starts_no_kernel`. Shell CI keeps Rinx's `standalone` / `octos-local` / `octos-remote` out of every graph. Native Desktop run: no kernel process started by launching Rinx | Met (process level + native launch) | -| 8. macOS desktop and OnePlus 6 ROM Home native flows | macOS Desktop: hosted launch only (hidden window, remote bridge). OnePlus 6: nothing | **Partially verified** on macOS; **ROM Home on device is unverified**, because the phone is in use by someone else and was not touched | +| 8. macOS desktop and OnePlus 6 ROM Home native flows | macOS: native standalone discovery and earlier hosted launch/close/reopen. OnePlus 6: standalone and separate hosted validation APKs launch; matrix.org discovery, single-event Back/edge swipe, IME dismissal, background/resume and restart verified (details below) | **Partially verified**: signed-in mini-app/approval flows and privileged production ROM integration remain unverified | Deployment completion criteria: @@ -508,13 +508,17 @@ Deployment completion criteria: | Standalone local | `standalone_rinx_owns_one_local_runtime_and_stops_only_it`: no kernel before the first request, then the packaged kernel (explicit path, never PATH) under Rinx's data root. Two mini apps share it with separate contexts. Exit stops Rinx's kernel and leaves another owner's running. A restart resumes the same peer | Met at process level | | Standalone remote | `standalone_rinx_uses_an_explicit_authenticated_remote_server` against `octos serve --auth-token `: a wrong token is refused, a turn runs in a server-provisioned workspace, the server token is never written to disk, an account change drops the late reply, and exit leaves the server running | Met at process level | | Hosted desktop | Launch before AppCard: native (Desktop#50 build, hidden window). System peer, question/answer and second-app survival: real-kernel broker tests. A signed-in Rinx mini app exchanging with the system agent natively: not run (needs a Matrix account) | Partially verified | -| Hosted ROM Home | ROM#34 builds and its tests pass on macOS and in CI; no APK was installed | Unverified (no device access) | +| Hosted ROM Home | Consolidated OctoSense test APK installed separately on OnePlus 6: native launch, Back, edge swipe, background/resume and restart. No Rinx-owned kernel starts before authorization | Partially verified; signed-in mini-app flows and the privileged installed Home/Bridge are not covered | Known limits and follow-ups: -- In one scripted run the kernel accepted a second `peer_send_input` to a peer - that had already answered once, but did not run it within two minutes. The - first exchange is covered. This is a kernel follow-up. +- The initial repeated-input check passed against `552767dd` after the + scripted model used unique tool-call IDs (System Apps #18). That did not + cover providers that reuse an ID in a later turn. The consolidated + OctoSense fixture deliberately reuses `call_1`; kernel `a6ea8505` handles + that case. Both the question/answer exchange and the next system-to-peer + input now pass on the real Android kernel. The earlier unique-ID result + alone was insufficient to claim repeated-input compatibility. - Rinx's `ServiceExecutor` now serves the first action set: `status`, `list_rooms`, `open_room`, `draft_message`, `read_room` (grant-gated), `open_mini_app` (the reviewed app only; Run still grants it) and @@ -528,3 +532,138 @@ Known limits and follow-ups: request. - Per-app token/tool budgets and fair scheduling beyond the kernel's existing limits are not part of this change. + +### Deployment validation follow-up (2026-09-26) + +The initial validation used System Apps `cc1cee3` and kernel `552767dd`, +matching the shell contracts before the repository consolidation. [Runtime packaging](../../packaging/README-octos.md) +builds the pinned executable for desktop and Android and includes its license. +Hosted builds continue to use only the injected service. + +Rechecked in isolated worktrees and disposable data directories: + +- All 20 app-peers tests pass, including five tests using the real pinned + kernel and a scripted model. The repeated-input regression is in + [System Apps #18](https://github.com/OctoSense-org/OctoSense-System-Apps/pull/18). +- Standalone local and authenticated remote process tests pass with the + aligned dependency. The local test also passes from a private bundle with + `RINX_OCTOS_BIN` removed, finding the adjacent packaged executable. +- Six packaging checks pass, including seven-character Git revisions from + shallow source builds. cargo-packager 0.10.1 produces a macOS bundle + with `Contents/MacOS/octos` and its license. This inclusion check reuses the + existing fast-build Rinx binary; it is not a notarized distribution test. +- Native Makepad instrumentation verifies the standalone development bundle's + login screen and hosted Rinx's launch, close and reopen in the rebuilt + Desktop #41 shell. Both native test processes exited after capture. The + hosted launch starts no child kernel before an authorized request. +- The pinned Android kernel cross-builds as an aarch64 ELF PIE with 16 KB + load-segment alignment and only Android system-library dependencies. +- Standalone Android builds as the separate `dev.makepad.rinx.validation` + package. APK v2/v3 signature verification passes and the bundled + `lib/arm64-v8a/liboctos.so` matches the staged kernel's SHA-256 exactly. + This development APK is installed under its separate package on OnePlus 6. + +Signed-in native mini-app AI/approval flows still require a disposable Matrix +login. No normal Rinx account or AI credentials are copied into the test apps. + +The merged ROM Home source also produced development-signed Home and Bridge +APKs using the ROM-patched cargo-makepad. The Home APK contains the exact pinned +kernel. Neither APK was installed; the installed Home remains untouched. + +#### OnePlus 6 follow-up (2026-09-27) + +The phone runs Android 15. The packaged kernel starts under the separate test +app's UID and reports `552767d`, the abbreviated pinned revision. Sixteen +checks pass against that Android executable over stdio with a scripted model: +two app peers and three isolated request contexts, streamed/persisted replies, +history isolation, host-token rejection, repeated turns, the system agent's +input/question/answer exchange and second input, and context closure while +another context remains usable. This exercises the kernel protocol, not a +signed-in native mini-app flow. + +The native standalone UI exposed a reqwest 0.13 Android verifier panic during +the browser-registration metadata request. Auxiliary HTTP clients now use the +same bundled trust roots as the Matrix client, retaining certificate and +hostname verification. This also covers article image downloads and the +optional agent transport. Nine homeserver tests pass. The rebuilt test APK +successfully discovers matrix.org and renders its sign-in choices on the phone. + +A separate `dev.makepad.octosense.rinxvalidation` APK launches Rinx in-process +and displays its sign-in screen. Its validation manifest omits the Home intent; +it does not replace the installed Home or Bridge. The unrelated privileged +bridge correctly rejects its test identity. Native Back testing exposed the +shell's Activity fallback closing the host before its Rust navigation handles +Back. Subsequent predictive-Back testing also caught duplicate compatibility +key events. The corrected callback and key routing are verified below. +Signed-in native AI/approval flows and production ROM integration are not yet +claimed complete. + +#### Consolidated runtime validation (2026-09-27) + +Rinx `e6443ae1` follows main’s app-peers pin `OctoSense@f38aa250`, Makepad +`6cf03859` and kernel `a6ea8505`. Runtime packaging follows that kernel pin. +The preceding results identify the earlier builds; these checks use the +updated graph: + +- Six packaging checks and nine homeserver tests pass. Both Rinx CI jobs pass. +- On macOS, Makepad instrumentation verifies native startup and public + matrix.org discovery. Real-kernel process tests pass for packaged local + discovery (without `RINX_OCTOS_BIN`), per-context isolation, restart and + owner-only shutdown, and for the authenticated remote deployment. +- OnePlus 6 standalone validation package version `2026092708` starts and + discovers matrix.org without the verifier panic. Its packaged kernel's + hash matches the source-build receipt and reports `a6ea850` on the phone. +- All 16 Android kernel checks pass using the consolidated scripted-model + fixture, including repeated tool-call IDs across turns. These are protocol + tests under the test app's UID, separate from native signed-in UI tests. +- The consolidated OctoSense dependency check finds one Rinx, one Makepad + runtime and one shared Octos service; the hosted graph contains no Rinx + standalone kernel features. +- The hosted validation APK (`2026092710`, OctoSense `33eaf68`, Rinx + `e6443ae1`) includes the same verified Android kernel. It fixes the renamed + phone crate's theme asset path and owns system Back through Android's + dispatcher. The Makepad overlay suppresses the compatibility Back key + while that callback is registered, preventing two navigations per press. + The clean pinned runtime plus both patches reproduces the locked tree. +- On OnePlus 6, the Back key and an edge swipe each produce exactly one + navigation event and return Rinx to the launcher without closing the host. + IME Back dismisses the keyboard before app navigation. Reopen and + background/resume keep the same host process. After process restart, + Rinx launches again and discovers matrix.org. No separate kernel process + exists under the host UID before an authorized request. These tests use + the separate validation identity, not the privileged Home/Bridge identity. + +The subsequent signed-in OnePlus check successfully read the Matrix profile +through the native mini-app, but exposed an account-binding error: login +invalidated the previous session before storing the new Matrix client, so +the injected assistant retained a signed-out account. Client replacement +now revokes old contexts, stores the new client, then binds the assistant. +Invalidation after login failure explicitly revokes instead of reading the +stale client. An offline regression using real SDK sessions and the broker +passes login, restore, account switch, login failure and logout for all +three deployments. Rinx's service dependency follows OctoSense `c806889` +and its renamed `octosense-kernel` crate. + +The signed-in test also found that the mini-app adapter wrapped every +completed result as `text`, hiding `session_id` and `messages` from history +callbacks. It now preserves the broker's result while retaining the reply +size limit; regression tests cover session, history, turn and error results. + +The follow-up OnePlus APK `2026092712` (OctoSense `0336018`, Rinx `11d794d6`) +includes both fixes and the merged shared shell from `c806889`. Native UI +checks pass for Matrix session restoration, profile access, Octos session +open/history, an AI reply, and a tool approval prompt whose Deny action +reaches the kernel. Closing during a delayed response returns to Discover +without a late UI update; reopening creates a new context and replies again. +System Back closes the mini-app; an edge swipe from Discover returns to the +launcher with the same host process. Process inspection finds exactly one +kernel child under that host. The standalone APK `2026092710` built from +the same Rinx source also starts and discovers matrix.org. + +AI replies use a temporary scripted provider with the real packaged Android +kernel. Its profile and its own ADB reverse tunnel are removed afterward. +No Matrix messages were sent and no room access was granted to the fixture. +These checks do not claim paid-provider inference, native cross-app agent +exchange, approval acceptance, or privileged production Home/Bridge +integration. The validation packages do not replace normal Rinx, Home or +Bridge. diff --git a/packaging/README-octos.md b/packaging/README-octos.md new file mode 100644 index 00000000..7d97cbd3 --- /dev/null +++ b/packaging/README-octos.md @@ -0,0 +1,83 @@ +# Standalone assistant runtime + +Standalone desktop and Android packages ship one Octos executable owned by +Rinx. `packaging/octos.lock.json` pins its source and features. The helper +refuses to package a revision different from `octos-cli` in `Cargo.lock`. +Update the System Apps dependency, lockfile and packaging pin together. + +OctoSense module builds do not use this packaging step. They link Rinx with +`default-features = false, features = ["octosense-module"]` and inject the +shell's scoped app-peer service. Mini apps open contexts on that peer; they +do not receive kernels of their own. + +## Desktop + +The existing cargo-packager hook builds Rinx, then runs: + +```sh +python3 tools/package-octos.py desktop +``` + +This builds the pinned kernel with `--locked --no-default-features --features +api,git,ast`, stages `dist/runtime/octos-`, its SHA-256 receipt and license, +and cargo-packager installs it beside Rinx. Runtime staging stays below +`dist/runtime/` so the release uploader cannot mistake a Windows kernel `.exe` +for a Rinx installer. macOS signs the nested kernel +before signing and notarizing the app. For cross-packaging, set +`RINX_KERNEL_TARGET` to the same target passed to cargo-packager; the Rust +target and platform linker must already be installed. + +For a development build: + +```sh +cargo build --profile fast +python3 tools/package-octos.py desktop --app-binary target/fast/rinx +cargo run --profile fast +``` + +The macOS development runner copies and signs the adjacent kernel inside +its development bundle. `--kernel ` can reuse a native executable +whose version reports the pinned revision. It is not a cross-build option. + +## Android + +Use the pinned cargo-makepad and a **full** NDK (its minimal installation +does not contain the CMake metadata needed by Rinx's native dependencies). +CMake and a working native build tool must be on `PATH`. + +```sh +cargo makepad android install-toolchain --full-ndk +rustup target add aarch64-linux-android +python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx --release --locked +``` + +With an explicitly installed SDK, pass its path both to the helper's +`--sdk` and cargo-makepad's `--sdk-path`. Otherwise the helper resolves +cargo-makepad's default SDK from the pinned Makepad checkout. + +The helper checks that the kernel is an aarch64 ELF PIE executable and +passes it as `MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=`. Android executes +it from the APK's native library directory, rather than Rinx's writable +data directory. Android release CI stages the same binary before packaging. + +These packaging steps cover desktop and Android. They do not provide a +local subprocess kernel on iOS. + +## Validation + +```sh +python3 tools/test_package_octos.py +RINX_TEST_OCTOS_BIN=/absolute/path/to/octos cargo test --locked \ + --test standalone_local --test standalone_remote -- --test-threads=1 +``` + +To verify production discovery, copy the `standalone_local` test executable +and the packaged kernel into a private directory beside one another (kernel +name `octos`, or `octos.exe` on Windows), then run the test with +`RINX_TEST_PACKAGED_OCTOS=1` as well. This clears `RINX_OCTOS_BIN` and exercises +Rinx's normal adjacent-executable lookup. The process tests currently use +Unix process inspection and are run on macOS; this is not a Windows test claim. + +Keep real-device installation and signed-in native UI evidence separate from +the process tests. A successful cross-build alone does not verify phone behavior. diff --git a/packaging/build-macos-dmg.sh b/packaging/build-macos-dmg.sh index 11c29c41..6e660e28 100755 --- a/packaging/build-macos-dmg.sh +++ b/packaging/build-macos-dmg.sh @@ -197,6 +197,7 @@ echo "==> Found unsigned DMG: $DMG_FILE" echo "==> Codesigning $APP_PATH..." xattr -cr "$APP_PATH" +codesign_with_retry "$APP_PATH/Contents/MacOS/octos" app codesign_with_retry "$APP_PATH/Contents/MacOS/rinx" app codesign_with_retry "$APP_PATH" app codesign --verify --verbose=2 "$APP_PATH" diff --git a/packaging/octos.lock.json b/packaging/octos.lock.json new file mode 100644 index 00000000..8b1cfa19 --- /dev/null +++ b/packaging/octos.lock.json @@ -0,0 +1,10 @@ +{ + "schema_version": 1, + "url": "https://github.com/octos-org/octos.git", + "revision": "a6ea8505170735f191a12bb47629a6728415d417", + "features": [ + "api", + "git", + "ast" + ] +} diff --git a/packaging/run-macos.sh b/packaging/run-macos.sh index 31a7c532..8c0030e0 100755 --- a/packaging/run-macos.sh +++ b/packaging/run-macos.sh @@ -65,6 +65,9 @@ trap 'forward_signal TERM 143' TERM trap 'forward_signal HUP 129' HUP inputs=$(/usr/bin/shasum -a 256 "$binary" "$packaging_dir/macos/Info.plist" "${BASH_SOURCE[0]}") +if [[ -f $binary_dir/octos ]]; then + inputs+=$'\n'$(/usr/bin/shasum -a 256 "$binary_dir/octos") +fi previous_inputs= if [[ -f $bundle_dir/inputs ]]; then previous_inputs=$(< "$bundle_dir/inputs"); fi if [[ $inputs != "$previous_inputs" || ! -f $bundle/Contents/MacOS/rinx ]]; then @@ -79,6 +82,10 @@ if [[ $inputs != "$previous_inputs" || ! -f $bundle/Contents/MacOS/rinx ]]; then # Copy before signing: a symlink or hard link would mutate Cargo's executable. mkdir -p "$staging/Rinx.app/Contents/MacOS" cp "$binary" "$staging/Rinx.app/Contents/MacOS/rinx" + if [[ -f $binary_dir/octos ]]; then + cp "$binary_dir/octos" "$staging/Rinx.app/Contents/MacOS/octos" + /usr/bin/codesign --force --sign - "$staging/Rinx.app/Contents/MacOS/octos" + fi cp "$packaging_dir/macos/Info.plist" "$staging/Rinx.app/Contents/Info.plist" plist="$staging/Rinx.app/Contents/Info.plist" /usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier org.octosense.rinx.development' "$plist" diff --git a/resources/i18n/en.json b/resources/i18n/en.json index a4ec3e7d..ac3f2891 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -1086,5 +1086,21 @@ "Assistant access": "Assistant access", "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
": "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
", "The assistant may not read any chat.": "The assistant may not read any chat.", - "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "Chats this account lets the assistant read. Revoke one and the assistant asks again." + "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "Chats this account lets the assistant read. Revoke one and the assistant asks again.", + "Bold": "Bold", + "Bulleted list": "Bulleted list", + "Code": "Code", + "Heading": "Heading", + "Horizontal rule": "Horizontal rule", + "Italic": "Italic", + "Numbered list": "Numbered list", + "Quote": "Quote", + "Undo": "Undo", + "Redo": "Redo", + "Move up": "Move up", + "Move down": "Move down", + "Remove block": "Remove block", + "Full width": "Full width", + "Medium width": "Medium width", + "Small width": "Small width" } diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 65d8b324..001379df 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -1086,5 +1086,21 @@ "Assistant access": "助理访问权限", "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
": "
  • 此账号允许助理读取的聊天。每一个都是你选择了“始终允许”;撤销后,助理会再次询问。
", "The assistant may not read any chat.": "助理不能读取任何聊天。", - "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "此账号允许助理读取的聊天。撤销后,助理会再次询问。" + "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "此账号允许助理读取的聊天。撤销后,助理会再次询问。", + "Bold": "粗体", + "Bulleted list": "无序列表", + "Code": "代码", + "Heading": "标题", + "Horizontal rule": "分隔线", + "Italic": "斜体", + "Numbered list": "有序列表", + "Quote": "引用", + "Undo": "撤销", + "Redo": "重做", + "Move up": "上移", + "Move down": "下移", + "Remove block": "删除内容块", + "Full width": "全宽", + "Medium width": "中等宽度", + "Small width": "小宽度" } diff --git a/resources/icons/article_bold.svg b/resources/icons/article_bold.svg new file mode 100644 index 00000000..4273df17 --- /dev/null +++ b/resources/icons/article_bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_bullet.svg b/resources/icons/article_bullet.svg new file mode 100644 index 00000000..1510c9cf --- /dev/null +++ b/resources/icons/article_bullet.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_down.svg b/resources/icons/article_down.svg new file mode 100644 index 00000000..b51ee7a5 --- /dev/null +++ b/resources/icons/article_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_heading.svg b/resources/icons/article_heading.svg new file mode 100644 index 00000000..9ca6f571 --- /dev/null +++ b/resources/icons/article_heading.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_italic.svg b/resources/icons/article_italic.svg new file mode 100644 index 00000000..c6357e8c --- /dev/null +++ b/resources/icons/article_italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_numbered.svg b/resources/icons/article_numbered.svg new file mode 100644 index 00000000..13499b3b --- /dev/null +++ b/resources/icons/article_numbered.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_palette.svg b/resources/icons/article_palette.svg new file mode 100644 index 00000000..182672bb --- /dev/null +++ b/resources/icons/article_palette.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_quote.svg b/resources/icons/article_quote.svg new file mode 100644 index 00000000..baee825a --- /dev/null +++ b/resources/icons/article_quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_redo.svg b/resources/icons/article_redo.svg new file mode 100644 index 00000000..16efa692 --- /dev/null +++ b/resources/icons/article_redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_refresh.svg b/resources/icons/article_refresh.svg new file mode 100644 index 00000000..b20296e0 --- /dev/null +++ b/resources/icons/article_refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_rule.svg b/resources/icons/article_rule.svg new file mode 100644 index 00000000..5c86ff23 --- /dev/null +++ b/resources/icons/article_rule.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_save.svg b/resources/icons/article_save.svg new file mode 100644 index 00000000..cae0ff27 --- /dev/null +++ b/resources/icons/article_save.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_spacing_comfortable.svg b/resources/icons/article_spacing_comfortable.svg new file mode 100644 index 00000000..70df7349 --- /dev/null +++ b/resources/icons/article_spacing_comfortable.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_spacing_compact.svg b/resources/icons/article_spacing_compact.svg new file mode 100644 index 00000000..1b4461bd --- /dev/null +++ b/resources/icons/article_spacing_compact.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_split.svg b/resources/icons/article_split.svg new file mode 100644 index 00000000..dc7c66d9 --- /dev/null +++ b/resources/icons/article_split.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_table.svg b/resources/icons/article_table.svg new file mode 100644 index 00000000..213e6867 --- /dev/null +++ b/resources/icons/article_table.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_type_large.svg b/resources/icons/article_type_large.svg new file mode 100644 index 00000000..fb09c7d7 --- /dev/null +++ b/resources/icons/article_type_large.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_type_small.svg b/resources/icons/article_type_small.svg new file mode 100644 index 00000000..29c2088c --- /dev/null +++ b/resources/icons/article_type_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_undo.svg b/resources/icons/article_undo.svg new file mode 100644 index 00000000..0fc4011f --- /dev/null +++ b/resources/icons/article_undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_unlink.svg b/resources/icons/article_unlink.svg new file mode 100644 index 00000000..d8c386ff --- /dev/null +++ b/resources/icons/article_unlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_up.svg b/resources/icons/article_up.svg new file mode 100644 index 00000000..8d0f5830 --- /dev/null +++ b/resources/icons/article_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_full.svg b/resources/icons/article_width_full.svg new file mode 100644 index 00000000..4678e17a --- /dev/null +++ b/resources/icons/article_width_full.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_medium.svg b/resources/icons/article_width_medium.svg new file mode 100644 index 00000000..d38bd3d7 --- /dev/null +++ b/resources/icons/article_width_medium.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_small.svg b/resources/icons/article_width_small.svg new file mode 100644 index 00000000..c9aaa1bd --- /dev/null +++ b/resources/icons/article_width_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/agent_chat/ops/backend.rs b/src/agent_chat/ops/backend.rs index b17b10f9..c7166daa 100644 --- a/src/agent_chat/ops/backend.rs +++ b/src/agent_chat/ops/backend.rs @@ -11,7 +11,7 @@ pub struct Transport(reqwest::Client); impl Transport { pub fn new() -> Result { Ok(Self( - reqwest::Client::builder() + crate::http::client_builder() .no_proxy() .redirect(reqwest::redirect::Policy::none()) .timeout(std::time::Duration::from_secs(15)) diff --git a/src/article_app/remote_images.rs b/src/article_app/remote_images.rs index 7cf494e4..5c8cd65f 100644 --- a/src/article_app/remote_images.rs +++ b/src/article_app/remote_images.rs @@ -1,7 +1,7 @@ //! Host-controlled HTTP(S) image loading. Renderers receive bounded PNG bytes only. use article_makepad::content::{Images, prepare_image}; use futures_util::{stream, StreamExt}; -use matrix_sdk::reqwest::{Client, redirect::Policy}; +use matrix_sdk::reqwest::redirect::Policy; use std::{collections::BTreeMap, sync::Arc, time::Duration}; fn allowed(url: &url::Url) -> bool { @@ -34,8 +34,8 @@ fn allowed(url: &url::Url) -> bool { } pub async fn load(urls: Vec) -> Images { - let Ok(client) = Client::builder() - .timeout(Duration::from_secs(20)) + let Ok(client) = crate::http::client_builder() + .timeout(Duration::from_secs(20)) .user_agent("Rinx article preview") .redirect(Policy::custom(|attempt| { if attempt.previous().len() >= 5 || !allowed(attempt.url()) { diff --git a/src/article_app/ui.rs b/src/article_app/ui.rs index 0ee18b46..db33b7d8 100644 --- a/src/article_app/ui.rs +++ b/src/article_app/ui.rs @@ -111,12 +111,27 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* mod.widgets.ArticleLabel = Label {width: Fill height: Fit flow: Flow.Right{wrap: true} draw_text +: {color: #x191919 text_style: theme.font_regular{font_size: 13}}} - mod.widgets.ArticleButton = RobrixNeutralIconButton {grab_key_focus: false height: 42 margin: 0 padding: Inset{left: 12 right: 12} draw_text +: {color: #x333333 text_style: theme.font_regular{font_size: 12}} draw_bg +: {color: #xffffff color_hover: #xf2f5f3 color_down: #xe7f2e9 border_radius: 6}} - mod.widgets.ArticlePrimary = mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_text +: {color: #xffffff color_hover: #xffffff color_down: #xffffff}} + mod.widgets.ArticleButton = RobrixNeutralIconButton { + grab_key_focus: false enable_long_press: true + width: 44 height: 44 margin: 0 padding: 0 spacing: 0 text: "" + align: Align{x: 0.5 y: 0.5} + icon_walk: Walk{width: 20 height: 20} + draw_icon +: {color: #x333333} + draw_bg +: {color: #xffffff color_hover: #xf2f5f3 color_down: #xe7f2e9 border_radius: 6} + } + mod.widgets.ArticlePrimary = mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_icon +: {color: #xffffff}} + mod.widgets.ArticleActions = View {width: Fill height: 44 flow: Right align: Align{x: 1.0 y: 0.5} spacing: 12} mod.widgets.ArticleInput = TextInput {width: Fill height: 44 draw_cursor +: {color: #x07c160} draw_bg +: {color: #xffffff color_hover: #xffffff color_focus: #xffffff color_empty: #xffffff border_color: #xe5e5e5 border_color_focus: #x07c160} draw_text +: {color: #x191919 color_focus: #x191919 color_hover: #x191919 text_style: theme.font_regular{font_size: 14}}} mod.widgets.ArticleHtml = Html {selectable: true width: Fill height: Fit padding: 0 font_size: 14 font_color: #x191919 draw_text.color: #x191919 text_style_normal: theme.font_regular{font_size: 14} text_style_bold: theme.font_bold{font_size: 14} text_style_italic: theme.font_italic{font_size: 14} text_style_bold_italic: theme.font_bold_italic{font_size: 14} rmath := ArticleMath {} rdiagram := ArticleDiagram {} rimage := ArticleImage {} remoji := ArticleEmoji {} rcode := ArticleCode {} rcell := ArticleCell {}} mod.widgets.ArticleRich = ArticleRichInput {width: Fill height: Fit draw_cursor +: {color: #x07c160} is_multiline: true flow: Flow.Right{wrap: true} padding: Inset{top: 4 bottom: 12 left: 0 right: 0} empty_text: #(crate::i18n::tr("Write your article…")) draw_bg +: {color: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000 color_empty: #x00000000 border_size: 0} draw_text +: {color: #x191919 color_hover: #x191919 color_focus: #x191919 text_style: theme.font_regular{font_size: 14}} draw_bold.text_style: theme.font_bold{font_size: 14} draw_italic.text_style: theme.font_italic{font_size: 14} draw_bold_italic.text_style: theme.font_bold_italic{font_size: 14}} - mod.widgets.ArticleTool = mod.widgets.ArticleButton {width: 34 height: 32 padding: 0 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x00000000 color_hover: #xf0f0f0 color_down: #xe6e6e6 border_size: 0}} + mod.widgets.ArticleIconButton = mod.widgets.ArticleButton { + width: 44 height: 44 padding: 0 spacing: 0 text: "" + align: Align{x: 0.5 y: 0.5} + enable_long_press: true + icon_walk: Walk{width: 20 height: 20} + draw_icon +: {color: #x333333} + } + mod.widgets.ArticleTool = mod.widgets.ArticleIconButton {width: 36 height: 36 draw_bg +: {color: #x00000000 color_hover: #xf0f0f0 color_down: #xe6e6e6 border_size: 0}} // A compact theme swatch for the writing view's style panel: a tiny page with the // theme's paper, ink and accent, and the theme's name below. mod.widgets.ArticleThemeSwatch = NavigationBarButton {width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 0.0} padding: 0 spacing: 6 @@ -149,10 +164,11 @@ script_mod! { } mod.widgets.ArticlePanel = #(ArticlePanel::register_widget(vm)) { ..mod.widgets.SolidView + icon_tooltip: CalloutTooltip {} width: Fill height: Fill flow: Down draw_bg.color: #xffffff padding: Inset{top: mod.widgets.SAFE_INSET_PAD_TOP + #(TOP) bottom: mod.widgets.SAFE_INSET_PAD_BOTTOM} header := SolidView {width: Fill height: 52 flow: Right align: Align{y: 0.5} padding: Inset{left: 8 right: 12} spacing: 8 draw_bg.color: #xededed - article_back := RobrixNeutralIconButton {width: 36 height: 44 margin: 0 padding: 12 spacing: 0 draw_bg +: {color: #x00000000 color_hover: #x00000000 color_down: #x00000000 border_size: 0} draw_icon +: {svg: ICON_CHEVRON_LEFT color: #x191919} icon_walk: Walk{width: 8 height: 14}} + article_back := RobrixNeutralIconButton {enable_long_press: true width: 36 height: 44 margin: 0 padding: 12 spacing: 0 draw_bg +: {color: #x00000000 color_hover: #x00000000 color_down: #x00000000 border_size: 0} draw_icon +: {svg: ICON_CHEVRON_LEFT color: #x191919} icon_walk: Walk{width: 8 height: 14}} article_heading := mod.widgets.ArticleLabel {width: Fit max_lines: 1 draw_text.text_style: theme.font_bold{font_size: 16}} header_fill := View {width: Fill height: Fill} // The writing view's article title, centred in the header as in the atlas. @@ -161,20 +177,20 @@ script_mod! { } // Writing view controls, as in the redesigned editor atlas. write_controls := View {visible: false width: Fit height: Fill flow: Right align: Align{y: 0.5} spacing: 8 - write_style := mod.widgets.ArticleButton {height: 32 draw_text +: {color: #x07a858} draw_bg +: {border_size: 1.0 border_color: #x9fdcb8}} - RoundedView {width: Fit height: 32 flow: Right padding: 2 spacing: 0 draw_bg +: {color: #xffffff border_radius: 6.0} - write_mode_source := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Edit")) i18n_text: "Edit" draw_bg +: {border_radius: 5}} - write_mode_split := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Split")) i18n_text: "Split" draw_bg +: {border_radius: 5}} - write_mode_preview := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Preview")) i18n_text: "Preview" draw_bg +: {border_radius: 5}} + write_style := mod.widgets.ArticleIconButton {draw_icon.color: #x07a858 draw_bg +: {border_size: 1.0 border_color: #x9fdcb8} draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} + RoundedView {width: Fit height: 48 flow: Right padding: 2 spacing: 0 draw_bg +: {color: #xffffff border_radius: 6.0} + write_mode_source := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_EDIT}} + write_mode_split := mod.widgets.ArticleIconButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_split.svg")}} + write_mode_preview := mod.widgets.ArticleIconButton {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } write_saved := mod.widgets.ArticleLabel {width: Fit draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 11}}} - write_publish := mod.widgets.ArticlePrimary {width: Fit height: 32 padding: Inset{left: 16 right: 16} text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + write_publish := mod.widgets.ArticleIconButton {draw_icon.color: #xffffff draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_icon +: {svg: ICON_SEND}} } - article_save := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Save")) i18n_text: "Save" visible: false} - article_done := mod.widgets.ArticleButton {visible: false text: #(crate::i18n::tr("Done")) i18n_text: "Done" draw_text.color: #x07a858} - article_close := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Close")) i18n_text: "Close"} + article_save := mod.widgets.ArticleIconButton {visible: false draw_icon +: {svg: crate_resource("self://resources/icons/article_save.svg")}} + article_done := mod.widgets.ArticleIconButton {visible: false draw_icon.color: #x07a858 draw_icon +: {svg: ICON_CHECKMARK}} + article_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} // Library: a new article, as the atlas's + in the header. - article_new := mod.widgets.ArticleTool {visible: false width: 40 height: 40 text: "+" draw_text +: {color: #x07a858 text_style: theme.font_regular{font_size: 24}}} + article_new := mod.widgets.ArticleIconButton {visible: false draw_icon.color: #x07a858 draw_icon +: {svg: ICON_ADD}} } details := View {width: Fill height: Fill flow: Down padding: 28 spacing: 24 // The text scrolls on short windows; the actions stay in view. @@ -186,7 +202,7 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Write visually. Add images, choose a theme, review and publish.")) i18n_text: "Write visually. Add images, choose a theme, review and publish."} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Sharing this app shares neither your drafts nor your account permissions.")) i18n_text: "Sharing this app shares neither your drafts nor your account permissions." draw_text.color: #x777777} } - article_continue := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue with Rinx")) i18n_text: "Continue with Rinx"} + article_continue := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHEVRON_RIGHT}} } consent := View {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 24 // The text scrolls on short windows; the actions stay in view. @@ -196,15 +212,17 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("• Save your drafts and selected images on this device\n\n• Upload images and publish only after confirmation\n\n• Update or withdraw your own articles after confirmation")) i18n_text: "• Save your drafts and selected images on this device\n\n• Upload images and publish only after confirmation\n\n• Update or withdraw your own articles after confirmation"} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your password and session token stay with Rinx. Permission expires when you close this app or after one hour.")) i18n_text: "Your password and session token stay with Rinx. Permission expires when you close this app or after one hour." draw_text.color: #x777777} } - article_allow := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Allow and open")) i18n_text: "Allow and open"} - article_cancel := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel"} + mod.widgets.ArticleActions { + article_cancel := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_CLOSE}} + article_allow := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_icon +: {svg: ICON_CHECKMARK}} + } } library := SolidView {visible: false width: Fill height: Fill flow: Down padding: 16 spacing: 12 draw_bg.color: #xffffff article_search := mod.widgets.ArticleInput {draw_bg +: {color: #xf5f5f5 color_hover: #xf5f5f5 color_empty: #xf5f5f5 border_size: 0} empty_text: #(crate::i18n::tr("Search articles")) i18n_empty_text: "Search articles"} - View {width: Fill height: 42 flow: Right spacing: 6 - drafts_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Drafts")) i18n_text: "Drafts"} - published_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Published")) i18n_text: "Published"} - withdrawn_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Withdrawn")) i18n_text: "Withdrawn"} + View {width: Fill height: 44 flow: Right spacing: 6 + drafts_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_EDIT}} + published_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_SEND}} + withdrawn_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_TRASH}} } library_empty := mod.widgets.ArticleLabel {text: #(crate::i18n::tr("No articles here yet. Create your first article.")) i18n_text: "No articles here yet. Create your first article." draw_text.color: #x777777} article_library := PortalList {width: Fill height: Fill @@ -222,39 +240,39 @@ script_mod! { Label {text: "›" draw_text +: {color: #xb0b0b0 text_style: theme.font_regular{font_size: 20}}} } } - article_import := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} text: #(crate::i18n::tr("Import Markdown or HTML file")) i18n_text: "Import Markdown or HTML file"} - article_share := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Share app")) i18n_text: "Share app"} + article_import := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} draw_icon +: {svg: ICON_IMPORT}} + article_share := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_SHARE}} } editor := View {visible: false width: Fill height: Fill flow: Right align: Align{x: 0.5} editor_sidebar := View {visible: false width: 230 height: Fill flow: Down padding: 18 spacing: 18 - sidebar_new := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("New article")) i18n_text: "New article"} - sidebar_library := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("My articles")) i18n_text: "My articles"} + sidebar_new := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_ADD}} + sidebar_library := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_FILE}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Article outline")) i18n_text: "Article outline" draw_text.text_style: theme.font_bold{font_size: 14}} outline := mod.widgets.ArticleLabel {draw_text.color: #x777777} View {width: Fill height: Fill} - sidebar_share := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Share app")) i18n_text: "Share app"} + sidebar_share := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_SHARE}} } editor_paper := SolidView {width: Fill height: Fill flow: Down padding: Inset{left: 20 right: 20 top: 16 bottom: 12} spacing: 8 draw_bg.color: #xffffff article_title := mod.widgets.ArticleInput {height: 52 empty_text: #(crate::i18n::tr("Article title")) i18n_empty_text: "Article title" draw_text.text_style: theme.font_bold{font_size: 18} draw_bg +: {border_size: 0 color: #x00000000 color_empty: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000}} article_author := mod.widgets.ArticleInput {height: 34 empty_text: #(crate::i18n::tr("Author")) i18n_empty_text: "Author" draw_bg +: {border_size: 0 color: #x00000000 color_empty: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000} draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 12}}} - format_bar := ScrollXView {width: Fill height: 42 flow: Right spacing: 4 - article_bold := mod.widgets.ArticleButton {text: "B" width: 38 draw_text.text_style: theme.font_bold{font_size: 14}} - article_italic := mod.widgets.ArticleButton {text: "I" width: 38 draw_text.text_style: theme.font_italic{font_size: 14}} - article_link := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Link")) i18n_text: "Link"} - article_h2 := mod.widgets.ArticleButton {text: "H2" width: 42} - article_quote := mod.widgets.ArticleButton {text: "❝" width: 38} - article_list := mod.widgets.ArticleButton {text: "•" width: 38} - article_undo := mod.widgets.ArticleButton {text: "↶" width: 38} - article_redo := mod.widgets.ArticleButton {text: "↷" width: 38} - block_up := mod.widgets.ArticleButton {text: "↑" width: 38} - block_down := mod.widgets.ArticleButton {text: "↓" width: 38} - block_remove := mod.widgets.ArticleButton {text: "−" width: 38} + format_bar := ScrollXView {width: Fill height: 44 flow: Right spacing: 4 + article_bold := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + article_italic := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + article_link := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_LINK}} + article_h2 := mod.widgets.ArticleButton { width: 42 draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + article_quote := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + article_list := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + article_undo := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_undo.svg")}} + article_redo := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_redo.svg")}} + block_up := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_up.svg")}} + block_down := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_down.svg")}} + block_remove := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: ICON_TRASH}} } article_blocks := PortalList {width: Fill height: Fill SourceBlock := View {width: Fill height: Fit flow: Down spacing: 6 padding: Inset{top: 8 bottom: 12} rendered := View {width: Fill height: Fit body := mod.widgets.ArticleHtml {}} source_editor := View {visible: false width: Fill height: Fit rich := mod.widgets.ArticleRich {}} - source_toggle := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Edit source")) draw_text.text_style: theme.font_regular{font_size: 11}} + source_toggle := mod.widgets.ArticleButton {height: 28 draw_icon +: {svg: ICON_VIEW_SOURCE}} } TextBlock := View {width: Fill height: Fit flow: Right spacing: 8 prefix := Label {visible: false width: 16 height: Fit padding: Inset{top: 5} draw_text +: {color: #x07a858 text_style: theme.font_regular{font_size: 14}}} @@ -263,29 +281,29 @@ script_mod! { Picture := View {width: Fill height: Fit flow: Down spacing: 6 padding: Inset{top: 8 bottom: 12} picture := Image {width: Fill height: 210 fit: ImageFit.Smallest} caption := mod.widgets.ArticleLabel {draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 11}}} - image_settings := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Image settings")) i18n_text: "Image settings"} + image_settings := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_SETTINGS}} } Rule := View {width: Fill height: 30 align: Align{y: 0.5} SolidView {width: Fill height: 1 draw_bg.color: #xdddddd}} } article_stats := mod.widgets.ArticleLabel {draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 10}}} ScrollXView {width: Fill height: 44 flow: Right spacing: 6 - article_add_text := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Text")) i18n_text: "Text"} - article_images := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Images")) i18n_text: "Images"} - article_theme := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Style")) i18n_text: "Style"} - article_cover := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Cover")) i18n_text: "Cover"} - article_source := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Source")) i18n_text: "Source"} + article_add_text := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + article_images := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_ADD_PHOTO}} + article_theme := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} + article_cover := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_ADD_PHOTO}} + article_source := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_VIEW_SOURCE}} } - article_preview := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Full preview")) i18n_text: "Full preview"} + article_preview := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } editor_inspector := View {visible: false width: 260 height: Fill flow: Down padding: 18 spacing: 20 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 16}}} - inspector_theme := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Choose theme")) i18n_text: "Choose theme"} + inspector_theme := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} inspector_html := mod.widgets.ArticleHtml {} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover preview")) i18n_text: "Cover preview"} inspector_cover := Image {width: Fill height: 120 fit: ImageFit.Biggest} - inspector_cover_button := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cover and summary")) i18n_text: "Cover and summary"} + inspector_cover_button := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} View {width: Fill height: Fill} - inspector_preview := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Full preview")) i18n_text: "Full preview"} + inspector_preview := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } } writer := View {visible: false width: Fill height: Fill flow: Overlay @@ -298,18 +316,18 @@ script_mod! { write_title_small_box := View {visible: false width: Fill height: Fit padding: Inset{left: 12 right: 12 top: 12} write_title_small := mod.widgets.ArticleInput {height: 48 empty_text: #(crate::i18n::tr("Article title")) i18n_empty_text: "Article title" draw_text.text_style: theme.font_bold{font_size: 16}} } - write_toolbar := View {width: Fill height: 40 flow: Right align: Align{y: 0.5} padding: Inset{left: 10 right: 10} spacing: 0 - wt_bold := mod.widgets.ArticleTool {text: "B" draw_text.text_style: theme.font_bold{font_size: 13}} - wt_italic := mod.widgets.ArticleTool {text: "I" draw_text.text_style: theme.font_italic{font_size: 13}} - wt_heading := mod.widgets.ArticleTool {text: "H" draw_text.text_style: theme.font_bold{font_size: 13}} - wt_quote := mod.widgets.ArticleTool {text: "❝" } - wt_bullet := mod.widgets.ArticleTool {text: "•" } - wt_numbered := mod.widgets.ArticleTool {text: "1." } - wt_link := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Link")) i18n_text: "Link"} - wt_image := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Image")) i18n_text: "Image"} - wt_code := mod.widgets.ArticleTool {text: "" width: 40} - wt_table := mod.widgets.ArticleTool {text: "⊞" } - wt_rule := mod.widgets.ArticleTool {text: "—" } + write_toolbar := View {width: Fill height: 44 flow: Right align: Align{y: 0.5} padding: Inset{left: 10 right: 10} spacing: 0 + wt_bold := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + wt_italic := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + wt_heading := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + wt_quote := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + wt_bullet := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + wt_numbered := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_numbered.svg")}} + wt_link := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_LINK}} + wt_image := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_ADD_PHOTO}} + wt_code := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_VIEW_SOURCE}} + wt_table := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_table.svg")}} + wt_rule := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_rule.svg")}} } SolidView {width: Fill height: 1 draw_bg.color: #xe5e5e5} write_source := mod.widgets.ArticleInput {height: Fill is_multiline: true flow: Flow.Right{wrap: true} padding: Inset{left: 16 right: 16 top: 12 bottom: 12} empty_text: #(crate::i18n::tr("Write Markdown here…")) i18n_empty_text: "Write Markdown here…" draw_text +: {text_style: theme.font_regular{font_size: 13 line_spacing: 1.5}} draw_bg +: {border_size: 0 color_focus: #xffffff}} @@ -317,14 +335,14 @@ script_mod! { write_bottom := View {visible: false width: Fill height: Fit flow: Down padding: Inset{left: 12 right: 12 bottom: 8} spacing: 6 write_saved_small := mod.widgets.ArticleLabel {draw_text +: {color: #x999999 text_style: theme.font_regular{font_size: 11}}} RoundedView {width: Fill height: 44 flow: Right align: Align{x: 0.5 y: 0.5} padding: Inset{left: 6 right: 6} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 6.0} - wb_bold := mod.widgets.ArticleTool {width: Fill text: "B" draw_text.text_style: theme.font_bold{font_size: 14}} - wb_italic := mod.widgets.ArticleTool {width: Fill text: "I" draw_text.text_style: theme.font_italic{font_size: 14}} - wb_heading := mod.widgets.ArticleTool {width: Fill text: "H" draw_text.text_style: theme.font_bold{font_size: 14}} - wb_quote := mod.widgets.ArticleTool {width: Fill text: "❝"} - wb_bullet := mod.widgets.ArticleTool {width: Fill text: "•"} - wb_table := mod.widgets.ArticleTool {width: Fill text: "⊞"} - wb_image := mod.widgets.ArticleTool {width: Fill text: #(crate::i18n::tr("Image")) i18n_text: "Image"} - wb_link := mod.widgets.ArticleTool {width: Fill text: #(crate::i18n::tr("Link")) i18n_text: "Link"} + wb_bold := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + wb_italic := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + wb_heading := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + wb_quote := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + wb_bullet := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + wb_table := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_table.svg")}} + wb_image := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} + wb_link := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: ICON_LINK}} } } } @@ -353,9 +371,9 @@ script_mod! { } } write_themes_panel := SolidView {visible: false width: 320 height: Fill flow: Down padding: 16 spacing: 10 draw_bg.color: #xffffff - View {width: Fill height: 32 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text.text_style: theme.font_bold{font_size: 14}} - write_themes_close := mod.widgets.ArticleButton {text: "×" width: 32 height: 32} + write_themes_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } write_theme_list := PortalList {width: Fill height: Fill Swatches := View {width: Fill height: 108 flow: Right spacing: 10 padding: Inset{bottom: 12} @@ -378,15 +396,15 @@ script_mod! { } // Phone preview: opens the style sheet. write_style_fab_box := View {visible: false width: Fill height: Fill align: Align{x: 1.0 y: 1.0} padding: Inset{right: 16 bottom: 64} - write_style_fab := mod.widgets.ArticleButton {width: Fit height: 40 padding: Inset{left: 16 right: 16} draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 13}} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #x9fdcb8 border_radius: 20.0}} + write_style_fab := mod.widgets.ArticleIconButton {draw_icon.color: #x07a858 draw_bg +: {border_size: 1.0 border_color: #x9fdcb8 border_radius: 22.0} draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} } // Phone style sheet (atlas: theme-sheet). Tapping a style previews it; 应用 keeps it, × restores the previous one. write_theme_sheet := SolidView {visible: false width: Fill height: Fill flow: Down align: Align{y: 1.0} draw_bg.color: #x00000055 RoundedView {width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 8 bottom: 20} spacing: 12 draw_bg +: {color: #xffffff border_radius: 12.0} View {width: Fill height: Fit align: Align{x: 0.5} RoundedView {width: 36 height: 4 draw_bg +: {color: #xdddddd border_radius: 2.0}}} - View {width: Fill height: 30 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text.text_style: theme.font_bold{font_size: 16}} - theme_sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + theme_sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } ScrollXView {width: Fill height: 108 flow: Right spacing: 10 s0 := mod.widgets.ArticleThemeSwatch {width: 68} @@ -402,22 +420,19 @@ script_mod! { s10 := mod.widgets.ArticleThemeSwatch {width: 68} s11 := mod.widgets.ArticleThemeSwatch {width: 68} } - theme_sheet_apply := mod.widgets.ArticlePrimary {height: 46 text: #(crate::i18n::tr("Apply")) i18n_text: "Apply"} + theme_sheet_apply := mod.widgets.ArticlePrimary {height: 46 draw_icon +: {svg: ICON_CHECKMARK}} } } // Phone insert-image sheet (atlas: insert-image). Camera and clipboard are not offered: Rinx cannot capture or paste images here yet. write_image_sheet := SolidView {visible: false width: Fill height: Fill flow: Down align: Align{y: 1.0} draw_bg.color: #x00000055 RoundedView {width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 8 bottom: 24} spacing: 12 draw_bg +: {color: #xffffff border_radius: 12.0} View {width: Fill height: Fit align: Align{x: 0.5} RoundedView {width: 36 height: 4 draw_bg +: {color: #xdddddd border_radius: 2.0}}} - View {width: Fill height: 30 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Insert image")) i18n_text: "Insert image" draw_text.text_style: theme.font_bold{font_size: 16}} - image_sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + image_sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } RoundedView {width: Fill height: Fit flow: Down draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 8.0} - image_sheet_pick := NavigationBarButton {width: Fill height: 56 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 16} spacing: 12 draw_bg +: {color_hover: #xf5f5f5 color_active: #xf5f5f5} - mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Choose from device")) i18n_text: "Choose from device" draw_text.text_style: theme.font_regular{font_size: 14}} - Label {text: "›" draw_text +: {color: #xb0b0b0 text_style: theme.font_regular{font_size: 20}}} - } + image_sheet_pick := mod.widgets.ArticleIconButton {width: Fill height: 56 draw_icon +: {svg: ICON_ADD_PHOTO}} } RoundedView {width: Fill height: 56 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 8} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 8.0} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Lay out multiple images automatically")) i18n_text: "Lay out multiple images automatically" draw_text.text_style: theme.font_regular{font_size: 14}} @@ -435,13 +450,13 @@ script_mod! { // Desktop publish sheet (atlas: publish-sheet): cover, summary and chat, then 发布. write_sheet := SolidView {visible: false width: Fill height: Fill align: Align{x: 0.5 y: 0.5} draw_bg.color: #x00000055 RoundedView {width: 520 height: Fit flow: Down padding: Inset{left: 22 right: 22 top: 18 bottom: 18} spacing: 8 draw_bg +: {color: #xffffff border_radius: 8.0} - View {width: Fill height: 32 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Publish article")) i18n_text: "Publish article" draw_text.text_style: theme.font_bold{font_size: 16}} - sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } View {width: Fill height: Fit flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Cover")) i18n_text: "Cover" draw_text.text_style: theme.font_bold{font_size: 12}} - sheet_cover_change := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Change")) i18n_text: "Change" draw_text.color: #x07a858} + sheet_cover_change := mod.widgets.ArticleTool {width: 44 padding: Inset{left: 8 right: 8} draw_icon.color: #x07a858 draw_icon +: {svg: ICON_ADD_PHOTO}} } sheet_cover := Image {width: Fill height: 150 fit: ImageFit.Biggest} sheet_no_cover := mod.widgets.ArticleLabel {text: #(crate::i18n::tr("This article will be published without a cover.")) i18n_text: "This article will be published without a cover." draw_text.color: #x888888} @@ -454,8 +469,8 @@ script_mod! { } } View {width: Fill height: 44 flow: Right spacing: 12 margin: Inset{top: 6} - sheet_cancel := mod.widgets.ArticleButton {width: Fill height: 44 align: Align{x: 0.5 y: 0.5} text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel" draw_bg +: {color: #xf2f2f2}} - sheet_publish := mod.widgets.ArticlePrimary {width: Fill height: 44 text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + sheet_cancel := mod.widgets.ArticleButton {width: Fill height: 44 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #xf2f2f2} draw_icon +: {svg: ICON_CLOSE}} + sheet_publish := mod.widgets.ArticlePrimary {width: Fill height: 44 draw_icon +: {svg: ICON_SEND}} } } } @@ -466,10 +481,10 @@ script_mod! { source_report := ScrollYView {visible: false width: Fill height: 160 flow: Down source_issues := mod.widgets.ArticleLabel {draw_text +: {color: #x996020 text_style: theme.font_regular{font_size: 12}}} } - source_apply := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Apply and return to visual editing")) i18n_text: "Apply and return to visual editing"} + source_apply := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } images := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 14 - image_pick := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Choose from device")) i18n_text: "Choose from device"} + image_pick := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_ADD_PHOTO}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Selected images stay on this device until you confirm publication.")) i18n_text: "Selected images stay on this device until you confirm publication." draw_text.color: #x777777} image_library := PortalList {width: Fill height: Fill Asset := NavigationBarButton {width: Fill height: 130 flow: Right spacing: 14 padding: 10 @@ -480,22 +495,22 @@ script_mod! { } link_page := View {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 article_link_url := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("HTTPS link")) i18n_empty_text: "HTTPS link"} - link_apply := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Apply link")) i18n_text: "Apply link"} - link_remove := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Remove link")) i18n_text: "Remove link"} + link_apply := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_LINK}} + link_remove := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_unlink.svg")}} } image_settings := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 selected_image := Image {width: Fill height: 190 fit: ImageFit.Smallest} image_caption := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Caption")) i18n_empty_text: "Caption"} image_alt := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Alternative text")) i18n_empty_text: "Alternative text"} View {width: Fill height: 44 flow: Right spacing: 8 - image_full := mod.widgets.ArticleButton {width: Fill text: "100%"} - image_medium := mod.widgets.ArticleButton {width: Fill text: "75%"} - image_small := mod.widgets.ArticleButton {width: Fill text: "50%"} + image_full := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_full.svg")}} + image_medium := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_medium.svg")}} + image_small := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_small.svg")}} } - image_replace := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Replace image")) i18n_text: "Replace image"} - image_remove := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Remove image")) i18n_text: "Remove image" draw_text.color: #xe34d4d} + image_replace := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} + image_remove := mod.widgets.ArticleButton {width: Fill draw_icon.color: #xe34d4d draw_icon +: {svg: ICON_TRASH}} View {width: Fill height: Fill} - image_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + image_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } themes := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 theme_list := PortalList {width: Fill height: Fill @@ -505,35 +520,35 @@ script_mod! { } } View {width: Fill height: 44 flow: Right spacing: 8 - size_normal := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Standard type")) i18n_text: "Standard type"} - size_large := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Large type")) i18n_text: "Large type"} + size_normal := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_type_small.svg")}} + size_large := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_type_large.svg")}} } View {width: Fill height: 44 flow: Right spacing: 8 - spacing_comfortable := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Comfortable")) i18n_text: "Comfortable"} - spacing_compact := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Compact")) i18n_text: "Compact"} + spacing_comfortable := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_spacing_comfortable.svg")}} + spacing_compact := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_spacing_compact.svg")}} } - theme_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + theme_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } cover := SolidView {visible: false width: Fill height: Fill draw_bg.color: #xffffff ScrollYView {width: Fill height: Fill flow: Down padding: 16 spacing: 10 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover")) i18n_text: "Cover" draw_text.text_style: theme.font_bold{font_size: 13}} View {width: Fill height: Fit flow: Overlay align: Align{x: 1.0 y: 1.0} cover_wide := Image {width: Fill height: 190 fit: ImageFit.Biggest} - cover_pick := mod.widgets.ArticleButton {height: 32 margin: 8 text: #(crate::i18n::tr("Choose cover image")) i18n_text: "Choose cover image"} + cover_pick := mod.widgets.ArticleButton {height: 32 margin: 8 draw_icon +: {svg: ICON_ADD_PHOTO}} } mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Share card")) i18n_text: "Share card" draw_text.text_style: theme.font_bold{font_size: 13}} cover_square := Image {width: 100 height: 100 fit: ImageFit.Biggest} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Summary")) i18n_text: "Summary" draw_text.text_style: theme.font_bold{font_size: 13}} cover_summary := mod.widgets.ArticleInput {height: 85 is_multiline: true flow: Flow.Right{wrap: true} empty_text: #(crate::i18n::tr("Summary")) i18n_empty_text: "Summary"} View {width: Fill height: 12} - cover_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + cover_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} // Crop and placement adjustments, below the atlas's primary action. View {width: Fill height: 16} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover focal point · horizontal / vertical")) i18n_text: "Cover focal point · horizontal / vertical" draw_text.color: #x888888} cover_x := Slider {width: Fill height: 30 min: 0 max: 1000 step: 1} cover_y := Slider {width: Fill height: 30 min: 0 max: 1000 step: 1} cover_show := CheckBox {text: #(crate::i18n::tr("Show cover at the top of the article"))} - cover_remove := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Remove cover")) i18n_text: "Remove cover"} + cover_remove := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_TRASH}} } } preview := SolidView {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 draw_bg.color: #xffffff @@ -547,12 +562,12 @@ script_mod! { } } preview_stats := mod.widgets.ArticleLabel {draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 11}}} - css_preview_open := mod.widgets.ArticleButton {width: Fill visible: #(cfg!(feature = "html_preview")) text: #(crate::i18n::tr("HTML/CSS preview (experimental)")) i18n_text: "HTML/CSS preview (experimental)"} - preview_check := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Publication review")) i18n_text: "Publication review"} + css_preview_open := mod.widgets.ArticleButton {width: Fill visible: #(cfg!(feature = "html_preview")) draw_icon +: {svg: ICON_HTML_FILE}} + preview_check := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } css_preview := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 10 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Tap links to open them. Return to the editor to edit or select text.")) i18n_text: "Tap links to open them. Return to the editor to edit or select text."} - css_preview_refresh := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Refresh preview")) i18n_text: "Refresh preview"} + css_preview_refresh := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_refresh.svg")}} css_preview_bitmap := mod.widgets.HtmlView {} } review := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 @@ -561,14 +576,14 @@ script_mod! { review_destination := mod.widgets.ArticleLabel {draw_text.color: #x576b95} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Images are uploaded only after your final confirmation.")) i18n_text: "Images are uploaded only after your final confirmation." draw_text.color: #x777777} View {width: Fill height: 12} - review_continue := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue")) i18n_text: "Continue"} + review_continue := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHEVRON_RIGHT}} } rooms := SolidView {visible: false width: Fill height: Fill flow: Down padding: 16 spacing: 12 draw_bg.color: #xffffff article_chat_search := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Search chats")) i18n_empty_text: "Search chats"} article_rooms := PortalList {width: Fill height: Fill Chat := mod.widgets.ArticleChatRow {} } - rooms_publish := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + rooms_publish := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_SEND}} } confirm := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 confirm_cover := Image {width: Fill height: 170 fit: ImageFit.Biggest} @@ -578,20 +593,22 @@ script_mod! { publish_room := mod.widgets.ArticleLabel {draw_text +: {color: #x576b95 text_style: theme.font_bold{font_size: 15}}} confirm_details := mod.widgets.ArticleLabel {} View {width: Fill height: 12} - article_confirm := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Confirm publish")) i18n_text: "Confirm publish"} - article_change := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Back to editing")) i18n_text: "Back to editing"} + mod.widgets.ArticleActions { + article_change := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_EDIT}} + article_confirm := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_icon +: {svg: ICON_CHECKMARK}} + } } publication := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 18 publication_state := mod.widgets.ArticleLabel {draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 24}}} publication_cover := Image {width: Fill height: 170 fit: ImageFit.Biggest} publication_title := mod.widgets.ArticleLabel {draw_text.text_style: theme.font_bold{font_size: 18}} publication_info := mod.widgets.ArticleLabel {} - publication_read := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Read full article")) i18n_text: "Read full article"} - publication_edit := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Edit article")) i18n_text: "Edit article"} - publication_withdraw := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Withdraw article")) i18n_text: "Withdraw article" draw_text.color: #xe34d4d} + publication_read := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} + publication_edit := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_EDIT}} + publication_withdraw := mod.widgets.ArticleButton {width: Fill draw_icon.color: #xe34d4d draw_icon +: {svg: ICON_TRASH}} View {width: Fill height: 12} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your local draft is preserved.")) i18n_text: "Your local draft is preserved." draw_text.color: #x777777} - publication_library := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("My articles")) i18n_text: "My articles"} + publication_library := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_FILE}} } published := View {visible: false width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: 32 spacing: 14 View {width: Fill height: 90} @@ -601,8 +618,8 @@ script_mod! { mod.widgets.ArticleLabel {width: Fit text: #(crate::i18n::tr("Published")) i18n_text: "Published" draw_text.text_style: theme.font_bold{font_size: 22}} published_info := mod.widgets.ArticleLabel {width: Fit draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 13}}} View {width: Fill height: Fill} - published_edit := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue editing")) i18n_text: "Continue editing"} - published_library := mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} text: #(crate::i18n::tr("Back to article list")) i18n_text: "Back to article list" draw_bg +: {border_size: 1.0 border_color: #xdddddd}} + published_edit := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_EDIT}} + published_library := mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {border_size: 1.0 border_color: #xdddddd} draw_icon +: {svg: ICON_FILE}} } withdraw := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 24 withdraw_title := mod.widgets.ArticleLabel {draw_text.text_style: theme.font_bold{font_size: 22}} @@ -611,8 +628,10 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Withdraw this article?")) i18n_text: "Withdraw this article?" draw_text.text_style: theme.font_bold{font_size: 20}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your local draft will remain. Copies already downloaded or forwarded cannot be recalled.")) i18n_text: "Your local draft will remain. Copies already downloaded or forwarded cannot be recalled."} View {width: Fill height: 12} - withdraw_confirm := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Confirm withdrawal")) i18n_text: "Confirm withdrawal" draw_bg +: {color: #xe34d4d color_hover: #xd54444 color_down: #xc33b3b}} - withdraw_cancel := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel"} + mod.widgets.ArticleActions { + withdraw_cancel := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_CLOSE}} + withdraw_confirm := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_bg +: {color: #xe34d4d color_hover: #xd54444 color_down: #xc33b3b} draw_icon +: {svg: ICON_TRASH}} + } } article_status := mod.widgets.ArticleLabel {padding: Inset{left: 18 right: 18 top: 4 bottom: 8} draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 11}}} } @@ -627,6 +646,10 @@ pub struct ArticlePanel { source: ScriptObjectRef, #[deref] view: View, + #[live] + icon_tooltip: CalloutTooltip, + #[rust] + icon_hint: Option, #[rust] active: bool, #[rust] @@ -652,6 +675,8 @@ pub struct ArticlePanel { #[rust] viewport_width: f64, #[rust] + icon_labels: BTreeMap, + #[rust] css_request: String, #[cfg(feature = "html_preview")] #[rust] @@ -997,7 +1022,6 @@ impl ArticlePanel { self.view(cx, ids!(write_theme_sheet)).set_visible(cx, theme_sheet); self.view(cx, ids!(write_image_sheet)).set_visible(cx, self.write_image_sheet && !wide); self.view(cx, ids!(write_style_fab_box)).set_visible(cx, !wide && mode == WriteMode::Preview && !theme_sheet); - self.button(cx, ids!(write_style_fab)).set_text(cx, &format!("Aa {}", tr("Style"))); if theme_sheet { for (index, &theme) in Theme::ALL.iter().enumerate() { let swatch = self.widget(cx, &[LiveId::from_str(&format!("s{index}"))]); @@ -1017,15 +1041,15 @@ impl ArticlePanel { for (id, selected) in [(id!(write_mode_source), mode == WriteMode::Source), (id!(write_mode_split), mode == WriteMode::Split), (id!(write_mode_preview), mode == WriteMode::Preview)] { let mut button = self.button(cx, &[id]); let (bg, ink) = if selected { (color(0xe9f8ef), color(0x07a858)) } else { (color(0xffffff), color(0x555555)) }; - script_apply_eval!(cx, button, {draw_bg +: {color: #(bg)} draw_text +: {color: #(ink) color_hover: #(ink)}}); + script_apply_eval!(cx, button, {draw_bg +: {color: #(bg)}}); + if let Some(mut button) = button.borrow_mut() { button.draw_icon.color = ink; } } - let style = format!("{}:{}", tr("Style"), tr(self.doc.theme.name())); - self.button(cx, ids!(write_style)).set_text(cx, &style); let accent = color(accent); - let mut style_button = self.button(cx, ids!(write_style)); - script_apply_eval!(cx, style_button, {draw_text +: {color: #(accent) color_hover: #(accent)}}); - let mut fab = self.button(cx, ids!(write_style_fab)); - script_apply_eval!(cx, fab, {draw_text +: {color: #(accent) color_hover: #(accent) color_down: #(accent)}}); + for id in [id!(write_style), id!(write_style_fab)] { + if let Some(mut button) = self.button(cx, &[id]).borrow_mut() { + button.draw_icon.color = accent; + } + } let saved = tr(if self.dirty { "Editing…" } else if self.write_error.is_some() { "Saved as source only" } else { "Saved" }); let (stats_ink, stats) = match &self.write_error { Some(e) => (color(0xc05a00), Some(crate::i18n::format("Can't publish yet: {0}", &[("0", tr(e).to_owned())]))), @@ -1196,6 +1220,163 @@ impl ArticlePanel { self.view(cx, ids!(write_table_popup)).set_visible(cx, false); self.view.redraw(cx); } + fn set_icon_hint(&mut self, cx: &mut Cx, id: LiveId, label: &str) { + let uid = self.button(cx, &[id]).widget_uid(); + self.icon_labels.insert(uid, label.to_owned()); + } + fn hide_icon_hint(&mut self, cx: &mut Cx) { + if self.icon_hint.take().is_some() { + self.icon_tooltip.hide(cx); + } + } + fn icon_hints(&mut self, cx: &mut Cx, event: &Event) { + match event { + Event::MouseMove(_) | Event::LongPress(_) => {} + Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseLeave(_) + | Event::TouchUpdate(_) | Event::ClearHover | Event::Scroll(_) + | Event::KeyDown(_) | Event::BackPressed { .. } => { + self.hide_icon_hint(cx); + return; + } + _ => return, + } + let mut hint = None; + 'buttons: + for (id, label) in [ + (id!(article_back), "Back"), + (id!(article_continue), "Continue with Rinx"), + (id!(article_allow), "Allow and open"), + (id!(article_cancel), "Cancel"), + (id!(drafts_tab), "Drafts"), + (id!(published_tab), "Published"), + (id!(withdrawn_tab), "Withdrawn"), + (id!(article_import), "Import Markdown or HTML file"), + (id!(article_share), "Share app"), + (id!(sidebar_new), "New article"), + (id!(sidebar_library), "My articles"), + (id!(sidebar_share), "Share app"), + (id!(article_h2), "Heading"), + (id!(article_quote), "Quote"), + (id!(article_list), "Bulleted list"), + (id!(article_undo), "Undo"), + (id!(article_redo), "Redo"), + (id!(block_up), "Move up"), + (id!(block_down), "Move down"), + (id!(block_remove), "Remove block"), + (id!(source_toggle), "Edit source"), + (id!(image_settings), "Image settings"), + (id!(article_add_text), "Text"), + (id!(article_images), "Images"), + (id!(article_theme), "Style"), + (id!(article_cover), "Cover"), + (id!(article_source), "Source"), + (id!(article_preview), "Full preview"), + (id!(inspector_theme), "Choose theme"), + (id!(inspector_cover_button), "Cover and summary"), + (id!(inspector_preview), "Full preview"), + (id!(theme_sheet_apply), "Apply"), + (id!(sheet_cover_change), "Change"), + (id!(sheet_cancel), "Cancel"), + (id!(sheet_publish), "Publish"), + (id!(source_apply), "Apply and return to visual editing"), + (id!(image_pick), "Choose from device"), + (id!(link_apply), "Apply link"), + (id!(link_remove), "Remove link"), + (id!(image_full), "Full width"), + (id!(image_medium), "Medium width"), + (id!(image_small), "Small width"), + (id!(image_replace), "Replace image"), + (id!(image_remove), "Remove image"), + (id!(image_done), "Done"), + (id!(size_normal), "Standard type"), + (id!(size_large), "Large type"), + (id!(spacing_comfortable), "Comfortable"), + (id!(spacing_compact), "Compact"), + (id!(theme_done), "Done"), + (id!(cover_pick), "Choose cover image"), + (id!(cover_done), "Done"), + (id!(cover_remove), "Remove cover"), + (id!(css_preview_open), "HTML/CSS preview (experimental)"), + (id!(preview_check), "Publication review"), + (id!(css_preview_refresh), "Refresh preview"), + (id!(review_continue), "Continue"), + (id!(rooms_publish), "Publish"), + (id!(article_confirm), "Confirm publish"), + (id!(article_change), "Back to editing"), + (id!(publication_read), "Read full article"), + (id!(publication_edit), "Edit article"), + (id!(publication_withdraw), "Withdraw article"), + (id!(publication_library), "My articles"), + (id!(published_edit), "Continue editing"), + (id!(published_library), "Back to article list"), + (id!(withdraw_confirm), "Confirm withdrawal"), + (id!(withdraw_cancel), "Cancel"), + (id!(image_sheet_pick), "Choose from device"), + (id!(article_save), "Save"), + (id!(article_done), "Done"), + (id!(article_close), "Close"), + (id!(article_new), "New article"), + (id!(write_style), "Article style"), + (id!(write_mode_source), "Edit"), + (id!(write_mode_split), "Split"), + (id!(write_mode_preview), "Preview"), + (id!(write_publish), "Publish"), + (id!(write_style_fab), "Article style"), + (id!(write_themes_close), "Close"), + (id!(theme_sheet_close), "Close"), + (id!(image_sheet_close), "Close"), + (id!(sheet_close), "Close"), + (id!(article_bold), "Bold"), + (id!(article_italic), "Italic"), + (id!(article_link), "Link"), + (id!(wt_bold), "Bold"), + (id!(wt_italic), "Italic"), + (id!(wt_heading), "Heading"), + (id!(wt_quote), "Quote"), + (id!(wt_bullet), "Bulleted list"), + (id!(wt_numbered), "Numbered list"), + (id!(wt_link), "Link"), + (id!(wt_image), "Image"), + (id!(wt_code), "Code"), + (id!(wt_table), "Insert table"), + (id!(wt_rule), "Horizontal rule"), + (id!(wb_bold), "Bold"), + (id!(wb_italic), "Italic"), + (id!(wb_heading), "Heading"), + (id!(wb_quote), "Quote"), + (id!(wb_bullet), "Bulleted list"), + (id!(wb_link), "Link"), + (id!(wb_image), "Image"), + (id!(wb_table), "Insert table"), + ] { + for button in self.view.widgets(cx, &[&[id]]).iter() { + if button.borrow::