12 Commits

Author SHA1 Message Date
fc59e52337 ci(version): 0.3.2 2026-06-20 18:59:43 +03:00
3e93a41400 fix(gismeteo): fix date-time row parsing 2026-06-20 18:59:27 +03:00
c2cd18386b refactor(easel): update api router 2026-06-16 21:18:16 +03:00
2bca3dd75a ci(version): 0.3.1 2026-06-16 20:17:37 +03:00
469bd9bc1f feat(easel): add version to header 2026-06-16 20:17:12 +03:00
027d1e2d55 build(docker): add docker build caches mount 2026-06-16 20:01:29 +03:00
7cf0012229 feat(schedule): update navigate icons 2026-06-16 20:00:46 +03:00
edc014d98c docs: update screenshot 2026-06-15 23:37:31 +03:00
1813ec213b ci(version): 0.3.0 2026-06-15 23:19:57 +03:00
1b700086f2 feat(weather): update navigate icons 2026-06-15 23:18:48 +03:00
02a6ffc931 feat(weather): new weather icons 2026-06-15 11:22:41 +03:00
2dfedfea57 fix(easel): fix header icons 2026-06-15 11:22:28 +03:00
25 changed files with 5391 additions and 5333 deletions

View File

@@ -7,7 +7,9 @@ WORKDIR /app
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -sSL https://install.python-poetry.org | python3 -
COPY pyproject.toml poetry.lock README.md ./ COPY pyproject.toml poetry.lock README.md ./
RUN poetry config virtualenvs.in-project true RUN poetry config virtualenvs.in-project true
RUN poetry install --with app --no-root RUN --mount=type=cache,target=/root/.cache/pypoetry/cache \
--mount=type=cache,target=/root/.cache/pypoetry/artifacts \
poetry install --with app --no-root
COPY locales ./locales COPY locales ./locales
RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po
@@ -15,7 +17,8 @@ FROM node:24 AS node-builder
ENV PATH=/app/node_modules/.bin:$PATH ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app WORKDIR /app
COPY static/package.json static/package-lock.json ./ COPY static/package.json static/package-lock.json ./
RUN npm ci RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY static ./ COPY static ./
RUN npm run build RUN npm run build
@@ -27,7 +30,6 @@ ENV TZ="Europe/Moscow"
COPY --from=builder /app ./ COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/ COPY gallery gallery/
#COPY --from=builder /app/gallery/easel/route/view/locales /app/gallery/easel/route/view/locales
COPY --from=builder --parents locales/**/*.mo ./ COPY --from=builder --parents locales/**/*.mo ./
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"] CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -4,8 +4,7 @@ from fastapi.staticfiles import StaticFiles
from gallery.sketch.bundle import ApiBundle from gallery.sketch.bundle import ApiBundle
from gallery.util import root_path from gallery.util import root_path
from .route import api, doc from .route import api, doc, view
from .route.view import router as view_router
def build_app(api_bundle: ApiBundle) -> FastAPI: def build_app(api_bundle: ApiBundle) -> FastAPI:
@@ -17,6 +16,6 @@ def build_app(api_bundle: ApiBundle) -> FastAPI:
app.state.api = api_bundle app.state.api = api_bundle
app.mount("/static", StaticFiles(directory=root_path / "static/dist")) app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
doc.mount(app) doc.mount(app)
api.mount(app) app.include_router(api.router)
app.include_router(view_router) app.include_router(view.router)
return app return app

View File

@@ -1,8 +1,7 @@
from fastapi import FastAPI from fastapi import APIRouter
from . import schedule, weather from . import schedule, weather
router = APIRouter(prefix="/api", tags=["API"])
def mount(app: FastAPI): router.include_router(weather.router)
weather.mount(app) router.include_router(schedule.router)
schedule.mount(app)

View File

