Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-14 - [Flet Dialog Accessibility & Pulse Indicators]
**Learning:** Dialogs in this Flet-based app often lack automatic focus and Enter-key submission, creating friction for keyboard-driven "Quick Recording" workflows. Additionally, static status labels for long-running tasks (like recording) benefit significantly from subtle animations to provide glancing feedback.
**Action:** Always ensure `autofocus=True` and `on_submit` are set for primary inputs in dialogs. Use `animate_opacity` in conjunction with existing periodic update tasks (like duration timers) to create lightweight, "alive" visual indicators.
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-14 - [Config Performance Optimization]
**Learning:** The `ConfigManager` was performing synchronous disk I/O on every configuration value lookup via `get_config_value`. While individually fast, these operations aggregate and block the event loop in an `asyncio` application, potentially causing micro-stutters in the UI.
**Action:** Implement memory caching in `ConfigManager` to ensure that subsequent reads are served from RAM, and only write-through to disk when settings are modified.
42 changes: 29 additions & 13 deletions app/core/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def __init__(self, run_path):
self.accounts_config_path = os.path.join(self.config_path, "accounts.json")
self.web_auth_config_path = os.path.join(self.config_path, "web_auth.json")

# Memory cache for configuration data to avoid redundant disk I/O
self._cache = {}
self._cache_lock = threading.Lock()

os.makedirs(os.path.dirname(self.default_config_path), exist_ok=True)
self.init()

Expand Down Expand Up @@ -72,21 +76,29 @@ def init_web_auth_config(self):
cookies_config = {}
self._init_config(self.web_auth_config_path, cookies_config)

@staticmethod
def _load_config(config_path, error_message):
"""Load configuration from a JSON file."""
def _load_config(self, config_path, error_message, use_cache=True):
"""Load configuration from a JSON file with memory caching."""
if use_cache:
with self._cache_lock:
if config_path in self._cache:
return self._cache[config_path]

config_data = {}
try:
with open(config_path, encoding="utf-8") as file:
return json.load(file)
if os.path.exists(config_path):
with open(config_path, encoding="utf-8") as file:
config_data = json.load(file)
else:
logger.error(f"Configuration file not found: {config_path}")
except json.JSONDecodeError:
logger.error(f"Invalid JSON format in file: {config_path}")
return {}
except FileNotFoundError:
logger.error(f"Configuration file not found: {config_path}")
return {}
except Exception as e:
logger.error(f"{error_message}: {e}")
return {}

if use_cache:
with self._cache_lock:
self._cache[config_path] = config_data
return config_data

def load_default_config(self):
return self._load_config(self.default_config_path, "An error occurred while loading default config")
Expand Down Expand Up @@ -118,14 +130,17 @@ def load_web_auth_config(self):

_write_lock = threading.Lock()

@staticmethod
async def _save_config(config_path, config, success_message, error_message):
"""Save configuration to a JSON file (thread-safe, atomic write)."""
async def _save_config(self, config_path, config, success_message, error_message):
"""Save configuration to a JSON file (thread-safe, atomic write) and update cache."""
try:
content = json.dumps(config, ensure_ascii=False, indent=4)

def _write_sync():
with ConfigManager._write_lock:
# Update memory cache before writing to disk to ensure consistency
with self._cache_lock:
self._cache[config_path] = config

dir_name = os.path.dirname(config_path)
fd, tmp_path = tempfile.mkstemp(suffix=".tmp", prefix=".cfg_", dir=str(dir_name))
try:
Expand Down Expand Up @@ -186,6 +201,7 @@ async def save_cookies_config(self, config):
)

def get_config_value(self, key: str, default: T = None) -> T:
"""Get configuration value from memory cache."""
user_config = self.load_user_config()
default_config = self.load_default_config()
return user_config.get(key, default_config.get(key, default))
40 changes: 38 additions & 2 deletions app/ui/components/business/recording_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def _create_card_components(self, recording: Recording):
)
card = ft.Card(key=str(recording.rec_id), content=card_container)

return {
card_data = {
"card": card,
"display_title_label": display_title_label,
"duration_label": duration_text_label,
Expand All @@ -167,6 +167,7 @@ def _create_card_components(self, recording: Recording):
"monitor_button": monitor_button,
"status_label": status_label,
}
return card_data

def get_card_background_color(self, recording: Recording):
is_dark_mode = self.app.page.theme_mode == ft.ThemeMode.DARK
Expand All @@ -184,6 +185,31 @@ def create_status_label(self, recording: Recording):
if not config:
return None

if recording.is_recording:
return ft.Container(
content=ft.Row(
[
ft.Container(
width=8,
height=8,
bgcolor=ft.Colors.RED_ACCENT,
border_radius=4,
animate_opacity=300,
),
ft.Text(config["text"], color=config["text_color"], size=12, weight=ft.FontWeight.BOLD),
],
alignment=ft.MainAxisAlignment.CENTER,
spacing=5,
tight=True,
),
bgcolor=config["bgcolor"],
border_radius=5,
padding=ft.Padding(5, 2, 5, 2),
width=75,
height=26,
alignment=ft.alignment.Alignment.CENTER,
)

return ft.Container(
content=ft.Text(config["text"], color=config["text_color"], size=12, weight=ft.FontWeight.BOLD),
bgcolor=config["bgcolor"],
Expand Down Expand Up @@ -219,9 +245,11 @@ async def update_card(self, recording):
title_row.controls[1] = new_status_label
else:
title_row.controls.append(new_status_label)
recording_card["status_label"] = new_status_label
else:
if len(title_row.controls) > 1:
title_row.controls.pop()
recording_card["status_label"] = None

if recording_card.get("duration_label"):
recording_card["duration_label"].value = self.app.record_manager.get_duration(recording)
Expand Down Expand Up @@ -412,9 +440,17 @@ async def update_duration(self, recording: Recording):

if recording.is_recording:
try:
duration_label = self.cards_obj[recording.rec_id]["duration_label"]
card_data = self.cards_obj[recording.rec_id]
duration_label = card_data["duration_label"]
duration_label.value = self.app.record_manager.get_duration(recording)
duration_label.update()

# Pulse recording dot
status_label = card_data.get("status_label")
if status_label and isinstance(status_label.content, ft.Row):
dot = status_label.content.controls[0]
dot.opacity = 0 if dot.opacity == 1 else 1
dot.update()
except (ft.FletPageDisconnectedException, AssertionError) as e:
logger.debug(f"Update duration failed: {e}")
break
Expand Down
2 changes: 2 additions & 0 deletions app/ui/components/business/recording_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ async def update_format_options(e):
label=self._["input_live_link"],
hint_text=self._["example"] + ":https://www.example.com/xxxxxx",
border_radius=5,
autofocus=True,
filled=False,
expand=True,
value=initial_values.get("url"),
on_change=on_url_change,
on_submit=on_confirm,
)

streamer_name_field = ft.TextField(
Expand Down
2 changes: 2 additions & 0 deletions app/ui/components/dialogs/search_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ def __init__(self, recordings_page, on_close=None):
self.query = ft.TextField(
hint_text=self._["search_keyword"],
expand=True,
autofocus=True,
border_radius=5,
border_color=ft.Colors.GREY_400,
focused_border_color=ft.Colors.PRIMARY,
hint_style=ft.TextStyle(color=ft.Colors.GREY_500, size=14),
on_submit=self.submit_query,
)
self.actions = [
ft.TextButton(
Expand Down
Loading