From d44c8076b496d9201e97459b8f10a9993db7f632 Mon Sep 17 00:00:00 2001 From: shmyga Date: Fri, 4 Sep 2026 15:42:04 +0300 Subject: [PATCH] feat(schedule): add tv schedule group page --- .../easel/route/view/common/utils/template.py | 10 ++++ gallery/easel/route/view/schedule/__init__.py | 21 +++++++ .../view/schedule/templates/channel.html | 19 ++++-- .../route/view/schedule/templates/group.html | 59 +++++++++++++++++++ static/src/app/schedule.ts | 29 ++++++++- static/src/lib/bootstrap-icons.scss | 3 + 6 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 gallery/easel/route/view/schedule/templates/group.html diff --git a/gallery/easel/route/view/common/utils/template.py b/gallery/easel/route/view/common/utils/template.py index 290966d..c724e45 100644 --- a/gallery/easel/route/view/common/utils/template.py +++ b/gallery/easel/route/view/common/utils/template.py @@ -1,6 +1,7 @@ import datetime import typing from pathlib import Path +from urllib.parse import parse_qsl, urlencode, urlsplit from babel.dates import format_date from fastapi import Request @@ -28,6 +29,14 @@ def base_context_processor(request: Request) -> dict: } +def build_url(request: Request, query: dict | None = None) -> str: + parts = urlsplit(str(request.url)) + if query: + query_dict = {**dict(parse_qsl(parts.query)), **query} + parts = parts._replace(query=urlencode(query_dict)) + return parts.geturl() + + def build_templates( templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None ) -> Jinja2Templates: @@ -46,6 +55,7 @@ def build_templates( "datetime": datetime, "tag_util": TagUtil, "DATE_FORMAT": "E, d MMMM Y", + "build_url": build_url, } ) if filters: diff --git a/gallery/easel/route/view/schedule/__init__.py b/gallery/easel/route/view/schedule/__init__.py index 5762552..518039d 100644 --- a/gallery/easel/route/view/schedule/__init__.py +++ b/gallery/easel/route/view/schedule/__init__.py @@ -68,3 +68,24 @@ async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, "response": response, }, ) + + +@router.get("/group", response_class=HTMLResponse) +async def get_group(request: AppRequest, ids: str, date: datetime.date | None = None, tag: str | None = None): + date = date or datetime.date.today() + if tag: + date = TagUtil.parse_tag(tag).date + channels = [item.split(":") for item in ids.split(" ")] + responses = [ + await request.app.state.api.get_api(ScheduleApi, provider).get_schedule(channel_id, date) + for provider, channel_id in channels + ] + return templates.TemplateResponse( + request=request, + name="group.html", + context={ + "tag_util": TagUtil, + "datetime": datetime, + "responses": responses, + }, + ) diff --git a/gallery/easel/route/view/schedule/templates/channel.html b/gallery/easel/route/view/schedule/templates/channel.html index 7ffec18..bfb67b8 100644 --- a/gallery/easel/route/view/schedule/templates/channel.html +++ b/gallery/easel/route/view/schedule/templates/channel.html @@ -1,4 +1,7 @@ {% extends "base.html" %} +{% set live = request.query_params.live == '1' %} +{% set date = response.date %} + {% block title %} {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}} @@ -10,19 +13,23 @@ locale=request.state.language)}} {% endblock %} {% block content %} -

- +

+ + href="."> + + + {{provider}} {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}} + href="{{build_url(request, query={'date': date + datetime.timedelta(days=1)})}}">

@@ -35,7 +42,7 @@ locale=request.state.language)}} {% for value in response.values %} - + {{value.start.strftime('%H:%M')}} ({{(value.end - value.start) | timedelta_format}}) diff --git a/gallery/easel/route/view/schedule/templates/group.html b/gallery/easel/route/view/schedule/templates/group.html new file mode 100644 index 0000000..a4a55c5 --- /dev/null +++ b/gallery/easel/route/view/schedule/templates/group.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% set live = request.query_params.live == '1' %} +{% set date = responses[0].date %} + +{% block title %} +{{_("TV program")}} +{% endblock %} + +{% block header %} +{{_("TV program")}} +{% endblock %} + +{% block content %} +

+ + + + + + + + + + {{format_date(date, DATE_FORMAT, locale=request.state.language)}} + + + +

+{% for response in responses %} +
+ {{response.channel.provider}} + {{response.channel.name}} +
+ + + + + + + + + {% for value in response.values %} + + + + + {% endfor %} + +
+ {{value.start.strftime('%H:%M')}} + ({{(value.end - value.start) | timedelta_format}}) + {{value.label}}
+{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/static/src/app/schedule.ts b/static/src/app/schedule.ts index 5db8d8f..926addf 100644 --- a/static/src/app/schedule.ts +++ b/static/src/app/schedule.ts @@ -37,6 +37,9 @@ export class ScheduleChannelManager { element.setAttribute("removable", ""); container.appendChild(element); } + const groupElement = new ScheduleChannelGroupElement(); + groupElement.setAttribute("channels", JSON.stringify(channels)); + container.append(groupElement); } } } @@ -58,10 +61,10 @@ class ScheduleChannelElement extends HTMLElement { + ${this.channel?.provider} ${this.channel?.name} - ${this.channel?.provider} ✕ @@ -99,6 +102,30 @@ class ScheduleChannelElement extends HTMLElement { customElements.define("schedule-channel", ScheduleChannelElement); +class ScheduleChannelGroupElement extends HTMLElement { + static observedAttributes = ["channels"]; + + channels: Channel[] | null = null; + + constructor() { + super(); + } + + connectedCallback() { + this.channels = JSON.parse(this.getAttribute("channels") || "[]"); + const ids = this.channels?.map((item) => `${item.provider}:${item.id}`).join("+"); + this.innerHTML = ` + + + Open All + + `; + this.classList = "list-group-item list-group-item-action"; + } +} +customElements.define("schedule-channel-group", ScheduleChannelGroupElement); + declare global { interface Window { scheduleChannelStorage: ScheduleChannelStorage; diff --git a/static/src/lib/bootstrap-icons.scss b/static/src/lib/bootstrap-icons.scss index 3dfbd0a..a84c2e9 100644 --- a/static/src/lib/bootstrap-icons.scss +++ b/static/src/lib/bootstrap-icons.scss @@ -45,4 +45,7 @@ &.bi-file-earmark-text { mask-image: url(bootstrap-icons/icons/file-earmark-text.svg); } + &.bi-cast { + mask-image: url(bootstrap-icons/icons/cast.svg); + } }