@@ -1,18 +1,20 @@
import datetime import datetime
from fastapi import FastAPI from fastapi import APIRouter
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.sketch.schedule.model import ChannelId, Schedule from gallery.sketch.schedule.model import ChannelId, Schedule
router = APIRouter(prefix="/schedule")
def mount(app: FastAPI):
@app.get("/api/schedule/channels", tags=["API"]) @router.get("/channels")
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]: async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]:
schedule_api = request.app.state.api.schedule schedule_api = request.app.state.api.schedule
return await schedule_api.get_channels() return await schedule_api.get_channels()
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
async def get_api_schedule_channel_schedule(request: AppRequest, channel: str, date: datetime.date) -> Schedule: @router.get("/{channel}/{date}")
async def get_api_schedule_channel_schedule(request: AppRequest, channel: str, date: datetime.date) -> Schedule:
schedule_api = request.app.state.api.schedule schedule_api = request.app.state.api.schedule
return await schedule_api.get_channel_schedule(ChannelId(channel), date) return await schedule_api.get_channel_schedule(ChannelId(channel), date)

View File

@@ -1,23 +1,26 @@
import datetime import datetime
from fastapi import FastAPI from fastapi import APIRouter
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.sketch.weather.model import Location, WeatherResponse from gallery.sketch.weather.model import Location, WeatherResponse
router = APIRouter(prefix="/weather")
def mount(app: FastAPI):
@app.get("/api/weather/locations", tags=["API"]) @router.get("/locations")
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]: async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]:
weather_api = request.app.state.api.weather weather_api = request.app.state.api.weather
return await weather_api.find_locations(query) return await weather_api.find_locations(query)
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
async def get_api_weather_day(request: AppRequest, location: str, date: datetime.date) -> WeatherResponse: @router.get("/{location}/day/{date}")
async def get_api_weather_day(request: AppRequest, location: str, date: datetime.date) -> WeatherResponse:
weather_api = request.app.state.api.weather weather_api = request.app.state.api.weather
return await weather_api.get_day(location, date) return await weather_api.get_day(location, date)
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
async def get_api_weather_days(request: AppRequest, location: str, days: int) -> WeatherResponse: @router.get("/{location}/days/{days}")
async def get_api_weather_days(request: AppRequest, location: str, days: int) -> WeatherResponse:
weather_api = request.app.state.api.weather weather_api = request.app.state.api.weather
return await weather_api.get_days(location, days) return await weather_api.get_days(location, days)

View File

@@ -5,7 +5,7 @@ from .schedule import router as schedule_router
from .translation import set_language from .translation import set_language
from .weather import router as weather_router from .weather import router as weather_router
router = APIRouter(dependencies=[Depends(set_language)]) router = APIRouter(tags=["view"], dependencies=[Depends(set_language)])
router.include_router(common_router) router.include_router(common_router)
router.include_router(weather_router) router.include_router(weather_router)
router.include_router(schedule_router) router.include_router(schedule_router)

View File

@@ -30,6 +30,9 @@
{% block header %}{% endblock %} {% block header %}{% endblock %}
</div> </div>
<ul class="navbar-nav flex-row flex-wrap ms-md-auto"> <ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<li class="nav-item me-2">
<span class="nav-link">{{ version }}</span>
</li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center" <button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
id="bd-language" id="bd-language"
@@ -37,7 +40,7 @@
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="{{_('Select language')}} (default)"> aria-label="{{_('Select language')}} (default)">
<span class="fi fir fi-gb me-2 language-icon-active"></span> <span class="fi fir fi-gb me-2 language-icon-active icon-header"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-language-text">{{_("Select language")}}</span> id="bd-language-text">{{_("Select language")}}</span>
</button> </button>
@@ -70,7 +73,7 @@
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"> aria-label="Toggle theme (auto)">
<span class="bi bi-circle-half me-2 opacity-50 theme-icon-active"></span> <span class="bi bi-circle-half me-2 opacity-50 theme-icon-active icon-header"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-theme-text">{{_("Toggle theme")}}</span> id="bd-theme-text">{{_("Toggle theme")}}</span>
</button> </button>

