feat(schedule): add tv schedule group page
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import typing
|
import typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import parse_qsl, urlencode, urlsplit
|
||||||
|
|
||||||
from babel.dates import format_date
|
from babel.dates import format_date
|
||||||
from fastapi import Request
|
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(
|
def build_templates(
|
||||||
templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None
|
templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None
|
||||||
) -> Jinja2Templates:
|
) -> Jinja2Templates:
|
||||||
@@ -46,6 +55,7 @@ def build_templates(
|
|||||||
"datetime": datetime,
|
"datetime": datetime,
|
||||||
"tag_util": TagUtil,
|
"tag_util": TagUtil,
|
||||||
"DATE_FORMAT": "E, d MMMM Y",
|
"DATE_FORMAT": "E, d MMMM Y",
|
||||||
|
"build_url": build_url,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if filters:
|
if filters:
|
||||||
|
|||||||
@@ -68,3 +68,24 @@ async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends,
|
|||||||
"response": response,
|
"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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% set live = request.query_params.live == '1' %}
|
||||||
|
{% set date = response.date %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
|
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
|
||||||
locale=request.state.language)}}
|
locale=request.state.language)}}
|
||||||
@@ -10,19 +13,23 @@ locale=request.state.language)}}
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h4>
|
<h4 class="mb-4">
|
||||||
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<a class="icon-link {{'disabled' if date == datetime.date.today() else ''}}"
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
|
href="{{build_url(request, query={'date': date - datetime.timedelta(days=1)})}}">
|
||||||
<i class="bi bi-arrow-left-square"></i>
|
<i class="bi bi-arrow-left-square"></i>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-link"
|
<a class="icon-link"
|
||||||
href="../../..">
|
href=".">
|
||||||
<i class="bi bi-arrow-up-square"></i>
|
<i class="bi bi-arrow-up-square"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="icon-link {{'text-secondary' if not live else ''}}"
|
||||||
|
href="{{build_url(request, query={'live': 0 if live else 1})}}">
|
||||||
|
<i class="bi bi-cast"></i>
|
||||||
|
</a>
|
||||||
<span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span>
|
<span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span>
|
||||||
<span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
|
<span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
|
||||||
<a class="icon-link"
|
<a class="icon-link"
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
|
href="{{build_url(request, query={'date': date + datetime.timedelta(days=1)})}}">
|
||||||
<i class="bi bi-arrow-right-square"></i>
|
<i class="bi bi-arrow-right-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
@@ -35,7 +42,7 @@ locale=request.state.language)}}
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<tr class="{{'table-success' if value.live else ''}}">
|
<tr class="{{'d-none' if (live and not value.live) else 'table-success' if (not live and value.live) else ''}}">
|
||||||
<td>
|
<td>
|
||||||
<span>{{value.start.strftime('%H:%M')}}</span>
|
<span>{{value.start.strftime('%H:%M')}}</span>
|
||||||
<span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span>
|
<span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
<app-link href="/schedule"
|
||||||
|
icon="tv">{{_("TV program")}}</app-link>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h4 class="mb-4">
|
||||||
|
<a class="icon-link {{'disabled' if date == datetime.date.today() else ''}}"
|
||||||
|
href="{{build_url(request, query={'date': date - datetime.timedelta(days=1)})}}">
|
||||||
|
<i class="bi bi-arrow-left-square"></i>
|
||||||
|
</a>
|
||||||
|
<a class="icon-link"
|
||||||
|
href=".">
|
||||||
|
<i class="bi bi-arrow-up-square"></i>
|
||||||
|
</a>
|
||||||
|
<a class="icon-link {{'text-secondary' if not live else ''}}"
|
||||||
|
href="{{build_url(request, query={'live': 0 if live else 1})}}">
|
||||||
|
<i class="bi bi-cast"></i>
|
||||||
|
</a>
|
||||||
|
<span>{{format_date(date, DATE_FORMAT, locale=request.state.language)}}</span>
|
||||||
|
<a class="icon-link"
|
||||||
|
href="{{build_url(request, query={'date': date + datetime.timedelta(days=1)})}}">
|
||||||
|
<i class="bi bi-arrow-right-square"></i>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
{% for response in responses %}
|
||||||
|
<h5>
|
||||||
|
<span class="badge rounded-pill text-bg-secondary fs-6">{{response.channel.provider}}</span>
|
||||||
|
<span>{{response.channel.name}}</span>
|
||||||
|
</h5>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for value in response.values %}
|
||||||
|
<tr class="{{'d-none' if (live and not value.live) else 'table-success' if (not live and value.live) else ''}}">
|
||||||
|
<td>
|
||||||
|
<span>{{value.start.strftime('%H:%M')}}</span>
|
||||||
|
<span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span>
|
||||||
|
</td>
|
||||||
|
<td>{{value.label}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
@@ -37,6 +37,9 @@ export class ScheduleChannelManager {
|
|||||||
element.setAttribute("removable", "");
|
element.setAttribute("removable", "");
|
||||||
container.appendChild(element);
|
container.appendChild(element);
|
||||||
}
|
}
|
||||||
|
const groupElement = new ScheduleChannelGroupElement();
|
||||||
|
groupElement.setAttribute("channels", JSON.stringify(channels));
|
||||||
|
container.append(groupElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,10 +61,10 @@ class ScheduleChannelElement extends HTMLElement {
|
|||||||
<a href="${this.channel?.provider}/${this.channel?.id}"
|
<a href="${this.channel?.provider}/${this.channel?.id}"
|
||||||
class="d-flex justify-content-between align-items-start text-decoration-none">
|
class="d-flex justify-content-between align-items-start text-decoration-none">
|
||||||
<span>
|
<span>
|
||||||
|
<span class="badge me-1 text-bg-secondary">${this.channel?.provider}</span>
|
||||||
<span class="text-primary">${this.channel?.name}</span>
|
<span class="text-primary">${this.channel?.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="badge text-bg-secondary">${this.channel?.provider}</span>
|
|
||||||
<span class="text-danger ms-2" data-remove>
|
<span class="text-danger ms-2" data-remove>
|
||||||
✕
|
✕
|
||||||
</span>
|
</span>
|
||||||
@@ -99,6 +102,30 @@ class ScheduleChannelElement extends HTMLElement {
|
|||||||
|
|
||||||
customElements.define("schedule-channel", ScheduleChannelElement);
|
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 = `
|
||||||
|
<a href="group?ids=${ids}"
|
||||||
|
class="d-flex justify-content-between align-items-start text-decoration-none">
|
||||||
|
<span>
|
||||||
|
<span class="text-primary">Open All</span>
|
||||||
|
</span>
|
||||||
|
</a>`;
|
||||||
|
this.classList = "list-group-item list-group-item-action";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define("schedule-channel-group", ScheduleChannelGroupElement);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
scheduleChannelStorage: ScheduleChannelStorage;
|
scheduleChannelStorage: ScheduleChannelStorage;
|
||||||
|
|||||||
Vendored
+3
@@ -45,4 +45,7 @@
|
|||||||
&.bi-file-earmark-text {
|
&.bi-file-earmark-text {
|
||||||
mask-image: url(bootstrap-icons/icons/file-earmark-text.svg);
|
mask-image: url(bootstrap-icons/icons/file-earmark-text.svg);
|
||||||
}
|
}
|
||||||
|
&.bi-cast {
|
||||||
|
mask-image: url(bootstrap-icons/icons/cast.svg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user