diff --git a/web.py b/web.py index 09b5ea9..cddb216 100644 --- a/web.py +++ b/web.py @@ -44,6 +44,15 @@ def _get_history_text() -> str: # ── Page ────────────────────────────────────────────────────────────────────── +ui.add_css(''' +.q-focus-helper { display: none !important; } +.q-btn:focus-visible, +.q-checkbox:focus-within, +.q-slider:focus-within, +.q-select:focus-within { outline: 2px solid currentColor !important; outline-offset: 2px !important; } +''', shared=True) + + @ui.page("/") async def index(): # Per-connection episode state @@ -97,24 +106,15 @@ async def index(): skip_downloaded_toggle = ui.checkbox( "Skip already downloaded", value=False, ).classes("text-sm") - episode_grid = ui.aggrid({ - "columnDefs": [ - { - "headerName": "", - "checkboxSelection": True, - "headerCheckboxSelection": True, - "width": 50, - "suppressSizeToFit": True, - }, - {"headerName": "Episode", "field": "label", "flex": 1}, - {"headerName": "Season", "field": "season", "width": 130}, + episode_table = ui.table( + columns=[ + {"name": "label", "label": "Episode", "field": "label", "align": "left"}, + {"name": "season", "label": "Season", "field": "season", "align": "left"}, ], - "rowData": [], - "rowSelection": "multiple", - "suppressRowClickSelection": False, - "domLayout": "normal", - "suppressHorizontalScroll": True, - }).classes("w-full").style("height: 400px") + rows=[], + row_key="url", + selection="multiple", + ).classes("w-full").style("max-height: 400px; overflow-y: auto") # Download options (always visible) options_section = ui.row().classes("w-full gap-6 items-center") @@ -124,17 +124,11 @@ async def index(): value=storage.get("quality", "1080p"), label="Quality", ).classes("w-28") - with ui.column().classes("flex-1 gap-0"): - threads_label = ui.label( - f"Parallel downloads: {storage.get('threads', 1)}" - ).classes("text-sm text-gray-500") - threads_slider = ui.slider( - min=1, max=8, step=1, - value=storage.get("threads", 1), - ).classes("w-full").on( - "update:model-value", - lambda e: threads_label.set_text(f"Parallel downloads: {int(e.args)}"), - ) + threads_input = ui.number( + label="Parallel downloads", + min=1, max=8, step=1, + value=storage.get("threads", 1), + ).classes("w-40") # Action buttons with ui.row().classes("gap-2 flex-wrap"): @@ -162,10 +156,10 @@ async def index(): # ── Helper functions ────────────────────────────────────────────────────── - async def _update_count(): - rows = await episode_grid.get_selected_rows() - visible = episode_grid.options.get("rowData", []) - episode_count_label.set_text(f"{len(rows)} / {len(visible)} episodes selected") + def _update_count(): + visible = episode_table.rows + selected = episode_table.selected + episode_count_label.set_text(f"{len(selected)} / {len(visible)} episodes selected") def _build_row_data(episodes_with_labels: list[tuple[str, str]]) -> list[dict]: enriched = [] @@ -199,13 +193,13 @@ def _apply_filters(): downloaded = _get_downloaded_urls() filtered = [r for r in filtered if r["url"] not in downloaded] - episode_grid.options["rowData"] = filtered - episode_grid.update() - ui.timer(0.1, lambda: episode_grid.run_grid_method("selectAll"), once=True) - episode_count_label.set_text(f"{len(filtered)} / {len(filtered)} episodes selected") + episode_table.rows = filtered + episode_table.selected = list(filtered) + episode_table.update() + _update_count() def _show_episodes(row_data: list[dict]): - """Populate the episode grid and reveal the download panel.""" + """Populate the episode table and reveal the download panel.""" all_row_data.clear() all_row_data.extend(row_data) @@ -291,7 +285,7 @@ def handle_season_filter(_e=None): _apply_filters() async def handle_download(): - selected_rows = await episode_grid.get_selected_rows() + selected_rows = episode_table.selected if not selected_rows: ui.notify("No episodes selected.", type="warning") return @@ -300,7 +294,7 @@ async def handle_download(): urls = [r["url"] for r in selected_rows] total = len(urls) quality = quality_select.value - threads = int(threads_slider.value) + threads = int(threads_input.value or 1) cfg = config.config.model_copy(update={"quality": quality}) db_path = f"{cfg.config_directory}/db.json" @@ -403,15 +397,19 @@ def handle_force_abort(): load_btn.on_click(handle_load_episodes) season_select.on("update:model-value", handle_season_filter) skip_downloaded_toggle.on("update:model-value", lambda _: _apply_filters()) - select_all_btn.on_click(lambda: ( - episode_grid.run_grid_method("selectAll"), - asyncio.ensure_future(_update_count()), - )) - deselect_all_btn.on_click(lambda: ( - episode_grid.run_grid_method("deselectAll"), - asyncio.ensure_future(_update_count()), - )) - episode_grid.on("selectionChanged", _update_count) + def handle_select_all(): + episode_table.selected = list(episode_table.rows) + episode_table.update() + _update_count() + + def handle_deselect_all(): + episode_table.selected = [] + episode_table.update() + _update_count() + + select_all_btn.on_click(handle_select_all) + deselect_all_btn.on_click(handle_deselect_all) + episode_table.on("selection", lambda: _update_count()) download_btn.on_click(handle_download) abort_btn.on_click(handle_abort) force_abort_btn.on_click(handle_force_abort)