View File

@@ -1,3 +1,4 @@
import datetime
from pathlib import Path from pathlib import Path
from babel.dates import format_date from babel.dates import format_date
@@ -7,6 +8,7 @@ from fastapi.templating import Jinja2Templates
from gallery.version import __version__ from gallery.version import __version__
from ...translation import _ from ...translation import _
from .tag import TagUtil
def is_widget(request: Request) -> bool: def is_widget(request: Request) -> bool:
@@ -31,6 +33,9 @@ def build_templates(templates_dir: Path | None = None, filters: dict | None = No
"_": _, "_": _,
"version": __version__, "version": __version__,
"format_date": format_date, "format_date": format_date,
"datetime": datetime,
"tag_util": TagUtil,
"DATE_FORMAT": "E, d MMMM Y",
} }
) )
if filters: if filters:

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}} {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -10,13 +11,19 @@
{% block content %} {% block content %}
<h4> <h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="button" <i class="bi bi-arrow-left-square"></i>
href="../..">⬆️</a> </a>
<span>{{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <a class="icon-link"
<a class="button" href="../..">
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> <i class="bi bi-arrow-up-square"></i>
</a>
<span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
</h4> </h4>
<table class="table"> <table class="table">
<thead> <thead>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}} {{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -10,13 +11,20 @@
{% block content %} {% block content %}
<h4> <h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="button" <i class="bi bi-arrow-left-square"></i>
href="..">⬆️</a> </a>
<span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <a class="icon-link"
<a class="button" href="..">
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> <i class="bi bi-arrow-up-square"></i>
</a>
<span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
</h4> </h4>
<div> <div>
<table class="table"> <table class="table">

View File

@@ -25,8 +25,6 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
request=request, request=request,
name="weather.html", name="weather.html",
context={ context={
"tag_util": TagUtil,
"datetime": datetime,
"response": response, "response": response,
}, },
) )

View File

@@ -1,6 +1,12 @@
import datetime import datetime
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection, WindDirectionDeg from gallery.sketch.weather.model import (
Cloudness,
Precipitation,
Sky,
WindDirection,
WindDirectionDeg,
)
def wind_direction_icon(wind_direction_deg: float) -> str: def wind_direction_icon(wind_direction_deg: float) -> str:

View File

@@ -9,16 +9,24 @@
{% block content %} {% block content %}
<h4> <h4>
{% if response.period == 'day' %} {% if response.period == 'day' %}
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="button" <i class="bi bi-arrow-left-square"></i>
href="../tag/days-10">⬆️</a> </a>
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <a class="icon-link"
<a class="button" href="../tag/days-10">
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> <i class="bi bi-arrow-up-square"></i>
</a>
<span>{{response.location}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
{% endif %} {% endif %}
{% if response.period == 'days' %} {% if response.period == 'days' %}
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <span>{{response.location}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<span>- {{format_date(response.date + datetime.timedelta(days=(response.values | length - 1)), DATE_FORMAT,
locale=request.state.language)}}</span>
{% endif %} {% endif %}
</h4> </h4>
<div class="table-responsive"> <div class="table-responsive">
@@ -58,8 +66,7 @@
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="{{ value.sky }}"> data-bs-title="{{ value.sky }}">
{% for icon in value.sky | cloudness_icon(value.date, response.period) %} {% for icon in value.sky | cloudness_icon(value.date, response.period) %}
<div class="wi wi-{{icon}} text-primary" <div class="wi wi-{{icon}} wi-xl text-primary"></div>
style="font-size: 3.5rem;"></div>
{% endfor %} {% endfor %}
</td> </td>
{% endfor %} {% endfor %}
@@ -95,7 +102,7 @@
<td class="wind" <td class="wind"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="{{ value.wind_direction }}"> data-bs-title="{{ value.wind_direction }}">
<div class="wi wi-wind-deg wi-{{value.wind_direction | wind_direction_icon}} fs-1 text-primary"></div> <div class="wi wi-wind-deg wi-{{value.wind_direction | wind_direction_icon}} wi-l text-primary"></div>
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>

View File

@@ -39,14 +39,11 @@ class DateParser(RowParser[datetime.datetime]):
KEY = "date" KEY = "date"
def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]: def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]:
datetime_date_tag = tag.select_one(".widget-row.widget-row-datetime-date > .row-item") datetime_time_row = tag.select_one(".widget-row.widget-row-datetime-time")
if datetime_date_tag: if datetime_time_row:
date_str = datetime_date_tag.find(text=True, recursive=False).text for item in datetime_time_row.select(".row-item > time-value"):
date = dateparser.parse(date_str, languages=["ru"]) timestamp = int(item.attrs["timestamp"])
for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"): time = datetime.datetime.fromtimestamp(timestamp)
time_str = item.text
time = dateparser.parse(time_str, languages=["ru"])
time = time.replace(year=date.year, month=date.month, day=date.day)
yield time yield time
else: else:
for item in tag.select(".widget-row.widget-row-date > .row-item"): for item in tag.select(".widget-row.widget-row-date > .row-item"):

View File

@@ -1,5 +1,3 @@
__version__ = "0.2.2"
import tomllib import tomllib
from pathlib import Path from pathlib import Path

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "gallery" name = "gallery"
version = "0.2.3" version = "0.3.2"
description = "" description = ""
authors = ["shmyga <shmyga.z@gmail.com>"] authors = ["shmyga <shmyga.z@gmail.com>"]
readme = "README.md" readme = "README.md"

View File

@@ -1,12 +1,12 @@
{ {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.3.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.3.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

View File

@@ -1,6 +1,6 @@
{ {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.3.2",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite build --watch" "dev": "vite build --watch"

View File

@@ -1,4 +1,6 @@
//$bootstrap-icons: "circle-half", "moon-stars-fill", "brightness-high", "gear", "sun-fill", "tv"; .icon-link {
vertical-align: -0.25rem;
}
.bi { .bi {
display: inline-block; display: inline-block;
@@ -13,12 +15,6 @@
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: currentColor; background-color: currentColor;
//@each $icon in $bootstrap-icons {
// &.bi-#{$icon} {
// mask-image: url("bootstrap-icons/icons/#{$icon}.svg");
// }
///
&.bi-circle-half { &.bi-circle-half {
mask-image: url(bootstrap-icons/icons/circle-half.svg); mask-image: url(bootstrap-icons/icons/circle-half.svg);
} }
@@ -37,4 +33,13 @@
&.bi-tv { &.bi-tv {
mask-image: url(bootstrap-icons/icons/tv.svg); mask-image: url(bootstrap-icons/icons/tv.svg);
} }
&.bi-arrow-left-square {
mask-image: url(bootstrap-icons/icons/arrow-left-square.svg);
}
&.bi-arrow-right-square {
mask-image: url(bootstrap-icons/icons/arrow-right-square.svg);
}
&.bi-arrow-up-square {
mask-image: url(bootstrap-icons/icons/arrow-up-square.svg);
}
} }

View File

@@ -12,4 +12,6 @@
.fir { .fir {
@extend .fis; @extend .fis;
border-radius: 50%; border-radius: 50%;
height: 1em;
border: 1px solid gray;
} }

View File

@@ -13,4 +13,11 @@
mask-position: 50%; mask-position: 50%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: currentColor; background-color: currentColor;
&.wi-l {
font-size: 2.5rem;
}
&.wi-xl {
font-size: 3.5rem;
}
} }

View File

@@ -12,11 +12,8 @@
} }
} }
.icon { .icon-header {
display: inline-block; @include font-size($h4-font-size);
width: 2rem;
height: 2rem;
background-size: contain;
} }
.app-header { .app-header {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long