1 Commits

Author SHA1 Message Date
a715bcca89 feat(weather): new weather icons 2026-06-14 18:23:11 +03:00
25 changed files with 5396 additions and 5458 deletions

View File

@@ -2,14 +2,12 @@ FROM python:3.12 AS builder
ENV POETRY_HOME="/opt/poetry" ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH" ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt update && \ RUN apt update && \
apt install -y gettext apt install -y gettext
WORKDIR /app 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 --mount=type=cache,target=/root/.cache/pypoetry/cache \ RUN poetry install --with app --no-root
--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
@@ -17,8 +15,7 @@ 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 --mount=type=cache,target=/root/.npm \ RUN npm ci
npm ci
COPY static ./ COPY static ./
RUN npm run build RUN npm run build
@@ -30,6 +27,7 @@ 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: 182 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -4,7 +4,8 @@ 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, view from .route import api, doc
from .route.view import router as view_router
def build_app(api_bundle: ApiBundle) -> FastAPI: def build_app(api_bundle: ApiBundle) -> FastAPI:
@@ -16,6 +17,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)
app.include_router(api.router) api.mount(app)
app.include_router(view.router) app.include_router(view_router)
return app return app

View File

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

View File

@@ -1,20 +1,18 @@
import datetime import datetime
from fastapi import APIRouter from fastapi import FastAPI
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"])
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]:
schedule_api = request.app.state.api.schedule
return await schedule_api.get_channels()
@router.get("/channels") @app.get("/api/schedule/{channel}/{date}", tags=["API"])
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]: 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_channels() return await schedule_api.get_channel_schedule(ChannelId(channel), date)
@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
return await schedule_api.get_channel_schedule(ChannelId(channel), date)

View File

@@ -1,26 +1,23 @@
import datetime import datetime
from fastapi import APIRouter from fastapi import FastAPI
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"])
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]:
weather_api = request.app.state.api.weather
return await weather_api.find_locations(query)
@router.get("/locations") @app.get("/api/weather/{location}/day/{date}", tags=["API"])
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]: 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.find_locations(query) return await weather_api.get_day(location, date)
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
@router.get("/{location}/day/{date}") async def get_api_weather_days(request: AppRequest, location: str, days: int) -> WeatherResponse:
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_days(location, days)
return await weather_api.get_day(location, date)
@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
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(tags=["view"], dependencies=[Depends(set_language)]) router = APIRouter(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,9 +30,6 @@
{% 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"
@@ -40,7 +37,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 icon-header"></span> <span class="fi fir fi-gb me-2 language-icon-active"></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>
@@ -73,7 +70,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 icon-header"></span> <span class="bi bi-circle-half me-2 opacity-50 theme-icon-active"></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,4 +1,3 @@
import datetime
from pathlib import Path from pathlib import Path
from babel.dates import format_date from babel.dates import format_date
@@ -8,7 +7,6 @@ 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:
@@ -33,9 +31,6 @@ 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,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -11,19 +10,13 @@ locale=request.state.language)}}
{% block content %} {% block content %}
<h4> <h4>
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}"> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<i class="bi bi-arrow-left-square"></i> <a class="button"
</a> href="../..">⬆️</a>
<a class="icon-link" <span>{{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
href="../.."> <a class="button"
<i class="bi bi-arrow-up-square"></i> href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</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,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, DATE_FORMAT, {{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -11,20 +10,13 @@ locale=request.state.language)}}
{% block content %} {% block content %}
<h4> <h4>
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}"> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<i class="bi bi-arrow-left-square"></i> <a class="button"
</a> href="..">⬆️</a>
<a class="icon-link" <span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
href=".."> <a class="button"
<i class="bi bi-arrow-up-square"></i> href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</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,6 +25,8 @@ 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,12 +1,6 @@
import datetime import datetime
from gallery.sketch.weather.model import ( from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection, WindDirectionDeg
Cloudness,
Precipitation,
Sky,
WindDirection,
WindDirectionDeg,
)
def wind_direction_icon(wind_direction_deg: float) -> str: def wind_direction_icon(wind_direction_deg: float) -> str:
@@ -14,7 +8,7 @@ def wind_direction_icon(wind_direction_deg: float) -> str:
if wind_direction == WindDirection.CALM: if wind_direction == WindDirection.CALM:
return "wind-calm" return "wind-calm"
else: else:
return f"wind-from-{wind_direction.name.lower()}" return f"wind-from-{wind_direction.name.lower()}"
def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]: def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]:

View File

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

View File

@@ -39,11 +39,14 @@ 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_time_row = tag.select_one(".widget-row.widget-row-datetime-time") datetime_date_tag = tag.select_one(".widget-row.widget-row-datetime-date > .row-item")
if datetime_time_row: if datetime_date_tag:
for item in datetime_time_row.select(".row-item > time-value"): date_str = datetime_date_tag.find(text=True, recursive=False).text
timestamp = int(item.attrs["timestamp"]) date = dateparser.parse(date_str, languages=["ru"])
time = datetime.datetime.fromtimestamp(timestamp) for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"):
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"):
@@ -170,12 +173,8 @@ class PrecipitationParser(RowParser[float]):
KEY = "precipitation" KEY = "precipitation"
def parse_row(self, tag: Tag) -> Iterable[float]: def parse_row(self, tag: Tag) -> Iterable[float]:
for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item"): for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"):
value = item.select_one("precipitation-value") yield float(item.text.replace(",", "."))
if value:
yield float(value.attrs["value"])
else:
yield 0
class PressureParser(RowParser[list[int]]): class PressureParser(RowParser[list[int]]):

View File

@@ -1,3 +1,5 @@
__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.3.4" version = "0.2.3"
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.3.4", "version": "0.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery", "name": "gallery",
"version": "0.3.4", "version": "0.2.3",
"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.3.4", "version": "0.2.3",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite build --watch" "dev": "vite build --watch"

View File

@@ -1,6 +1,4 @@
.icon-link { //$bootstrap-icons: "circle-half", "moon-stars-fill", "brightness-high", "gear", "sun-fill", "tv";
vertical-align: -0.25rem;
}
.bi { .bi {
display: inline-block; display: inline-block;
@@ -15,6 +13,12 @@
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);
} }
@@ -33,13 +37,4 @@
&.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,6 +12,4 @@
.fir { .fir {
@extend .fis; @extend .fis;
border-radius: 50%; border-radius: 50%;
height: 1em;
border: 1px solid gray;
} }

View File

@@ -13,11 +13,4 @@
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,8 +12,11 @@
} }
} }
.icon-header { .icon {
@include font-size($h4-font-size); display: inline-block;
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