v0.15.11
This commit is contained in:
Generated
+1
-1
@@ -719,7 +719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.15.10"
|
||||
version = "0.15.11"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.15.10"
|
||||
version = "0.15.11"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+17
-17
@@ -1,14 +1,14 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
5fad199d087740ebfaa9d10ed484a409c0968947b1cd63cb9195d96eec08bd4b ./Cargo.lock
|
||||
7587c59b86b2d68649c7b8c815495d16d21626271f2795519a395c988f168e10 ./Cargo.toml
|
||||
2cce9f785964fb3c7c8ca9a7c37983f433e8d36f4eeb38feb8ce6bf0e432d837 ./Cargo.lock
|
||||
d328063759f1b237ab9c4d83f9341c2f99e61fe0f172627a5b82786f7d3b2847 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
c85d6b49b2e81e8f8b553d48fa23188c78fbfc9dca73d341984cc88741e9ad54 ./README.md
|
||||
1357e8d73dcd936f73b93a445b8efaa41b25ef40c30e3f212ba38cd24cb7bfae ./SOURCE_MANIFEST.sha256
|
||||
0688a06f2ad42c4ac6cd55de4347a4607c0dcf5c89effe23ffbc072b7ace8979 ./README.md
|
||||
f12d116d020194917075a25902769811ad0c397352501edf04c6a70df7775107 ./SOURCE_MANIFEST.sha256
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
d0fa90377cb302dfe4e1eb41e976e5ca9b67f6ba7c33bfda0611a3a8925b2ccf ./docs/API.md
|
||||
af38b4976805099c8060f279a313808bfa2bfbbdff48f2cbed627e3f9b07510d ./docs/FLOW.md
|
||||
d37cb744c1153bb8ba555575108c5a56e179afa850900a307a0d7f78559ddb69 ./docs/openapi.json
|
||||
9470febdeefedea0cd3e11a520d23ce902bc5f3e716db3e2a46766c0c689247e ./docs/API.md
|
||||
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
|
||||
516f42b4eb703d852907f1a305ef66cf0ac6871d0186b635558f9e3bae997180 ./docs/openapi.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
f73f2ef3ce1af50aaa0834c9bc4136c40b4e8d6074c899b63af4dd07db26c8fa ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
337a9267c67f4a80db4adaed866407ead703c1a062d37adcb910829d745345d3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -30,10 +30,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
969eae3da93f9ded22b2d4e77181274dd0aef1e95664cf8b2856467afee83696 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
21c6c44328caaf94ed326baa999a9f6a387286f79ae348bf30d3ff6ac3cd3f1e ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
d5fcb2ace520975ba35d7e97bdb17854b5d14d90d0434a8de70536e7cdc099cd ./ha-addon/repository/gree-controller/README.md
|
||||
0aa28495bd7951f3437f7acd17e4df18708ad66612010793d07d3229963b6159 ./ha-addon/repository/gree-controller/config.yaml
|
||||
b780c1f7a4cb91fab9731afe7bba1cbaa332a383c2f1d529cb87f88c4ade1f4b ./ha-addon/repository/gree-controller/README.md
|
||||
b87304c0aa4c9860a877c86fa6fde8b49b5e214b1b8aec5724cebeffb9d5e8bd ./ha-addon/repository/gree-controller/config.yaml
|
||||
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
f362982769d78851084392f6e4311e023e49bc7856ce197f3cc21b09dac13679 ./lang/en.json
|
||||
2a465025571229fe381888676ba144a8382dc952961cfb5b58791d2cdfa96937 ./lang/pl.json
|
||||
4c08eb68d926820016d18a4def98cde4afb8b5a872741d51dc783b219728efd9 ./lang/en.json
|
||||
3709aed65ef9ab9608691f05eccc81b2d596323eccd7c919e5986c179a524d7f ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
81f0140c2a90ae97e93a1dbe0219032b4de0174f40af62e18fa8df211a876f8c ./scripts/api_dev_test.py
|
||||
5de58548e81c7ba571a875090c59a2bdbcab42aeaaa8ccc0e0925274b4b05f9c ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -199,10 +199,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
5124a8a70061e35762cc9ee8646d7203c57dd7959a4a4ee818f4bc9a5fc22bda ./web/css/styles.css
|
||||
8275cbe14577317709baff1ea7efb2b9b2708c3aefb350f771d4bd24530d3c02 ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
80798cec4157be8beff6d69747270ee1842b386d07caddfd7c3c008f9662cd73 ./web/index.html
|
||||
c2e94a624d3d4e113cb9d98b2ba314a39e4ae6a2ad9c23ddc8d6558d1327324e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
@@ -210,7 +210,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
|
||||
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
|
||||
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
|
||||
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
|
||||
99f6399c6748be7c6eb77db64367668a7d0b1f8736c27e981c77edb70b493166 ./web/js-dynamic/flows.js
|
||||
f165bbf2148cf1e593d8866b4cdc71887da903d3eaa9a32d4c033aff944c5d01 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
**Version: 0.15.10**
|
||||
**Version: 0.15.11**
|
||||
|
||||
Version 0.15.10 expands Visual Flow keyboard authoring and portability: blocks can be created, selected, moved, copied, pasted, duplicated and deleted from the keyboard, the editor includes an in-app shortcut reference, and any Flow can be exported directly from the Flow list without opening it.
|
||||
Version 0.15.11 refines Visual Flow editing: keyboard actions use a compact in-editor status instead of intrusive success toasts, right-click opens a context menu for block/canvas/connection actions, and `Ctrl/Cmd+Z` undoes graph and setting edits. Direct Flow export from the Flow list remains available.
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
> Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`.
|
||||
@@ -271,7 +271,7 @@ Schedules select profiles or a custom target for selected ISO weekdays (`1=Monda
|
||||
|
||||
The reusable Flow preset library is stored as standalone JSON files in `presets/`. Each file contains its name, description and complete Flow graph, while the UI loads the embedded preset catalog dynamically instead of hard-coding graphs in JavaScript. The library has category tabs, search, local favorites/recent presets, a graph preview and requirement checks before applying a preset.
|
||||
|
||||
Available blocks include weekday/time/date ranges, the application Night mode, outdoor/device/zone temperature, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability checks, reusable Shared Inputs, a constant diagnostic source, AND/OR/NOT, thermostat actions, direct GREE actions and group actions. The editor supports keyboard-first block search/creation, save, multi-select, copy/cut/paste/duplicate, delete, movement and canvas zoom/fit with an in-app shortcut reference. Saved Flows can also be exported directly from the Flow list. Flow exports keep the existing format version and carry definitions of referenced Shared Inputs, so import can create missing sources or safely remap ID collisions. The GREE action uses the existing `DeviceCommand` fields (power, mode, target, fan, swing, quiet, turbo, light, air, xfan, health and sleep). Multiple branches can feed an action; the compiled runtime preserves the graph logic. The editor keeps the Interpretation / Flow cycle details collapsed until requested.
|
||||
Available blocks include weekday/time/date ranges, the application Night mode, outdoor/device/zone temperature, house/device/zone/group state, arbitrary Home Assistant state/numeric/attribute/availability checks, reusable Shared Inputs, a constant diagnostic source, AND/OR/NOT, thermostat actions, direct GREE actions and group actions. The editor supports keyboard-first block search/creation, save, multi-select, copy/cut/paste/duplicate, delete, movement, `Ctrl/Cmd+Z` undo and canvas zoom/fit with an in-app shortcut reference. Shortcut success feedback stays inside the editor, and right-click exposes context actions for blocks, the canvas and connections. Saved Flows can also be exported directly from the Flow list. Flow exports keep the existing format version and carry definitions of referenced Shared Inputs, so import can create missing sources or safely remap ID collisions. The GREE action uses the existing `DeviceCommand` fields (power, mode, target, fan, swing, quiet, turbo, light, air, xfan, health and sleep). Multiple branches can feed an action; the compiled runtime preserves the graph logic. The editor keeps the Interpretation / Flow cycle details collapsed until requested.
|
||||
|
||||
Quick preset/setpoint overrides normally hand control back at the next schedule boundary. Temporary Quick Thermostat adds explicit start/finish rules such as duration, exact time, temperature reached/stable or next schedule boundary.
|
||||
|
||||
|
||||
+16
-16
@@ -1,13 +1,13 @@
|
||||
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
5fad199d087740ebfaa9d10ed484a409c0968947b1cd63cb9195d96eec08bd4b ./Cargo.lock
|
||||
7587c59b86b2d68649c7b8c815495d16d21626271f2795519a395c988f168e10 ./Cargo.toml
|
||||
2cce9f785964fb3c7c8ca9a7c37983f433e8d36f4eeb38feb8ce6bf0e432d837 ./Cargo.lock
|
||||
d328063759f1b237ab9c4d83f9341c2f99e61fe0f172627a5b82786f7d3b2847 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
c85d6b49b2e81e8f8b553d48fa23188c78fbfc9dca73d341984cc88741e9ad54 ./README.md
|
||||
0688a06f2ad42c4ac6cd55de4347a4607c0dcf5c89effe23ffbc072b7ace8979 ./README.md
|
||||
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
|
||||
d0fa90377cb302dfe4e1eb41e976e5ca9b67f6ba7c33bfda0611a3a8925b2ccf ./docs/API.md
|
||||
af38b4976805099c8060f279a313808bfa2bfbbdff48f2cbed627e3f9b07510d ./docs/FLOW.md
|
||||
d37cb744c1153bb8ba555575108c5a56e179afa850900a307a0d7f78559ddb69 ./docs/openapi.json
|
||||
9470febdeefedea0cd3e11a520d23ce902bc5f3e716db3e2a46766c0c689247e ./docs/API.md
|
||||
43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md
|
||||
516f42b4eb703d852907f1a305ef66cf0ac6871d0186b635558f9e3bae997180 ./docs/openapi.json
|
||||
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
|
||||
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
|
||||
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
|
||||
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
|
||||
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
|
||||
f73f2ef3ce1af50aaa0834c9bc4136c40b4e8d6074c899b63af4dd07db26c8fa ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
337a9267c67f4a80db4adaed866407ead703c1a062d37adcb910829d745345d3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
|
||||
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
|
||||
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
|
||||
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
|
||||
969eae3da93f9ded22b2d4e77181274dd0aef1e95664cf8b2856467afee83696 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
21c6c44328caaf94ed326baa999a9f6a387286f79ae348bf30d3ff6ac3cd3f1e ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
d5fcb2ace520975ba35d7e97bdb17854b5d14d90d0434a8de70536e7cdc099cd ./ha-addon/repository/gree-controller/README.md
|
||||
0aa28495bd7951f3437f7acd17e4df18708ad66612010793d07d3229963b6159 ./ha-addon/repository/gree-controller/config.yaml
|
||||
b780c1f7a4cb91fab9731afe7bba1cbaa332a383c2f1d529cb87f88c4ade1f4b ./ha-addon/repository/gree-controller/README.md
|
||||
b87304c0aa4c9860a877c86fa6fde8b49b5e214b1b8aec5724cebeffb9d5e8bd ./ha-addon/repository/gree-controller/config.yaml
|
||||
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
|
||||
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
|
||||
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
|
||||
@@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
f362982769d78851084392f6e4311e023e49bc7856ce197f3cc21b09dac13679 ./lang/en.json
|
||||
2a465025571229fe381888676ba144a8382dc952961cfb5b58791d2cdfa96937 ./lang/pl.json
|
||||
4c08eb68d926820016d18a4def98cde4afb8b5a872741d51dc783b219728efd9 ./lang/en.json
|
||||
3709aed65ef9ab9608691f05eccc81b2d596323eccd7c919e5986c179a524d7f ./lang/pl.json
|
||||
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
|
||||
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
|
||||
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
|
||||
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
|
||||
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
|
||||
81f0140c2a90ae97e93a1dbe0219032b4de0174f40af62e18fa8df211a876f8c ./scripts/api_dev_test.py
|
||||
5de58548e81c7ba571a875090c59a2bdbcab42aeaaa8ccc0e0925274b4b05f9c ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -198,10 +198,10 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
5124a8a70061e35762cc9ee8646d7203c57dd7959a4a4ee818f4bc9a5fc22bda ./web/css/styles.css
|
||||
8275cbe14577317709baff1ea7efb2b9b2708c3aefb350f771d4bd24530d3c02 ./web/css/styles.css
|
||||
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
80798cec4157be8beff6d69747270ee1842b386d07caddfd7c3c008f9662cd73 ./web/index.html
|
||||
c2e94a624d3d4e113cb9d98b2ba314a39e4ae6a2ad9c23ddc8d6558d1327324e ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
|
||||
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
|
||||
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
|
||||
@@ -209,7 +209,7 @@ e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynam
|
||||
752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
|
||||
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js
|
||||
b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js
|
||||
99f6399c6748be7c6eb77db64367668a7d0b1f8736c27e981c77edb70b493166 ./web/js-dynamic/flows.js
|
||||
f165bbf2148cf1e593d8866b4cdc71887da903d3eaa9a32d4c033aff944c5d01 ./web/js-dynamic/flows.js
|
||||
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
|
||||
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
|
||||
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.15.10**.
|
||||
HTTP and WebSocket API for GREE Controller **0.15.11**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -241,7 +241,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.15.10",
|
||||
"version": "0.15.11",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -304,7 +304,7 @@ Returns the initial Web UI snapshot:
|
||||
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
|
||||
"control_plan_revision": 42,
|
||||
"system": {
|
||||
"version": "0.15.10",
|
||||
"version": "0.15.11",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ Shared Flow inputs show the Flows that reference them and link directly to those
|
||||
|
||||
Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js-dynamic/flows.js`.
|
||||
|
||||
The canvas supports drag-box multi-selection, additive Shift/Ctrl/Cmd selection, `Ctrl/Cmd+A`, Select all / Clear selection toolbar actions, and dragging the whole selected group while preserving relative positions. Selected blocks can be duplicated from the toolbar or with `Ctrl/Cmd+D`; `Ctrl/Cmd+C`, `Ctrl/Cmd+X` and `Ctrl/Cmd+V` copy/cut/paste the selected subgraph and preserve connections whose endpoints are both selected. Keyboard-first authoring also supports `A`, `Insert` or `Ctrl/Cmd+K` to open block search, `Enter` to insert the first matching block, `Ctrl/Cmd+S` to save, Delete/Backspace to remove selected blocks, Escape to clear selection/cancel a pending connection, arrow keys to move the selection (Shift for 1 px precision), `Ctrl/Cmd+0` to fit, `Ctrl/Cmd++/-` to zoom and `?` to open the built-in shortcut reference.
|
||||
The canvas supports drag-box multi-selection, additive Shift/Ctrl/Cmd selection, `Ctrl/Cmd+A`, Select all / Clear selection toolbar actions, and dragging the whole selected group while preserving relative positions. Selected blocks can be duplicated from the toolbar or with `Ctrl/Cmd+D`; `Ctrl/Cmd+C`, `Ctrl/Cmd+X` and `Ctrl/Cmd+V` copy/cut/paste the selected subgraph and preserve connections whose endpoints are both selected. Keyboard-first authoring also supports `A`, `Insert` or `Ctrl/Cmd+K` to open block search, `Enter` to insert the first matching block, `Ctrl/Cmd+S` to save, `Ctrl/Cmd+Z` to undo graph/setting edits, Delete/Backspace to remove selected blocks, Escape to clear selection/cancel a pending connection, arrow keys to move the selection (Shift for 1 px precision), `Ctrl/Cmd+0` to fit, `Ctrl/Cmd++/-` to zoom and `?` to open the built-in shortcut reference. Shortcut success messages are shown as a small status inside the Flow editor instead of global toasts. Right-click opens a context menu: blocks expose copy/cut/duplicate/delete plus paste/add, blank canvas exposes undo/add/paste/select-all/fit, and connections can be removed directly. Context-menu paste on blank canvas places the copied subgraph at the clicked position.
|
||||
|
||||
## Good next stateful blocks
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.15.10",
|
||||
"version": "0.15.11",
|
||||
"summary": "Local HTTP/WebSocket API for GREE Controller",
|
||||
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
|
||||
"license": {
|
||||
@@ -5967,7 +5967,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.15.10"
|
||||
"example": "0.15.11"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.15.10",
|
||||
"version": "0.15.11",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.15.11
|
||||
|
||||
- Routine Visual Flow edit confirmations now appear as a compact status inside the editor instead of global toasts for save/copy/cut/paste/duplicate/undo actions.
|
||||
- Adds right-click context menus: selected blocks can be copied, cut, duplicated or deleted; the canvas can add/paste/select-all/fit/undo; and connections can be removed directly.
|
||||
- Context-menu paste can place a copied subgraph at the clicked canvas position while preserving internal connections.
|
||||
- Adds `Ctrl/Cmd+Z` undo with a bounded edit history for block creation/deletion, connections, paste/duplicate/cut, block movement, inspector setting changes, template application, Flow name changes and Enabled state changes.
|
||||
- Updates the built-in shortcut reference and PL/EN labels for the new undo and context-menu workflow.
|
||||
|
||||
## 0.15.10
|
||||
|
||||
- Visual Flow adds keyboard-first authoring: `A`, `Insert` or `Ctrl/Cmd+K` opens block search, and `Enter` inserts the first matching block.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`.
|
||||
|
||||
Version 0.15.10 expands Visual Flow keyboard authoring and portability with keyboard-first block creation/editing, a built-in shortcut reference and direct Flow export from the Flow list.
|
||||
Version 0.15.11 refines Visual Flow editing with compact in-editor shortcut feedback, right-click context menus and `Ctrl/Cmd+Z` undo while retaining keyboard-first authoring and direct Flow export from the Flow list.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "GREE Controller"
|
||||
version: "0.15.10"
|
||||
version: "0.15.11"
|
||||
slug: "gree_controller"
|
||||
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
|
||||
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
|
||||
|
||||
+15
-1
@@ -1436,6 +1436,20 @@
|
||||
"flow.shortcutZoom": "Zoom in / out",
|
||||
"flow.shortcutHelp": "Show this shortcut list",
|
||||
"flow.shortcutCut": "Cut selected blocks",
|
||||
"flow.blocksCut": "Cut blocks: {count}"
|
||||
"flow.blocksCut": "Cut blocks: {count}",
|
||||
"flow.shortcutUndo": "Undo the last change",
|
||||
"flow.undoDone": "Last change undone",
|
||||
"flow.nothingToUndo": "Nothing to undo",
|
||||
"flow.contextUndo": "Undo",
|
||||
"flow.contextAddBlock": "Add block here",
|
||||
"flow.contextPaste": "Paste",
|
||||
"flow.contextPasteHere": "Paste here",
|
||||
"flow.contextCopy": "Copy selection ({count})",
|
||||
"flow.contextCut": "Cut selection ({count})",
|
||||
"flow.contextDuplicate": "Duplicate selection ({count})",
|
||||
"flow.contextDelete": "Delete selection ({count})",
|
||||
"flow.contextDeleteConnection": "Delete connection",
|
||||
"flow.rightClickKey": "Right click",
|
||||
"flow.shortcutContextMenu": "Flow context menu"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -1436,6 +1436,20 @@
|
||||
"flow.shortcutZoom": "Powiększ / pomniejsz",
|
||||
"flow.shortcutHelp": "Pokaż tę listę skrótów",
|
||||
"flow.shortcutCut": "Wytnij zaznaczone bloki",
|
||||
"flow.blocksCut": "Wycięto bloki: {count}"
|
||||
"flow.blocksCut": "Wycięto bloki: {count}",
|
||||
"flow.shortcutUndo": "Cofnij ostatnią zmianę",
|
||||
"flow.undoDone": "Cofnięto ostatnią zmianę",
|
||||
"flow.nothingToUndo": "Brak zmian do cofnięcia",
|
||||
"flow.contextUndo": "Cofnij",
|
||||
"flow.contextAddBlock": "Dodaj blok tutaj",
|
||||
"flow.contextPaste": "Wklej",
|
||||
"flow.contextPasteHere": "Wklej tutaj",
|
||||
"flow.contextCopy": "Kopiuj zaznaczenie ({count})",
|
||||
"flow.contextCut": "Wytnij zaznaczenie ({count})",
|
||||
"flow.contextDuplicate": "Duplikuj zaznaczenie ({count})",
|
||||
"flow.contextDelete": "Usuń zaznaczenie ({count})",
|
||||
"flow.contextDeleteConnection": "Usuń połączenie",
|
||||
"flow.rightClickKey": "Prawy przycisk",
|
||||
"flow.shortcutContextMenu": "Menu kontekstowe Flow"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.15.10.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.15.11.
|
||||
|
||||
Default mode is read-only and safe to run against a real controller.
|
||||
Use --settings-write to additionally round-trip all split settings resources and
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
|
||||
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.15.10"
|
||||
DEFAULT_EXPECTED_VERSION = "0.15.11"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -599,7 +599,7 @@ def run_suite(args: argparse.Namespace) -> int:
|
||||
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
|
||||
|
||||
runner.run("public health", lambda: test_health(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.15.10 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.15.11 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
|
||||
@@ -10463,3 +10463,74 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* Visual Flow editor feedback and context menu */
|
||||
.flow-editor-notice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
max-width: min(300px, 32vw);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: var(--surface-2);
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0;
|
||||
transform: translateY(2px);
|
||||
transition: opacity .14s ease, transform .14s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flow-editor-notice.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.flow-editor-notice.is-error {
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
.flow-context-menu {
|
||||
position: fixed;
|
||||
z-index: 1200;
|
||||
min-width: 190px;
|
||||
max-width: min(280px, calc(100vw - 16px));
|
||||
padding: 6px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 16px 38px rgba(0, 0, 0, .28);
|
||||
}
|
||||
.flow-context-menu[hidden] { display: none; }
|
||||
.flow-context-menu button {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 7px 10px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
font-size: .82rem;
|
||||
}
|
||||
.flow-context-menu button:hover:not(:disabled),
|
||||
.flow-context-menu button:focus-visible:not(:disabled) {
|
||||
background: var(--accent-soft);
|
||||
outline: none;
|
||||
}
|
||||
.flow-context-menu button:disabled {
|
||||
opacity: .45;
|
||||
cursor: default;
|
||||
}
|
||||
.flow-context-separator {
|
||||
height: 1px;
|
||||
margin: 5px 4px;
|
||||
background: var(--line);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.flow-editor-notice { max-width: 42vw; }
|
||||
}
|
||||
|
||||
+4
-1
@@ -1004,7 +1004,7 @@
|
||||
data-i18n="flow.clearSelection">Wyczyść zaznaczenie</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-shortcuts"
|
||||
data-i18n="flow.shortcuts">Skróty</button><span id="flowSelectionCount"
|
||||
class="badge"></span></div>
|
||||
class="badge"></span><span id="flowEditorNotice" class="flow-editor-notice" role="status" aria-live="polite"></span></div>
|
||||
<div class="flow-zoom-controls" role="group" data-i18n-aria="flow.zoomControls" aria-label="Canvas zoom">
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out" data-ui-icon="minus"></button><span id="flowZoomLabel"
|
||||
@@ -1044,6 +1044,7 @@
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
<div id="flowContextMenu" class="flow-context-menu" role="menu" hidden></div>
|
||||
|
||||
<dialog id="flowShortcutDialog" class="sheet flow-shortcut-dialog">
|
||||
<div class="dialog-head">
|
||||
@@ -1056,6 +1057,7 @@
|
||||
<div><span class="flow-shortcut-keys"><kbd>A</kbd><kbd>Insert</kbd><kbd>Ctrl/Cmd+K</kbd></span><span data-i18n="flow.shortcutAdd">Dodaj / wyszukaj blok</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Enter</kbd></span><span data-i18n="flow.shortcutAddFirst">Dodaj pierwszy wynik wyszukiwania</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+S</kbd></span><span data-i18n="flow.shortcutSave">Zapisz Flow</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+Z</kbd></span><span data-i18n="flow.shortcutUndo">Cofnij ostatnią zmianę</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+A</kbd></span><span data-i18n="flow.shortcutSelectAll">Zaznacz wszystkie bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+C</kbd></span><span data-i18n="flow.shortcutCopy">Kopiuj zaznaczone bloki</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+X</kbd></span><span data-i18n="flow.shortcutCut">Wytnij zaznaczone bloki</span></div>
|
||||
@@ -1068,6 +1070,7 @@
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd+0</kbd></span><span data-i18n="flow.shortcutFit">Dopasuj cały Flow do widoku</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>Ctrl/Cmd++ / -</kbd></span><span data-i18n="flow.shortcutZoom">Powiększ / pomniejsz</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd>?</kbd></span><span data-i18n="flow.shortcutHelp">Pokaż tę listę skrótów</span></div>
|
||||
<div><span class="flow-shortcut-keys"><kbd data-i18n="flow.rightClickKey">Prawy przycisk</kbd></span><span data-i18n="flow.shortcutContextMenu">Menu kontekstowe Flow</span></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
+227
-39
@@ -151,7 +151,7 @@ function commitFlowName({ keepEditingOnEmpty = true } = {}) {
|
||||
if (keepEditingOnEmpty) { app.flowNameEditing = true; input.focus(); }
|
||||
return false;
|
||||
}
|
||||
if (app.flowDraft.name !== name) { app.flowDraft.name = name; app.flowDirty = true; }
|
||||
if (app.flowDraft.name !== name) { app.flowDraft.name = name; app.flowDirty = true; flowHistoryCommit(); }
|
||||
app.flowNameEditing = false; renderFlowNameMode();
|
||||
return true;
|
||||
}
|
||||
@@ -169,6 +169,7 @@ function openFlowEditor(id = '', { push = true } = {}) {
|
||||
$('#flowNaturalPreview')?.classList.remove('is-expanded');
|
||||
$('#flowNaturalPreview .flow-preview-toggle')?.setAttribute('aria-expanded', 'false');
|
||||
renderFlowEditor();
|
||||
flowHistoryReset();
|
||||
requestAnimationFrame(() => { if (window.matchMedia('(max-width: 760px)').matches && app.flowDraft?.nodes?.length) fitFlowToView({ maxZoom: .9 }); });
|
||||
startFlowSharedInputValueRefresh();
|
||||
if (push) updateBrowserUrl(`/flows/${flow?.id || 'new'}`);
|
||||
@@ -181,6 +182,7 @@ function closeFlowEditor({ push = true, force = false } = {}) {
|
||||
const blockDialog = $('#flowBlockDialog'); if (blockDialog?.open) blockDialog.close();
|
||||
$('#flowEditor').hidden = true; document.body.classList.remove('flow-editor-open');
|
||||
app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowNameEditing = false;
|
||||
flowHistoryClear(); hideFlowContextMenu(); clearFlowEditorNotice();
|
||||
if (push) updateBrowserUrl('/flows');
|
||||
return true;
|
||||
}
|
||||
@@ -389,6 +391,117 @@ function renderFlowSaveStatus() {
|
||||
status.classList.toggle('is-dirty', app.flowDirty || isNew);
|
||||
}
|
||||
|
||||
let flowEditorNoticeTimer = null;
|
||||
function clearFlowEditorNotice() {
|
||||
if (flowEditorNoticeTimer) clearTimeout(flowEditorNoticeTimer);
|
||||
flowEditorNoticeTimer = null;
|
||||
const host = $('#flowEditorNotice');
|
||||
if (!host) return;
|
||||
host.classList.remove('is-visible', 'is-error');
|
||||
host.textContent = '';
|
||||
}
|
||||
function flowEditorNotice(message, { error = false, timeout = 1700 } = {}) {
|
||||
const host = $('#flowEditorNotice');
|
||||
if (!host || !message) return;
|
||||
if (flowEditorNoticeTimer) clearTimeout(flowEditorNoticeTimer);
|
||||
host.textContent = message;
|
||||
host.classList.toggle('is-error', error);
|
||||
host.classList.add('is-visible');
|
||||
flowEditorNoticeTimer = setTimeout(() => {
|
||||
host.classList.remove('is-visible', 'is-error');
|
||||
flowEditorNoticeTimer = null;
|
||||
}, timeout);
|
||||
}
|
||||
function notifyFlowEditorAction(message, mode = 'toast') {
|
||||
if (!message || mode === false || mode === 'none') return;
|
||||
if (mode === 'inline') flowEditorNotice(message);
|
||||
else toast(message);
|
||||
}
|
||||
|
||||
let flowHistory = [];
|
||||
let flowHistoryApplying = false;
|
||||
const FLOW_HISTORY_LIMIT = 100;
|
||||
function flowHistorySnapshot() {
|
||||
if (!app.flowDraft) return null;
|
||||
const enabled = $('#flowEnabled');
|
||||
return {
|
||||
draft: JSON.parse(JSON.stringify(app.flowDraft)),
|
||||
enabled: enabled ? enabled.checked : app.flowDraft.enabled !== false,
|
||||
selectedIds: [...(app.flowSelectedNodeIds || [])],
|
||||
selectedId: app.flowSelectedNodeId || null,
|
||||
connectFrom: app.flowConnectFrom || null,
|
||||
dirty: app.flowDirty === true,
|
||||
};
|
||||
}
|
||||
function flowHistoryKey(snapshot) {
|
||||
if (!snapshot) return '';
|
||||
return JSON.stringify({ draft: snapshot.draft, enabled: snapshot.enabled, dirty: snapshot.dirty });
|
||||
}
|
||||
function flowHistoryClear() { flowHistory = []; flowHistoryApplying = false; }
|
||||
function flowHistoryReset() {
|
||||
const snapshot = flowHistorySnapshot();
|
||||
flowHistory = snapshot ? [snapshot] : [];
|
||||
flowHistoryApplying = false;
|
||||
}
|
||||
function flowHistoryCommit() {
|
||||
if (flowHistoryApplying || !app.flowDraft) return;
|
||||
const snapshot = flowHistorySnapshot();
|
||||
if (!snapshot) return;
|
||||
const previous = flowHistory[flowHistory.length - 1];
|
||||
if (previous && flowHistoryKey(previous) === flowHistoryKey(snapshot)) return;
|
||||
flowHistory.push(snapshot);
|
||||
if (flowHistory.length > FLOW_HISTORY_LIMIT) flowHistory.splice(0, flowHistory.length - FLOW_HISTORY_LIMIT);
|
||||
}
|
||||
function canUndoFlowEdit() { return flowHistory.length > 1; }
|
||||
function undoFlowEdit({ notify = 'inline' } = {}) {
|
||||
if (!app.flowDraft || !canUndoFlowEdit()) {
|
||||
if (notify === 'inline') flowEditorNotice(tr('flow.nothingToUndo'));
|
||||
return false;
|
||||
}
|
||||
flowHistoryApplying = true;
|
||||
flowHistory.pop();
|
||||
const snapshot = flowHistory[flowHistory.length - 1];
|
||||
app.flowDraft = JSON.parse(JSON.stringify(snapshot.draft));
|
||||
const valid = new Set(app.flowDraft.nodes.map(node => node.id));
|
||||
app.flowSelectedNodeIds = (snapshot.selectedIds || []).filter(id => valid.has(id));
|
||||
app.flowSelectedNodeId = snapshot.selectedId && valid.has(snapshot.selectedId)
|
||||
? snapshot.selectedId
|
||||
: (app.flowSelectedNodeIds[app.flowSelectedNodeIds.length - 1] || null);
|
||||
app.flowConnectFrom = snapshot.connectFrom && valid.has(snapshot.connectFrom) ? snapshot.connectFrom : null;
|
||||
app.flowDirty = snapshot.dirty === true;
|
||||
app.flowNameEditing = false;
|
||||
const enabled = $('#flowEnabled'); if (enabled) enabled.checked = snapshot.enabled !== false;
|
||||
renderFlowEditor();
|
||||
flowHistoryApplying = false;
|
||||
notifyFlowEditorAction(tr('flow.undoDone'), notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
let flowContextPoint = null;
|
||||
let flowContextEdgeId = null;
|
||||
let flowBlockInsertPoint = null;
|
||||
function hideFlowContextMenu() {
|
||||
const menu = $('#flowContextMenu');
|
||||
if (menu) menu.hidden = true;
|
||||
flowContextEdgeId = null;
|
||||
}
|
||||
function showFlowContextMenu(items, event) {
|
||||
const menu = $('#flowContextMenu');
|
||||
if (!menu) return;
|
||||
menu.innerHTML = items.map(item => item.separator
|
||||
? '<div class="flow-context-separator" role="separator"></div>'
|
||||
: `<button type="button" role="menuitem" data-flow-context-action="${esc(item.action)}" ${item.disabled ? 'disabled' : ''}>${esc(item.label)}</button>`).join('');
|
||||
menu.hidden = false;
|
||||
menu.style.left = '0px';
|
||||
menu.style.top = '0px';
|
||||
const rect = menu.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const left = Math.max(margin, Math.min(event.clientX, window.innerWidth - rect.width - margin));
|
||||
const top = Math.max(margin, Math.min(event.clientY, window.innerHeight - rect.height - margin));
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
function setFlowZoom(value, { render = true } = {}) {
|
||||
const next = clamp(Number(value) || 1, .1, 1.35);
|
||||
app.flowZoom = Math.round(next * 20) / 20;
|
||||
@@ -456,7 +569,8 @@ function renderFlowBlockLibrary(filter = '') {
|
||||
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
|
||||
}
|
||||
|
||||
function openFlowBlockLibrary() {
|
||||
function openFlowBlockLibrary({ point = null } = {}) {
|
||||
flowBlockInsertPoint = point ? { x: Number(point.x || 0), y: Number(point.y || 0) } : null;
|
||||
const search = $('#flowBlockSearch'); if (search) search.value = '';
|
||||
renderFlowBlockLibrary('');
|
||||
$('#flowBlockDialog')?.showModal();
|
||||
@@ -702,29 +816,32 @@ function flowUpstreamNodes(id) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function addFlowNode(kind) {
|
||||
function addFlowNode(kind, { point = flowBlockInsertPoint } = {}) {
|
||||
if (!app.flowDraft || !FLOW_NODE_META[kind]) return;
|
||||
const count = app.flowDraft.nodes.length;
|
||||
const workspace = $('#flowWorkspace');
|
||||
const viewportX = workspace ? (workspace.scrollLeft / (app.flowZoom || 1)) + 48 : 80;
|
||||
const viewportY = workspace ? (workspace.scrollTop / (app.flowZoom || 1)) + 54 : 70;
|
||||
const node = { id: newFlowId('node'), kind, x: Math.max(36, viewportX + (count % 3) * 24), y: Math.max(36, viewportY + (count % 3) * 24), config: flowDefaultConfig(kind) };
|
||||
app.flowDraft.nodes.push(node); app.flowSelectedNodeId = node.id; app.flowSelectedNodeIds = [node.id]; app.flowDirty = true; renderFlowEditor();
|
||||
const baseX = point ? Number(point.x || 0) : viewportX + (count % 3) * 24;
|
||||
const baseY = point ? Number(point.y || 0) : viewportY + (count % 3) * 24;
|
||||
const node = { id: newFlowId('node'), kind, x: Math.max(36, baseX), y: Math.max(36, baseY), config: flowDefaultConfig(kind) };
|
||||
flowBlockInsertPoint = null;
|
||||
app.flowDraft.nodes.push(node); app.flowSelectedNodeId = node.id; app.flowSelectedNodeIds = [node.id]; app.flowDirty = true; renderFlowEditor(); flowHistoryCommit();
|
||||
requestAnimationFrame(() => $(`[data-flow-node="${CSS.escape(node.id)}"]`)?.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' }));
|
||||
}
|
||||
function removeFlowNode(id) {
|
||||
if (!app.flowDraft) return;
|
||||
app.flowDraft.nodes = app.flowDraft.nodes.filter(node => node.id !== id); app.flowDraft.edges = app.flowDraft.edges.filter(edge => edge.from !== id && edge.to !== id);
|
||||
if (app.flowSelectedNodeId === id) app.flowSelectedNodeId = null; app.flowSelectedNodeIds = (app.flowSelectedNodeIds || []).filter(nodeId => nodeId !== id); if (app.flowConnectFrom === id) app.flowConnectFrom = null; app.flowDirty = true; renderFlowEditor();
|
||||
if (app.flowSelectedNodeId === id) app.flowSelectedNodeId = null; app.flowSelectedNodeIds = (app.flowSelectedNodeIds || []).filter(nodeId => nodeId !== id); if (app.flowConnectFrom === id) app.flowConnectFrom = null; app.flowDirty = true; renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
function connectFlowNodes(from, to) {
|
||||
if (!app.flowDraft || !from || !to || from === to) return;
|
||||
if (app.flowDraft.edges.some(edge => edge.from === from && edge.to === to)) return;
|
||||
app.flowDraft.edges.push({ id: newFlowId('edge'), from, to }); app.flowDirty = true; app.flowConnectFrom = null; renderFlowEditor();
|
||||
app.flowDraft.edges.push({ id: newFlowId('edge'), from, to }); app.flowDirty = true; app.flowConnectFrom = null; renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
function removeFlowEdge(id) {
|
||||
if (!app.flowDraft) return;
|
||||
app.flowDraft.edges = app.flowDraft.edges.filter(edge => edge.id !== id); app.flowDirty = true; renderFlowEditor();
|
||||
app.flowDraft.edges = app.flowDraft.edges.filter(edge => edge.id !== id); app.flowDirty = true; renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
|
||||
|
||||
@@ -1040,7 +1157,7 @@ function applyFlowTemplate(key) {
|
||||
app.flowDraft.description = flowPresetText(preset.description); app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowDirty = true;
|
||||
if (!app.flowDraft.id || flowCurrentName() === tr('flow.newDefaultName')) { app.flowDraft.name = flowPresetText(preset.name) || preset.id; if (app.flowNameEditing) $('#flowName').value = app.flowDraft.name; renderFlowNameMode(); }
|
||||
markFlowPresetRecent(preset.id);
|
||||
$('#flowTemplateDialog')?.close(); renderFlowEditor();
|
||||
$('#flowTemplateDialog')?.close(); renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
|
||||
|
||||
@@ -1149,14 +1266,14 @@ async function persistFlowDraft(body) {
|
||||
return api(app.flowDraft.id ? `/api/flows/${encodeURIComponent(app.flowDraft.id)}` : '/api/flows', { method: app.flowDraft.id ? 'PUT' : 'POST', body });
|
||||
}
|
||||
|
||||
async function applySavedFlow(saved, messageKey) {
|
||||
async function applySavedFlow(saved, messageKey, { notify = 'inline' } = {}) {
|
||||
const index = app.flows.findIndex(flow => flow.id === saved.id); if (index >= 0) app.flows[index] = saved; else app.flows.push(saved);
|
||||
app.flowDraft = flowDraftFrom(saved); app.flowDirty = false; app.flowNameEditing = false;
|
||||
$('#flowEnabled').checked = saved.enabled === true;
|
||||
renderFlows(); renderFlowEditor(); updateBrowserUrl(`/flows/${saved.id}`, true); await loadBootstrap(); toast(tr(messageKey));
|
||||
renderFlows(); renderFlowEditor(); flowHistoryReset(); updateBrowserUrl(`/flows/${saved.id}`, true); await loadBootstrap(); notifyFlowEditorAction(tr(messageKey), notify);
|
||||
}
|
||||
|
||||
async function saveFlow() {
|
||||
async function saveFlow({ notify = 'inline' } = {}) {
|
||||
if (!app.flowDraft) return;
|
||||
if (app.flowNameEditing && !commitFlowName()) return toast(tr('flow.nameRequired'), true);
|
||||
const name = flowCurrentName(); if (!name) return toast(tr('flow.nameRequired'), true);
|
||||
@@ -1164,14 +1281,14 @@ async function saveFlow() {
|
||||
if (app.flowDraft.id) body.expected_revision = Number(app.flowDraft.revision || 0);
|
||||
try {
|
||||
const saved = await persistFlowDraft(body);
|
||||
await applySavedFlow(saved, 'flow.saved');
|
||||
await applySavedFlow(saved, 'flow.saved', { notify });
|
||||
} catch (error) {
|
||||
if (error.status !== 400) return toast(error.message, true);
|
||||
const saveDraft = confirm(tr('flow.saveAsDraftConfirm', { reason: error.message }));
|
||||
if (!saveDraft) return toast(error.message, true);
|
||||
try {
|
||||
const saved = await persistFlowDraft({ ...body, enabled: false, draft: true });
|
||||
await applySavedFlow(saved, 'flow.savedAsDraft');
|
||||
await applySavedFlow(saved, 'flow.savedAsDraft', { notify });
|
||||
} catch (draftError) { toast(draftError.message, true); }
|
||||
}
|
||||
}
|
||||
@@ -1214,7 +1331,7 @@ function clearFlowSelection() { setFlowSelection([], null); }
|
||||
let flowClipboard = null;
|
||||
let flowClipboardPasteCount = 0;
|
||||
|
||||
function copySelectedFlowNodes({ notify = true } = {}) {
|
||||
function copySelectedFlowNodes({ notify = 'inline' } = {}) {
|
||||
if (!app.flowDraft || !(app.flowSelectedNodeIds || []).length) return false;
|
||||
const selected = new Set(app.flowSelectedNodeIds);
|
||||
const nodes = app.flowDraft.nodes
|
||||
@@ -1226,21 +1343,25 @@ function copySelectedFlowNodes({ notify = true } = {}) {
|
||||
if (!nodes.length) return false;
|
||||
flowClipboard = { nodes, edges };
|
||||
flowClipboardPasteCount = 0;
|
||||
if (notify) toast(tr('flow.blocksCopied', { count: nodes.length }));
|
||||
notifyFlowEditorAction(tr('flow.blocksCopied', { count: nodes.length }), notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
function pasteFlowNodes({ notify = true } = {}) {
|
||||
function pasteFlowNodes({ notify = 'inline', point = null } = {}) {
|
||||
if (!app.flowDraft || !flowClipboard?.nodes?.length) return false;
|
||||
flowClipboardPasteCount += 1;
|
||||
const offset = 36 * flowClipboardPasteCount;
|
||||
const minX = Math.min(...flowClipboard.nodes.map(node => Number(node.x || 0)));
|
||||
const minY = Math.min(...flowClipboard.nodes.map(node => Number(node.y || 0)));
|
||||
const dx = point ? Number(point.x || 0) - minX : offset;
|
||||
const dy = point ? Number(point.y || 0) - minY : offset;
|
||||
const idMap = new Map(flowClipboard.nodes.map(node => [node.id, newFlowId('node')]));
|
||||
const copies = flowClipboard.nodes.map(node => ({
|
||||
...node,
|
||||
id: idMap.get(node.id),
|
||||
config: JSON.parse(JSON.stringify(node.config || {})),
|
||||
x: Math.max(12, Number(node.x || 0) + offset),
|
||||
y: Math.max(12, Number(node.y || 0) + offset),
|
||||
x: Math.max(12, Number(node.x || 0) + dx),
|
||||
y: Math.max(12, Number(node.y || 0) + dy),
|
||||
}));
|
||||
const copiedEdges = flowClipboard.edges.map(edge => ({
|
||||
...edge,
|
||||
@@ -1253,22 +1374,24 @@ function pasteFlowNodes({ notify = true } = {}) {
|
||||
app.flowSelectedNodeIds = copies.map(node => node.id);
|
||||
app.flowSelectedNodeId = copies[copies.length - 1]?.id || null;
|
||||
app.flowDirty = true;
|
||||
renderFlowEditor();
|
||||
if (notify) toast(tr('flow.blocksPasted', { count: copies.length }));
|
||||
renderFlowEditor(); flowHistoryCommit();
|
||||
notifyFlowEditorAction(tr('flow.blocksPasted', { count: copies.length }), notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
function duplicateSelectedFlowNodes() {
|
||||
function duplicateSelectedFlowNodes({ notify = 'inline' } = {}) {
|
||||
const count = (app.flowSelectedNodeIds || []).length;
|
||||
if (!count || !copySelectedFlowNodes({ notify: false })) return;
|
||||
if (pasteFlowNodes({ notify: false })) toast(tr('flow.blocksDuplicated', { count }));
|
||||
if (!count || !copySelectedFlowNodes({ notify: false })) return false;
|
||||
if (!pasteFlowNodes({ notify: false })) return false;
|
||||
notifyFlowEditorAction(tr('flow.blocksDuplicated', { count }), notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cutSelectedFlowNodes() {
|
||||
function cutSelectedFlowNodes({ notify = 'inline' } = {}) {
|
||||
const count = (app.flowSelectedNodeIds || []).length;
|
||||
if (!count || !copySelectedFlowNodes({ notify: false })) return false;
|
||||
removeSelectedFlowNodes();
|
||||
toast(tr('flow.blocksCut', { count }));
|
||||
notifyFlowEditorAction(tr('flow.blocksCut', { count }), notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1278,7 +1401,7 @@ function removeSelectedFlowNodes() {
|
||||
app.flowDraft.nodes = app.flowDraft.nodes.filter(node => !selected.has(node.id));
|
||||
app.flowDraft.edges = app.flowDraft.edges.filter(edge => !selected.has(edge.from) && !selected.has(edge.to));
|
||||
if (app.flowConnectFrom && selected.has(app.flowConnectFrom)) app.flowConnectFrom = null;
|
||||
app.flowSelectedNodeIds = []; app.flowSelectedNodeId = null; app.flowDirty = true; renderFlowEditor();
|
||||
app.flowSelectedNodeIds = []; app.flowSelectedNodeId = null; app.flowDirty = true; renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
|
||||
function nudgeSelectedFlowNodes(dx, dy) {
|
||||
@@ -1290,7 +1413,7 @@ function nudgeSelectedFlowNodes(dx, dy) {
|
||||
node.y = Math.max(12, Number(node.y || 0) + dy);
|
||||
});
|
||||
app.flowDirty = true;
|
||||
renderFlowEditor();
|
||||
renderFlowEditor(); flowHistoryCommit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1347,7 +1470,7 @@ function updateFlowConfig(input) {
|
||||
else if (key === 'min_direction_changes' && node.kind === 'oscillates') node.config.min_direction_changes = Math.max(1, Math.floor(Number(input.value || 1)));
|
||||
else if (['value', 'setpoint', 'target_temperature', 'cooldown_seconds', 'fan_speed'].includes(key) && ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric', 'zone_thermostat', 'device_action', 'device_feature_action', 'group_action', 'ha_service_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
else node.config[key] = input.value;
|
||||
app.flowDirty = true; renderFlowEditor();
|
||||
app.flowDirty = true; renderFlowEditor(); flowHistoryCommit();
|
||||
}
|
||||
|
||||
let flowDrag = null;
|
||||
@@ -1425,7 +1548,7 @@ document.addEventListener('pointerdown', event => {
|
||||
} else if (!selected.includes(node.id)) selected = [node.id];
|
||||
app.flowSelectedNodeIds = selected; app.flowSelectedNodeId = node.id; renderFlowEditor();
|
||||
const starts = selected.map(id => flowNodeById(id)).filter(Boolean).map(item => ({ id: item.id, left: Number(item.x || 0), top: Number(item.y || 0) }));
|
||||
flowDrag = { x: event.clientX, y: event.clientY, starts };
|
||||
flowDrag = { x: event.clientX, y: event.clientY, starts, moved: false };
|
||||
nodeEl.setPointerCapture?.(event.pointerId); event.preventDefault();
|
||||
});
|
||||
document.addEventListener('pointermove', event => {
|
||||
@@ -1437,6 +1560,7 @@ document.addEventListener('pointermove', event => {
|
||||
}
|
||||
if (!flowDrag || !app.flowDraft) return;
|
||||
let dx = (event.clientX - flowDrag.x) / (app.flowZoom || 1), dy = (event.clientY - flowDrag.y) / (app.flowZoom || 1);
|
||||
flowDrag.moved = flowDrag.moved || Math.abs(dx) > .5 || Math.abs(dy) > .5;
|
||||
const minLeft = Math.min(...flowDrag.starts.map(item => item.left));
|
||||
const minTop = Math.min(...flowDrag.starts.map(item => item.top));
|
||||
dx = Math.max(dx, 12 - minLeft); dy = Math.max(dy, 12 - minTop);
|
||||
@@ -1460,9 +1584,51 @@ document.addEventListener('pointerup', event => {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (flowDrag?.moved) flowHistoryCommit();
|
||||
flowDrag = null;
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', event => {
|
||||
if ($('#flowEditor')?.hidden || !app.flowDraft) return;
|
||||
const workspace = event.target.closest?.('#flowWorkspace');
|
||||
if (!workspace || event.target.closest?.('#flowContextMenu,input,select,textarea,[contenteditable="true"]')) return;
|
||||
event.preventDefault();
|
||||
flowContextPoint = flowCanvasPoint(event);
|
||||
flowContextEdgeId = null;
|
||||
const nodeEl = event.target.closest?.('[data-flow-node]');
|
||||
const edgeEl = event.target.closest?.('[data-flow-edge]');
|
||||
if (edgeEl) {
|
||||
flowContextEdgeId = edgeEl.dataset.flowEdge;
|
||||
showFlowContextMenu([{ action: 'delete-edge', label: tr('flow.contextDeleteConnection') }], event);
|
||||
return;
|
||||
}
|
||||
if (nodeEl) {
|
||||
const id = nodeEl.dataset.flowNode;
|
||||
if (!(app.flowSelectedNodeIds || []).includes(id)) {
|
||||
app.flowSelectedNodeIds = [id]; app.flowSelectedNodeId = id; renderFlowEditor();
|
||||
}
|
||||
const count = (app.flowSelectedNodeIds || []).length;
|
||||
showFlowContextMenu([
|
||||
{ action: 'copy', label: tr('flow.contextCopy', { count }) },
|
||||
{ action: 'cut', label: tr('flow.contextCut', { count }) },
|
||||
{ action: 'duplicate', label: tr('flow.contextDuplicate', { count }) },
|
||||
{ action: 'delete', label: tr('flow.contextDelete', { count }) },
|
||||
{ separator: true },
|
||||
{ action: 'paste', label: tr('flow.contextPaste'), disabled: !flowClipboard?.nodes?.length },
|
||||
{ action: 'add', label: tr('flow.contextAddBlock') },
|
||||
], event);
|
||||
return;
|
||||
}
|
||||
showFlowContextMenu([
|
||||
{ action: 'undo', label: tr('flow.contextUndo'), disabled: !canUndoFlowEdit() },
|
||||
{ action: 'add', label: tr('flow.contextAddBlock') },
|
||||
{ action: 'paste', label: tr('flow.contextPasteHere'), disabled: !flowClipboard?.nodes?.length },
|
||||
{ separator: true },
|
||||
{ action: 'select-all', label: tr('flow.selectAll'), disabled: !(app.flowDraft.nodes || []).length },
|
||||
{ action: 'fit', label: tr('flow.fitView'), disabled: !(app.flowDraft.nodes || []).length },
|
||||
], event);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
const flowCard = event.target.closest?.('[data-flow-card-id]');
|
||||
if (flowCard && event.target === flowCard && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openFlowEditor(flowCard.dataset.flowCardId); return; }
|
||||
@@ -1472,18 +1638,20 @@ document.addEventListener('keydown', event => {
|
||||
|
||||
const modifier = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
if (modifier && key === 's') { event.preventDefault(); saveFlow(); return; }
|
||||
if (modifier && key === 's') { event.preventDefault(); saveFlow({ notify: 'inline' }); return; }
|
||||
if (modifier && key === 'k') { event.preventDefault(); openFlowBlockLibrary(); return; }
|
||||
if (modifier && key === 'z' && !event.shiftKey) { event.preventDefault(); undoFlowEdit({ notify: 'inline' }); return; }
|
||||
if (modifier && key === 'a') { event.preventDefault(); selectAllFlowNodes(); return; }
|
||||
if (modifier && key === 'c') { event.preventDefault(); copySelectedFlowNodes(); return; }
|
||||
if (modifier && key === 'x') { event.preventDefault(); cutSelectedFlowNodes(); return; }
|
||||
if (modifier && key === 'v') { event.preventDefault(); pasteFlowNodes(); return; }
|
||||
if (modifier && key === 'd') { event.preventDefault(); duplicateSelectedFlowNodes(); return; }
|
||||
if (modifier && key === 'c') { event.preventDefault(); copySelectedFlowNodes({ notify: 'inline' }); return; }
|
||||
if (modifier && key === 'x') { event.preventDefault(); cutSelectedFlowNodes({ notify: 'inline' }); return; }
|
||||
if (modifier && key === 'v') { event.preventDefault(); pasteFlowNodes({ notify: 'inline' }); return; }
|
||||
if (modifier && key === 'd') { event.preventDefault(); duplicateSelectedFlowNodes({ notify: 'inline' }); return; }
|
||||
if (modifier && event.key === '0') { event.preventDefault(); fitFlowToView(); return; }
|
||||
if (modifier && (event.key === '=' || event.key === '+')) { event.preventDefault(); setFlowZoom((app.flowZoom || 1) + .1); return; }
|
||||
if (modifier && event.key === '-') { event.preventDefault(); setFlowZoom((app.flowZoom || 1) - .1); return; }
|
||||
if (!modifier && !event.altKey && (key === 'a' || event.key === 'Insert')) { event.preventDefault(); openFlowBlockLibrary(); return; }
|
||||
if (!modifier && event.key === '?') { event.preventDefault(); openFlowShortcuts(); return; }
|
||||
if (event.key === 'Escape' && !$('#flowContextMenu')?.hidden) { event.preventDefault(); hideFlowContextMenu(); return; }
|
||||
if (event.key === 'Escape' && ((app.flowSelectedNodeIds || []).length || app.flowConnectFrom)) {
|
||||
event.preventDefault(); app.flowConnectFrom = null; clearFlowSelection(); return;
|
||||
}
|
||||
@@ -1498,10 +1666,29 @@ document.addEventListener('keydown', event => {
|
||||
});
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const contextAction = event.target.closest?.('[data-flow-context-action]');
|
||||
if (contextAction) {
|
||||
const action = contextAction.dataset.flowContextAction;
|
||||
const point = flowContextPoint ? { ...flowContextPoint } : null;
|
||||
const edgeId = flowContextEdgeId;
|
||||
hideFlowContextMenu();
|
||||
if (action === 'copy') copySelectedFlowNodes({ notify: 'inline' });
|
||||
else if (action === 'cut') cutSelectedFlowNodes({ notify: 'inline' });
|
||||
else if (action === 'duplicate') duplicateSelectedFlowNodes({ notify: 'inline' });
|
||||
else if (action === 'delete') removeSelectedFlowNodes();
|
||||
else if (action === 'paste') pasteFlowNodes({ notify: 'inline', point });
|
||||
else if (action === 'add') openFlowBlockLibrary({ point });
|
||||
else if (action === 'select-all') selectAllFlowNodes();
|
||||
else if (action === 'fit') fitFlowToView();
|
||||
else if (action === 'undo') undoFlowEdit({ notify: 'inline' });
|
||||
else if (action === 'delete-edge' && edgeId) removeFlowEdge(edgeId);
|
||||
return;
|
||||
}
|
||||
if (!event.target.closest?.('#flowContextMenu')) hideFlowContextMenu();
|
||||
const flowCard = event.target.closest?.('[data-flow-card-id]');
|
||||
if (flowCard && !event.target.closest?.('button,summary,details,input,select,textarea,a,label')) { openFlowEditor(flowCard.dataset.flowCardId); return; }
|
||||
const edge = event.target.closest?.('[data-flow-edge]'); if (edge) { removeFlowEdge(edge.dataset.flowEdge); return; }
|
||||
const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); const dialog = add.closest?.('#flowBlockDialog'); if (dialog?.open) dialog.close(); return; }
|
||||
const add = event.target.closest?.('[data-flow-add]'); if (add) { const dialog = add.closest?.('#flowBlockDialog'); if (!dialog) flowBlockInsertPoint = null; addFlowNode(add.dataset.flowAdd); if (dialog?.open) dialog.close(); return; }
|
||||
const templateCategory = event.target.closest?.('[data-flow-template-category]'); if (templateCategory) { renderFlowPresetBrowser(templateCategory.dataset.flowTemplateCategory); return; }
|
||||
const templateFavorite = event.target.closest?.('[data-flow-template-favorite]'); if (templateFavorite) { toggleFlowPresetFavorite(templateFavorite.dataset.flowTemplateFavorite); return; }
|
||||
const templatePreview = event.target.closest?.('[data-flow-template-preview]'); if (templatePreview) { previewFlowTemplate(templatePreview.dataset.flowTemplatePreview); return; }
|
||||
@@ -1520,7 +1707,7 @@ document.addEventListener('click', event => {
|
||||
else if (action === 'toggle-flow-enabled') toggleFlowEnabled(actionButton.dataset.id, actionButton.dataset.value === 'true');
|
||||
else if (action === 'save-flow') saveFlow();
|
||||
else if (action === 'flow-mobile-actions') $('#flowEditorActionsDialog')?.showModal();
|
||||
else if (action === 'flow-add-block') openFlowBlockLibrary();
|
||||
else if (action === 'flow-add-block') openFlowBlockLibrary({ point: null });
|
||||
else if (action === 'flow-zoom-out') setFlowZoom((app.flowZoom || 1) - .1);
|
||||
else if (action === 'flow-zoom-in') setFlowZoom((app.flowZoom || 1) + .1);
|
||||
else if (action === 'flow-fit') fitFlowToView();
|
||||
@@ -1574,5 +1761,6 @@ $('#flowName')?.addEventListener('blur', () => {
|
||||
if (!app.flowDraft || !app.flowNameEditing) return;
|
||||
if ($('#flowName').value.trim()) commitFlowName();
|
||||
});
|
||||
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowEnabledLabel(); renderFlowSaveStatus(); } });
|
||||
window.addEventListener('resize', () => { if (!$('#flowEditor')?.hidden) renderFlowEdges(); });
|
||||
$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowEnabledLabel(); renderFlowSaveStatus(); flowHistoryCommit(); } });
|
||||
window.addEventListener('resize', () => { hideFlowContextMenu(); if (!$('#flowEditor')?.hidden) renderFlowEdges(); });
|
||||
$('#flowWorkspace')?.addEventListener('scroll', hideFlowContextMenu, { passive: true });
|
||||
|
||||
Reference in New Issue
Block a user