Skip to content

Commit

Permalink
feat(core): add recording and replaying indicators, avoid replaying w…
Browse files Browse the repository at this point in the history
…hile recording and vice versa, move keypad events to its own reducer as it has grown too big
  • Loading branch information
sassanh committed Oct 30, 2024
1 parent 6580b58 commit 86de77c
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 156 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- feat(ip): use pythonping to perform a real ping test instead to determine the internet connection status instead of opening a socket
- feat(core): user can start/end recording actioning by hitting r, actions will be recorded in `recordings/` directory and the last recording can be replayed by hitting `ctrl+r` - closes #187
- feat(core): use new `SpinnerWidget` of ubo-gui to show unknown progress in notifications, and add `General` sub menu to `System` settings menu to host ubo-pod/ubo-app related settings, currently it has `Debug` toggle to control a debug feature of `HeadlessWidget` - closes #190
- feat(core): add recording and replaying indicators, avoid replaying while recording and vice versa, move keypad events to its own reducer as it has grown too big

## Version 1.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"is_footer_visible": true,
"is_header_visible": true,
"is_recording": false,
"is_replaying": false,
"menu": {
"_type": "HeadlessMenu",
"items": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"is_footer_visible": true,
"is_header_visible": true,
"is_recording": false,
"is_replaying": false,
"menu": {
"_type": "HeadlessMenu",
"items": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
],
"is_connected": true
},
"keypad": {
"_type": "KeypadState",
"depth": 1
},
"lightdm": {
"_type": "LightDMState",
"is_active": true,
Expand All @@ -111,6 +115,7 @@
"is_footer_visible": true,
"is_header_visible": true,
"is_recording": false,
"is_replaying": false,
"menu": {
"_type": "HeadlessMenu",
"items": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
],
"is_connected": true
},
"keypad": {
"_type": "KeypadState",
"depth": 1
},
"lightdm": {
"_type": "LightDMState",
"is_active": false,
Expand All @@ -111,6 +115,7 @@
"is_footer_visible": true,
"is_header_visible": true,
"is_recording": false,
"is_replaying": false,
"menu": {
"_type": "HeadlessMenu",
"items": [
Expand Down
3 changes: 2 additions & 1 deletion ubo_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ubo_app.error_handlers import setup_error_handling
from ubo_app.logging import setup_logging
from ubo_app.setup import setup
from ubo_app.utils import IS_RPI

dotenv.load_dotenv(Path(__file__).parent / '.dev.env')
dotenv.load_dotenv(Path(__file__).parent / '.env')
Expand Down Expand Up @@ -55,7 +56,7 @@ def main() -> None:

headless_kivy.config.setup_headless_kivy(
headless_kivy.config.SetupHeadlessConfig(
bandwidth_limit=70 * 1000 * 1000 // 8 // 2,
bandwidth_limit=70 * 1000 * 1000 // 8 // 2 if IS_RPI else 0,
bandwidth_limit_window=0.03,
bandwidth_limit_overhead=10000,
region_size=60,
Expand Down
60 changes: 60 additions & 0 deletions ubo_app/menu_app/menu_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from functools import cached_property
from typing import TYPE_CHECKING

from kivy.animation import Animation
from kivy.metrics import dp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.relativelayout import RelativeLayout
from redux import AutorunOptions
from ubo_gui.app import UboApp
Expand Down Expand Up @@ -97,6 +99,30 @@ def handle_is_header_visible_change(
elif self.header_content in self.header_layout.children:
self.header_layout.remove_widget(self.header_content)

def handle_is_recording_change(
self: MenuAppHeader,
is_recording: bool, # noqa: FBT001
) -> None:
if is_recording:
if self.recording_sign not in self.header_content.children:
self.header_content.add_widget(self.recording_sign)
self.sign_animation.start(self.recording_sign)
elif self.recording_sign in self.header_content.children:
self.header_content.remove_widget(self.recording_sign)
self.sign_animation.cancel(self.recording_sign)

def handle_is_replaying_change(
self: MenuAppHeader,
is_replaying: bool, # noqa: FBT001
) -> None:
if is_replaying:
if self.replaying_sign not in self.header_content.children:
self.header_content.add_widget(self.replaying_sign)
self.sign_animation.start(self.replaying_sign)
elif self.replaying_sign in self.header_content.children:
self.header_content.remove_widget(self.replaying_sign)
self.sign_animation.cancel(self.replaying_sign)

@cached_property
def header(self: MenuAppHeader) -> Widget | None:
self.header_content = RelativeLayout()
Expand All @@ -115,6 +141,30 @@ def header(self: MenuAppHeader) -> Widget | None:
)
self.header_content.add_widget(self.progress_layout)

self.recording_sign = Label(
text='󰑊',
font_size=dp(20),
color=(1, 0, 0, 1),
pos_hint={'right': 1},
size_hint=(None, 1),
)
self.recording_sign.bind(texture_size=self.recording_sign.setter('size'))
self.replaying_sign = Label(
text='󰑙',
font_size=dp(20),
color=(0, 1, 0, 1),
pos_hint={'right': 1},
size_hint=(None, 1),
)
self.replaying_sign.bind(texture_size=self.replaying_sign.setter('size'))
self.sign_animation = (
Animation(opacity=1, duration=0.1)
+ Animation(duration=1)
+ Animation(opacity=0, duration=0.1)
+ Animation(duration=0.5)
)
self.sign_animation.repeat = True

self.notification_widgets = {}

self.header_layout = BoxLayout()
Expand All @@ -134,4 +184,14 @@ def header(self: MenuAppHeader) -> Widget | None:
options=AutorunOptions(keep_ref=False),
)(self.handle_is_header_visible_change)

store.autorun(
lambda state: state.main.is_recording,
options=AutorunOptions(keep_ref=False),
)(self.handle_is_recording_change)

store.autorun(
lambda state: state.main.is_replaying,
options=AutorunOptions(keep_ref=False),
)(self.handle_is_replaying_change)

return self.header_layout
190 changes: 190 additions & 0 deletions ubo_app/services/000-keypad/reducer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"""Keypad reducer."""

from __future__ import annotations

import math
from dataclasses import replace
from typing import TYPE_CHECKING

from redux import (
CombineReducerInitAction,
CompleteReducerResult,
FinishEvent,
InitializationActionError,
ReducerResult,
)

from ubo_app.store.core.types import (
MainEvent,
MenuChooseByIndexEvent,
MenuEvent,
MenuGoBackEvent,
MenuGoHomeEvent,
MenuScrollDirection,
MenuScrollEvent,
ReplayRecordedSequenceAction,
ScreenshotEvent,
SetMenuPathAction,
SnapshotEvent,
ToggleRecordingAction,
)
from ubo_app.store.services.audio import AudioChangeVolumeAction, AudioDevice
from ubo_app.store.services.keypad import (
Key,
KeypadAction,
KeypadKeyPressAction,
KeypadKeyReleaseAction,
KeypadState,
)
from ubo_app.store.services.notifications import Notification, NotificationsAddAction

if TYPE_CHECKING:
from ubo_app.store.services.audio import AudioAction


def reducer(
state: KeypadState | None,
action: KeypadAction,
) -> (
ReducerResult[
KeypadState,
AudioAction
| NotificationsAddAction
| ToggleRecordingAction
| ReplayRecordedSequenceAction,
FinishEvent | MenuEvent | MainEvent,
]
| None
):
"""Keypad reducer."""
if state is None:
if isinstance(action, CombineReducerInitAction):
return KeypadState()

raise InitializationActionError(action)

if isinstance(action, KeypadKeyPressAction):
if action.pressed_keys == {action.key}:
if action.key == Key.UP and state.depth == 1:
return CompleteReducerResult(
state=state,
actions=[
AudioChangeVolumeAction(
amount=0.05,
device=AudioDevice.OUTPUT,
),
],
)
if action.key == Key.DOWN and state.depth == 1:
return CompleteReducerResult(
state=state,
actions=[
AudioChangeVolumeAction(
amount=-0.05,
device=AudioDevice.OUTPUT,
),
],
)

if action.key == Key.L1:
return CompleteReducerResult(
state=state,
events=[MenuChooseByIndexEvent(index=0)],
)
if action.key == Key.L2:
return CompleteReducerResult(
state=state,
events=[MenuChooseByIndexEvent(index=1)],
)
if action.key == Key.L3:
return CompleteReducerResult(
state=state,
events=[MenuChooseByIndexEvent(index=2)],
)
if action.key == Key.UP:
return CompleteReducerResult(
state=state,
events=[MenuScrollEvent(direction=MenuScrollDirection.UP)],
)
if action.key == Key.DOWN:
return CompleteReducerResult(
state=state,
events=[MenuScrollEvent(direction=MenuScrollDirection.DOWN)],
)
else:
if action.pressed_keys == {Key.HOME, Key.L1} and action.key == Key.L1:
return CompleteReducerResult(
state=state,
events=[ScreenshotEvent()],
)
if action.pressed_keys == {Key.HOME, Key.L2} and action.key == Key.L2:
return CompleteReducerResult(
state=state,
events=[SnapshotEvent()],
)
if action.pressed_keys == {Key.HOME, Key.L3} and action.key == Key.L3:
return CompleteReducerResult(
state=state,
actions=[ToggleRecordingAction()],
)
if action.pressed_keys == {Key.BACK, Key.L3} and action.key == Key.L3:
return CompleteReducerResult(
state=state,
actions=[ReplayRecordedSequenceAction()],
)
if action.pressed_keys == {Key.HOME, Key.BACK} and action.key == Key.BACK:
return CompleteReducerResult(
state=state,
events=[FinishEvent()],
)

# DEMO {
if action.pressed_keys == {Key.HOME, Key.UP} and action.key == Key.UP:
return CompleteReducerResult(
state=state,
actions=[
NotificationsAddAction(
notification=Notification(
title='Test notification with progress',
content='This is a test notification with progress',
progress=0.5,
),
),
],
)
if action.pressed_keys == {Key.HOME, Key.DOWN} and action.key == Key.DOWN:
return CompleteReducerResult(
state=state,
actions=[
NotificationsAddAction(
notification=Notification(
icon='',
title='Test notification with spinner',
content='This is a test notification with spinner',
progress=math.nan,
),
),
],
)
# DEMO }
return state

if isinstance(action, KeypadKeyReleaseAction):
if len(action.pressed_keys) == 0:
if action.key == Key.BACK:
return CompleteReducerResult(
state=state,
events=[MenuGoBackEvent()],
)
if action.key == Key.HOME:
return CompleteReducerResult(
state=state,
events=[MenuGoHomeEvent()],
)

return state

if isinstance(action, SetMenuPathAction):
return replace(state, depth=action.depth)

return state
6 changes: 4 additions & 2 deletions ubo_app/services/000-keypad/ubo_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ubo_handle import register
from ubo_handle import Service, register


def setup() -> None:
def setup(service: Service) -> None:
from reducer import reducer
from setup import init_service

service.register_reducer(reducer)
init_service()


Expand Down
Loading

0 comments on commit 86de77c

Please sign in to comment.