Skip to content
Open
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
94 changes: 46 additions & 48 deletions web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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"):
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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"

Expand Down Expand Up @@ -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)
Expand Down