diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4a8242..871ed076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - refactor(core): housekeeping: rename `extra_information` to `qr_code_generation_instructions` in `ubo_input`, add `.tmpl` extension for extension files, use `textarea` for `LONG` input field type in web dashboard, rename `..._HOST` env variables to `..._ADDRESS`, use underscore thousand separators for big numbers in codebase - feat(docker): support docker compositions and add a way to import `docker-compose.yml` files - feat(docker): add instructions and icon for docker compositions +- refactor(core): rerender screen when rendering on the display is resumed like when returning from viewfinder to avoid artifacts ## Version 1.1.0 diff --git a/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc b/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc index 77d45a0e..d31f14fc 100644 --- a/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc @@ -27,6 +27,7 @@ "container_ip": null, "docker_id": null, "id": "envoy_grpc", + "instructions": null, "label": "Envoy for gRPC", "ports": [], "status": "not_available" @@ -36,6 +37,7 @@ "container_ip": null, "docker_id": null, "id": "home_assistant", + "instructions": null, "label": "Home Assistant", "ports": [], "status": "not_available" @@ -45,6 +47,7 @@ "container_ip": null, "docker_id": null, "id": "home_bridge", + "instructions": null, "label": "Home Bridge", "ports": [], "status": "not_available" @@ -54,6 +57,7 @@ "container_ip": null, "docker_id": null, "id": "ngrok", + "instructions": null, "label": "Ngrok", "ports": [], "status": "not_available" @@ -63,6 +67,7 @@ "container_ip": null, "docker_id": null, "id": "ollama", + "instructions": null, "label": "Ollama", "ports": [], "status": "not_available" @@ -72,6 +77,7 @@ "container_ip": null, "docker_id": null, "id": "open_webui", + "instructions": null, "label": "Open WebUI", "ports": [], "status": "not_available" @@ -81,6 +87,7 @@ "container_ip": null, "docker_id": null, "id": "pi_hole", + "instructions": null, "label": "Pi-hole", "ports": [], "status": "not_available" @@ -90,6 +97,7 @@ "container_ip": null, "docker_id": null, "id": "portainer", + "instructions": null, "label": "Portainer", "ports": [], "status": "not_available" diff --git a/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc b/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc index a97cd8e5..530ad06e 100644 --- a/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc @@ -27,6 +27,7 @@ "container_ip": null, "docker_id": null, "id": "envoy_grpc", + "instructions": null, "label": "Envoy for gRPC", "ports": [], "status": "not_available" @@ -36,6 +37,7 @@ "container_ip": null, "docker_id": null, "id": "home_assistant", + "instructions": null, "label": "Home Assistant", "ports": [], "status": "not_available" @@ -45,6 +47,7 @@ "container_ip": null, "docker_id": null, "id": "home_bridge", + "instructions": null, "label": "Home Bridge", "ports": [], "status": "not_available" @@ -54,6 +57,7 @@ "container_ip": null, "docker_id": null, "id": "ngrok", + "instructions": null, "label": "Ngrok", "ports": [], "status": "not_available" @@ -63,6 +67,7 @@ "container_ip": null, "docker_id": null, "id": "ollama", + "instructions": null, "label": "Ollama", "ports": [], "status": "not_available" @@ -72,6 +77,7 @@ "container_ip": null, "docker_id": null, "id": "open_webui", + "instructions": null, "label": "Open WebUI", "ports": [], "status": "not_available" @@ -81,6 +87,7 @@ "container_ip": null, "docker_id": null, "id": "pi_hole", + "instructions": null, "label": "Pi-hole", "ports": [], "status": "not_available" @@ -90,6 +97,7 @@ "container_ip": null, "docker_id": null, "id": "portainer", + "instructions": null, "label": "Portainer", "ports": [], "status": "not_available" diff --git a/ubo_app/menu_app/menu.py b/ubo_app/menu_app/menu.py index 6b1a187c..031d293a 100644 --- a/ubo_app/menu_app/menu.py +++ b/ubo_app/menu_app/menu.py @@ -9,6 +9,7 @@ from ubo_app.menu_app.menu_footer import MenuAppFooter from ubo_app.menu_app.menu_header import MenuAppHeader from ubo_app.store.main import store +from ubo_app.store.services.display import DisplayRerenderEvent from ubo_app.store.settings.types import SettingsSetDebugModeEvent @@ -19,6 +20,10 @@ def set_debug_mode(self: MenuApp, event: SettingsSetDebugModeEvent) -> None: """Set the debug mode.""" self.root.show_update_regions = event.is_enabled + def rerender(self: MenuApp) -> None: + """Re-render the application.""" + self.root.previous_frame = None + @override def on_start(self: MenuApp) -> None: """Start the application.""" @@ -31,3 +36,4 @@ def on_start(self: MenuApp) -> None: self.set_debug_mode, keep_ref=False, ) + store.subscribe_event(DisplayRerenderEvent, self.rerender, keep_ref=False) diff --git a/ubo_app/menu_app/menu_notification_handler.py b/ubo_app/menu_app/menu_notification_handler.py index 1ff1115f..18b8770d 100644 --- a/ubo_app/menu_app/menu_notification_handler.py +++ b/ubo_app/menu_app/menu_notification_handler.py @@ -79,8 +79,6 @@ def close(_: object = None) -> None: store.dispatch( NotificationsClearAction(notification=notification.value), ) - if notification.value.on_dismiss: - notification.value.on_dismiss() if notification.value.on_close: notification.value.on_close() @@ -163,10 +161,11 @@ def dismiss(_: object = None) -> None: def run_notification_action(action: NotificationActionItem) -> None: result = action.action() - if action.dismiss_notification: - dismiss() - else: - close() + if action.close_notification: + if action.dismiss_notification: + dismiss() + else: + close() return result items: list[NotificationActionItem | None] = [] @@ -198,7 +197,7 @@ def open_info() -> None: for action in notification.value.actions ] - if notification.value.dismissable: + if notification.value.show_dismiss_action: items.append( NotificationActionItem( icon='󰆴', diff --git a/ubo_app/services/000-display/reducer.py b/ubo_app/services/000-display/reducer.py index 7fbddf02..fdfac6f2 100644 --- a/ubo_app/services/000-display/reducer.py +++ b/ubo_app/services/000-display/reducer.py @@ -4,13 +4,16 @@ from dataclasses import replace from redux import ( + CompleteReducerResult, InitAction, InitializationActionError, + ReducerResult, ) from ubo_app.store.services.display import ( DisplayAction, DisplayPauseAction, + DisplayRerenderEvent, DisplayResumeAction, DisplayState, ) @@ -21,7 +24,7 @@ def reducer( state: DisplayState | None, action: Action, -) -> DisplayState: +) -> ReducerResult[DisplayState, None, DisplayRerenderEvent]: if state is None: if isinstance(action, InitAction): return DisplayState() @@ -31,6 +34,9 @@ def reducer( return replace(state, is_paused=True) if isinstance(action, DisplayResumeAction): - return replace(state, is_paused=False) + return CompleteReducerResult( + state=replace(state, is_paused=False), + events=[DisplayRerenderEvent()], + ) return state diff --git a/ubo_app/services/040-camera/reducer.py b/ubo_app/services/040-camera/reducer.py index f341066f..4cbab977 100644 --- a/ubo_app/services/040-camera/reducer.py +++ b/ubo_app/services/040-camera/reducer.py @@ -30,9 +30,7 @@ CameraState, CameraStopViewfinderEvent, ) -from ubo_app.store.services.keypad import ( - KeypadKeyPressAction, -) +from ubo_app.store.services.keypad import KeypadKeyPressAction from ubo_app.store.services.notifications import ( Notification, NotificationDispatchItem, @@ -103,11 +101,12 @@ def reducer( pattern=action.description.pattern, ), icon='󰄀', - dismiss_notification=False, + close_notification=False, ), ], - dismissable=False, - on_dismiss=functools.partial( + show_dismiss_action=False, + dismiss_on_close=True, + on_close=functools.partial( store.dispatch, InputCancelAction(id=action.description.id), ), @@ -157,6 +156,7 @@ def reducer( for key, value in match.groupdict().items() }, ), + NotificationsClearByIdAction(id='camera:qrcode'), ], events=[ CameraStopViewfinderEvent(id=None), diff --git a/ubo_app/services/040-camera/setup.py b/ubo_app/services/040-camera/setup.py index 25d77fb0..63a47505 100644 --- a/ubo_app/services/040-camera/setup.py +++ b/ubo_app/services/040-camera/setup.py @@ -12,7 +12,6 @@ import png from debouncer import DebounceOptions, debounce from kivy.clock import Clock, mainthread -from typing_extensions import override from ubo_gui.page import PageWidget from ubo_app.store.core.types import CloseApplicationAction, OpenApplicationAction @@ -57,10 +56,7 @@ def check_codes(codes: list[str]) -> None: store.dispatch(CameraReportBarcodeAction(codes=codes)) -class CameraApplication(PageWidget): - @override - def go_back(self: CameraApplication) -> bool: - return True +class CameraApplication(PageWidget): ... def initialize_camera() -> Picamera2 | None: @@ -192,7 +188,7 @@ def feed_viewfinder_locked(_: object) -> None: store.dispatch(DisplayPauseAction()) - def handle_stop_viewfinder() -> None: + def handle_stop_viewfinder(_: object = None) -> None: unsubscribe() with fs_lock: nonlocal is_running @@ -206,6 +202,8 @@ def handle_stop_viewfinder() -> None: picamera2.stop() picamera2.close() + application.bind(on_close=handle_stop_viewfinder) + unsubscribe = store.subscribe_event( CameraStopViewfinderEvent, handle_stop_viewfinder, diff --git a/ubo_app/services/050-users/setup.py b/ubo_app/services/050-users/setup.py index 8c774ecd..778dcc01 100644 --- a/ubo_app/services/050-users/setup.py +++ b/ubo_app/services/050-users/setup.py @@ -129,7 +129,7 @@ async def delete_account(event: UsersDeleteUserEvent) -> None: dismiss_notification=True, ), ], - dismissable=False, + show_dismiss_action=False, dismiss_on_close=True, on_close=lambda: loop.call_soon_threadsafe(notification_future.cancel), ), diff --git a/ubo_app/services/090-web-ui/reducer.py b/ubo_app/services/090-web-ui/reducer.py index ae62ed15..a52abac3 100644 --- a/ubo_app/services/090-web-ui/reducer.py +++ b/ubo_app/services/090-web-ui/reducer.py @@ -61,7 +61,7 @@ def reducer( extra_information=action.description.extra_information, expiration_timestamp=datetime.datetime.now(tz=datetime.UTC), color='#ffffff', - dismissable=False, + show_dismiss_action=False, dismiss_on_close=True, on_close=functools.partial( store.dispatch, diff --git a/ubo_app/setup.py b/ubo_app/setup.py index 240ec929..30af8392 100644 --- a/ubo_app/setup.py +++ b/ubo_app/setup.py @@ -12,6 +12,8 @@ from fake import Fake from redux import FinishAction, FinishEvent +from ubo_app.utils import IS_TEST_ENV + if TYPE_CHECKING: from ubo_gui.menu.types import Callable @@ -124,10 +126,11 @@ async def fake_monitor_unit(unit: str, callback: Callable[[str], None]) -> None: import ubo_app.display as _ # noqa: F401 from ubo_app.store.main import store - store.subscribe_event(FinishEvent, _clear_signal_handlers) + if not IS_TEST_ENV: + store.subscribe_event(FinishEvent, _clear_signal_handlers) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) from ubo_gui import setup as setup_ubo_gui diff --git a/ubo_app/store/services/display.py b/ubo_app/store/services/display.py index 5d81035f..cecefa48 100644 --- a/ubo_app/store/services/display.py +++ b/ubo_app/store/services/display.py @@ -17,6 +17,9 @@ class DisplayPauseAction(DisplayAction): ... class DisplayResumeAction(DisplayAction): ... +class DisplayRerenderEvent(DisplayEvent): ... + + class DisplayRenderEvent(DisplayEvent): data: bytes rectangle: tuple[int, int, int, int] diff --git a/ubo_app/store/services/notifications.py b/ubo_app/store/services/notifications.py index 36a278e2..6abd0033 100644 --- a/ubo_app/store/services/notifications.py +++ b/ubo_app/store/services/notifications.py @@ -81,6 +81,7 @@ class Chime(StrEnum): class NotificationActionItem(ActionItem): background_color: Color | Callable[[], Color] = SECONDARY_COLOR_LIGHT dismiss_notification: bool = False + close_notification: bool = True class NotificationDispatchItem(DispatchItem, NotificationActionItem): ... @@ -110,10 +111,9 @@ class Notification(Immutable): expiration_timestamp: datetime | None = None display_type: NotificationDisplayType = NotificationDisplayType.NOT_SET flash_time: float = NOTIFICATIONS_FLASH_TIME - dismissable: bool = True + show_dismiss_action: bool = True dismiss_on_close: bool = False on_close: Callable[[], Any] | None = None - on_dismiss: Callable[[], Any] | None = None blink: bool = True progress: float | None = None progress_weight: float = 1 diff --git a/ubo_app/store/update_manager/utils.py b/ubo_app/store/update_manager/utils.py index e5216f2a..8c85447e 100644 --- a/ubo_app/store/update_manager/utils.py +++ b/ubo_app/store/update_manager/utils.py @@ -126,7 +126,7 @@ async def download_files() -> None: icon='󰇚', blink=False, progress=0, - dismissable=False, + show_dismiss_action=False, ), ), ) @@ -168,7 +168,7 @@ async def download_files() -> None: icon='󰇚', blink=False, progress=1 / packages_count, - dismissable=False, + show_dismiss_action=False, ), ), ) @@ -220,7 +220,7 @@ async def download_files() -> None: icon='󰇚', blink=False, progress=min((counter + 1) / packages_count, 1), - dismissable=False, + show_dismiss_action=False, ), ), ) @@ -257,7 +257,7 @@ async def download_files() -> None: color=INFO_COLOR, icon='󰇚', progress=1, - dismissable=False, + show_dismiss_action=False, ), ), ) @@ -354,7 +354,7 @@ def dispatch_notification(is_presented: bool, _: float = 0) -> None: # noqa: FB display_type=NotificationDisplayType.BACKGROUND if is_presented else NotificationDisplayType.STICKY, - dismissable=False, + show_dismiss_action=False, dismiss_on_close=False, color=INFO_COLOR, progress=math.nan, diff --git a/ubo_app/utils/__init__.py b/ubo_app/utils/__init__.py index a9fadd0b..88d2fff6 100644 --- a/ubo_app/utils/__init__.py +++ b/ubo_app/utils/__init__.py @@ -1,4 +1,6 @@ # ruff: noqa: D100, D101, D102, D103, D104, D107 +import os from pathlib import Path IS_RPI = Path('/etc/rpi-issue').exists() +IS_TEST_ENV = 'PYTEST_CURRENT_TEST' in os.environ diff --git a/ubo_app/utils/input.py b/ubo_app/utils/input.py index 7c0a83e7..c1a52b78 100644 --- a/ubo_app/utils/input.py +++ b/ubo_app/utils/input.py @@ -79,7 +79,7 @@ def set_result(method: InputMethod) -> None: ), expiration_timestamp=datetime.datetime.now(tz=datetime.UTC), color='#ffffff', - dismissable=False, + show_dismiss_action=False, dismiss_on_close=True, actions=[ NotificationActionItem(