Compare commits

..
65 Commits
Author SHA1 Message Date
shmyga a19de3e01d ci(version): 0.5.4 2026-08-21 10:20:30 +03:00
shmyga f79f754f1b fix(weather): optional pressure and humidity in weather value 2026-08-21 10:19:52 +03:00
shmyga 555b7f4392 ci(version): 0.5.3 2026-08-14 20:59:10 +03:00
shmyga b412a9cee3 fix(static): add autoprefixer 2026-08-14 20:58:33 +03:00
shmyga b0245a1714 build(python): update pyproject.toml format 2026-07-30 15:18:28 +03:00
shmyga 91b530f84c build(docker): add DOCKER_TAG env variable 2026-07-29 11:31:27 +03:00
shmyga 2b1dfad929 build(npm): add babel and postcss webpack rules 2026-07-24 14:00:24 +03:00
shmyga 1df83ec34d ci(version): 0.5.2 2026-07-22 22:16:16 +03:00
shmyga 1c4dac74ca feat(weather): inline weather svg icons 2026-07-22 22:16:16 +03:00
shmyga 75343a21b9 ci(version): 0.5.1 2026-07-22 22:16:16 +03:00
shmyga 83fabdd538 fix(static): fix bootstrap usage 2026-07-22 22:16:16 +03:00
shmyga 29364bb7d4 ci(version): 0.5.0 2026-07-22 22:16:15 +03:00
shmyga 195bd76608 build(scripts): add save docker action 2026-07-22 22:15:08 +03:00
shmyga 436c6c7364 fix(gismeteo): add heavy_rain variant 2026-07-22 22:15:08 +03:00
shmyga 0a734f9c43 feat(static): colorize weather icons 2026-07-22 22:15:08 +03:00
shmyga 6171ec8a26 refactor(static): refactor static module 2026-07-22 22:15:08 +03:00
shmyga 72fd148a54 build(static): fix webpack build warngins 2026-07-22 22:15:06 +03:00
shmyga 12774db70f refactor(static): refactor weather-icons lib 2026-07-22 22:14:41 +03:00
shmyga 0008e99c66 feat(static): use webpack instead of vite 2026-07-22 22:14:41 +03:00
shmyga bad5dff2b2 ci(version): 0.4.2 2026-07-22 21:52:30 +03:00
shmyga e2af31b812 fix(gismeteo): fix empty wind direction value in parser 2026-07-22 21:52:15 +03:00
shmyga 6c76dae760 ci(version): 0.4.1 2026-07-02 14:17:55 +03:00
shmyga 28951df9b5 fix(openweather): optional visability field in forecast model 2026-07-02 14:17:48 +03:00
shmyga 5aa290c772 ci(version): 0.4.0 2026-07-02 14:06:15 +03:00
shmyga 13a70990e5 feat(weather): add hail precipitation variant 2026-07-02 02:59:49 +03:00
shmyga a3fab92e3d feat(view): fix index page 2026-07-02 02:43:43 +03:00
shmyga 4fdb7e9717 fix(view): fix index pages api resolving 2026-07-02 01:30:38 +03:00
shmyga 1d13f6e05d refactor(doc): update docs static 2026-07-01 22:48:24 +03:00
shmyga fcb000afb0 test: fix tests 2026-07-01 21:05:49 +03:00
shmyga 0730b9f567 build(poetry): update python dependencies 2026-07-01 20:53:01 +03:00
shmyga 081aa8d17f feat(openweather): add location name to forecast response 2026-07-01 16:40:16 +03:00
shmyga 29fe06462e feat(schedule): implement multiple schedule providers 2026-06-27 16:03:28 +03:00
shmyga 8b20eb1220 test(openweather): add openweather location seacrh test 2026-06-26 16:27:36 +03:00
shmyga 5333690c47 feat(weather): implement provider in weather api endpoints 2026-06-25 19:19:21 +03:00
shmyga d82fc4ea46 feat(weather): improve weather locations list 2026-06-24 21:02:06 +03:00
shmyga df2f1d0d81 refactor(easel): add view root module 2026-06-24 16:00:39 +03:00
shmyga 9192ca3e4a feat(easel): implement multiple weather providers 2026-06-24 16:00:39 +03:00
shmyga e37ca6eeac feat(easel): add api provider 2026-06-24 16:00:39 +03:00
shmyga 657a62eab5 ci(version): 0.3.4 2026-06-24 15:54:43 +03:00
shmyga d8e2be013a fix(gismeteo): fix precipitation parser 2026-06-24 15:54:35 +03:00
shmyga a91ec12d22 ci(version): 0.3.3 2026-06-22 15:33:36 +03:00
shmyga 9c862863e5 fix(gismeteo): fix precipitation parser 2026-06-22 15:33:19 +03:00
shmyga fc59e52337 ci(version): 0.3.2 2026-06-20 18:59:43 +03:00
shmyga 3e93a41400 fix(gismeteo): fix date-time row parsing 2026-06-20 18:59:27 +03:00
shmyga c2cd18386b refactor(easel): update api router 2026-06-16 21:18:16 +03:00
shmyga 2bca3dd75a ci(version): 0.3.1 2026-06-16 20:17:37 +03:00
shmyga 469bd9bc1f feat(easel): add version to header 2026-06-16 20:17:12 +03:00
shmyga 027d1e2d55 build(docker): add docker build caches mount 2026-06-16 20:01:29 +03:00
shmyga 7cf0012229 feat(schedule): update navigate icons 2026-06-16 20:00:46 +03:00
shmyga edc014d98c docs: update screenshot 2026-06-15 23:37:31 +03:00
shmyga 1813ec213b ci(version): 0.3.0 2026-06-15 23:19:57 +03:00
shmyga 1b700086f2 feat(weather): update navigate icons 2026-06-15 23:18:48 +03:00
shmyga 02a6ffc931 feat(weather): new weather icons 2026-06-15 11:22:41 +03:00
shmyga 2dfedfea57 fix(easel): fix header icons 2026-06-15 11:22:28 +03:00
shmyga 8012d9b8ed fix(gismeteo): add max_date validation 2026-06-14 10:23:38 +03:00
shmyga 3a6faa85be fix(gismeteo): add new variants to sky parser 2026-06-14 10:23:13 +03:00
shmyga 1af61aa3c7 feat(easel): add sky details tooltip 2026-06-14 10:22:43 +03:00
shmyga 315838604e feat(easel): add widget mode support 2026-06-14 09:48:27 +03:00
shmyga f368e6717c style: format code 2026-06-12 00:16:15 +03:00
shmyga 91e2c9d123 ci(version): 0.2.3 2026-06-10 11:49:56 +03:00
shmyga 91d9c37612 build(vscode): add settings 2026-06-10 11:49:48 +03:00
shmyga b5f2c272bb feat(easel): update navbar 2026-06-10 11:37:49 +03:00
shmyga eec72c77ab docs: add screenshot 2026-05-07 01:11:42 +03:00
shmyga 160ec2b48b ci(version): 0.2.2 2026-04-24 14:12:39 +03:00
shmyga 7c57f939c0 feat(static): update and optimizate 2026-04-24 14:12:14 +03:00
145 changed files with 15629 additions and 8215 deletions
+5 -1
View File
@@ -2,4 +2,8 @@ DOCKER_REPO=git.shmyga.ru
DOCKER_GROUP=infernalgames
DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP"
VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+')
DOCKER_PROJECTS=("gallery")
PYTHON_VERSION=$(grep -m 1 'python' ./pyproject.toml | grep -oP 'python\s*=\s*"\^?\K[^"]+')
DOCKER_PROJECTS=("gallery")
DOCKER_TAG=latest
OPENWEATHER_KEY="<EMPTY>"
+3 -1
View File
@@ -4,4 +4,6 @@
.venv
#.vscode
static/node_modules
static/dist
static/dist
.env
dist
+9
View File
@@ -0,0 +1,9 @@
{
"python-envs.pythonProjects": [
{
"path": ".",
"envManager": "ms-python.python:poetry",
"packageManager": "ms-python.python:poetry"
}
]
}
+13 -11
View File
@@ -1,33 +1,35 @@
FROM python:3.12 AS builder
FROM python:3.14 AS builder
ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt update && \
apt install -y gettext
WORKDIR /app
RUN curl -sSL https://install.python-poetry.org | python3 -
COPY pyproject.toml poetry.lock README.md ./
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 --extras app --no-root
COPY locales ./locales
RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po
FROM node:24 AS node-builder
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY static/package.json static/package-lock.json ./
RUN npm ci
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY static ./
RUN npm run build
FROM python:3.12-slim
FROM python:3.14-slim
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
RUN apt update && \
apt install -y locales gettext && \
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG=ru_RU.UTF-8
ENV LC_ALL=ru_RU.UTF-8
ENV TZ="Europe/Moscow"
COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/
RUN cd gallery/easel/route/view/locales/ru/LC_MESSAGES && msgfmt messages.po
COPY --from=builder --parents locales/**/*.mo ./
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]
+4
View File
@@ -1,5 +1,9 @@
# API Gallery
Weather and TV program API
![API Gallery](docs/screenshot.png "API Gallery")
## View
https://api.shmyga.ru
+1
View File
@@ -12,6 +12,7 @@ services:
build: .
environment:
- REDIS_HOST=redis
- OPENWEATHER_KEY=$OPENWEATHER_KEY
- DEBUG=1
ports:
- 8000:80
+5 -4
View File
@@ -1,17 +1,18 @@
name: gallery
name: gallery-${DOCKER_TAG}
services:
redis:
container_name: gallery-redis
container_name: gallery-${DOCKER_TAG}-redis
image: redis:alpine
stop_grace_period: 3s
volumes:
- redis_data:/data
app:
container_name: gallery-app
image: ${DOCKER_ROOT}/gallery
container_name: gallery-${DOCKER_TAG}-app
image: ${DOCKER_ROOT}/gallery:${DOCKER_TAG}
environment:
- REDIS_HOST=redis
- OPENWEATHER_KEY=$OPENWEATHER_KEY
depends_on:
- redis
ports:
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

+4
View File
@@ -8,6 +8,7 @@
"python.testing.pytestArgs": ["tests", "-s"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.terminal.useEnvFile": true,
"python-envs.pythonProjects": [
{
"path": ".",
@@ -34,6 +35,8 @@
"request": "launch",
"module": "uvicorn",
"args": ["gallery.main:app", "--reload", "--log-config", "gallery/logging.yaml"],
"justMyCode": true,
"consoleTitle": "gallery:app",
},
{
"name": "gallery:static",
@@ -41,6 +44,7 @@
"request": "launch",
"type": "node-terminal",
"command": "npm run dev",
"consoleTitle": "gallery:static",
},
],
},
+4 -10
View File
@@ -1,19 +1,13 @@
import locale as _locale
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from gallery.sketch.bundle import ApiBundle
from gallery.util import root_path
from .route import api, doc
from .route.view import router as view_router
DEFAULT_LOCALE = "ru_RU.UTF-8"
from .route import api, doc, view
def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI:
_locale.setlocale(_locale.LC_TIME, locale)
def build_app(api_bundle: ApiBundle) -> FastAPI:
app = FastAPI(
title="Gallery",
docs_url=None,
@@ -22,6 +16,6 @@ def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI
app.state.api = api_bundle
app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
doc.mount(app)
api.mount(app)
app.include_router(view_router)
app.include_router(api.router)
app.include_router(view.router)
return app
View File
+17
View File
@@ -0,0 +1,17 @@
from fastapi import HTTPException, status
from gallery.easel.core import AppRequest
from gallery.sketch.api import API
def api_resolver(api_type: type[API]):
def get_api(request: AppRequest, provider: str) -> API:
providers = request.app.state.api.get_api_providers(api_type)
if provider not in providers:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"provider": f"'{provider}' not in {providers}"},
)
return request.app.state.api.get_api(api_type, provider)
return get_api
+9
View File
@@ -0,0 +1,9 @@
from typing import Annotated
from fastapi import Depends
from gallery.sketch.schedule.api import ScheduleApi
from .api import api_resolver
ScheduleApiDepends = Annotated[ScheduleApi, Depends(api_resolver(ScheduleApi))]
+9
View File
@@ -0,0 +1,9 @@
from typing import Annotated
from fastapi import Depends
from gallery.sketch.weather.api import WeatherApi
from .api import api_resolver
WeatherApiDepends = Annotated[WeatherApi, Depends(api_resolver(WeatherApi))]
+4 -5
View File
@@ -1,8 +1,7 @@
from fastapi import FastAPI
from fastapi import APIRouter
from . import schedule, weather
def mount(app: FastAPI):
weather.mount(app)
schedule.mount(app)
router = APIRouter(prefix="/api")
router.include_router(weather.router)
router.include_router(schedule.router)
+20 -13
View File
@@ -1,20 +1,27 @@
import datetime
from fastapi import FastAPI
from fastapi import APIRouter
from gallery.easel.core import AppRequest
from gallery.sketch.schedule.model import ChannelId, Schedule
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, Schedule
router = APIRouter(prefix="/schedule", tags=["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("/providers")
async def get_api_weather_providers(request: AppRequest) -> list[str]:
return request.app.state.api.get_api_providers(ScheduleApi)
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
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)
@router.get("/{provider}/channels")
async def find_api_schedule_channels(schedule_api: ScheduleApiDepends, query: str) -> list[Channel]:
return await schedule_api.find_channels(query)
@router.get("/{provider}/{channel}/{date}")
async def get_api_schedule_channel_schedule(
schedule_api: ScheduleApiDepends, channel: str, date: datetime.date
) -> Schedule:
return await schedule_api.get_schedule(channel, date)
+21 -20
View File
@@ -1,29 +1,30 @@
import datetime
from fastapi import FastAPI
from fastapi import APIRouter
from gallery.easel.core import AppRequest
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import Location, WeatherResponse
router = APIRouter(prefix="/weather", tags=["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)
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
async def get_api_weather_day(
request: AppRequest, location: str, date: datetime.date
) -> WeatherResponse:
weather_api = request.app.state.api.weather
return await weather_api.get_day(location, date)
@router.get("/providers")
async def get_api_weather_providers(request: AppRequest) -> list[str]:
return request.app.state.api.get_api_providers(WeatherApi)
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
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)
@router.get("/{provider}/locations")
async def find_api_weather_locations(weather_api: WeatherApiDepends, query: str) -> list[Location]:
return await weather_api.find_locations(query)
@router.get("/{provider}/{location}/day/{date}")
async def get_api_weather_day(weather_api: WeatherApiDepends, location: str, date: datetime.date) -> WeatherResponse:
return await weather_api.get_day(location, date)
@router.get("/{provider}/{location}/days/{days}")
async def get_api_weather_days(weather_api: WeatherApiDepends, location: str, days: int) -> WeatherResponse:
return await weather_api.get_days(location, days)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends
from .common import router as common_router
from .root import router as root_router
from .schedule import router as schedule_router
from .translation import set_language
from .weather import router as weather_router
router = APIRouter(dependencies=[Depends(set_language)])
router.include_router(common_router)
router = APIRouter(tags=["view"], dependencies=[Depends(set_language)], include_in_schema=False)
router.include_router(root_router)
router.include_router(weather_router)
router.include_router(schedule_router)
@@ -1,41 +0,0 @@
from pathlib import Path
from typing import NamedTuple
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from gallery.version import __version__
from ..translation import _
class Section(NamedTuple):
link: str
title: str
icon: str
SECTIONS = [
Section("weather", "Weather", "brightness-high"),
Section("schedule", "TV program", "tv"),
]
base_dir = Path(__file__).parent
router = APIRouter()
templates = Jinja2Templates(directory=base_dir / "templates")
templates.env.globals.update({"_": _})
@router.get("/", response_class=HTMLResponse)
async def get_section_list(request: Request):
return templates.TemplateResponse(
request=request,
name="root_index.html",
context={
"version": __version__,
"sections": SECTIONS,
},
)
@@ -12,19 +12,23 @@
<link rel="stylesheet"
href="/static/gallery.css?v={{version}}">
<script type="module"
src="/static/gallery.es.js?v={{version}}"></script>
src="/static/gallery.js?v={{version}}"></script>
<link rel="icon"
href="/favicon.ico?v={{version}}"
type="image/x-icon">
{% endblock %}
</head>
<body>
<body class="{{ is_widget and 'widget' or ''}}">
<div class="app col-lg-8 mx-auto p-3 py-md-5">
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<app-link href="/"
icon="gear">API Gallery</app-link>
{% block header %}{% endblock %}
{% if not is_widget %}
<header class="app-header pb-3 mb-5 border-bottom">
<div>{{request.query_params.widget}}</div>
<div class="link-list">
<app-link href="/"
icon="gear">API Gallery</app-link>
{% block header %}{% endblock %}
</div>
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<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"
@@ -32,10 +36,10 @@
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Select language (default)">
<span class="me-2 language-icon-active">🇬🇧</span>
aria-label="{{_('Select language')}} (default)">
<span class="fi fir fi-gb me-2 language-icon-active icon-header"></span>
<span class="d-lg-none ms-2"
id="bd-language-text">Select language</span>
id="bd-language-text">{{_("Select language")}}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-language-text">
@@ -44,8 +48,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-language-value="en"
aria-pressed="false">
<span class="me-2 language-icon">🇬🇧</span>
English
<span class="fi fir fi-gb me-2 language-icon-active"></span>
{{_("English")}}
</button>
</li>
<li>
@@ -53,8 +57,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-language-value="ru"
aria-pressed="false">
<span class="me-2 language-icon">🇷🇺</span>
Russian
<span class="fi fir fi-ru me-2 language-icon-active"></span>
{{_("Russian")}}
</button>
</li>
</ul>
@@ -66,9 +70,9 @@
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)">
<i class="bi me-2 opacity-50 theme-icon-active"></i>
<span class="bi bi-circle-half me-2 opacity-50 theme-icon-active icon-header"></span>
<span class="d-lg-none ms-2"
id="bd-theme-text">Toggle theme</span>
id="bd-theme-text">{{_("Toggle theme")}}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-theme-text">
@@ -77,8 +81,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false">
<i class="bi bi-sun-fill me-2 opacity-50 theme-icon"></i>
Light
<span class="bi bi-sun-fill me-2 opacity-50 theme-icon"></span>
{{_("Light")}}
</button>
</li>
<li>
@@ -86,8 +90,8 @@
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false">
<i class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></i>
Dark
<span class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></span>
{{_("Dark")}}
</button>
</li>
<li>
@@ -95,30 +99,27 @@
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true">
<i class="bi bi-circle-half me-2 opacity-50 theme-icon"></i>
Auto
<span class="bi bi-circle-half me-2 opacity-50 theme-icon"></span>
{{_("Auto")}}
</button>
</li>
</ul>
</li>
</ul>
</header>
{% endif %}
<main>
{% block content %}{% endblock %}
</main>
<footer class="pt-5 my-5 text-muted border-top">
Created by shmyga &middot; &copy; 2026
{% if not is_widget %}
<footer class="app-footer pt-5 my-5 text-muted border-top">
<div class="d-flex justify-content-between">
<span>Created by shmyga &middot; &copy; 2026</span>
<span>v{{ version }}</span>
</div>
</footer>
{% endif %}
</div>
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const widget = params.get('widget') || window.location.hostname.startsWith('weather');
if (widget) {
document.body.classList.add('widget');
}
}());
</script>
</body>
</html>
@@ -1,26 +0,0 @@
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>View</h1>
<div class="list-group mb-5">
{% for section in sections %}
<a href="{{section.link}}"
class="list-group-item list-group-item-action px-4">
<app-link href="{{section.link}}"
icon="{{section.icon}}">
{{_(section.title)}}
</app-link>
</a>
{% endfor %}
</div>
<hr class="col-3 col-md-2 mb-5">
<h1>Docs</h1>
<a href="/docs"
target="_blank">
<h4>Swagger</h4>
</a>
{% endblock %}
@@ -0,0 +1,53 @@
import datetime
import typing
from pathlib import Path
from babel.dates import format_date
from fastapi import Request
from fastapi.templating import Jinja2Templates
from gallery.easel.core import AppRequest
from gallery.version import __version__
from ...translation import _
from .tag import TagUtil
ContextProcessor = typing.Callable[[AppRequest], dict[str, typing.Any]]
def is_widget(request: Request) -> bool:
return (request.url.hostname and request.url.hostname.startswith("weather")) or (
request.query_params.get("widget") is not None
)
def base_context_processor(request: Request) -> dict:
return {
"is_widget": is_widget(request),
"provider": request.query_params.get("provider") or request.path_params.get("provider"),
}
def build_templates(
templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None
) -> Jinja2Templates:
directory = [Path(__file__).parent.parent / "templates"]
if templates_dir:
directory.append(templates_dir)
context_processors: list[ContextProcessor] = [base_context_processor]
if context_processor:
context_processors.append(context_processor)
templates = Jinja2Templates(directory=directory, context_processors=context_processors)
templates.env.globals.update(
{
"_": _,
"version": __version__,
"format_date": format_date,
"datetime": datetime,
"tag_util": TagUtil,
"DATE_FORMAT": "E, d MMMM Y",
}
)
if filters:
templates.env.filters.update(filters)
return templates
@@ -1,15 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Weather"
msgstr "Погода"
msgid "TV program"
msgstr "Телепрограмма"
+36
View File
@@ -0,0 +1,36 @@
from pathlib import Path
from typing import NamedTuple
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from ..common.utils.template import build_templates
class Section(NamedTuple):
link: str
title: str
icon: str
SECTIONS = [
Section("weather", "Weather", "brightness-high"),
Section("schedule", "TV program", "tv"),
]
base_dir = Path(__file__).parent
router = APIRouter()
templates = build_templates(Path(__file__).parent / "templates")
@router.get("/", response_class=HTMLResponse)
async def get_section_list(request: Request):
return templates.TemplateResponse(
request=request,
name="root_index.html",
context={
"sections": SECTIONS,
},
)
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{_("Index")}}{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>{{_("View")}}</h1>
<div class="list-group mb-3">
{% for section in sections %}
<app-link href="{{section.link}}"
icon="{{section.icon}}"
class="list-group-item list-group-item-action">
{{_(section.title)}}
</app-link>
{% endfor %}
</div>
<h1>{{_("Docs")}}</h1>
<div class="list-group mb-3">
<app-link href="/docs"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
Swagger UI
</app-link>
<app-link href="/redoc"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
ReDoc
</app-link>
</div>
{% endblock %}
+31 -46
View File
@@ -3,81 +3,66 @@ from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from gallery.easel.core import AppRequest
from gallery.sketch.schedule.catalog import BUNDLE
from gallery.version import __version__
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi
from ..common.util import TagType, TagUtil
from ..translation import _
from ..common.utils.tag import TagType, TagUtil
from ..common.utils.template import build_templates
from .filters import timedelta_format
base_dir = Path(__file__).parent
templates = Jinja2Templates(
directory=[
base_dir.parent / "common/templates",
base_dir / "templates",
]
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(ScheduleApi),
}
templates = build_templates(
Path(__file__).parent / "templates",
{
"timedelta_format": timedelta_format,
},
context_procesor,
)
templates.env.globals.update({"_": _})
templates.env.filters["timedelta_format"] = timedelta_format
router = APIRouter()
router = APIRouter(prefix="/schedule")
@router.get("/schedule", response_class=HTMLResponse)
async def get_schedule_list(request: AppRequest):
schedule_api = request.app.state.api.schedule
channels = await schedule_api.get_channels()
channels_data = BUNDLE.select_items(channels)
@router.get("/", response_class=HTMLResponse)
async def get_schedule_index(request: AppRequest, provider: str | None = None, query: str | None = None):
if query and provider:
schedule_api = api_resolver(ScheduleApi)(request, provider)
channels = await schedule_api.find_channels(query)
else:
channels = []
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"version": __version__,
"channels": channels_data,
"channels": channels,
},
)
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse)
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
tag_value = TagUtil.parse_tag(tag)
schedule_api = request.app.state.api.schedule
results = await schedule_api.get_all_schedules(tag_value.date)
return templates.TemplateResponse(
request=request,
name="schedule.html",
context={
"version": __version__,
"tag_util": TagUtil,
"datetime": datetime,
"response": results[0],
"responses": results,
"live": live,
},
)
@router.get("/schedule/{channel}", response_class=RedirectResponse)
@router.get("/{provider}/{channel}", response_class=RedirectResponse)
async def get_channel_default(channel: str):
return RedirectResponse(f"{channel}/tag/today")
@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, channel: str, tag: str):
@router.get("/{provider}/{channel}/tag/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: str):
tag_value = TagUtil.parse_tag(tag)
schedule_api = request.app.state.api.schedule
if tag_value.type == TagType.DAY:
response = await schedule_api.get_channel_schedule(channel, tag_value.date)
response = await schedule_api.get_schedule(channel, tag_value.date)
else:
raise ValueError(tag)
return templates.TemplateResponse(
request=request,
name="channel.html",
context={
"version": __version__,
"tag_util": TagUtil,
"datetime": datetime,
"response": response,
@@ -1,37 +1,45 @@
{% extends "base.html" %}
{% block title %}
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}
{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}
<h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="../..">⬆️</a>
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<i class="bi bi-arrow-left-square"></i>
</a>
<a class="icon-link"
href="../../..">
<i class="bi bi-arrow-up-square"></i>
</a>
<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>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
</h4>
<table class="table">
<thead>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
{% for value in response.values %}
<tr class="{{'table-success' if value.live else ''}}">
<td>{{value.start.strftime('%H:%M')}}</td>
<td>{{(value.end - value.start) | timedelta_format}}</td>
<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 %}
@@ -3,16 +3,60 @@
{% block content %}
<h1>{{_("TV program")}}</h1>
<div class="list-group mb-5">
<a href="schedule/tag/today"
class="list-group-item list-group-item-action px-4">
<span class="fw-bold">Все</span>
</a>
<form action=""
method="get"
class="mb-4">
<div class="input-group mb-3">
<input type="text"
class="form-control"
id="query"
name="query"
placeholder="{{_('Enter the channel name')}}">
<input type="hidden"
class="form-control"
id="provider"
name="provider"
value="{{provider or providers[0]}}">
<button id="providerBtn"
class="btn btn-secondary dropdown-toggle"
type="text"
data-bs-toggle="dropdown"
aria-expanded="false">{{provider or providers[0]}}</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for provider in providers %}
<li>
<a class="dropdown-item"
onclick="provider.value='{{provider}}'; providerBtn.innerText='{{provider}}'">{{provider}}</a>
</li>
{% endfor %}
</ul>
<button class="btn btn-primary"
type="submit">{{_("Search")}}</button>
</div>
</form>
{% if channels %}
<ul id="channels"
class="list-group mb-5">
{% for channel in channels %}
<a href="schedule/{{channel.id}}"
class="list-group-item list-group-item-action px-4">
<span class="text-primary">{{channel.name}}</span>
</a>
<schedule-channel channel="{{channel.model_dump() | tojson | forceescape}}"></schedule-channel>
{% endfor %}
</div>
</ul>
<hr>
{% endif %}
<ul id="storedChannels"
class="list-group mb-5">
</ul>
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const searchQuery = params.get('query');
if (searchQuery) {
document.querySelector('#query').value = searchQuery;
}
document.addEventListener("DOMContentLoaded", (event) => {
scheduleChannelManager.loadChannels('#storedChannels');
});
})();
</script>
{% endblock %}
@@ -1,54 +0,0 @@
{% extends "base.html" %}
{% block title %}
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}}
{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}
<h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="..">⬆️</a>
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4>
<div>
<table class="table">
<thead>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
{% for response in responses %}
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
{% if values|length > 0 %}
<tr class="table-primary fs-4">
<td colspan="3">
<div>{{response.channel.name}}</div>
</td>
<td></td>
<td></td>
</tr>
{% for value in values %}
<tr class="{{'table-success' if not live and value.live else ''}}">
<td>{{value.start.strftime('%H:%M')}}</td>
<td>{{(value.end - value.start) | timedelta_format}}</td>
<td>{{value.label}}</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+4 -7
View File
@@ -1,12 +1,11 @@
import gettext
from contextvars import ContextVar
from pathlib import Path
from fastapi import Cookie, Header, Request
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
ContextVar("translation")
)
from gallery.util import root_path
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = ContextVar("translation")
async def set_language(
@@ -18,9 +17,7 @@ async def set_language(
lang = language or accept_language.split(",")[0].split("-")[0]
try:
t = gettext.translation(
"messages", localedir=Path(__file__).parent / "locales", languages=[lang]
)
t = gettext.translation("messages", localedir=root_path / "locales", languages=[lang])
except FileNotFoundError:
t = gettext.NullTranslations()
+51 -36
View File
@@ -1,30 +1,35 @@
import datetime
from pathlib import Path
from fastapi import APIRouter, FastAPI
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from gallery.easel.core import AppRequest
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import WeatherResponse
from gallery.version import __version__
from ..common.util import TagType, TagUtil
from ..translation import _
from .filters import cloudness_icon, wind_direction_icon
from ..common.utils.tag import TagType, TagUtil
from ..common.utils.template import build_templates
from .filters import cloudness_icon, weather_icon_svg, wind_direction_icon
base_dir = Path(__file__).parent
templates = Jinja2Templates(
directory=[
base_dir.parent / "common/templates",
base_dir / "templates",
]
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(WeatherApi),
}
templates = build_templates(
Path(__file__).parent / "templates",
{
"wind_direction_icon": wind_direction_icon,
"cloudness_icon": cloudness_icon,
"weather_icon_svg": weather_icon_svg,
},
context_procesor,
)
templates.env.globals.update({"_": _})
templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon
def build_weather_response(request: AppRequest, response: WeatherResponse):
@@ -32,52 +37,55 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
request=request,
name="weather.html",
context={
"version": __version__,
"tag_util": TagUtil,
"datetime": datetime,
"response": response,
},
)
router = APIRouter()
@router.get("/weather", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, query: str | None = None):
weather_api = request.app.state.api.weather
locations = (await weather_api.find_locations(query)) if query else []
router = APIRouter(prefix="/weather")
@router.get("/", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, provider: str | None = None, query: str | None = None):
if query and provider:
weather_api = api_resolver(WeatherApi)(request, provider)
locations = await weather_api.find_locations(query)
else:
locations = []
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"version": __version__,
"locations": locations,
},
)
@router.get("/weather/{location}", response_class=RedirectResponse)
async def get_weather_default(location: str):
@router.get("/{provider}/{location}", response_class=RedirectResponse)
async def get_weather(location: str):
return RedirectResponse(f"{location}/tag/today")
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
weather_api = request.app.state.api.weather
@router.get("/{provider}/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather_day(
request: AppRequest,
weather_api: WeatherApiDepends,
location: str,
date: datetime.date,
):
response = await weather_api.get_day(location, date)
return build_weather_response(request, response)
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, location: str, days: int):
weather_api = request.app.state.api.weather
@router.get("/{provider}/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
response = await weather_api.get_days(location, days)
return build_weather_response(request, response)
@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
async def get_weather_tag(request: AppRequest, location: str, tag: str):
@router.get("/{provider}/{location}/tag/{tag}", response_class=HTMLResponse)
async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
tag_value = TagUtil.parse_tag(tag)
weather_api = request.app.state.api.weather
if tag_value.type == TagType.DAY:
response = await weather_api.get_day(location, tag_value.date)
elif tag_value.type == TagType.DAYS:
@@ -85,3 +93,10 @@ async def get_weather_tag(request: AppRequest, location: str, tag: str):
else:
raise ValueError(tag)
return build_weather_response(request, response)
@router.get("/zmiyevka-184640/tag/{tag}", response_class=RedirectResponse)
async def get_zmiyevka_weather_compat(request: AppRequest):
path_parts = request.url.path.split("/")
path_parts.insert(2, "gismeteo")
return RedirectResponse("/".join(path_parts))
+60 -27
View File
@@ -1,3 +1,7 @@
import datetime
from markupsafe import Markup
from gallery.sketch.weather.model import (
Cloudness,
Precipitation,
@@ -5,44 +9,73 @@ from gallery.sketch.weather.model import (
WindDirection,
WindDirectionDeg,
)
from gallery.util import root_path
def wind_direction_icon(wind_direction_deg: float) -> str:
wind_direction = WindDirectionDeg(wind_direction_deg).direction
return {
WindDirection.N: "⬇️",
WindDirection.NE: "↙️",
WindDirection.E: "⬅️",
WindDirection.SE: "↖️",
WindDirection.S: "⬆️",
WindDirection.SW: "↗️",
WindDirection.W: "➡️",
WindDirection.NW: "↘️",
WindDirection.CALM: "",
}.get(wind_direction, wind_direction)
if wind_direction == WindDirection.CALM:
return "wind-calm"
else:
return f"wind-from-{wind_direction.name.lower()}"
def cloudness_icon(sky: Sky) -> list[str]:
def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]:
day = (3 < date.hour < 22) if period == "day" else True
day_prefix = "day" if day else "night-alt"
main_icon = ""
if sky.thunder:
if sky.cloudness == Cloudness.CLEAR:
main_icon = "🌩️"
if sky.precipitation == Precipitation.NO:
main_icon = "⚡"
else:
main_icon = "⛈️"
main_icon = {
Precipitation.NO: "lightning",
Precipitation.SMALL_RAIN: "storm-showers",
Precipitation.RAIN: "thunderstorm",
Precipitation.HEAVY_RAIN: "thunderstorm",
Precipitation.SHOWER: "thunderstorm",
Precipitation.SNOW: "storm-showers",
Precipitation.HEAVY_SNOW: "storm-showers",
}[sky.precipitation]
if sky.cloudness == Cloudness.PARTLY_CLOUDY:
main_icon = f"{day_prefix}-{main_icon}"
elif sky.precipitation == Precipitation.NO:
main_icon = {
Cloudness.CLEAR: "☀️",
Cloudness.PARTLY_CLOUDY: "🌤️",
Cloudness.CLOUDY: "⛅",
Cloudness.MAINLY_CLOUDY: "☁️",
Cloudness.CLEAR: "day-sunny" if day else "night-clear",
Cloudness.PARTLY_CLOUDY: f"{day_prefix}-cloudy",
Cloudness.CLOUDY: "cloud",
Cloudness.MAINLY_CLOUDY: "cloudy",
}[sky.cloudness]
elif sky.precipitation in [Precipitation.SNOW, Precipitation.HEAVY_SNOW]:
main_icon = "🌨️"
else:
main_icon = "🌧️"
main_icon = {
Precipitation.SMALL_RAIN: "showers",
Precipitation.RAIN: "rain-mix",
Precipitation.HEAVY_RAIN: "rain",
Precipitation.SHOWER: "rain",
Precipitation.SNOW: "snow",
Precipitation.HEAVY_SNOW: "snow",
Precipitation.HAIL: "hail",
}[sky.precipitation]
if sky.cloudness == Cloudness.PARTLY_CLOUDY:
main_icon = f"{day_prefix}-{main_icon}"
icons = [main_icon]
if sky.fog:
icons.append("🌫️")
return icons
class WeatherIconBundle:
def __init__(self):
self._icon_path = root_path / "static/dist/weather-icons/icons"
self._cache = {}
def _load_icon(self, icon: str) -> Markup:
icon_file = self._icon_path / f"wi-{icon}.svg"
return Markup(icon_file.read_text())
def get(self, icon: str) -> Markup:
if icon not in self._cache:
self._cache[icon] = self._load_icon(icon)
return self._cache[icon]
WEATHER_ICON_BUNDLE = WeatherIconBundle()
def weather_icon_svg(icon: str) -> Markup:
return WEATHER_ICON_BUNDLE.get(icon)
@@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Weather{% endblock %}
{% block title %}{{_("Weather")}}{% endblock %}
{% block content %}
<h1>Weather</h1>
<h1>{{_("Weather")}}</h1>
<form action=""
method="get"
class="mb-4">
@@ -11,63 +11,52 @@
class="form-control"
id="query"
name="query"
placeholder="Enter the city name">
placeholder="{{_('Enter the city name')}}">
<input type="hidden"
class="form-control"
id="provider"
name="provider"
value="{{provider or providers[0]}}">
<button id="providerBtn"
class="btn btn-secondary dropdown-toggle"
type="text"
data-bs-toggle="dropdown"
aria-expanded="false">{{provider or providers[0]}}</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for provider in providers %}
<li>
<a class="dropdown-item"
onclick="provider.value='{{provider}}'; providerBtn.innerText='{{provider}}'">{{provider}}</a>
</li>
{% endfor %}
</ul>
<button class="btn btn-primary"
type="submit">Search</button>
type="submit">{{_("Search")}}</button>
</div>
</form>
{% if locations %}
<ul id="locations"
class="list-group mb-5">
{% for location in locations %}
<a href="weather/{{location.id}}"
class="list-group-item list-group-item-action px-4"
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
<span class="text-primary">{{location.name}}</span>
<span class="small ms-1 text-secondary">
{{location.country}}, {{location.district}}, {{location.subdistrict}}
</span>
<span></span>
</a>
<weather-location location="{{location.model_dump() | tojson | forceescape}}"></weather-location>
{% endfor %}
</ul>
<hr>
{% endif %}
<ul id="storedLocations"
class="list-group mb-5">
</ul>
<script>
(function () {
document.loadLocations = () => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
const container = document.querySelector('#locations');
container.innerHTML = '';
for (const [id, name] of Object.entries(locations)) {
const element = document.createElement('a');
element.href = `weather/${id}`;
element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
element.innerHTML = `
<span class="text-primary me-auto">${name}</span>
<span class="text-danger" onclick="removeLocation('${id}'); event.preventDefault();">&#x2715;</span>
`;
container.appendChild(element);
}
}
document.saveLocation = (location) => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
locations[location.id] = location.name;
window.localStorage.setItem('locations', JSON.stringify(locations));
}
document.removeLocation = (id) => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
delete locations[id];
window.localStorage.setItem('locations', JSON.stringify(locations));
document.loadLocations();
}
const params = new URLSearchParams(window.location.search);
const searchQuery = params.get('query');
if (searchQuery) {
document.querySelector('#query').value = searchQuery;
} else {
document.loadLocations();
}
document.addEventListener("DOMContentLoaded", (event) => {
weatherLocationManager.loadLocations('#storedLocations');
});
})();
</script>
{% endblock %}
@@ -2,23 +2,32 @@
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/weather" icon="brightness-high">{{_("Weather")}}</app-link>
<app-link href="/weather"
icon="brightness-high">{{_("Weather")}}</app-link>
{% endblock %}
{% block content %}
<h4>
{% if response.period == 'day' %}
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="../tag/days-10">⬆️</a>
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<i class="bi bi-arrow-left-square"></i>
</a>
<a class="icon-link"
href="../tag/days-10">
<i class="bi bi-arrow-up-square"></i>
</a>
<span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span>
<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 %}
{% if response.period == 'days' %}
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</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 %}
</h4>
<div class="table-responsive">
@@ -36,9 +45,9 @@
{% endif %}
{% if response.period == 'days' %}
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
<span class="value">
<span class="value {{'text-danger' if value.date.weekday() in [5,6] else ''}}">
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
{{value.date.strftime('%a %d')}}
{{format_date(value.date, 'E d', locale=request.state.language)}}
</a>
</span>
</td>
@@ -49,14 +58,17 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Облачность
{{_("Cloudiness")}}
</td>
</tr>
<tr>
{% for value in response.values %}
<td class="cloudness">
{% for icon in value.sky | cloudness_icon %}
<div class="icon">{{icon}}</div>
<td class="cloudness"
data-bs-toggle="tooltip"
data-bs-title="{{ value.sky }}">
{% for icon in value.sky | cloudness_icon(value.date, response.period) %}
<div class="wi-svg wi-l wi-border"
style="margin: 0 auto;">{{ icon | weather_icon_svg }}</div>
{% endfor %}
</td>
{% endfor %}
@@ -65,7 +77,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Температура, °C
{{_("Temperature, °C")}}
</td>
</tr>
<tr>
@@ -84,13 +96,16 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Направление ветра
{{_("Wind direction")}}
</td>
</tr>
<tr>
{% for value in response.values %}
<td class="wind">
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
<td class="wind"
data-bs-toggle="tooltip"
data-bs-title="{{ value.wind_direction }}">
<div class="wi-svg wi-s wi-{{value.wind_direction | wind_direction_icon}}"
style="margin: 0 auto;">{{ 'wind-deg' | weather_icon_svg }}</div>
</td>
{% endfor %}
</tr>
@@ -98,7 +113,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Скорость ветра, м/с
{{_("Wind speed, m/s")}}
</td>
</tr>
<tr>
@@ -118,14 +133,14 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Осадки, мм
{{_("Precipitation, mm")}}
</td>
</tr>
<tr>
{% for value in response.values %}
<td class="precipitation"
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
<span class="value">{{value.precipitation or ' '}}</span>
<span class="value">{{(value.precipitation | round(2)) if value.precipitation else ' '}}</span>
</td>
{% endfor %}
</tr>
@@ -133,7 +148,7 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Давление, мм рт. ст.
{{_("Pressure, mmHg")}}
</td>
</tr>
<tr>
@@ -151,14 +166,14 @@
<tr>
<td colspan="{{response.values | length}}"
class="header">
Влажность, %
{{_("Humidity, %")}}
</td>
</tr>
<tr>
{% for value in response.values %}
<td class="humidity"
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
<span class="value">{{value.humidity}}</span>
{% if value.humidity > -1%}<span class="value">{{value.humidity}}</span>{% endif %}
</td>
{% endfor %}
</tr>
+11 -1
View File
@@ -4,6 +4,7 @@ import logging
from typing import Any
from bs4 import BeautifulSoup
from fastapi import HTTPException, status
from gallery.sketch.source import ApiSource
from gallery.sketch.weather.api import WeatherApi
@@ -84,10 +85,14 @@ class GismeteoApi(WeatherApi):
Location(
id=f"{item['slug']}-{item['id']}",
name=item["translations"]["kk"]["city"]["name"],
provider=self.provider,
lat=item["coordinates"]["latitude"],
lon=item["coordinates"]["longitude"],
country=item["translations"]["kk"]["country"]["name"],
district=item["translations"]["kk"]["district"]["name"],
country_code=item["country"]["code"].lower(),
district=(
item["translations"]["kk"]["district"]["name"] if item["translations"]["kk"]["district"] else ""
),
subdistrict=(
item["translations"]["kk"]["subdistrict"]["name"]
if "subdistrict" in item["translations"]["kk"]
@@ -98,6 +103,11 @@ class GismeteoApi(WeatherApi):
return result
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
max_date = datetime.date.today() + datetime.timedelta(days=9)
if date > max_date:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail={"max_date": max_date.strftime("%Y-%m-%d")}
)
data = await self.SOURCE.request(f"weather-{location_id}/{datehelp.dump(date)}")
return self._parse_oneday(date, data)
+1
View File
@@ -4,6 +4,7 @@ from bs4 import Tag
T = TypeVar("T")
class WidgetParser:
def parse_widget(self, tag: Tag) -> Tag:
raise NotImplementedError
+48 -42
View File
@@ -1,4 +1,5 @@
import datetime
import logging
import re
from typing import Iterable
@@ -15,6 +16,8 @@ from gallery.sketch.weather.model import (
from .core import BaseWidgetParser, RowParser
logger = logging.getLogger("gismeteo")
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items")
@@ -36,16 +39,11 @@ class DateParser(RowParser[datetime.datetime]):
KEY = "date"
def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]:
datetime_date_tag = tag.select_one(
".widget-row.widget-row-datetime-date > .row-item"
)
if datetime_date_tag:
date_str = datetime_date_tag.find(text=True, recursive=False).text
date = dateparser.parse(date_str, languages=["ru"])
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)
datetime_time_row = tag.select_one(".widget-row.widget-row-datetime-time")
if datetime_time_row:
for item in datetime_time_row.select(".row-item > time-value"):
timestamp = int(item.attrs["timestamp"])
time = datetime.datetime.fromtimestamp(timestamp)
yield time
else:
for item in tag.select(".widget-row.widget-row-date > .row-item"):
@@ -59,6 +57,7 @@ class SkyParser(RowParser[Sky]):
CLOUDNESS_MAP: dict[str, Cloudness] = {
"ясно": Cloudness.CLEAR,
"безоблачно": Cloudness.CLEAR,
"малооблачно": Cloudness.PARTLY_CLOUDY,
"облачно": Cloudness.CLOUDY,
"пасмурно": Cloudness.MAINLY_CLOUDY,
@@ -66,36 +65,52 @@ class SkyParser(RowParser[Sky]):
PRECIPITATION_MAP: dict[str, Precipitation] = {
"без осадков": Precipitation.NO,
"небольшой дождь": Precipitation.SMALL_RAIN,
"сильный дождь": Precipitation.HEAVY_RAIN,
"небольшой дождь": Precipitation.SMALL_RAIN,
"сильный дождь": Precipitation.HEAVY_RAIN,
"очень сильный дождь": Precipitation.HEAVY_RAIN,
"ливневый дождь": Precipitation.SHOWER,
"дождь": Precipitation.RAIN,
"ливень": Precipitation.SHOWER,
"снег": Precipitation.SNOW,
"небольшой снег": Precipitation.SNOW,
"сильный снег": Precipitation.HEAVY_SNOW,
"мокрый снег": Precipitation.SNOW,
"небольшой снег": Precipitation.SNOW,
"сильный снег": Precipitation.HEAVY_SNOW,
"мокрый снег": Precipitation.SNOW,
"снег с дождём": Precipitation.SNOW,
"сильный снег с дождём": Precipitation.HEAVY_SNOW,
"небольшой снег с дождём": Precipitation.SNOW,
"небольшой мокрый снег": Precipitation.SNOW,
"сильный снег с дождём": Precipitation.HEAVY_SNOW,
"небольшой снег с дождём": Precipitation.SNOW,
"небольшой мокрый снег": Precipitation.SNOW,
"град": Precipitation.HAIL,
}
THUNDER = "гроза"
FOG = "дымка"
def parse_row(self, tag: Tag) -> Iterable[Sky]:
for item in tag.select(".widget-row[data-row=icon-tooltip] > .row-item"):
sky_str = item.attrs["data-tooltip"]
values = {item.strip().lower() for item in sky_str.split(",")}
cloudness = Cloudness.CLEAR
precipitation = Precipitation.NO
thunder = "гроза" in values
fog = "дымка" in values
thunder = False
fog = False
if self.THUNDER in values:
thunder = True
values.remove(self.THUNDER)
if self.FOG in values:
fog = True
values.remove(self.FOG)
for k, v in self.CLOUDNESS_MAP.items():
if k in values:
cloudness = v
values.remove(k)
break
for k, v in self.PRECIPITATION_MAP.items():
if k in values:
precipitation = v
values.remove(k)
break
if values:
logger.warning("unknown sky values: %s:", values)
yield Sky(
cloudness=cloudness,
precipitation=precipitation,
@@ -108,21 +123,15 @@ class TemperatureParser(RowParser[list[int]]):
KEY = "temperature"
def parse_row(self, tag: Tag) -> Iterable[list[int]]:
for item in tag.select(
".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"
):
yield [
int(value.attrs["value"]) for value in item.select("temperature-value")
]
for item in tag.select(".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"):
yield [int(value.attrs["value"]) for value in item.select("temperature-value")]
class WindSpeedParser(RowParser[int]):
KEY = "wind_speed"
def parse_row(self, tag: Tag) -> Iterable[int]:
for item in tag.select(
".widget-row-wind > .row-item > .wind-speed > speed-value"
):
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > speed-value"):
yield int(item.attrs["value"])
@@ -140,6 +149,7 @@ class WindDirectionParser(RowParser[WindDirection]):
WIND_DIRECTION_MAP: dict[str, WindDirection] = {
"—": WindDirection.CALM,
"": WindDirection.CALM,
"штиль": WindDirection.CALM,
"с": WindDirection.N,
"св": WindDirection.NE,
@@ -152,32 +162,28 @@ class WindDirectionParser(RowParser[WindDirection]):
}
def parse_row(self, tag: Tag) -> Iterable[float]:
for item in tag.select(
".widget-row-wind > .row-item > .wind-speed > .wind-direction"
):
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > .wind-direction"):
wind_direction_str = item.text.lower().strip()
yield WindDirectionDeg.from_direction(
self.WIND_DIRECTION_MAP[wind_direction_str]
).value
yield WindDirectionDeg.from_direction(self.WIND_DIRECTION_MAP[wind_direction_str]).value
class PrecipitationParser(RowParser[float]):
KEY = "precipitation"
def parse_row(self, tag: Tag) -> Iterable[float]:
for item in tag.select(
".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"
):
yield float(item.text.replace(",", "."))
for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item"):
value = item.select_one("precipitation-value")
if value:
yield float(value.attrs["value"])
else:
yield 0
class PressureParser(RowParser[list[int]]):
KEY = "pressure"
def parse_row(self, tag: Tag) -> Iterable[list[int]]:
for item in tag.select(
".widget-row-chart[data-row=pressure] > .chart > .values > .value"
):
for item in tag.select(".widget-row-chart[data-row=pressure] > .chart > .values > .value"):
yield [int(value.attrs["value"]) for value in item.select("pressure-value")]
+25 -37
View File
@@ -1,10 +1,9 @@
import datetime
import json
import logging
from bs4 import BeautifulSoup
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue
from gallery.sketch.schedule.model import Channel, Schedule, ScheduleValue
from gallery.sketch.source import ApiSource
logger = logging.getLogger("matchtv")
@@ -14,43 +13,30 @@ class MatchTvApi(ScheduleApi):
PROVIDER = "matchtv"
SOURCE = ApiSource("https://matchtv.ru")
async def get_channels(self) -> list[ChannelId]:
return [
ChannelId.MATCH_TV,
ChannelId.MATCH_IGRA,
ChannelId.MATCH_ARENA,
ChannelId.MATCH_FUTBOL_1,
ChannelId.MATCH_FUTBOL_2,
ChannelId.MATCH_FUTBOL_3,
ChannelId.MATCH_STRANA,
]
async def find_channels(self, query: str) -> list[Channel]:
endpoint = "api/v1/channels"
data = json.loads(await self.SOURCE.request(endpoint))
result = []
query = query.lower()
for item in data["result"]:
if query in item["name"].lower() or query in item["alias"]:
channel_id = item["alias"]
name = item["name"].split("|")[0].strip()
result.append(Channel(id=channel_id, name=name, provider=self.provider))
return result
async def get_channel_schedule(
self, channel_id: ChannelId, date: datetime.date
) -> Schedule:
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}"
data = await self.SOURCE.request(endpoint)
soup = BeautifulSoup(data, features="html.parser")
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"api/v1/channels/{channel_id}/tv-schedule?date={date:%Y%m%d}"
data = json.loads(await self.SOURCE.request(endpoint))
channel_data = data["result"]["channels"][0]
channel = Channel(id=channel_data["alias"], name=channel_data["name"], provider=self.provider)
values = []
channel_name = (
soup.select_one(".p-tv-guide-header__title")
.text.replace("Телепрограмма ", "")
.strip()
)
current_day = datetime.datetime.combine(
date.today(), datetime.datetime.min.time()
)
current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time())
end = current_day + datetime.timedelta(days=1, hours=6)
prev_value: ScheduleValue | None = None
for item in soup.select(
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
):
title = item.select_one(
".p-tv-guide-schedule-channel-transmission__title"
).text.strip()
time_str = item.select_one(
".p-tv-guide-schedule-channel-transmission__time-block"
).text.strip()
for item in channel_data["schedule"]:
title = item["title"]
time_str = item["time"]
hours, minutes = map(int, time_str.split(":"))
item_date = current_day.replace(hour=hours, minute=minutes)
if prev_value is not None and item_date.hour < prev_value.start.hour:
@@ -63,5 +49,7 @@ class MatchTvApi(ScheduleApi):
prev_value.end = item_date
prev_value = value
return Schedule(
channel=Channel(id=channel_id, name=channel_name), date=date, values=values
channel=channel,
date=date,
values=values,
)
+34 -10
View File
@@ -1,6 +1,7 @@
import datetime
import logging
from collections import defaultdict
from os import environ
from aiocache import cached
@@ -9,7 +10,9 @@ from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
from gallery.sketch.weather.util import merge_weather_values
from gallery.util import TimeUnit
from .openweather import Forecast, OpenWeather
from .openweather import Forecast
from .openweather import Location as OpenWeatherLocation
from .openweather import OpenWeather
from .parser import FORECAST_ITEM_PARSER
logger = logging.getLogger("openweather")
@@ -17,24 +20,47 @@ logger = logging.getLogger("openweather")
class OpenWeatherApi(WeatherApi):
PROVIDER = "openweather"
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf")
SOURCE = OpenWeather(environ["OPENWEATHER_KEY"])
@classmethod
def _parse_location(cls, location_id: str) -> tuple[float, float]:
return tuple(map(float, location_id.split(":", maxsplit=2)))
@cached(
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.location",
alias="redis",
ttl=TimeUnit.DAY,
)
async def _get_location(self, location_id: str) -> OpenWeatherLocation:
return await self.SOURCE.get_location(*self._parse_location(location_id))
@cached(
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
alias="redis",
ttl=TimeUnit.HOUR,
)
async def _get_location_forecast(self, location_id: str) -> Forecast:
return await self.SOURCE.get_forecast(*self._parse_location(location_id))
async def find_locations(self, query: str) -> list[Location]:
raise NotImplementedError
result = await self.SOURCE.find_locations(query)
return [
Location(
id=f"{item.lat}:{item.lon}",
name=item.name,
provider=self.provider,
lat=item.lat,
lon=item.lon,
country=item.country,
country_code=item.country.lower(),
district=item.state or "",
subdistrict="",
)
for item in result
]
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id)
values = []
for item in data.list:
@@ -42,25 +68,23 @@ class OpenWeatherApi(WeatherApi):
if value.date.date() == date:
values.append(value)
return WeatherResponse(
location=location_id,
location=location.name,
date=date,
period="day",
values=values,
)
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id)
values_by_date: dict[datetime.datetime, list[WeatherValue]] = defaultdict(list)
for item in data.list:
value = FORECAST_ITEM_PARSER.parse(item)
item_date = value.date.replace(hour=0, minute=0)
values_by_date[item_date].append(value)
values = [
merge_weather_values(date, values)
for date, values in values_by_date.items()
]
values = [merge_weather_values(date, values) for date, values in values_by_date.items()]
return WeatherResponse(
location=location_id,
location=location.name,
date=datetime.date.today(),
period="days",
values=list(sorted(values, key=lambda item: item.date)),
@@ -1,5 +0,0 @@
from pathlib import Path
from gallery.sketch.mock import MockData
OPENWEATHER_MOCK_DATA = MockData(Path(__file__).parent / "data")
+25 -5
View File
@@ -53,7 +53,7 @@ class ForecastItem(Model):
weather: list[Weather]
clouds: Clouds
wind: Wind
visibility: int
visibility: int | None = None
pop: float
rain: Rain | None = None
sys: Sys
@@ -67,6 +67,15 @@ class Forecast(Model):
list: list[ForecastItem]
class Location(Model):
name: str
lat: float
lon: float
country: str
state: str | None = None
local_names: dict[str, str] | None = None
class OpenWeather:
BASE_URL = "https://api.openweathermap.org"
@@ -75,9 +84,20 @@ class OpenWeather:
self._source = ApiSource(self.BASE_URL)
async def get_forecast(self, lat: float, lon: float) -> Forecast:
endpoint = (
f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
)
endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
response = await self._source.request(endpoint)
response_data = json.loads(response)
return Forecast(**response_data)
return Forecast.model_validate(response_data)
async def find_locations(self, query: str, limit: int = 5) -> list[Location]:
endpoint = f"geo/1.0/direct?q={query}&limit={limit}&appid={self._api_key}"
response = await self._source.request(endpoint)
response_data = json.loads(response)
return [Location.model_validate(item) for item in response_data]
async def get_location(self, lat: float, lon: float) -> Location:
limit = 1
endpoint = f"geo/1.0/reverse?lat={lat}&lon={lon}&limit={limit}&appid={self._api_key}"
response = await self._source.request(endpoint)
response_data = json.loads(response)
return Location.model_validate(response_data[0])
+3 -11
View File
@@ -24,11 +24,7 @@ class ForecastItemParser:
def parse(self, item: ForecastItem) -> WeatherValue:
item_date = datetime.datetime.fromtimestamp(item.dt, datetime.UTC)
item_date = (
item_date.replace(tzinfo=datetime.timezone.utc)
.astimezone(tz=None)
.replace(tzinfo=None)
)
item_date = item_date.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).replace(tzinfo=None)
value = build_weather_value(item_date)
# TODO parse temperature interval flag
value.temperature = [round(item.main.temp)]
@@ -38,12 +34,8 @@ class ForecastItemParser:
value.wind_speed = round(item.wind.speed)
value.wind_gust = round(item.wind.gust)
value.wind_direction = item.wind.deg
value.sky.cloudness = self.CLOUDNESS_MAP.get(
item.weather[0].description, Cloudness.CLEAR
)
value.sky.precipitation = self.PRECIPITATION_MAP.get(
item.weather[0].description, Precipitation.NO
)
value.sky.cloudness = self.CLOUDNESS_MAP.get(item.weather[0].description, Cloudness.CLEAR)
value.sky.precipitation = self.PRECIPITATION_MAP.get(item.weather[0].description, Precipitation.NO)
if item.rain:
value.precipitation = round(item.rain.interval_3h, 1)
return value
+20 -24
View File
@@ -1,27 +1,15 @@
import datetime
import json
import logging
from bs4 import BeautifulSoup
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue
from gallery.sketch.schedule.model import Channel, Schedule, ScheduleValue
from gallery.sketch.source import ApiSource
logger = logging.getLogger("matchtv")
CHANNELS_MAP: dict[ChannelId, str] = {
ChannelId.MATCH_TV: "match-tv-49",
ChannelId.MATCH_IGRA: "match-igra-1174",
ChannelId.MATCH_ARENA: "match-arena-1173",
ChannelId.MATCH_FUTBOL_1: "match-futbol-1-646",
ChannelId.MATCH_FUTBOL_2: "match-futbol-2-593",
ChannelId.MATCH_FUTBOL_3: "match-futbol-3-797",
ChannelId.MATCH_STRANA: "match-strana-1356",
ChannelId.MATCH_PLANETA: "match-planeta-1177",
# ChannelId.EUROSPORT: "eurosport-677",
# ChannelId.EUROSPORT_2: "eurosport-2-720",
ChannelId.START: "start-103",
}
HEADERS: dict[str, str] = {
"Accept": (
@@ -57,22 +45,28 @@ class YandexTvApi(ScheduleApi):
PROVIDER = "yandextv"
SOURCE = ApiSource("https://tv.yandex.ru", headers=HEADERS)
async def get_channels(self) -> list[ChannelId]:
return list(CHANNELS_MAP.keys())
async def find_channels(self, query: str) -> list[Channel]:
url = (
"https://suggest-multi.yandex.ru/suggest-tv2?"
f"v=4&uil=ru&lr=10&count_channels=4&count_programs=4&sn=50&part={query}"
)
_, values = json.loads(await self.SOURCE.request(url))
result = []
for _, name, content in values:
if content["label"] == "Каналы":
channel_id = content["url"].split("/")[-1]
result.append(Channel(id=channel_id, name=name, provider=self.provider))
return result
async def get_channel_schedule(
self, channel_id: ChannelId, date: datetime.date
) -> Schedule:
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}"
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"channels/{channel_id}?date={date:%Y-%m-%d}"
data = await self.SOURCE.request(endpoint)
soup = BeautifulSoup(data, features="html.parser")
if soup.select_one(".CheckboxCaptcha") is not None:
raise RuntimeError("Captcha")
values = []
channel_name = soup.select_one(".channel-header__text").text.strip()
current_day = datetime.datetime.combine(
date.today(), datetime.datetime.min.time()
)
current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time())
end = current_day + datetime.timedelta(days=1, hours=6)
prev_value: ScheduleValue | None = None
for item in soup.select(".channel-schedule .channel-schedule__event"):
@@ -90,5 +84,7 @@ class YandexTvApi(ScheduleApi):
prev_value.end = item_date
prev_value = value
return Schedule(
channel=Channel(id=channel_id, name=channel_name), date=date, values=values
channel=Channel(id=channel_id, name=channel_name, provider=self.provider),
date=date,
values=values,
)
+5
View File
@@ -2,8 +2,13 @@ from typing import TypeVar
class Api:
TYPE: str
PROVIDER: str
@property
def type(self) -> str:
return self.TYPE
@property
def provider(self) -> str:
return self.PROVIDER
+14 -23
View File
@@ -1,28 +1,19 @@
from collections import defaultdict
from .api import API, Api
from .schedule.api import ScheduleApi
from .weather.api import WeatherApi
class ApiBundle(list[Api]):
def __init__(self, values: list[Api]) -> None:
super().__init__(values)
class ApiBundle:
def __init__(self, values: list[Api]):
self._values = values
self._providers_by_api = defaultdict(list)
self._api_map = {}
for value in self._values:
self._providers_by_api[value.type].append(value.provider)
self._api_map[(value.type, value.provider)] = value
def get_api_by_provider(self, provider: str) -> Api:
for value in self:
if value.PROVIDER == provider:
return value
raise ValueError(provider)
def get_api_providers(self, api_type: type[API]) -> list[str]:
return self._providers_by_api[api_type.TYPE]
def get_api_by_type(self, api_type: type[API]) -> API:
for value in self:
if isinstance(value, api_type):
return value
raise ValueError(api_type)
@property
def weather(self) -> WeatherApi:
return self.get_api_by_type(WeatherApi)
@property
def schedule(self) -> ScheduleApi:
return self.get_api_by_type(ScheduleApi)
def get_api(self, api_type: type[API], provider: str) -> API:
return self._api_map[(api_type.TYPE, provider)]
-14
View File
@@ -1,14 +0,0 @@
from typing import Generic, TypeVar
T = TypeVar("T")
class CatalogBundle(Generic[T]):
def __init__(self, items: list[T]) -> None:
self._items_by_id = {item.id: item for item in items}
def get_item(self, item_id: str) -> T:
return self._items_by_id[item_id]
def select_items(self, ids: list[str]) -> list[T]:
return [self._items_by_id[id_] for id_ in ids]
+4 -18
View File
@@ -1,28 +1,14 @@
import asyncio
import datetime
from ..api import Api
from .model import ChannelId, Schedule
from .model import Channel, Schedule
class ScheduleApi(Api):
INTERVAL: float = 0.5
TYPE = "schedule"
async def get_channels(self) -> list[ChannelId]:
async def find_channels(self, query: str) -> list[Channel]:
raise NotImplementedError
async def get_channel_schedule(
self, channel_id: ChannelId, date: datetime.date
) -> Schedule:
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
raise NotImplementedError
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
channels = await self.get_channels()
results = []
for channel in channels:
results.append(
await self.get_channel_schedule(channel_id=channel, date=date)
)
if self.INTERVAL > 0:
await asyncio.sleep(self.INTERVAL)
return results
+7 -18
View File
@@ -6,20 +6,20 @@ from gallery.sketch.cached import CachedApi, CachePreset
from gallery.util import TimeUnit
from .api import ScheduleApi
from .model import ChannelId, Schedule
from .model import Channel, Schedule
CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6)
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
CACHE_KEY = "schedule"
CACHE_KEY = ScheduleApi.TYPE
@cached(
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels",
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.find.{query}",
**CACHE_PRESET._asdict(),
)
async def get_channels(self) -> list[ChannelId]:
return await self._api.get_channels()
async def find_channels(self, query: str) -> list[Channel]:
return await self._api.find_channels(query)
@cached(
key_builder=lambda fun, self, channel_id, date: (
@@ -27,16 +27,5 @@ class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
),
**CACHE_PRESET._asdict(),
)
async def get_channel_schedule(
self, channel_id: ChannelId, date: datetime.date
) -> Schedule:
return await self._api.get_channel_schedule(channel_id, date)
@cached(
key_builder=lambda fun, self, date: (
f"api.{self.CACHE_KEY}.{self.provider}.all.{date}"
),
**CACHE_PRESET._asdict(),
)
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
return await self._api.get_all_schedules(date)
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
return await self._api.get_schedule(channel_id, date)
-21
View File
@@ -1,21 +0,0 @@
from gallery.sketch.catalog import CatalogBundle
from .model import Channel, ChannelId
BUNDLE = CatalogBundle(
[
Channel(id=ChannelId.MATCH_TV, name="Матч ТВ"),
Channel(id=ChannelId.MATCH_IGRA, name="Матч! Игра"),
Channel(id=ChannelId.MATCH_ARENA, name="Матч! Арена"),
Channel(id=ChannelId.MATCH_FUTBOL_1, name="Футбол 1"),
Channel(id=ChannelId.MATCH_FUTBOL_2, name="Футбол 2"),
Channel(id=ChannelId.MATCH_FUTBOL_3, name="Футбол 3"),
Channel(id=ChannelId.MATCH_STRANA, name="Матч! Страна"),
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
Channel(id=ChannelId.EUROSPORT, name="Europsort"),
Channel(id=ChannelId.EUROSPORT_2, name="Europsort 2"),
Channel(id=ChannelId.START, name="Старт!"),
Channel(id=ChannelId.TEST, name="Тест"),
]
)
+2 -20
View File
@@ -1,5 +1,4 @@
import datetime
from enum import StrEnum
from pydantic import BaseModel
@@ -9,27 +8,10 @@ class Model(BaseModel):
use_enum_values = True
class ChannelId(StrEnum):
MATCH_TV = "matchtv"
MATCH_IGRA = "igra"
MATCH_ARENA = "arena"
MATCH_FUTBOL_1 = "futbol-1"
MATCH_FUTBOL_2 = "futbol-2"
MATCH_FUTBOL_3 = "futbol-3"
MATCH_STRANA = "strana"
MATCH_PLANETA = "planeta"
EUROSPORT = "eurosport"
EUROSPORT_2 = "eurosport-2"
START = "start"
TEST = "test"
def __str__(self) -> str:
return self.value
class Channel(Model):
id: ChannelId
id: str
name: str
provider: str
class ScheduleValue(Model):
+5 -4
View File
@@ -7,9 +7,7 @@ logger = logging.getLogger("source")
class ApiSource:
DEFAULT_USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
)
DEFAULT_TIMEOUT = 30.0
@@ -28,7 +26,10 @@ class ApiSource:
self._headers = headers
async def request(self, endpoint: str) -> str:
url = f"{self._base_url}/{endpoint}"
if endpoint.startswith("https:"):
url = endpoint
else:
url = f"{self._base_url}/{endpoint}"
logger.info(url)
headers = {"User-Agent": self._user_agent, **(self._headers or {})}
async with aiohttp.ClientSession(
+1
View File
@@ -5,6 +5,7 @@ from .model import Location, WeatherResponse
class WeatherApi(Api):
TYPE = "weather"
async def find_locations(self, query: str) -> list[Location]:
raise NotImplementedError
+1 -1
View File
@@ -11,7 +11,7 @@ CACHE_PRESET = DEFAULT_CACHE_PRESET
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
CACHE_KEY = "weather"
CACHE_KEY = WeatherApi.TYPE
@cached(
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",
+36 -30
View File
@@ -1,7 +1,8 @@
import datetime
from enum import Enum
from enum import StrEnum, auto
from typing import Self
from pydantic import BaseModel
from pydantic import BaseModel, Field
class Model(BaseModel):
@@ -12,28 +13,31 @@ class Model(BaseModel):
class Location(Model):
id: str
name: str
provider: str
lat: float
lon: float
country: str
country_code: str
district: str
subdistrict: str
class Cloudness(str, Enum):
CLEAR = "clear"
PARTLY_CLOUDY = "party_cloudy"
CLOUDY = "cloudy"
MAINLY_CLOUDY = "mainly_cloudy"
class Cloudness(StrEnum):
CLEAR = auto()
PARTLY_CLOUDY = auto()
CLOUDY = auto()
MAINLY_CLOUDY = auto()
class Precipitation(str, Enum):
NO = "no"
SMALL_RAIN = "small_rain"
RAIN = "rain"
HEAVY_RAIN = "heavy_rain"
SHOWER = "shower"
SNOW = "snow"
HEAVY_SNOW = "heavy_snow"
class Precipitation(StrEnum):
NO = auto()
SMALL_RAIN = auto()
RAIN = auto()
HEAVY_RAIN = auto()
SHOWER = auto()
SNOW = auto()
HEAVY_SNOW = auto()
HAIL = auto()
class Sky(Model):
@@ -43,16 +47,16 @@ class Sky(Model):
fog: bool
class WindDirection(str, Enum):
CALM = "calm"
N = "N"
NE = "NE"
E = "E"
SE = "SE"
S = "S"
SW = "SW"
W = "W"
NW = "NW"
class WindDirection(StrEnum):
CALM = auto()
N = auto()
NE = auto()
E = auto()
SE = auto()
S = auto()
SW = auto()
W = auto()
NW = auto()
class WindDirectionDeg(float):
@@ -66,7 +70,9 @@ class WindDirectionDeg(float):
# pylint:disable=too-many-return-statements
def to_direction(self) -> WindDirection:
if self > 337.5 or self <= 22.25:
if self == -1:
return WindDirection.CALM
elif self > 337.5 or self <= 22.25:
return WindDirection.N
elif self <= 67.5:
return WindDirection.NE
@@ -83,10 +89,10 @@ class WindDirectionDeg(float):
elif self <= 337.5:
return WindDirection.NW
else:
return WindDirection.CALM
raise ValueError(self)
@classmethod
def from_direction(cls, direction: WindDirection) -> "WindDirectionDeg":
def from_direction(cls, direction: WindDirection) -> Self:
return cls(
{
WindDirection.CALM: -1,
@@ -110,8 +116,8 @@ class WeatherValue(Model):
wind_gust: int
wind_direction: float
precipitation: float
pressure: list[int]
humidity: int
pressure: list[int] = Field(default=[])
humidity: int = Field(default=-1)
class WeatherResponse(Model):
+1 -3
View File
@@ -23,9 +23,7 @@ def build_weather_value(date: datetime.datetime) -> WeatherValue:
)
def merge_weather_values(
date: datetime.datetime, values: list[WeatherValue]
) -> WeatherValue:
def merge_weather_values(date: datetime.datetime, values: list[WeatherValue]) -> WeatherValue:
result = build_weather_value(date)
temperatures = []
pressures = []
+4 -1
View File
@@ -1 +1,4 @@
__version__ = "0.2.1"
import tomllib
from pathlib import Path
__version__ = tomllib.loads((Path(__file__).parent.parent / "pyproject.toml").read_text())["project"]["version"]
+77
View File
@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Index"
msgstr "Содержание"
msgid "View"
msgstr "Просмотр"
msgid "Docs"
msgstr "Документация"
msgid "Toggle theme"
msgstr "Переключить тему"
msgid "Light"
msgstr "Светлая"
msgid "Dark"
msgstr "Тёмная"
msgid "Auto"
msgstr "Авто"
msgid "Select language"
msgstr "Выберите язык"
msgid "English"
msgstr "Английский"
msgid "Russian"
msgstr "Русский"
# weather
msgid "Weather"
msgstr "Погода"
msgid "Enter the city name"
msgstr "Введите название города"
msgid "Search"
msgstr "Поиск"
msgid "Cloudiness"
msgstr "Облачность"
msgid "Temperature, °C"
msgstr "Температура, °C"
msgid "Wind direction"
msgstr "Направление ветра"
msgid "Wind speed, m/s"
msgstr "Скорость ветра, м/с"
msgid "Precipitation, mm"
msgstr "Осадки, мм"
msgid "Pressure, mmHg"
msgstr "Давление, мм рт. ст."
msgid "Humidity, %"
msgstr "Влажность, %"
# tv
msgid "TV program"
msgstr "Телепрограмма"
msgid "Live broadcasts"
msgstr "Прямые трансляции"
Generated
+2341 -1224
View File
File diff suppressed because it is too large Load Diff
+34 -29
View File
@@ -1,42 +1,47 @@
[tool.poetry]
[project]
name = "gallery"
version = "0.2.1"
description = ""
authors = ["shmyga <shmyga.z@gmail.com>"]
version = "0.5.4"
readme = "README.md"
authors = [{ name = "shmyga", email = "shmyga.z@gmail.com" }]
requires-python = '>=3.14,<4.0'
dependencies = [
'aiohttp (>=3.14,<4.0)',
'beautifulsoup4 (>=4.15,<5.0)',
'dateparser (>=1.4,<2.0)',
'pydantic (>=2.13,<3.0)',
'aiocache[redis] (>=0.12,<0.13)',
]
[project.optional-dependencies]
app = [
"fastapi[standard] (>=0.139,<0.140)",
"jinja2 (>=3.1,<4.0)",
"babel (>=2.18,<3.0)",
]
[dependency-groups]
test = ["pytest (>=9.1,<10.0)", "pytest-asyncio (>=1.4,<2.0)"]
dev = ["pylint (>=4.0.6,<5.0.0)", "black (>=26.5,<27.0)", "isort (>=8.0,<9.0)"]
[project.scripts]
gallery = "gallery.main:run"
[tool.poetry]
packages = [{ include = "gallery" }]
[tool.poetry.dependencies]
python = "^3.12"
aiohttp = "^3.9.5"
beautifulsoup4 = "^4.12.3"
dateparser = "^1.2.0"
pydantic = "^2.8.2"
aiocache = {extras = ["redis"], version = "^0.12.2"}
[tool.poetry.group.app.dependencies]
fastapi = "^0.111.1"
jinja2 = "^3.1.4"
[tool.poetry.group.test.dependencies]
pytest = "^8.3.1"
pytest-asyncio = "^0.23.8"
[tool.poetry.group.dev.dependencies]
pylint = "^3.2.6"
black = "^24.4.2"
isort = "^5.13.2"
requires-poetry = '>=2.0.0,<3.0.0'
[build-system]
requires = ["poetry-core"]
requires = ['poetry-core (>=2.0.0,<3.0.0)']
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
gallery = "gallery.main:run"
[tool.black]
line-length = 120
[tool.isort]
profile = "black"
[tool.pytest.ini_options]
addopts = "-p no:warnings"
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.poetry_bumpversion.file."gallery/version.py"]
+12 -2
View File
@@ -29,9 +29,19 @@ publish () {
for PROJECT in "${DOCKER_PROJECTS[@]}"; do
IFS=: read -r PROJECT_NAME PROJECT_TARGET <<< "$PROJECT"
docker tag $DOCKER_GROUP/$PROJECT_NAME $DOCKER_ROOT/$PROJECT_NAME:$VERSION
docker tag $DOCKER_GROUP/$PROJECT_NAME $DOCKER_ROOT/$PROJECT_NAME:latest
docker tag $DOCKER_GROUP/$PROJECT_NAME $DOCKER_ROOT/$PROJECT_NAME:$DOCKER_TAG
docker push $DOCKER_ROOT/$PROJECT_NAME:$VERSION
docker push $DOCKER_ROOT/$PROJECT_NAME:latest
docker push $DOCKER_ROOT/$PROJECT_NAME:$DOCKER_TAG
done
}
save () {
echo "save: $1"
. "$1/.env"
mkdir -p "$1/dist"
for PROJECT in "${DOCKER_PROJECTS[@]}"; do
IFS=: read -r PROJECT_NAME PROJECT_TARGET <<< "$PROJECT"
docker save --output "$1/dist/$PROJECT_NAME-$VERSION.tar" "$DOCKER_GROUP/$PROJECT_NAME"
done
}
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd "$(dirname $(dirname "$0"))" || exit
TARGET="gallery"
poetry run isort $TARGET
poetry run black $TARGET -q
+5 -1
View File
@@ -2,4 +2,8 @@
set -e
cd "$(dirname $(dirname "$0"))" || exit
poetry run pylint gallery
TARGET="gallery"
poetry run pylint $TARGET -sn
poetry run isort $TARGET --check-only
poetry run black $TARGET -q --check --diff
+1 -1
View File
@@ -2,5 +2,5 @@
set -e
cd "$(dirname $(dirname "$0"))" || exit
cd gallery/easel/route/view/locales/ru/LC_MESSAGES || exit
cd locales/ru/LC_MESSAGES || exit
msgfmt messages.po
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -e
cd "$(dirname $(dirname "$0"))" || exit
# env
if [[ ! -f .env ]]; then
cp .env-base .env
fi
source .env
# python
poetry env use ${PYTHON_VERSION}
poetry install
# static
cd static || exit
if [[ -f $HOME/.nvm/nvm.sh ]]; then
source "$HOME/.nvm/nvm.sh"
nvm use
fi
npm ci
+1 -1
View File
@@ -2,4 +2,4 @@
set -e
cd "$(dirname $(dirname "$0"))" || exit
poetry run pytest tests
OPENWEATHER_KEY="" poetry run pytest tests
+4048 -619
View File
File diff suppressed because it is too large Load Diff
+31 -6
View File
@@ -1,9 +1,10 @@
{
"name": "gallery",
"version": "0.2.1",
"version": "0.5.4",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
"build": "webpack --mode production",
"dev": "webpack --watch --mode development"
},
"author": "shmyga <shmyga.z@gmail.com>",
"license": "ISC",
@@ -12,9 +13,33 @@
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"sass": "^1.99.0"
"flag-icons": "^7.5.0"
},
"devDependencies": {
"vite": "^8.0.9"
}
"@babel/core": "^8.0.1",
"@babel/preset-env": "^8.0.2",
"@types/bootstrap": "^5.2.11",
"@types/node": "^26.1.0",
"autoprefixer": "^10.5.4",
"babel-loader": "^10.1.1",
"copy-webpack-plugin": "^14.0.0",
"core-js": "^3.49.0",
"css-loader": "^7.1.4",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.10.2",
"postcss": "^8.5.22",
"postcss-loader": "^8.2.1",
"sass": "^1.101.0",
"sass-loader": "^17.0.0",
"style-loader": "^4.0.0",
"ts-loader": "^9.6.2",
"ts-node": "^10.9.2",
"typescript": "^6.0.3",
"webpack": "^5.108.3",
"webpack-cli": "^7.1.0"
},
"browserslist": [
"> 0.5%",
"last 2 versions"
]
}
@@ -19,7 +19,7 @@
setLanguage(getPreferredLanguage());
const showActiveLanguage = (language: string, focus = false) => {
const languageSwitcher = document.querySelector("#bd-language");
const languageSwitcher = document.querySelector<HTMLElement>("#bd-language");
if (!languageSwitcher) {
return;
@@ -27,22 +27,26 @@
const languageSwitcherText = document.querySelector("#bd-language-text");
const activeLanguageIcon = document.querySelector(".language-icon-active");
const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
const activeLanguageIconContent = btnToActive?.querySelector("span")?.textContent;
const btnToActive = document.querySelector<HTMLElement>(`[data-bs-language-value="${language}"]`);
const activeLanguageIconClass = (btnToActive?.querySelector(".fi")?.className.match(/fi-[\w-]+/) || [])[0] || "";
document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
element.classList.remove("active");
element.setAttribute("aria-pressed", "false");
});
btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true");
activeLanguageIcon.textContent = activeLanguageIconContent;
const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
btnToActive?.classList.add("active");
btnToActive?.setAttribute("aria-pressed", "true");
const classesToRemove = Array.from(activeLanguageIcon?.classList || []).filter((className) =>
className.startsWith("fi-"),
);
activeLanguageIcon?.classList.remove(...classesToRemove);
activeLanguageIcon?.classList.add(activeLanguageIconClass);
const languageSwitcherLabel = `${languageSwitcherText?.textContent} (${btnToActive?.dataset.bsLanguageValue})`;
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);
if (focus) {
languageSwitcher.focus();
languageSwitcher?.focus();
}
};
@@ -25,7 +25,7 @@
setTheme(getPreferredTheme());
const showActiveTheme = (theme: string, focus = false) => {
const themeSwitcher = document.querySelector("#bd-theme");
const themeSwitcher = document.querySelector<HTMLElement>("#bd-theme");
if (!themeSwitcher) {
return;
@@ -33,24 +33,26 @@
const themeSwitcherText = document.querySelector("#bd-theme-text");
const activeThemeIcon = document.querySelector(".theme-icon-active");
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
const activeThemeIconClass = btnToActive.querySelector("i.bi").className.match(/bi-[\w-]+/)[0];
const btnToActive = document.querySelector<HTMLElement>(`[data-bs-theme-value="${theme}"]`);
const activeThemeIconClass = (btnToActive?.querySelector(".bi")?.className.match(/bi-[\w-]+/) || [])[0] || "";
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
element.classList.remove("active");
element.setAttribute("aria-pressed", "false");
});
btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true");
const classesToRemove = Array.from(activeThemeIcon.classList).filter((className) => className.startsWith("bi-"));
activeThemeIcon.classList.remove(...classesToRemove);
activeThemeIcon.classList.add(activeThemeIconClass);
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
btnToActive?.classList.add("active");
btnToActive?.setAttribute("aria-pressed", "true");
const classesToRemove = Array.from(activeThemeIcon?.classList || []).filter((className) =>
className.startsWith("bi-"),
);
activeThemeIcon?.classList.remove(...classesToRemove);
activeThemeIcon?.classList.add(activeThemeIconClass);
const themeSwitcherLabel = `${themeSwitcherText?.textContent} (${btnToActive?.dataset.bsThemeValue})`;
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
if (focus) {
themeSwitcher.focus();
themeSwitcher?.focus();
}
};
@@ -66,7 +68,7 @@
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
toggle.addEventListener("click", () => {
const theme = toggle.getAttribute("data-bs-theme-value") || '';
const theme = toggle.getAttribute("data-bs-theme-value") || "";
setStoredTheme(theme);
setTheme(theme);
showActiveTheme(theme, true);
+6
View File
@@ -0,0 +1,6 @@
import { Tooltip } from "bootstrap";
document.addEventListener("DOMContentLoaded", (event) => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map((tooltipTriggerEl) => new Tooltip(tooltipTriggerEl));
});
@@ -1,13 +1,13 @@
class AppLinkElement extends HTMLElement {
static observedAttributes = ["icon", "href"];
static observedAttributes = ["icon", "href", "target"];
constructor() {
super();
this.innerHTML = `
<a href="${this.getAttribute("href")}"
<a href="${this.getAttribute("href")}" target="${this.getAttribute("target") || ""}"
class="d-flex align-items-center text-body text-decoration-none">
<span class="fs-4">
<i class="bi bi-${this.getAttribute("icon")}"></i>
<span class="bi bi-${this.getAttribute("icon")} me-1"></span>
<span>${this.textContent}</span>
</span>
</a>`;
+110
View File
@@ -0,0 +1,110 @@
export interface Channel {
id: string;
name: string;
provider: string;
}
export class ScheduleChannelStorage {
private storageKey: string = "schedule:channels";
getAll(): Channel[] {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
return Object.values(objects);
}
add(object: Channel) {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
objects[object.id] = object;
window.localStorage.setItem(this.storageKey, JSON.stringify(objects));
}
remove(id: string) {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
delete objects[id];
window.localStorage.setItem(this.storageKey, JSON.stringify(objects));
}
}
export class ScheduleChannelManager {
loadChannels(selector: string) {
const channels = window.scheduleChannelStorage.getAll();
const container = document.querySelector(selector);
if (container) {
container.innerHTML = "";
for (const channel of channels) {
const element = new ScheduleChannelElement();
element.setAttribute("channel", JSON.stringify(channel));
element.setAttribute("removable", "");
container.appendChild(element);
}
}
}
}
class ScheduleChannelElement extends HTMLElement {
static observedAttributes = ["channel", "removable"];
channel: Channel | null = null;
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
}
connectedCallback() {
this.channel = JSON.parse(this.getAttribute("channel") || "{}");
this.innerHTML = `
<a href="${this.channel?.provider}/${this.channel?.id}"
class="d-flex justify-content-between align-items-start text-decoration-none">
<span>
<span class="text-primary">${this.channel?.name}</span>
</span>
<span>
<span class="badge text-bg-secondary">${this.channel?.provider}</span>
<span class="text-danger ms-2" data-remove>
&#x2715;
</span>
</span>
</a>`;
this.classList = "list-group-item list-group-item-action";
this.addEventListener("click", this.handleClick);
if (this.hasAttribute("removable")) {
this.querySelector<HTMLElement>("[data-remove]")?.addEventListener("click", this.handleRemoveClick);
} else {
this.querySelector("[data-remove]")?.remove();
}
}
disconnectedCallback() {
this.removeEventListener("click", this.handleClick);
this.querySelector<HTMLElement>("[data-remove]")?.removeEventListener("click", this.handleRemoveClick);
}
handleClick(event: MouseEvent) {
if (this.channel) {
window.scheduleChannelStorage.add(this.channel);
}
}
handleRemoveClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.channel) {
window.scheduleChannelStorage.remove(this.channel.id);
}
this.remove();
}
}
customElements.define("schedule-channel", ScheduleChannelElement);
declare global {
interface Window {
scheduleChannelStorage: ScheduleChannelStorage;
scheduleChannelManager: ScheduleChannelManager;
}
}
window.scheduleChannelStorage = new ScheduleChannelStorage();
window.scheduleChannelManager = new ScheduleChannelManager();
+118
View File
@@ -0,0 +1,118 @@
export interface Location {
id: string;
name: string;
provider: string;
country_code: string;
country?: string;
district?: string;
subdistrict?: string;
}
export class WeatherLocationStorage {
private storageKey: string = "weather:locations";
getAll(): Location[] {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
return Object.values(locations);
}
add(location: Location) {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
locations[location.id] = location;
window.localStorage.setItem(this.storageKey, JSON.stringify(locations));
}
remove(id: string) {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
delete locations[id];
window.localStorage.setItem(this.storageKey, JSON.stringify(locations));
}
}
export class WeatherLocationManager {
loadLocations(selector: string) {
const locations = window.weatherLocationStorage.getAll();
const container = document.querySelector(selector);
if (container) {
container.innerHTML = "";
for (const location of locations) {
const element = new WeatherLocationElement();
element.setAttribute("location", JSON.stringify(location));
element.setAttribute("removable", "");
container.appendChild(element);
}
}
}
}
class WeatherLocationElement extends HTMLElement {
static observedAttributes = ["location", "removable"];
location: Location | null = null;
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
}
connectedCallback() {
this.location = JSON.parse(this.getAttribute("location") || "{}");
this.innerHTML = `
<a href="${this.location?.provider}/${this.location?.id}"
class="d-flex justify-content-between align-items-start text-decoration-none">
<span>
<span class="fi fi-${this.location?.country_code} me-1"></span>
<span class="text-primary">${this.location?.name}</span>
<span class="small ms-1 text-secondary">
${this.location?.country}, ${this.location?.district}, ${this.location?.subdistrict}
</span>
</span>
<span>
<span class="badge text-bg-secondary">${this.location?.provider}</span>
<span class="text-danger ms-2" data-remove>
&#x2715;
</span>
</span>
</a>`;
this.classList = 'list-group-item list-group-item-action';
this.addEventListener("click", this.handleClick);
if (this.hasAttribute("removable")) {
this.querySelector<HTMLElement>("[data-remove]")?.addEventListener("click", this.handleRemoveClick);
} else {
this.querySelector("[data-remove]")?.remove();
}
}
disconnectedCallback() {
this.removeEventListener("click", this.handleClick);
this.querySelector<HTMLElement>("[data-remove]")?.removeEventListener("click", this.handleRemoveClick);
}
handleClick(event: MouseEvent) {
if (this.location) {
window.weatherLocationStorage.add(this.location);
}
}
handleRemoveClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.location) {
window.weatherLocationStorage.remove(this.location.id);
}
this.remove();
}
}
customElements.define("weather-location", WeatherLocationElement);
declare global {
interface Window {
weatherLocationStorage: WeatherLocationStorage;
weatherLocationManager: WeatherLocationManager;
}
}
window.weatherLocationStorage = new WeatherLocationStorage();
window.weatherLocationManager = new WeatherLocationManager();
+8 -5
View File
@@ -1,5 +1,8 @@
import "./main.scss";
import "bootstrap";
import "./theme";
import "./language";
import "./components";
import "./app/common/language";
import "./app/common/theme";
import "./app/common/tooltips";
import "./app/components/app-link";
import "./app/schedule";
import "./app/weather";
import { init } from "./lib/weather-icons";
init("/static/weather-icons/icons");
+48
View File
@@ -0,0 +1,48 @@
.icon-link {
vertical-align: -0.25rem;
}
.bi {
display: inline-block;
text-transform: none;
line-height: 1;
vertical-align: -0.125em;
width: 1em;
height: 1em;
mask-size: contain;
mask-position: 50%;
mask-repeat: no-repeat;
background-color: currentColor;
&.bi-circle-half {
mask-image: url(bootstrap-icons/icons/circle-half.svg);
}
&.bi-moon-stars-fill {
mask-image: url(bootstrap-icons/icons/moon-stars-fill.svg);
}
&.bi-brightness-high {
mask-image: url(bootstrap-icons/icons/brightness-high.svg);
}
&.bi-gear {
mask-image: url(bootstrap-icons/icons/gear.svg);
}
&.bi-sun-fill {
mask-image: url(bootstrap-icons/icons/sun-fill.svg);
}
&.bi-tv {
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);
}
&.bi-file-earmark-text {
mask-image: url(bootstrap-icons/icons/file-earmark-text.svg);
}
}
+48
View File
@@ -0,0 +1,48 @@
// scss-docs-start import-stack
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
//@import "bootstrap/scss/images";
//@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
//@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
//@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
//@import "bootstrap/scss/card";
//@import "bootstrap/scss/accordion";
//@import "bootstrap/scss/breadcrumb";
//@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
//@import "bootstrap/scss/alert";
//@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
//@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts";
//@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
//@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel";
//@import "bootstrap/scss/spinners";
//@import "bootstrap/scss/offcanvas";
//@import "bootstrap/scss/placeholders";
// Helpers
@import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
// scss-docs-end import-stack
+10
View File
@@ -0,0 +1,10 @@
@use "flag-icons/sass/flag-icons" with (
$flag-icons-path: "flag-icons/flags",
);
.fir {
@extend .fis;
border-radius: 50%;
height: 1em;
border: 1px solid gray;
}
+3
View File
@@ -0,0 +1,3 @@
@use "./weather-icons/sass/weather-icons" with (
$weather-icons-path: "weather-icons/icons"
);
@@ -0,0 +1,3 @@
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path id="cloud" d="M4.61,16.88c0-1.15,0.36-2.17,1.08-3.07c0.72-0.9,1.63-1.48,2.74-1.73c0.31-1.37,1.02-2.49,2.11-3.37s2.35-1.32,3.76-1.32 c1.38,0,2.61,0.43,3.69,1.28s1.78,1.95,2.1,3.29h0.33c0.9,0,1.73,0.22,2.49,0.65s1.37,1.03,1.81,1.79c0.44,0.76,0.67,1.58,0.67,2.48 c0,0.88-0.21,1.7-0.63,2.45s-1,1.35-1.73,1.8c-0.73,0.45-1.54,0.69-2.41,0.72H9.41c-1.34-0.06-2.47-0.57-3.4-1.53 C5.08,19.37,4.61,18.22,4.61,16.88z M6.32,16.88c0,0.87,0.3,1.62,0.9,2.26s1.33,0.98,2.19,1.03h11.19c0.86-0.04,1.59-0.39,2.19-1.03 c0.61-0.64,0.91-1.4,0.91-2.26c0-0.88-0.33-1.63-0.98-2.27c-0.65-0.64-1.42-0.96-2.32-0.96H18.8c-0.11,0-0.17-0.06-0.17-0.18 l-0.07-0.57c-0.11-1.08-0.58-1.99-1.4-2.72c-0.82-0.73-1.77-1.1-2.86-1.1c-1.09,0-2.05,0.37-2.85,1.1 c-0.81,0.73-1.27,1.64-1.37,2.72l-0.08,0.57c0,0.12-0.07,0.18-0.2,0.18H9.27c-0.84,0.1-1.54,0.46-2.1,1.07S6.32,16.05,6.32,16.88z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,3 @@
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path id="cloud" d="M3.89,17.6c0-0.99,0.31-1.88,0.93-2.65s1.41-1.27,2.38-1.49c0.26-1.17,0.85-2.14,1.78-2.88c0.93-0.75,2-1.12,3.22-1.12 c1.18,0,2.24,0.36,3.16,1.09c0.93,0.73,1.53,1.66,1.8,2.8h0.27c1.18,0,2.18,0.41,3.01,1.24s1.25,1.83,1.25,3 c0,1.18-0.42,2.18-1.25,3.01s-1.83,1.25-3.01,1.25H8.16c-0.58,0-1.13-0.11-1.65-0.34S5.52,21,5.14,20.62 c-0.38-0.38-0.68-0.84-0.91-1.36S3.89,18.17,3.89,17.6z M5.34,17.6c0,0.76,0.28,1.42,0.82,1.96s1.21,0.82,1.99,0.82h9.28 c0.77,0,1.44-0.27,1.99-0.82c0.55-0.55,0.83-1.2,0.83-1.96c0-0.76-0.27-1.42-0.83-1.96c-0.55-0.54-1.21-0.82-1.99-0.82h-1.39 c-0.1,0-0.15-0.05-0.15-0.15l-0.07-0.49c-0.1-0.94-0.5-1.73-1.19-2.35s-1.51-0.93-2.45-0.93c-0.94,0-1.76,0.31-2.46,0.94 c-0.7,0.62-1.09,1.41-1.18,2.34l-0.07,0.42c0,0.1-0.05,0.15-0.16,0.15l-0.45,0.07c-0.72,0.06-1.32,0.36-1.81,0.89 C5.59,16.24,5.34,16.87,5.34,17.6z M14.19,8.88c-0.1,0.09-0.08,0.16,0.07,0.21c0.43,0.19,0.79,0.37,1.08,0.55 c0.11,0.03,0.19,0.02,0.22-0.03c0.61-0.57,1.31-0.86,2.12-0.86c0.81,0,1.5,0.27,2.1,0.81c0.59,0.54,0.92,1.21,0.99,2l0.09,0.64h1.42 c0.65,0,1.21,0.23,1.68,0.7c0.47,0.47,0.7,1.02,0.7,1.66c0,0.6-0.21,1.12-0.62,1.57s-0.92,0.7-1.53,0.77c-0.1,0-0.15,0.05-0.15,0.16 v1.13c0,0.11,0.05,0.16,0.15,0.16c1.01-0.06,1.86-0.46,2.55-1.19s1.04-1.6,1.04-2.6c0-1.06-0.37-1.96-1.12-2.7 c-0.75-0.75-1.65-1.12-2.7-1.12h-0.15c-0.26-1-0.81-1.82-1.65-2.47c-0.83-0.65-1.77-0.97-2.8-0.97C16.28,7.29,15.11,7.82,14.19,8.88 z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.63 13.83 C 3.34 12.93 4.25 12.36 5.37 12.10 C 5.67 10.74 6.37 9.62 7.45 8.75 C 8.53 7.88 9.77 7.44 11.18 7.44 C 12.21 7.44 13.11 7.66 13.88 8.10 C 13.97 8.01 15.37 9.00 15.31 9.06 C 16.19 9.93 16.75 10.91 17.00 12.00 L 17.32 12.00 C 18.68 12.00 19.84 12.48 20.81 13.44 C 20.84 13.38 22.01 14.72 21.86 14.90 C 22.14 15.57 22.28 16.24 22.28 16.90 C 22.28 17.80 22.05 18.63 21.61 19.39 C 21.17 20.15 20.56 20.75 19.80 21.19 C 19.04 21.63 18.21 21.85 17.32 21.85 L 6.49 21.85 C 5.60 21.85 4.78 21.63 4.02 21.19 C 3.26 20.75 2.66 20.15 2.22 19.39 C 1.78 18.63 1.56 17.80 1.56 16.90 C 1.56 15.75 1.92 14.73 2.63 13.83 Z M 4.22 19.22 C 4.86 19.87 5.61 20.19 6.49 20.19 L 17.33 20.19 C 18.21 20.19 18.98 19.87 19.61 19.23 C 20.24 18.59 20.56 17.82 20.56 16.91 C 20.56 16.03 20.24 15.28 19.60 14.64 C 18.96 14.00 18.20 13.68 17.32 13.68 L 15.71 13.68 C 15.59 13.68 15.52 13.62 15.50 13.51 L 15.43 12.93 C 15.33 11.85 14.88 10.95 14.06 10.22 C 13.25 9.49 12.29 9.12 11.19 9.12 C 10.12 9.12 9.18 9.48 8.38 10.21 C 7.58 10.93 7.11 11.84 6.97 12.92 L 6.90 13.46 C 6.90 13.58 6.83 13.64 6.71 13.64 L 6.21 13.67 C 5.37 13.77 4.67 14.12 4.11 14.73 C 3.55 15.34 3.27 16.06 3.27 16.90 C 3.27 17.80 3.59 18.57 4.22 19.22 Z"/>
<g id="sun">
<path d="M 17.89 6.23 C 18.60 6.23 19.28 6.36 19.93 6.64 C 20.58 6.91 21.13 7.29 21.60 7.76 C 22.07 8.23 22.45 8.78 22.72 9.43 C 22.99 10.08 23.13 10.76 23.13 11.47 C 23.13 12.49 22.36 14.03 21.87 14.83 L 20.85 13.50 C 21.12 12.95 21.45 12.02 21.45 11.47 C 21.45 10.48 21.10 9.65 20.41 8.96 C 19.72 8.27 18.88 7.93 17.89 7.93 C 17.03 7.93 15.96 8.51 15.31 9.07 L 13.92 8.12 C 14.96 7.14 16.54 6.23 17.89 6.23 Z"/>
<path d="M 24.72 11.60 C 24.72 11.83 24.81 12.02 24.98 12.18 C 25.14 12.34 25.35 12.42 25.59 12.42 L 27.63 12.42 C 27.86 12.42 28.05 12.34 28.21 12.19 C 28.37 12.04 28.44 11.84 28.44 11.60 C 28.44 11.36 28.36 11.16 28.21 11.00 C 28.06 10.84 27.86 10.75 27.63 10.75 L 25.59 10.75 C 25.35 10.75 25.15 10.83 24.98 11.00 C 24.80 11.17 24.72 11.37 24.72 11.60 Z"/>
<path d="M 23.25 17.91 C 23.25 18.15 23.33 18.36 23.50 18.54 L 24.15 19.17 C 24.30 19.33 24.49 19.41 24.73 19.41 C 24.97 19.41 25.17 19.33 25.33 19.16 C 25.49 18.99 25.57 18.79 25.57 18.54 C 25.57 18.32 25.49 18.12 25.33 17.96 L 24.68 17.31 C 24.52 17.15 24.33 17.07 24.11 17.07 C 23.87 17.07 23.67 17.15 23.51 17.31 C 23.34 17.47 23.25 17.67 23.25 17.91 Z"/>
<path d="M 22.46 6.07 C 22.46 6.33 22.53 6.53 22.68 6.69 C 22.89 6.85 23.10 6.93 23.30 6.93 C 23.48 6.93 23.68 6.85 23.89 6.69 L 25.32 5.26 C 25.48 5.08 25.56 4.87 25.56 4.62 C 25.56 4.38 25.48 4.18 25.32 4.02 C 25.16 3.86 24.96 3.78 24.73 3.78 C 24.49 3.78 24.30 3.86 24.15 4.02 L 22.68 5.45 C 22.53 5.64 22.46 5.84 22.46 6.07 Z"/>
<path d="M 16.94 3.78 C 16.94 4.04 17.02 4.24 17.17 4.40 C 17.32 4.56 17.52 4.63 17.76 4.63 C 18.02 4.63 18.22 4.55 18.38 4.40 C 18.54 4.24 18.61 4.04 18.61 3.78 L 18.61 1.73 C 18.61 1.49 18.53 1.30 18.37 1.14 C 18.21 0.98 18.01 0.91 17.76 0.91 C 17.52 0.91 17.33 0.99 17.17 1.14 C 17.01 1.29 16.94 1.49 16.94 1.73 L 16.94 3.78 L 16.94 3.78 Z"/>
<path d="M 9.97 4.63 C 9.97 4.87 10.05 5.08 10.21 5.26 L 10.87 5.90 C 11.12 6.09 11.33 6.17 11.51 6.15 C 11.72 6.15 11.90 6.06 12.06 5.89 C 12.22 5.72 12.30 5.51 12.30 5.27 C 12.30 5.03 12.21 4.83 12.04 4.68 L 11.45 4.02 C 11.27 3.86 11.07 3.78 10.84 3.78 C 10.60 3.78 10.39 3.86 10.22 4.03 C 10.05 4.19 9.97 4.39 9.97 4.63 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.55 13.78 C 3.27 12.88 4.18 12.30 5.29 12.04 C 5.60 10.67 6.31 9.54 7.40 8.67 C 8.49 7.80 9.75 7.36 11.16 7.36 C 12.17 7.36 13.07 7.58 13.87 8.02 C 13.94 7.94 15.35 8.96 15.30 9.00 C 16.14 9.81 16.70 10.80 17.00 11.96 L 17.29 11.96 C 18.68 11.96 19.84 12.44 20.80 13.40 C 20.84 13.33 22.02 14.68 21.86 14.87 C 22.13 15.47 22.27 16.14 22.27 16.88 C 22.27 18.22 21.81 19.37 20.88 20.34 C 19.96 21.30 18.83 21.81 17.49 21.85 C 17.36 21.85 17.29 21.79 17.29 21.68 L 17.29 20.34 C 17.29 20.22 17.36 20.16 17.49 20.16 C 18.34 20.11 19.07 19.77 19.68 19.13 C 20.29 18.49 20.59 17.74 20.59 16.87 C 20.59 16.00 20.26 15.24 19.61 14.60 C 18.96 13.96 18.18 13.64 17.29 13.64 L 15.67 13.64 C 15.56 13.64 15.50 13.57 15.50 13.45 L 15.43 12.87 C 15.32 11.79 14.85 10.89 14.03 10.16 C 13.21 9.43 12.26 9.07 11.17 9.07 C 10.07 9.07 9.12 9.43 8.31 10.16 C 7.50 10.88 7.05 11.79 6.94 12.87 L 6.87 13.42 C 6.87 13.54 6.80 13.60 6.67 13.60 L 6.14 13.64 C 5.31 13.74 4.61 14.09 4.04 14.71 C 3.47 15.33 3.19 16.05 3.19 16.88 C 3.19 17.74 3.49 18.50 4.09 19.14 C 4.69 19.78 5.42 20.13 6.28 20.17 C 6.40 20.17 6.46 20.23 6.46 20.35 L 6.46 21.69 C 6.46 21.80 6.40 21.86 6.28 21.86 C 4.95 21.80 3.81 21.29 2.88 20.33 C 1.95 19.37 1.48 18.22 1.48 16.88 C 1.48 15.73 1.84 14.70 2.55 13.78 Z"/>
<g id="sun">
<path d="M 17.77 6.27 C 18.71 6.27 19.59 6.51 20.40 6.98 C 21.21 7.46 21.85 8.10 22.33 8.91 C 22.80 9.72 23.04 10.60 23.04 11.54 C 23.04 12.55 22.38 14.09 21.88 14.91 L 20.82 13.45 C 21.09 12.91 21.32 12.10 21.32 11.55 C 21.32 10.57 20.97 9.74 20.28 9.06 C 19.59 8.37 18.76 8.03 17.78 8.03 C 16.87 8.03 15.97 8.45 15.33 9.00 L 13.88 8.00 C 14.88 6.98 16.31 6.27 17.77 6.27 Z"/>
<path d="M 24.74 11.55 C 24.74 11.79 24.83 11.99 25.00 12.15 C 25.18 12.33 25.38 12.41 25.62 12.41 L 27.65 12.41 C 27.89 12.41 28.09 12.33 28.26 12.16 C 28.43 11.99 28.52 11.79 28.52 11.55 C 28.52 11.32 28.44 11.12 28.27 10.96 C 28.10 10.80 27.89 10.72 27.65 10.72 L 25.62 10.72 C 25.37 10.72 25.16 10.80 24.99 10.96 C 24.83 11.12 24.74 11.32 24.74 11.55 Z"/>
<path d="M 23.28 17.92 C 23.28 18.15 23.36 18.35 23.52 18.52 L 24.18 19.15 C 24.32 19.33 24.52 19.42 24.78 19.42 C 25.02 19.42 25.21 19.33 25.35 19.15 C 25.53 18.99 25.62 18.79 25.62 18.55 C 25.62 18.31 25.53 18.11 25.35 17.94 L 24.70 17.32 C 24.54 17.14 24.35 17.06 24.12 17.06 C 23.89 17.06 23.69 17.14 23.52 17.31 C 23.36 17.48 23.28 17.69 23.28 17.92 Z"/>
<path d="M 22.47 6.02 C 22.47 6.26 22.55 6.46 22.72 6.62 C 22.87 6.79 23.06 6.88 23.30 6.88 C 23.53 6.88 23.74 6.79 23.90 6.62 L 25.34 5.18 C 25.52 5.03 25.61 4.83 25.61 4.58 C 25.61 4.34 25.52 4.14 25.35 3.97 C 25.18 3.80 24.97 3.72 24.74 3.72 C 24.52 3.72 24.33 3.81 24.17 3.99 L 22.72 5.42 C 22.56 5.57 22.47 5.78 22.47 6.02 Z"/>
<path d="M 16.92 3.73 C 16.92 3.97 17.00 4.17 17.17 4.34 C 17.34 4.51 17.54 4.59 17.78 4.59 C 18.01 4.59 18.21 4.51 18.37 4.34 C 18.53 4.17 18.61 3.97 18.61 3.73 L 18.61 1.67 C 18.61 1.43 18.53 1.23 18.37 1.06 C 18.21 0.89 18.02 0.81 17.78 0.81 C 17.54 0.81 17.34 0.89 17.17 1.06 C 17.00 1.23 16.92 1.43 16.92 1.67 L 16.92 3.73 Z"/>
<path d="M 9.94 4.57 C 9.94 4.82 10.02 5.02 10.18 5.17 L 10.83 5.82 C 10.99 5.98 11.17 6.07 11.37 6.09 C 11.58 6.12 11.78 6.04 11.98 5.86 C 12.18 5.68 12.28 5.46 12.28 5.22 C 12.28 4.98 12.20 4.78 12.04 4.62 L 11.40 3.98 C 11.21 3.81 11.01 3.73 10.78 3.73 C 10.54 3.73 10.33 3.81 10.17 3.97 C 10.02 4.14 9.94 4.34 9.94 4.57 Z"/>
</g>
<g id="rain">
<path d="M 14.23 21.08 C 14.23 21.24 14.28 21.39 14.38 21.53 C 14.48 21.68 14.64 21.78 14.84 21.84 C 14.93 21.86 15.01 21.87 15.09 21.87 C 15.48 21.87 15.74 21.67 15.88 21.26 L 16.91 18.08 C 16.99 17.85 16.96 17.63 16.84 17.43 C 16.72 17.23 16.55 17.10 16.32 17.04 C 16.08 16.97 15.87 16.99 15.68 17.10 C 15.49 17.21 15.36 17.37 15.28 17.61 L 14.26 20.81 C 14.25 20.94 14.23 21.03 14.23 21.08 Z"/>
<path d="M 13.51 23.64 C 13.51 23.77 13.53 23.87 13.58 23.95 C 13.67 24.16 13.82 24.30 14.03 24.39 C 14.14 24.44 14.25 24.47 14.38 24.47 C 14.44 24.47 14.54 24.45 14.68 24.41 C 14.91 24.32 15.06 24.18 15.14 23.97 C 15.22 23.75 15.22 23.54 15.14 23.34 C 15.06 23.14 14.92 22.99 14.72 22.89 C 14.49 22.78 14.28 22.77 14.06 22.86 C 13.85 22.95 13.69 23.10 13.58 23.33 C 13.53 23.41 13.51 23.51 13.51 23.64 Z"/>
<path d="M 10.06 24.03 C 10.06 24.19 10.11 24.35 10.22 24.51 C 10.33 24.67 10.49 24.78 10.70 24.84 C 10.81 24.86 10.89 24.88 10.94 24.88 C 11.09 24.88 11.22 24.85 11.32 24.80 C 11.52 24.72 11.66 24.53 11.75 24.23 L 13.55 18.09 C 13.62 17.85 13.60 17.64 13.49 17.44 C 13.38 17.24 13.22 17.11 12.99 17.05 C 12.75 16.98 12.53 17.00 12.33 17.11 C 12.13 17.22 11.99 17.38 11.92 17.62 L 10.08 23.81 C 10.07 23.92 10.06 24.00 10.06 24.03 Z"/>
<path d="M 9.33 26.72 C 9.33 26.85 9.35 26.95 9.38 27.01 C 9.47 27.23 9.62 27.38 9.83 27.46 C 9.92 27.51 10.04 27.53 10.18 27.53 C 10.24 27.53 10.34 27.51 10.48 27.47 C 10.70 27.39 10.86 27.24 10.95 27.02 C 11.04 26.80 11.05 26.58 10.95 26.36 C 10.85 26.14 10.70 25.99 10.50 25.91 C 10.30 25.83 10.09 25.83 9.88 25.91 C 9.69 25.98 9.55 26.10 9.46 26.26 C 9.37 26.42 9.33 26.58 9.33 26.72 Z"/>
<path d="M 7.46 21.10 C 7.46 21.24 7.49 21.37 7.55 21.48 C 7.74 21.79 8.04 21.94 8.44 21.94 C 8.76 21.94 8.99 21.72 9.13 21.29 L 10.17 18.07 C 10.25 17.83 10.23 17.60 10.10 17.40 C 9.97 17.20 9.79 17.07 9.55 17.03 C 9.34 16.98 9.13 17.00 8.93 17.10 C 8.73 17.21 8.59 17.38 8.52 17.60 L 7.49 20.82 C 7.47 20.92 7.46 21.02 7.46 21.10 Z"/>
<path d="M 6.82 23.94 C 6.92 24.16 7.07 24.31 7.28 24.39 C 7.48 24.49 7.69 24.50 7.91 24.41 C 8.13 24.33 8.28 24.18 8.36 23.96 C 8.46 23.74 8.47 23.53 8.38 23.31 C 8.30 23.10 8.15 22.95 7.93 22.87 C 7.73 22.76 7.52 22.75 7.31 22.84 C 7.09 22.93 6.94 23.08 6.84 23.31 C 6.74 23.49 6.73 23.70 6.82 23.94 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.64 13.83 C 3.36 12.93 4.27 12.36 5.37 12.10 C 5.68 10.72 6.38 9.60 7.47 8.73 C 8.56 7.86 9.81 7.43 11.23 7.43 C 12.14 7.43 13.02 7.64 13.88 8.05 C 13.93 8.00 15.31 8.99 15.28 9.02 C 16.16 9.84 16.74 10.82 17.00 11.96 L 17.31 11.96 C 18.64 11.96 19.79 12.43 20.77 13.37 C 20.80 13.31 21.98 14.64 21.82 14.83 C 22.13 15.53 22.29 16.22 22.29 16.90 C 22.29 18.24 21.83 19.39 20.91 20.35 C 19.99 21.31 18.86 21.82 17.53 21.86 C 17.40 21.86 17.33 21.80 17.33 21.69 L 17.33 20.32 C 17.33 20.20 17.40 20.14 17.53 20.14 C 18.40 20.07 19.13 19.72 19.72 19.10 C 20.31 18.48 20.61 17.74 20.61 16.89 C 20.61 16.00 20.29 15.24 19.65 14.61 C 19.01 13.98 18.24 13.66 17.33 13.66 L 15.73 13.66 C 15.60 13.66 15.54 13.60 15.54 13.49 L 15.47 12.91 C 15.35 11.82 14.89 10.91 14.08 10.19 C 13.27 9.46 12.32 9.10 11.23 9.10 C 10.13 9.10 9.18 9.46 8.37 10.19 C 7.56 10.91 7.11 11.82 7.00 12.90 L 6.94 13.41 C 6.94 13.52 6.87 13.58 6.74 13.58 L 6.22 13.66 C 5.38 13.73 4.69 14.08 4.12 14.70 C 3.55 15.32 3.27 16.05 3.27 16.89 C 3.27 17.74 3.57 18.48 4.16 19.10 C 4.75 19.73 5.48 20.07 6.35 20.14 C 6.46 20.14 6.52 20.20 6.52 20.32 L 6.52 21.69 C 6.52 21.80 6.46 21.86 6.35 21.86 C 5.01 21.80 3.88 21.29 2.95 20.33 C 2.02 19.37 1.56 18.23 1.56 16.90 C 1.56 15.75 1.92 14.73 2.64 13.83 Z"/>
<g id="sun">
<path d="M 17.76 6.32 C 18.47 6.32 19.14 6.46 19.79 6.73 C 20.43 7.01 20.99 7.38 21.46 7.85 C 21.93 8.32 22.30 8.87 22.58 9.52 C 22.86 10.17 23.00 10.85 23.00 11.56 C 23.00 12.55 22.32 14.09 21.82 14.89 L 20.81 13.42 C 21.08 12.88 21.28 12.11 21.28 11.56 C 21.28 10.57 20.94 9.73 20.25 9.04 C 19.57 8.35 18.74 8.00 17.76 8.00 C 16.83 8.00 15.88 8.39 15.22 9.01 L 13.89 8.03 C 14.90 6.99 16.29 6.32 17.76 6.32 Z"/>
<path d="M 24.68 11.56 C 24.68 11.78 24.77 11.97 24.94 12.13 C 25.11 12.30 25.31 12.38 25.54 12.38 L 27.58 12.38 C 27.82 12.38 28.02 12.30 28.19 12.14 C 28.36 11.98 28.44 11.79 28.44 11.55 C 28.44 11.31 28.35 11.11 28.18 10.94 C 28.01 10.77 27.81 10.69 27.58 10.69 L 25.54 10.69 C 25.31 10.69 25.11 10.77 24.94 10.94 C 24.77 11.12 24.68 11.32 24.68 11.56 Z"/>
<path d="M 23.23 17.87 C 23.23 18.10 23.31 18.30 23.47 18.48 L 24.12 19.14 C 24.28 19.30 24.48 19.38 24.70 19.38 C 24.94 19.38 25.14 19.30 25.30 19.13 C 25.47 18.96 25.55 18.75 25.55 18.50 C 25.55 18.27 25.47 18.08 25.30 17.93 L 24.68 17.27 C 24.49 17.11 24.29 17.04 24.06 17.04 C 23.83 17.04 23.63 17.12 23.47 17.28 C 23.31 17.44 23.23 17.64 23.23 17.87 Z"/>
<path d="M 22.42 6.05 C 22.42 6.28 22.51 6.48 22.69 6.65 C 22.87 6.82 23.06 6.90 23.24 6.90 C 23.40 6.90 23.61 6.82 23.86 6.65 L 25.30 5.22 C 25.47 5.04 25.55 4.83 25.55 4.59 C 25.55 4.35 25.47 4.14 25.30 3.98 C 25.13 3.82 24.93 3.74 24.70 3.74 C 24.48 3.74 24.29 3.82 24.12 3.99 L 22.69 5.42 C 22.51 5.61 22.42 5.82 22.42 6.05 Z"/>
<path d="M 16.90 3.73 C 16.90 3.98 16.98 4.19 17.14 4.35 C 17.30 4.51 17.50 4.59 17.75 4.59 C 17.99 4.59 18.18 4.51 18.34 4.35 C 18.50 4.19 18.57 3.98 18.57 3.73 L 18.57 1.69 C 18.57 1.45 18.49 1.26 18.34 1.10 C 18.18 0.94 17.99 0.87 17.75 0.87 C 17.51 0.87 17.31 0.95 17.15 1.10 C 16.99 1.25 16.90 1.45 16.90 1.69 L 16.90 3.73 Z"/>
<path d="M 9.94 4.59 C 9.94 4.84 10.02 5.05 10.18 5.21 L 10.84 5.86 C 11.26 6.18 11.66 6.18 12.05 5.86 C 12.21 5.68 12.29 5.47 12.29 5.22 C 12.29 4.99 12.21 4.79 12.05 4.63 L 11.41 3.98 C 11.22 3.81 11.02 3.73 10.79 3.73 C 10.56 3.73 10.36 3.81 10.19 3.98 C 10.02 4.15 9.94 4.36 9.94 4.59 Z"/>
</g>
<path id="lightning" d="M 9.06 28.17 L 9.36 28.17 L 14.68 20.32 C 14.72 20.28 14.73 20.23 14.70 20.18 C 14.67 20.13 14.63 20.11 14.56 20.11 L 12.38 20.11 L 14.68 15.90 C 14.75 15.76 14.71 15.68 14.54 15.68 L 11.60 15.68 C 11.52 15.68 11.45 15.73 11.38 15.82 L 9.22 21.54 C 9.20 21.68 9.24 21.75 9.36 21.75 L 11.53 21.75 L 9.06 28.17 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.54 13.86 C 3.25 12.96 4.17 12.39 5.29 12.13 C 5.60 10.75 6.29 9.62 7.37 8.75 C 8.45 7.88 9.70 7.44 11.11 7.44 C 12.04 7.44 12.96 7.66 13.85 8.10 C 13.88 8.06 15.25 9.07 15.23 9.09 C 16.07 9.94 16.64 10.91 16.92 12.01 L 17.23 12.01 C 18.58 12.01 19.75 12.50 20.73 13.48 C 20.76 13.44 21.93 14.78 21.79 14.95 C 22.04 15.62 22.17 16.28 22.17 16.96 C 22.17 18.29 21.71 19.43 20.80 20.38 C 19.88 21.33 18.76 21.83 17.43 21.87 C 17.30 21.87 17.23 21.81 17.23 21.70 L 17.23 20.37 C 17.23 20.25 17.30 20.19 17.43 20.19 C 18.29 20.14 19.01 19.82 19.60 19.19 C 20.19 18.56 20.48 17.82 20.48 16.95 C 20.48 16.05 20.16 15.28 19.52 14.64 C 18.88 14.00 18.12 13.68 17.23 13.68 L 15.63 13.68 C 15.51 13.68 15.43 13.62 15.41 13.51 L 15.34 12.93 C 15.24 11.86 14.78 10.96 13.96 10.24 C 13.15 9.52 12.20 9.16 11.10 9.16 C 10.00 9.16 9.06 9.51 8.27 10.23 C 7.48 10.95 7.01 11.86 6.89 12.95 L 6.81 13.50 C 6.81 13.59 6.76 13.64 6.65 13.64 L 6.12 13.72 C 5.28 13.79 4.58 14.13 4.02 14.75 C 3.46 15.36 3.18 16.10 3.18 16.94 C 3.18 17.81 3.47 18.56 4.06 19.18 C 4.65 19.80 5.37 20.14 6.23 20.18 C 6.35 20.18 6.41 20.24 6.41 20.36 L 6.41 21.69 C 6.41 21.80 6.35 21.86 6.23 21.86 C 4.89 21.80 3.76 21.29 2.85 20.35 C 1.94 19.41 1.48 18.27 1.48 16.95 C 1.48 15.79 1.83 14.76 2.54 13.86 L 2.54 13.86 Z"/>
<g id="sun">
<path d="M 17.71 6.36 C 18.42 6.36 19.11 6.50 19.75 6.78 C 20.39 7.06 20.95 7.43 21.42 7.90 C 21.89 8.37 22.27 8.92 22.54 9.57 C 22.81 10.22 22.95 10.89 22.95 11.60 C 22.95 12.64 22.33 14.15 21.81 14.98 L 20.73 13.53 C 21.02 12.98 21.26 12.21 21.26 11.60 C 21.26 10.62 20.91 9.80 20.22 9.12 C 19.53 8.44 18.69 8.10 17.70 8.10 C 16.74 8.10 15.87 8.45 15.22 9.08 L 13.85 8.11 C 14.86 7.03 16.22 6.36 17.71 6.36 L 17.71 6.36 Z"/>
<path d="M 24.66 11.60 C 24.66 11.84 24.75 12.03 24.92 12.19 C 25.10 12.37 25.31 12.46 25.54 12.46 L 27.57 12.46 C 27.80 12.46 28.00 12.38 28.16 12.21 C 28.32 12.04 28.40 11.84 28.40 11.60 C 28.40 11.36 28.32 11.16 28.16 11.00 C 28.00 10.83 27.81 10.75 27.57 10.75 L 25.54 10.75 C 25.30 10.75 25.10 10.83 24.92 11.00 C 24.74 11.17 24.66 11.37 24.66 11.60 Z"/>
<path d="M 23.18 17.94 C 23.18 18.17 23.27 18.37 23.45 18.53 L 24.06 19.16 C 24.26 19.32 24.46 19.40 24.67 19.40 C 24.87 19.40 25.07 19.32 25.27 19.16 C 25.44 19.00 25.52 18.81 25.52 18.57 C 25.52 18.34 25.44 18.14 25.27 17.95 L 24.62 17.34 C 24.47 17.17 24.28 17.09 24.05 17.09 C 23.82 17.09 23.62 17.17 23.45 17.34 C 23.27 17.51 23.18 17.71 23.18 17.94 Z"/>
<path d="M 22.40 6.09 C 22.40 6.34 22.48 6.54 22.63 6.69 C 22.99 7.05 23.39 7.05 23.84 6.69 L 25.27 5.26 C 25.44 5.09 25.52 4.88 25.52 4.63 C 25.52 4.39 25.44 4.19 25.27 4.03 C 25.10 3.86 24.90 3.78 24.67 3.78 C 24.44 3.78 24.24 3.86 24.06 4.02 L 22.63 5.50 C 22.48 5.65 22.40 5.84 22.40 6.09 Z"/>
<path d="M 16.88 3.83 C 16.88 4.06 16.96 4.25 17.11 4.41 C 17.26 4.56 17.46 4.64 17.70 4.64 C 17.94 4.64 18.15 4.56 18.32 4.41 C 18.49 4.26 18.57 4.06 18.57 3.83 L 18.57 1.76 C 18.57 1.53 18.48 1.33 18.31 1.16 C 18.14 0.99 17.93 0.91 17.70 0.91 C 17.47 0.91 17.27 0.99 17.12 1.16 C 16.96 1.33 16.89 1.53 16.89 1.76 L 16.89 3.83 L 16.88 3.83 Z"/>
<path d="M 9.92 4.66 C 9.92 4.90 10.00 5.10 10.16 5.26 L 10.82 5.90 C 10.96 6.06 11.14 6.14 11.36 6.16 C 11.58 6.18 11.79 6.09 11.98 5.90 C 12.14 5.74 12.22 5.54 12.22 5.31 C 12.22 5.07 12.14 4.87 11.98 4.71 L 11.35 4.06 C 11.20 3.90 11.00 3.82 10.77 3.80 C 10.54 3.80 10.34 3.88 10.17 4.05 C 10.01 4.22 9.92 4.42 9.92 4.66 Z"/>
</g>
<g id="rain">
<path d="M 14.88 18.81 C 14.88 18.98 14.93 19.14 15.04 19.29 C 15.15 19.44 15.31 19.55 15.53 19.61 C 15.55 19.61 15.59 19.62 15.65 19.63 C 15.71 19.64 15.76 19.65 15.79 19.65 C 15.90 19.65 16.02 19.62 16.16 19.56 C 16.37 19.45 16.50 19.28 16.56 19.04 L 16.80 18.14 C 16.86 17.91 16.84 17.70 16.73 17.51 C 16.62 17.32 16.45 17.18 16.23 17.11 C 16.00 17.04 15.78 17.06 15.59 17.17 C 15.39 17.28 15.26 17.44 15.19 17.68 L 14.91 18.59 C 14.91 18.61 14.91 18.64 14.90 18.70 C 14.89 18.73 14.88 18.77 14.88 18.81 Z"/>
<path d="M 14.27 21.12 C 14.27 21.35 14.35 21.54 14.51 21.69 C 14.66 21.85 14.85 21.93 15.09 21.93 C 15.33 21.93 15.52 21.85 15.68 21.70 C 15.84 21.54 15.91 21.35 15.91 21.12 C 15.91 20.88 15.83 20.69 15.68 20.53 C 15.52 20.37 15.33 20.30 15.09 20.30 C 14.85 20.30 14.66 20.38 14.50 20.53 C 14.34 20.68 14.27 20.88 14.27 21.12 Z"/>
<path d="M 13.53 24.08 C 13.53 24.25 13.58 24.41 13.68 24.56 C 13.78 24.71 13.93 24.82 14.14 24.88 C 14.17 24.88 14.22 24.89 14.28 24.90 C 14.34 24.91 14.39 24.92 14.42 24.92 C 14.83 24.92 15.08 24.70 15.18 24.26 L 15.32 23.66 C 15.39 23.45 15.37 23.24 15.25 23.03 C 15.13 22.82 14.96 22.69 14.74 22.62 C 14.49 22.56 14.26 22.58 14.06 22.70 C 13.86 22.82 13.72 22.99 13.65 23.23 L 13.56 23.82 C 13.56 23.83 13.56 23.87 13.55 23.93 C 13.54 24.00 13.53 24.04 13.53 24.08 Z"/>
<path d="M 10.77 21.93 C 10.76 22.08 10.80 22.24 10.91 22.40 C 11.01 22.56 11.16 22.66 11.36 22.70 C 11.59 22.76 11.80 22.74 12.00 22.64 C 12.20 22.54 12.33 22.35 12.41 22.08 L 12.67 21.18 C 12.74 20.96 12.72 20.75 12.60 20.55 C 12.48 20.35 12.31 20.22 12.07 20.15 C 11.85 20.08 11.64 20.11 11.43 20.23 C 11.22 20.35 11.09 20.53 11.02 20.76 L 10.80 21.66 C 10.78 21.74 10.77 21.83 10.77 21.93 Z"/>
<path d="M 10.15 24.20 C 10.15 24.43 10.23 24.62 10.39 24.78 C 10.55 24.94 10.75 25.02 10.97 25.02 C 11.21 25.02 11.40 24.94 11.56 24.79 C 11.72 24.63 11.79 24.44 11.79 24.20 C 11.79 23.97 11.71 23.78 11.56 23.62 C 11.40 23.46 11.21 23.39 10.97 23.39 C 10.73 23.39 10.54 23.47 10.38 23.62 C 10.22 23.77 10.15 23.97 10.15 24.20 Z"/>
<path d="M 9.36 27.10 C 9.36 27.27 9.41 27.43 9.52 27.59 C 9.63 27.75 9.79 27.86 10.01 27.92 C 10.10 27.94 10.18 27.95 10.25 27.95 C 10.68 27.95 10.95 27.75 11.05 27.34 L 11.18 26.75 C 11.24 26.49 11.21 26.27 11.10 26.07 C 10.99 25.87 10.81 25.75 10.58 25.70 C 10.37 25.63 10.16 25.65 9.95 25.77 C 9.74 25.89 9.61 26.06 9.54 26.28 L 9.40 26.88 C 9.37 26.99 9.36 27.07 9.36 27.10 Z"/>
<path d="M 8.21 18.81 C 8.20 18.97 8.24 19.12 8.34 19.26 C 8.44 19.41 8.60 19.51 8.82 19.58 C 9.03 19.64 9.23 19.62 9.44 19.51 C 9.65 19.40 9.79 19.23 9.86 19.00 L 10.13 18.10 C 10.20 17.86 10.18 17.64 10.06 17.45 C 9.94 17.26 9.76 17.13 9.52 17.06 C 9.30 16.99 9.09 17.01 8.89 17.13 C 8.69 17.24 8.55 17.41 8.48 17.63 L 8.24 18.55 C 8.22 18.71 8.21 18.80 8.21 18.81 Z"/>
<path d="M 7.60 21.08 C 7.60 21.30 7.68 21.49 7.84 21.65 C 8.00 21.83 8.19 21.91 8.40 21.91 C 8.64 21.91 8.84 21.83 9.00 21.67 C 9.17 21.51 9.25 21.32 9.25 21.08 C 9.25 20.85 9.17 20.65 9.00 20.49 C 8.83 20.33 8.63 20.25 8.40 20.25 C 8.17 20.25 7.98 20.33 7.82 20.48 C 7.66 20.63 7.60 20.85 7.60 21.08 Z"/>
<path d="M 6.83 23.98 C 6.83 24.15 6.88 24.32 6.99 24.49 C 7.10 24.66 7.26 24.77 7.46 24.84 C 7.69 24.91 7.91 24.90 8.10 24.80 C 8.30 24.71 8.43 24.52 8.50 24.24 L 8.64 23.63 C 8.69 23.40 8.66 23.19 8.54 23.00 C 8.42 22.80 8.25 22.67 8.02 22.60 C 7.79 22.53 7.58 22.56 7.38 22.68 C 7.18 22.80 7.06 22.97 7.00 23.20 L 6.86 23.79 C 6.84 23.85 6.83 23.91 6.83 23.98 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.59 13.82 C 3.31 12.92 4.22 12.34 5.33 12.08 C 5.64 10.72 6.34 9.60 7.43 8.72 C 8.52 7.84 9.78 7.40 11.19 7.40 C 12.18 7.40 13.08 7.62 13.90 8.06 C 13.95 8.01 15.32 9.01 15.30 9.03 C 16.14 9.83 16.70 10.81 16.97 11.99 L 17.30 11.99 C 18.66 11.99 19.82 12.47 20.79 13.44 C 20.82 13.39 21.99 14.72 21.86 14.89 C 22.14 15.51 22.28 16.18 22.28 16.90 C 22.28 18.23 21.82 19.38 20.89 20.35 C 19.96 21.31 18.83 21.82 17.50 21.86 C 17.37 21.86 17.31 21.80 17.31 21.69 L 17.31 20.35 C 17.31 20.24 17.38 20.18 17.50 20.18 C 18.35 20.13 19.08 19.79 19.68 19.15 C 20.28 18.51 20.58 17.76 20.58 16.89 C 20.58 16.01 20.26 15.26 19.61 14.62 C 18.96 13.98 18.20 13.66 17.31 13.66 L 15.69 13.66 C 15.58 13.66 15.52 13.59 15.52 13.47 L 15.45 12.90 C 15.33 11.82 14.87 10.91 14.05 10.18 C 13.23 9.45 12.28 9.08 11.19 9.08 C 10.10 9.08 9.15 9.45 8.34 10.18 C 7.53 10.91 7.08 11.82 6.97 12.90 L 6.90 13.44 C 6.90 13.55 6.83 13.61 6.70 13.61 L 6.17 13.66 C 5.34 13.76 4.64 14.11 4.07 14.73 C 3.50 15.35 3.22 16.07 3.22 16.89 C 3.22 17.75 3.52 18.51 4.12 19.15 C 4.72 19.79 5.45 20.14 6.31 20.18 C 6.42 20.18 6.48 20.24 6.48 20.35 L 6.48 21.69 C 6.48 21.80 6.42 21.86 6.31 21.86 C 4.96 21.80 3.83 21.29 2.90 20.33 C 1.97 19.37 1.51 18.22 1.51 16.89 C 1.51 15.74 1.87 14.72 2.59 13.82 L 2.59 13.82 Z"/>
<g id="sun">
<path d="M 17.78 6.32 C 18.72 6.32 19.59 6.55 20.40 7.02 C 21.21 7.49 21.85 8.13 22.32 8.94 C 22.79 9.75 23.03 10.63 23.03 11.58 C 23.03 12.61 22.43 14.13 21.90 14.97 L 20.81 13.48 C 21.11 12.92 21.31 12.16 21.31 11.59 C 21.31 10.60 20.97 9.76 20.28 9.07 C 19.60 8.38 18.76 8.03 17.78 8.03 C 16.86 8.03 15.99 8.43 15.30 9.05 L 13.91 8.07 C 14.92 7.01 16.29 6.32 17.78 6.32 L 17.78 6.32 Z"/>
<path d="M 24.72 11.58 C 24.72 11.82 24.81 12.01 24.98 12.17 C 25.16 12.35 25.36 12.43 25.60 12.43 L 27.63 12.43 C 27.87 12.43 28.07 12.35 28.24 12.18 C 28.41 12.01 28.49 11.81 28.49 11.58 C 28.49 11.34 28.41 11.14 28.24 10.97 C 28.07 10.80 27.87 10.71 27.63 10.71 L 25.60 10.71 C 25.36 10.71 25.16 10.80 24.98 10.97 C 24.80 11.14 24.72 11.34 24.72 11.58 Z"/>
<path d="M 23.26 17.91 C 23.26 18.15 23.34 18.36 23.50 18.54 L 24.15 19.17 C 24.33 19.31 24.53 19.38 24.75 19.38 L 24.77 19.40 C 25.00 19.40 25.19 19.32 25.35 19.16 C 25.51 19.00 25.59 18.79 25.59 18.55 C 25.59 18.31 25.50 18.12 25.33 17.97 L 24.71 17.31 C 24.53 17.15 24.32 17.07 24.09 17.07 C 23.86 17.07 23.66 17.15 23.50 17.32 C 23.34 17.49 23.26 17.67 23.26 17.91 Z"/>
<path d="M 22.45 6.06 C 22.45 6.30 22.54 6.50 22.72 6.65 C 22.86 6.81 23.04 6.89 23.27 6.91 C 23.50 6.93 23.71 6.84 23.89 6.65 L 25.33 5.22 C 25.51 5.05 25.59 4.84 25.59 4.59 C 25.59 4.35 25.51 4.14 25.34 3.98 C 25.17 3.82 24.97 3.74 24.73 3.74 C 24.52 3.74 24.33 3.82 24.15 3.99 L 22.72 5.43 C 22.54 5.60 22.45 5.81 22.45 6.06 Z"/>
<path d="M 16.92 3.78 C 16.92 4.01 17.00 4.21 17.17 4.37 C 17.34 4.53 17.54 4.61 17.78 4.61 C 18.01 4.61 18.21 4.53 18.37 4.38 C 18.53 4.22 18.61 4.03 18.61 3.79 L 18.61 1.73 C 18.61 1.47 18.53 1.26 18.38 1.10 C 18.22 0.94 18.03 0.86 17.79 0.86 C 17.54 0.86 17.33 0.94 17.17 1.11 C 17.01 1.28 16.93 1.48 16.93 1.73 L 16.93 3.78 L 16.92 3.78 Z"/>
<path d="M 9.94 4.60 C 9.94 4.84 10.02 5.04 10.19 5.21 L 10.84 5.87 C 11.03 6.02 11.24 6.09 11.46 6.09 C 11.67 6.09 11.87 6.01 12.04 5.86 C 12.21 5.70 12.30 5.51 12.30 5.27 C 12.30 5.03 12.22 4.81 12.06 4.63 L 11.42 3.98 C 11.24 3.81 11.04 3.73 10.82 3.73 C 10.58 3.73 10.37 3.82 10.20 3.99 C 10.03 4.16 9.94 4.37 9.94 4.60 Z"/>
</g>
<g id="rain">
<path d="M 13.67 23.77 C 13.67 23.93 13.72 24.09 13.82 24.24 C 13.92 24.39 14.08 24.50 14.28 24.56 C 14.39 24.58 14.48 24.60 14.53 24.60 C 14.70 24.60 14.87 24.55 15.02 24.45 C 15.17 24.35 15.27 24.19 15.32 23.96 L 16.90 18.08 C 16.96 17.85 16.94 17.63 16.83 17.44 C 16.72 17.24 16.55 17.11 16.33 17.04 C 16.09 16.97 15.88 16.99 15.68 17.11 C 15.48 17.22 15.35 17.39 15.29 17.62 L 13.71 23.53 C 13.69 23.68 13.67 23.76 13.67 23.77 Z"/>
<path d="M 9.52 26.83 C 9.52 27.02 9.57 27.19 9.67 27.35 C 9.77 27.51 9.94 27.61 10.19 27.65 C 10.30 27.67 10.39 27.69 10.45 27.69 C 10.61 27.69 10.76 27.63 10.90 27.52 C 11.04 27.40 11.13 27.24 11.17 27.04 L 13.57 18.11 C 13.63 17.88 13.61 17.66 13.50 17.47 C 13.39 17.28 13.22 17.14 13.00 17.07 C 12.77 17.00 12.55 17.02 12.35 17.14 C 12.15 17.25 12.01 17.42 11.95 17.65 L 9.55 26.58 C 9.53 26.73 9.52 26.82 9.52 26.83 Z"/>
<path d="M 6.91 23.75 C 6.91 23.92 6.96 24.08 7.07 24.24 C 7.18 24.40 7.34 24.51 7.56 24.57 C 7.67 24.59 7.76 24.61 7.83 24.61 C 8.22 24.61 8.48 24.40 8.60 23.97 L 10.18 18.09 C 10.25 17.85 10.22 17.63 10.10 17.42 C 9.98 17.21 9.80 17.09 9.57 17.04 C 9.35 16.97 9.14 16.99 8.94 17.11 C 8.74 17.22 8.60 17.39 8.53 17.62 L 6.95 23.53 C 6.93 23.66 6.91 23.73 6.91 23.75 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.63 13.81 C 3.35 12.92 4.28 12.35 5.40 12.09 C 5.70 10.73 6.38 9.61 7.47 8.74 C 8.56 7.87 9.81 7.43 11.22 7.43 C 12.25 7.43 13.16 7.64 13.95 8.07 C 13.99 8.02 15.39 9.03 15.36 9.06 C 16.21 9.87 16.77 10.84 17.03 11.98 L 17.37 11.98 C 18.73 11.98 19.88 12.45 20.84 13.41 C 20.88 13.34 22.03 14.71 21.86 14.92 C 22.13 15.51 22.27 16.17 22.27 16.88 C 22.27 17.76 22.07 18.57 21.65 19.32 C 21.24 20.07 20.67 20.66 19.94 21.12 C 19.21 21.58 18.42 21.82 17.54 21.84 C 17.43 21.84 17.37 21.78 17.37 21.67 L 17.37 20.33 C 17.37 20.21 17.43 20.15 17.54 20.15 C 18.38 20.08 19.10 19.73 19.70 19.10 C 20.30 18.47 20.60 17.73 20.60 16.88 C 20.60 16.00 20.28 15.25 19.64 14.61 C 19.00 13.97 18.25 13.65 17.37 13.65 L 15.70 13.65 C 15.58 13.65 15.52 13.59 15.52 13.47 L 15.45 12.90 C 15.35 11.82 14.89 10.92 14.08 10.19 C 13.27 9.46 12.31 9.10 11.22 9.10 C 10.12 9.10 9.17 9.46 8.36 10.19 C 7.55 10.91 7.10 11.82 6.99 12.90 L 6.93 13.43 C 6.93 13.55 6.86 13.61 6.73 13.61 L 6.23 13.65 C 5.39 13.75 4.69 14.10 4.13 14.71 C 3.57 15.32 3.29 16.04 3.29 16.88 C 3.29 17.74 3.58 18.49 4.18 19.13 C 4.78 19.77 5.50 20.11 6.35 20.15 C 6.46 20.15 6.52 20.21 6.52 20.33 L 6.52 21.67 C 6.52 21.78 6.46 21.84 6.35 21.84 C 5.01 21.77 3.88 21.27 2.95 20.31 C 2.02 19.35 1.56 18.21 1.56 16.88 C 1.56 15.73 1.92 14.70 2.63 13.81 Z"/>
<g id="sun">
<path d="M 17.78 6.41 C 18.49 6.41 19.17 6.54 19.82 6.82 C 20.46 7.09 21.02 7.46 21.48 7.93 C 21.95 8.40 22.33 8.95 22.60 9.60 C 22.87 10.25 23.01 10.93 23.01 11.64 C 23.01 12.61 22.34 14.13 21.89 14.91 L 20.88 13.46 C 21.13 12.92 21.33 12.14 21.33 11.65 C 21.33 10.67 20.98 9.83 20.29 9.14 C 19.60 8.45 18.76 8.10 17.78 8.10 C 16.88 8.10 16.02 8.47 15.37 9.06 L 13.98 8.07 C 14.98 7.06 16.34 6.41 17.78 6.41 Z"/>
<path d="M 24.71 11.64 C 24.71 11.86 24.79 12.06 24.95 12.22 C 25.11 12.38 25.31 12.46 25.53 12.46 L 27.57 12.46 C 27.83 12.46 28.04 12.38 28.20 12.23 C 28.36 12.07 28.44 11.88 28.44 11.64 C 28.44 11.39 28.36 11.18 28.19 11.02 C 28.02 10.86 27.82 10.78 27.57 10.78 L 25.53 10.78 C 25.30 10.78 25.10 10.86 24.94 11.03 C 24.79 11.20 24.71 11.41 24.71 11.64 Z"/>
<path d="M 23.24 17.95 C 23.24 18.18 23.33 18.39 23.50 18.58 L 24.12 19.22 C 24.33 19.43 24.53 19.53 24.74 19.53 C 24.93 19.53 25.13 19.43 25.32 19.22 C 25.50 19.04 25.59 18.83 25.58 18.61 C 25.57 18.38 25.49 18.18 25.32 18.01 L 24.67 17.35 C 24.51 17.19 24.32 17.11 24.10 17.11 C 23.86 17.11 23.66 17.19 23.49 17.36 C 23.33 17.52 23.24 17.72 23.24 17.95 Z"/>
<path d="M 22.45 6.12 C 22.45 6.37 22.53 6.57 22.68 6.73 C 22.89 6.90 23.09 6.98 23.30 6.98 C 23.49 6.98 23.68 6.90 23.89 6.73 L 25.32 5.30 C 25.48 5.12 25.56 4.91 25.56 4.67 C 25.56 4.43 25.48 4.23 25.32 4.07 C 25.16 3.91 24.96 3.83 24.73 3.83 C 24.50 3.83 24.30 3.91 24.12 4.07 L 22.68 5.50 C 22.53 5.68 22.45 5.88 22.45 6.12 Z"/>
<path d="M 16.90 3.83 C 16.90 4.08 16.98 4.28 17.14 4.44 C 17.30 4.60 17.50 4.68 17.75 4.68 C 18.00 4.68 18.20 4.60 18.36 4.44 C 18.52 4.28 18.60 4.08 18.60 3.83 L 18.60 1.81 C 18.60 1.56 18.52 1.35 18.36 1.19 C 18.20 1.03 18.00 0.95 17.75 0.95 C 17.50 0.95 17.30 1.03 17.14 1.19 C 16.98 1.35 16.90 1.56 16.90 1.81 L 16.90 3.83 Z"/>
<path d="M 9.96 4.68 C 9.96 4.93 10.04 5.14 10.21 5.30 L 10.87 5.95 C 11.21 6.29 11.60 6.29 12.04 5.95 C 12.20 5.78 12.28 5.57 12.28 5.34 C 12.28 5.11 12.20 4.91 12.04 4.73 L 11.41 4.07 C 11.25 3.91 11.05 3.83 10.81 3.83 C 10.58 3.83 10.38 3.91 10.21 4.08 C 10.04 4.24 9.96 4.44 9.96 4.68 Z"/>
</g>
<g id="rain">
<path d="M 15.03 18.92 C 15.03 19.08 15.08 19.24 15.18 19.40 C 15.28 19.56 15.43 19.67 15.63 19.72 L 15.88 19.75 C 16.07 19.75 16.25 19.69 16.40 19.57 C 16.55 19.45 16.64 19.29 16.68 19.10 L 16.95 18.11 C 17.02 17.87 17.00 17.66 16.88 17.46 C 16.77 17.26 16.60 17.13 16.37 17.07 C 16.14 17.00 15.92 17.02 15.73 17.13 C 15.53 17.24 15.40 17.41 15.34 17.63 L 15.04 18.69 C 15.04 18.77 15.03 18.85 15.03 18.92 Z"/>
<path d="M 13.63 23.68 C 13.65 24.06 13.86 24.33 14.26 24.51 L 14.51 24.55 C 14.67 24.55 14.83 24.50 14.98 24.39 C 15.13 24.28 15.24 24.12 15.30 23.89 L 15.59 22.88 C 15.65 22.64 15.62 22.42 15.50 22.22 C 15.38 22.02 15.20 21.89 14.97 21.85 C 14.76 21.78 14.56 21.80 14.35 21.92 C 14.14 22.04 14.01 22.21 13.94 22.43 L 13.67 23.45 C 13.66 23.47 13.66 23.50 13.65 23.53 C 13.64 23.56 13.64 23.59 13.63 23.61 C 13.62 23.63 13.63 23.66 13.63 23.68 Z"/>
<path d="M 10.85 21.96 C 10.85 22.13 10.90 22.30 11.01 22.47 C 11.12 22.64 11.27 22.75 11.48 22.82 C 11.71 22.89 11.92 22.87 12.12 22.77 C 12.31 22.67 12.45 22.48 12.52 22.21 L 12.76 21.20 C 12.83 20.97 12.81 20.75 12.70 20.55 C 12.59 20.35 12.42 20.21 12.20 20.14 C 11.95 20.07 11.72 20.10 11.52 20.22 C 11.32 20.34 11.19 20.52 11.15 20.75 L 10.87 21.78 C 10.85 21.81 10.85 21.87 10.85 21.96 Z"/>
<path d="M 9.50 26.75 C 9.50 26.91 9.56 27.08 9.67 27.25 C 9.78 27.42 9.95 27.54 10.16 27.61 C 10.17 27.61 10.20 27.61 10.26 27.62 C 10.32 27.63 10.37 27.63 10.41 27.63 C 10.55 27.63 10.67 27.61 10.78 27.56 C 10.97 27.48 11.11 27.29 11.19 26.98 L 11.46 25.99 C 11.53 25.76 11.51 25.54 11.39 25.34 C 11.27 25.14 11.10 25.00 10.88 24.94 C 10.65 24.87 10.43 24.89 10.23 25.01 C 10.03 25.13 9.89 25.30 9.83 25.52 L 9.55 26.54 C 9.51 26.63 9.50 26.70 9.50 26.75 Z"/>
<path d="M 8.28 18.86 C 8.28 19.24 8.49 19.50 8.92 19.65 C 9.14 19.73 9.35 19.71 9.56 19.60 C 9.77 19.49 9.90 19.31 9.97 19.07 L 10.21 18.04 C 10.28 17.83 10.26 17.63 10.14 17.42 C 10.03 17.21 9.86 17.07 9.63 17.00 C 9.39 16.94 9.16 16.96 8.96 17.08 C 8.76 17.20 8.64 17.37 8.59 17.60 L 8.29 18.62 C 8.29 18.70 8.28 18.78 8.28 18.86 Z"/>
<path d="M 6.97 23.58 C 6.97 23.76 7.02 23.94 7.13 24.11 C 7.24 24.29 7.39 24.40 7.58 24.47 C 7.77 24.54 7.98 24.52 8.19 24.41 C 8.41 24.30 8.55 24.12 8.63 23.86 L 8.88 22.81 C 8.95 22.60 8.93 22.40 8.81 22.19 C 8.69 21.98 8.52 21.84 8.30 21.77 C 8.05 21.71 7.83 21.74 7.63 21.85 C 7.43 21.96 7.31 22.15 7.26 22.38 L 6.98 23.37 C 6.98 23.42 6.97 23.49 6.97 23.58 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.64 13.85 C 3.35 12.96 4.26 12.38 5.38 12.10 C 5.69 10.75 6.38 9.63 7.47 8.76 C 8.56 7.89 9.80 7.45 11.21 7.45 C 12.19 7.45 13.09 7.67 13.90 8.11 C 13.93 8.08 15.31 9.05 15.30 9.06 C 16.12 9.85 16.68 10.83 16.98 12.00 L 17.30 12.00 C 18.66 12.00 19.82 12.48 20.77 13.44 C 20.82 13.36 22.01 14.68 21.85 14.88 C 22.14 15.48 22.28 16.15 22.28 16.91 C 22.28 18.23 21.82 19.36 20.89 20.32 C 19.96 21.28 18.84 21.78 17.51 21.82 C 17.38 21.82 17.32 21.76 17.32 21.65 L 17.32 20.32 C 17.32 20.19 17.39 20.13 17.51 20.13 C 18.35 20.08 19.08 19.74 19.68 19.11 C 20.28 18.47 20.58 17.74 20.58 16.90 C 20.58 16.01 20.26 15.25 19.62 14.61 C 18.98 13.97 18.21 13.65 17.31 13.65 L 15.70 13.65 C 15.59 13.65 15.53 13.59 15.53 13.48 L 15.46 12.90 C 15.36 11.83 14.90 10.93 14.08 10.20 C 13.27 9.47 12.32 9.11 11.23 9.11 C 10.13 9.11 9.18 9.47 8.38 10.20 C 7.58 10.93 7.13 11.84 7.02 12.93 L 6.94 13.44 C 6.94 13.57 6.88 13.63 6.76 13.63 L 6.23 13.69 C 5.39 13.79 4.70 14.15 4.13 14.76 C 3.56 15.38 3.28 16.10 3.28 16.92 C 3.28 17.76 3.58 18.49 4.18 19.13 C 4.78 19.77 5.51 20.11 6.36 20.15 C 6.47 20.15 6.53 20.22 6.53 20.34 L 6.53 21.67 C 6.53 21.78 6.47 21.84 6.36 21.84 C 5.50 21.82 4.69 21.58 3.96 21.13 C 3.22 20.68 2.64 20.08 2.22 19.34 C 1.79 18.60 1.58 17.79 1.58 16.93 C 1.58 15.77 1.93 14.75 2.64 13.85 Z"/>
<g id="sun">
<path d="M 17.77 6.38 C 18.71 6.38 19.59 6.61 20.39 7.08 C 21.19 7.55 21.83 8.19 22.30 8.99 C 22.77 9.79 23.00 10.66 23.00 11.60 C 23.00 12.58 22.65 13.82 21.87 14.96 C 21.53 15.47 20.78 13.48 20.78 13.48 C 21.03 12.96 21.28 12.14 21.28 11.60 C 21.28 10.64 20.94 9.80 20.25 9.11 C 19.57 8.42 18.74 8.07 17.77 8.07 C 16.83 8.07 15.99 8.45 15.30 9.09 L 13.94 8.12 C 14.97 7.03 16.27 6.38 17.77 6.38 Z"/>
<path d="M 24.67 11.60 C 24.67 11.84 24.76 12.03 24.93 12.19 C 25.10 12.37 25.31 12.46 25.55 12.46 L 27.57 12.46 C 27.80 12.46 28.00 12.38 28.17 12.21 C 28.34 12.04 28.42 11.84 28.42 11.60 C 28.42 11.36 28.34 11.16 28.17 11.00 C 28.00 10.84 27.80 10.75 27.57 10.75 L 25.55 10.75 C 25.31 10.75 25.11 10.83 24.93 11.00 C 24.75 11.17 24.67 11.37 24.67 11.60 Z"/>
<path d="M 23.22 17.91 C 23.22 18.16 23.30 18.37 23.46 18.53 L 24.10 19.16 C 24.34 19.32 24.56 19.40 24.74 19.40 C 24.95 19.40 25.13 19.31 25.30 19.14 C 25.47 18.97 25.55 18.76 25.55 18.53 C 25.55 18.30 25.46 18.11 25.29 17.95 L 24.67 17.30 C 24.49 17.14 24.29 17.06 24.06 17.06 C 23.83 17.06 23.63 17.14 23.47 17.31 C 23.30 17.47 23.22 17.67 23.22 17.91 Z"/>
<path d="M 22.42 6.11 C 22.42 6.34 22.50 6.54 22.67 6.70 C 22.82 6.86 23.01 6.94 23.23 6.96 C 23.45 6.98 23.66 6.89 23.85 6.70 L 25.28 5.27 C 25.46 5.09 25.54 4.89 25.54 4.66 C 25.54 4.42 25.45 4.22 25.28 4.05 C 25.11 3.88 24.91 3.80 24.68 3.80 C 24.46 3.80 24.27 3.88 24.10 4.05 L 22.67 5.51 C 22.50 5.67 22.42 5.87 22.42 6.11 Z"/>
<path d="M 16.90 3.84 C 16.90 4.07 16.98 4.27 17.15 4.42 C 17.32 4.57 17.52 4.65 17.76 4.65 C 18.00 4.65 18.19 4.57 18.35 4.42 C 18.51 4.26 18.58 4.07 18.58 3.84 L 18.58 1.80 C 18.58 1.56 18.50 1.36 18.34 1.19 C 18.18 1.02 18.00 0.94 17.77 0.94 C 17.54 0.94 17.34 1.03 17.17 1.20 C 17.00 1.37 16.91 1.57 16.91 1.80 L 16.91 3.84 L 16.90 3.84 Z"/>
<path d="M 9.97 4.68 C 9.97 4.92 10.05 5.12 10.21 5.27 L 10.87 5.93 C 11.03 6.09 11.21 6.18 11.40 6.18 C 11.61 6.21 11.81 6.14 12.01 5.96 C 12.21 5.78 12.31 5.57 12.31 5.33 C 12.31 5.09 12.23 4.87 12.07 4.69 L 11.43 4.08 C 11.28 3.91 11.09 3.83 10.85 3.83 C 10.60 3.83 10.39 3.91 10.22 4.08 C 10.05 4.24 9.97 4.44 9.97 4.68 Z"/>
</g>
<g id="snow">
<path d="M 14.32 24.61 C 14.32 24.82 14.40 25.01 14.55 25.18 C 14.73 25.34 14.93 25.42 15.15 25.42 C 15.39 25.42 15.58 25.34 15.74 25.19 C 15.90 25.03 15.97 24.84 15.97 24.61 C 15.97 24.37 15.89 24.18 15.73 24.02 C 15.57 23.86 15.38 23.79 15.14 23.79 C 14.90 23.79 14.70 23.87 14.54 24.03 C 14.40 24.18 14.32 24.38 14.32 24.61 Z"/>
<path d="M 14.32 20.98 C 14.32 21.22 14.40 21.42 14.56 21.59 C 14.72 21.76 14.92 21.84 15.15 21.84 C 15.38 21.84 15.58 21.76 15.74 21.59 C 15.90 21.42 15.98 21.22 15.98 20.98 C 15.98 20.75 15.90 20.56 15.74 20.40 C 15.58 20.24 15.39 20.16 15.15 20.16 C 14.91 20.16 14.72 20.24 14.56 20.40 C 14.40 20.56 14.32 20.76 14.32 20.98 Z"/>
<path d="M 11.10 26.56 C 11.10 26.78 11.18 26.97 11.34 27.13 C 11.51 27.30 11.70 27.38 11.92 27.38 C 12.16 27.38 12.36 27.30 12.52 27.15 C 12.69 26.99 12.77 26.80 12.77 26.56 C 12.77 26.32 12.69 26.12 12.52 25.96 C 12.35 25.79 12.15 25.71 11.92 25.71 C 11.70 25.71 11.51 25.79 11.34 25.96 C 11.18 26.13 11.10 26.33 11.10 26.56 Z"/>
<path d="M 11.10 19.30 C 11.10 19.53 11.18 19.72 11.34 19.88 C 11.50 20.04 11.70 20.12 11.92 20.12 C 12.16 20.12 12.36 20.04 12.52 19.88 C 12.69 19.72 12.77 19.53 12.77 19.29 C 12.77 19.06 12.69 18.86 12.52 18.70 C 12.35 18.54 12.15 18.46 11.92 18.46 C 11.69 18.46 11.50 18.54 11.34 18.70 C 11.18 18.86 11.10 19.07 11.10 19.30 Z"/>
<path d="M 11.10 22.90 C 11.10 23.12 11.18 23.32 11.34 23.50 C 11.50 23.66 11.70 23.74 11.92 23.74 C 12.16 23.74 12.36 23.66 12.52 23.50 C 12.68 23.34 12.77 23.14 12.77 22.90 C 12.77 22.67 12.69 22.47 12.52 22.30 C 12.35 22.13 12.15 22.05 11.92 22.05 C 11.69 22.05 11.50 22.13 11.34 22.30 C 11.18 22.47 11.10 22.67 11.10 22.90 Z"/>
<path d="M 7.92 24.61 C 7.92 24.82 8.00 25.01 8.16 25.18 C 8.34 25.34 8.53 25.42 8.74 25.42 C 8.98 25.42 9.17 25.34 9.33 25.19 C 9.49 25.03 9.56 24.84 9.56 24.61 C 9.56 24.37 9.48 24.18 9.32 24.02 C 9.16 23.86 8.97 23.79 8.73 23.79 C 8.50 23.79 8.30 23.87 8.14 24.02 C 8.00 24.17 7.92 24.37 7.92 24.61 Z"/>
<path d="M 7.92 20.98 C 7.92 21.22 8.00 21.42 8.16 21.59 C 8.32 21.76 8.51 21.84 8.75 21.84 C 8.98 21.84 9.18 21.76 9.34 21.59 C 9.50 21.42 9.58 21.22 9.58 20.98 C 9.58 20.75 9.50 20.56 9.34 20.40 C 9.18 20.24 8.99 20.16 8.75 20.16 C 8.52 20.16 8.32 20.24 8.16 20.40 C 8.00 20.56 7.92 20.76 7.92 20.98 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.56 13.78 C 3.28 12.88 4.19 12.30 5.30 12.04 C 5.60 10.67 6.31 9.55 7.40 8.67 C 8.49 7.79 9.75 7.35 11.17 7.35 C 12.18 7.35 13.08 7.57 13.89 8.00 C 13.95 7.94 15.36 8.98 15.31 9.02 C 16.15 9.78 16.72 10.76 17.01 11.95 L 17.32 11.95 C 18.71 11.95 19.88 12.43 20.84 13.39 C 20.88 13.32 22.04 14.66 21.88 14.86 C 22.17 15.48 22.31 16.15 22.31 16.88 C 22.31 17.76 22.10 18.58 21.67 19.33 C 21.24 20.08 20.66 20.69 19.93 21.14 C 19.20 21.60 18.39 21.84 17.51 21.86 C 17.38 21.86 17.31 21.80 17.31 21.69 L 17.31 20.35 C 17.31 20.23 17.38 20.17 17.51 20.17 C 18.37 20.12 19.09 19.78 19.69 19.14 C 20.29 18.50 20.59 17.75 20.59 16.88 C 20.59 15.99 20.27 15.23 19.62 14.59 C 18.97 13.95 18.20 13.63 17.31 13.63 L 15.70 13.63 C 15.58 13.63 15.52 13.57 15.52 13.46 L 15.44 12.87 C 15.33 11.79 14.86 10.88 14.04 10.15 C 13.22 9.42 12.27 9.05 11.18 9.05 C 10.08 9.05 9.13 9.42 8.32 10.15 C 7.51 10.88 7.06 11.79 6.95 12.87 L 6.88 13.46 C 6.85 13.55 6.78 13.60 6.66 13.60 L 6.15 13.63 C 5.31 13.73 4.60 14.09 4.04 14.70 C 3.48 15.31 3.20 16.04 3.20 16.88 C 3.20 17.64 3.43 18.31 3.90 18.90 C 4.31 19.44 5.07 19.86 5.69 20.05 L 5.07 21.67 C 4.10 21.36 3.12 20.71 2.49 19.88 C 1.82 19.00 1.49 18.00 1.49 16.88 C 1.49 15.73 1.85 14.70 2.56 13.78 L 2.56 13.78 Z"/>
<path id="lightning" d="M 5.68 20.06 L 6.75 17.18 C 6.81 17.08 6.89 17.03 6.98 17.03 L 9.94 17.03 C 10.11 17.03 10.15 17.11 10.08 17.25 L 7.59 21.90 L 9.78 21.90 C 9.85 21.90 9.90 21.92 9.92 21.97 C 9.94 22.02 9.94 22.07 9.90 22.11 L 5.54 27.77 L 5.25 27.77 L 6.68 23.56 L 4.55 23.56 C 4.41 23.56 4.36 23.48 4.41 23.34 L 5.07 21.64 C 5.01 21.62 5.60 20.04 5.68 20.06 L 5.68 20.06 Z"/>
<g id="rain">
<path d="M 15.21 18.86 C 15.21 19.04 15.26 19.20 15.37 19.36 C 15.48 19.52 15.64 19.63 15.86 19.69 C 16.03 19.75 16.23 19.73 16.47 19.64 C 16.67 19.55 16.81 19.36 16.90 19.07 L 17.17 18.07 C 17.23 17.82 17.21 17.60 17.09 17.40 C 16.97 17.20 16.80 17.08 16.56 17.03 C 16.33 16.96 16.12 16.98 15.92 17.09 C 15.72 17.20 15.59 17.37 15.52 17.59 L 15.23 18.65 C 15.22 18.79 15.21 18.86 15.21 18.86 Z"/>
<path d="M 13.84 23.68 C 13.84 23.82 13.87 23.96 13.94 24.07 C 14.07 24.28 14.25 24.43 14.48 24.50 C 14.59 24.54 14.69 24.56 14.76 24.56 C 14.89 24.56 14.99 24.54 15.07 24.48 C 15.27 24.41 15.42 24.21 15.52 23.88 L 15.77 22.87 C 15.84 22.63 15.82 22.42 15.70 22.22 C 15.59 22.02 15.42 21.89 15.19 21.83 C 14.96 21.76 14.74 21.78 14.54 21.90 C 14.34 22.01 14.20 22.18 14.13 22.41 L 13.85 23.45 C 13.85 23.53 13.84 23.61 13.84 23.68 Z"/>
<path d="M 11.01 22.00 C 11.00 22.16 11.05 22.32 11.15 22.47 C 11.25 22.62 11.41 22.73 11.63 22.79 C 11.84 22.86 12.05 22.84 12.25 22.73 C 12.45 22.62 12.59 22.43 12.67 22.17 L 12.97 21.14 C 13.04 20.92 13.01 20.71 12.89 20.51 C 12.77 20.31 12.59 20.17 12.35 20.10 C 12.12 20.03 11.91 20.05 11.71 20.17 C 11.51 20.29 11.37 20.46 11.30 20.70 L 11.06 21.75 C 11.03 21.90 11.01 21.98 11.01 22.00 Z"/>
<path d="M 9.67 26.80 C 9.67 26.95 9.72 27.11 9.83 27.27 C 9.94 27.43 10.09 27.54 10.29 27.61 C 10.40 27.64 10.49 27.65 10.54 27.65 C 10.69 27.65 10.82 27.62 10.92 27.57 C 11.13 27.49 11.28 27.30 11.35 27.00 L 11.62 25.97 C 11.68 25.72 11.65 25.50 11.54 25.30 C 11.43 25.10 11.24 24.98 11.01 24.93 C 10.80 24.86 10.60 24.89 10.39 25.00 C 10.18 25.12 10.04 25.29 9.97 25.52 L 9.72 26.56 C 9.69 26.70 9.67 26.78 9.67 26.80 Z"/>
</g>
<g id="sun">
<path d="M 15.68 6.72 C 16.36 6.42 17.06 6.27 17.78 6.27 C 18.73 6.27 19.61 6.51 20.42 6.98 C 21.23 7.45 21.87 8.09 22.34 8.90 C 22.81 9.71 23.05 10.59 23.05 11.54 C 23.05 12.51 22.34 14.04 21.86 14.84 L 20.81 13.46 C 21.05 12.97 21.34 12.12 21.34 11.55 C 21.34 10.57 20.99 9.73 20.30 9.05 C 19.61 8.36 18.77 8.02 17.79 8.02 C 16.89 8.02 15.93 8.45 15.29 9.00 L 13.87 7.98 C 14.33 7.54 15.08 6.98 15.69 6.72 L 15.68 6.72 Z"/>
<path d="M 24.73 11.58 C 24.73 11.82 24.82 12.02 24.99 12.17 C 25.15 12.35 25.35 12.43 25.59 12.43 L 27.65 12.43 C 27.89 12.43 28.09 12.35 28.26 12.18 C 28.43 12.01 28.51 11.81 28.51 11.58 C 28.51 11.35 28.43 11.14 28.26 10.98 C 28.09 10.82 27.89 10.74 27.65 10.74 L 25.59 10.74 C 25.35 10.74 25.14 10.82 24.98 10.98 C 24.81 11.14 24.73 11.34 24.73 11.58 Z"/>
<path d="M 23.26 17.95 C 23.26 18.18 23.34 18.38 23.51 18.55 L 24.16 19.18 C 24.34 19.35 24.55 19.43 24.78 19.43 L 24.80 19.45 C 25.02 19.45 25.20 19.36 25.34 19.18 C 25.52 19.02 25.60 18.82 25.60 18.58 C 25.60 18.35 25.51 18.15 25.34 17.97 L 24.72 17.35 C 24.54 17.17 24.34 17.08 24.11 17.08 C 23.87 17.08 23.67 17.17 23.51 17.34 C 23.35 17.51 23.26 17.72 23.26 17.95 Z"/>
<path d="M 22.49 6.04 C 22.49 6.28 22.57 6.48 22.72 6.64 C 22.86 6.80 23.04 6.88 23.27 6.90 C 23.50 6.92 23.71 6.83 23.90 6.64 L 25.34 5.20 C 25.52 5.04 25.60 4.84 25.60 4.60 C 25.60 4.36 25.51 4.16 25.34 4.00 C 25.18 3.82 24.98 3.74 24.74 3.74 C 24.51 3.74 24.32 3.83 24.16 4.00 L 22.72 5.44 C 22.56 5.59 22.49 5.79 22.49 6.04 Z"/>
<path d="M 16.91 3.75 C 16.91 3.99 16.99 4.19 17.16 4.36 C 17.33 4.53 17.53 4.61 17.76 4.61 C 18.00 4.61 18.20 4.53 18.36 4.36 C 18.52 4.19 18.60 3.99 18.60 3.75 L 18.60 1.69 C 18.60 1.45 18.52 1.24 18.36 1.08 C 18.20 0.91 18.00 0.82 17.76 0.82 C 17.52 0.82 17.32 0.90 17.16 1.07 C 17.00 1.24 16.91 1.44 16.91 1.68 L 16.91 3.75 Z"/>
<path d="M 9.90 4.59 C 9.90 4.82 9.98 5.02 10.15 5.19 L 10.80 5.85 C 10.96 6.01 11.14 6.09 11.35 6.11 C 11.56 6.14 11.76 6.07 11.96 5.88 C 12.16 5.70 12.26 5.49 12.26 5.24 C 12.26 5.01 12.18 4.81 12.01 4.64 L 11.38 3.98 C 11.22 3.82 11.02 3.74 10.78 3.74 C 10.53 3.74 10.32 3.82 10.15 3.98 C 9.99 4.16 9.90 4.36 9.90 4.59 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,3 @@
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path id="sun" d="M4.37,14.62c0-0.24,0.08-0.45,0.25-0.62c0.17-0.16,0.38-0.24,0.6-0.24h2.04c0.23,0,0.42,0.08,0.58,0.25 c0.15,0.17,0.23,0.37,0.23,0.61S8,15.06,7.85,15.23c-0.15,0.17-0.35,0.25-0.58,0.25H5.23c-0.23,0-0.43-0.08-0.6-0.25 C4.46,15.06,4.37,14.86,4.37,14.62z M7.23,21.55c0-0.23,0.08-0.43,0.23-0.61l1.47-1.43c0.15-0.16,0.35-0.23,0.59-0.23 c0.24,0,0.44,0.08,0.6,0.23s0.24,0.34,0.24,0.57c0,0.24-0.08,0.46-0.24,0.64L8.7,22.14c-0.41,0.32-0.82,0.32-1.23,0 C7.31,21.98,7.23,21.78,7.23,21.55z M7.23,7.71c0-0.23,0.08-0.43,0.23-0.61C7.66,6.93,7.87,6.85,8.1,6.85 c0.22,0,0.42,0.08,0.59,0.24l1.43,1.47c0.16,0.15,0.24,0.35,0.24,0.59c0,0.24-0.08,0.44-0.24,0.6s-0.36,0.24-0.6,0.24 c-0.24,0-0.44-0.08-0.59-0.24L7.47,8.32C7.31,8.16,7.23,7.95,7.23,7.71z M9.78,14.62c0-0.93,0.23-1.8,0.7-2.6s1.1-1.44,1.91-1.91 s1.67-0.7,2.6-0.7c0.7,0,1.37,0.14,2.02,0.42c0.64,0.28,1.2,0.65,1.66,1.12c0.47,0.47,0.84,1.02,1.11,1.66 c0.27,0.64,0.41,1.32,0.41,2.02c0,0.94-0.23,1.81-0.7,2.61c-0.47,0.8-1.1,1.43-1.9,1.9c-0.8,0.47-1.67,0.7-2.61,0.7 s-1.81-0.23-2.61-0.7c-0.8-0.47-1.43-1.1-1.9-1.9C10.02,16.43,9.78,15.56,9.78,14.62z M11.48,14.62c0,0.98,0.34,1.81,1.03,2.5 c0.68,0.69,1.51,1.04,2.49,1.04s1.81-0.35,2.5-1.04s1.04-1.52,1.04-2.5c0-0.96-0.35-1.78-1.04-2.47c-0.69-0.68-1.52-1.02-2.5-1.02 c-0.97,0-1.8,0.34-2.48,1.02C11.82,12.84,11.48,13.66,11.48,14.62z M14.14,22.4c0-0.24,0.08-0.44,0.25-0.6s0.37-0.24,0.6-0.24 c0.24,0,0.45,0.08,0.61,0.24s0.24,0.36,0.24,0.6v1.99c0,0.24-0.08,0.45-0.25,0.62c-0.17,0.17-0.37,0.25-0.6,0.25 s-0.44-0.08-0.6-0.25c-0.17-0.17-0.25-0.38-0.25-0.62V22.4z M14.14,6.9V4.86c0-0.23,0.08-0.43,0.25-0.6C14.56,4.09,14.76,4,15,4 s0.43,0.08,0.6,0.25c0.17,0.17,0.25,0.37,0.25,0.6V6.9c0,0.23-0.08,0.42-0.25,0.58S15.23,7.71,15,7.71s-0.44-0.08-0.6-0.23 S14.14,7.13,14.14,6.9z M19.66,20.08c0-0.23,0.08-0.42,0.23-0.56c0.15-0.16,0.34-0.23,0.56-0.23c0.24,0,0.44,0.08,0.6,0.23 l1.46,1.43c0.16,0.17,0.24,0.38,0.24,0.61c0,0.23-0.08,0.43-0.24,0.59c-0.4,0.31-0.8,0.31-1.2,0l-1.42-1.42 C19.74,20.55,19.66,20.34,19.66,20.08z M19.66,9.16c0-0.25,0.08-0.45,0.23-0.59l1.42-1.47c0.17-0.16,0.37-0.24,0.59-0.24 c0.24,0,0.44,0.08,0.6,0.25c0.17,0.17,0.25,0.37,0.25,0.6c0,0.25-0.08,0.46-0.24,0.62l-1.46,1.43c-0.18,0.16-0.38,0.24-0.6,0.24 c-0.23,0-0.41-0.08-0.56-0.24S19.66,9.4,19.66,9.16z M21.92,14.62c0-0.24,0.08-0.44,0.24-0.62c0.16-0.16,0.35-0.24,0.57-0.24h2.02 c0.23,0,0.43,0.09,0.6,0.26c0.17,0.17,0.26,0.37,0.26,0.6s-0.09,0.43-0.26,0.6c-0.17,0.17-0.37,0.25-0.6,0.25h-2.02 c-0.23,0-0.43-0.08-0.58-0.25S21.92,14.86,21.92,14.62z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 2.61 13.81 C 3.32 12.91 4.23 12.34 5.34 12.08 C 5.65 10.71 6.35 9.59 7.44 8.72 C 8.53 7.85 9.79 7.41 11.20 7.41 C 12.19 7.41 13.09 7.63 13.90 8.06 C 13.93 8.02 15.32 9.02 15.30 9.04 C 16.11 9.81 16.68 10.78 16.99 11.95 L 17.30 11.95 C 18.64 11.95 19.81 12.44 20.80 13.42 C 20.84 13.35 22.02 14.69 21.85 14.90 C 22.14 15.50 22.28 16.17 22.28 16.91 C 22.28 18.25 21.82 19.40 20.89 20.36 C 19.96 21.32 18.83 21.83 17.50 21.87 C 17.37 21.87 17.30 21.81 17.30 21.70 L 17.30 20.33 C 17.30 20.21 17.37 20.15 17.50 20.15 C 18.37 20.08 19.10 19.73 19.69 19.11 C 20.28 18.49 20.58 17.75 20.58 16.90 C 20.58 16.00 20.26 15.24 19.62 14.61 C 18.98 13.98 18.21 13.66 17.30 13.66 L 15.69 13.66 C 15.57 13.66 15.51 13.60 15.51 13.49 L 15.45 12.91 C 15.33 11.83 14.86 10.92 14.05 10.19 C 13.24 9.46 12.29 9.09 11.20 9.09 C 10.11 9.09 9.15 9.46 8.34 10.19 C 7.53 10.92 7.07 11.83 6.96 12.91 L 6.90 13.42 C 6.90 13.54 6.83 13.60 6.70 13.60 L 6.18 13.66 C 5.35 13.76 4.65 14.11 4.08 14.73 C 3.51 15.35 3.23 16.07 3.23 16.90 C 3.23 17.65 3.46 18.32 3.93 18.91 C 4.37 19.46 5.03 19.89 5.68 20.08 L 5.07 21.72 C 4.07 21.40 3.12 20.70 2.50 19.86 C 1.85 18.99 1.52 18.01 1.52 16.90 C 1.52 15.75 1.90 14.71 2.61 13.81 Z"/>
<path id="lightning" d="M 5.70 20.07 L 6.77 17.19 C 6.83 17.10 6.91 17.05 7.00 17.05 L 9.94 17.05 C 10.10 17.05 10.15 17.13 10.08 17.27 L 7.61 21.90 L 9.78 21.90 C 9.85 21.90 9.90 21.92 9.92 21.97 C 9.95 22.02 9.95 22.07 9.91 22.11 L 5.74 27.50 L 5.45 27.50 L 6.70 23.55 L 4.57 23.55 C 4.43 23.55 4.39 23.48 4.43 23.34 L 5.07 21.64 C 5.03 21.63 5.65 20.06 5.70 20.07 Z"/>
<g id="sun">
<path d="M 17.78 6.33 C 18.72 6.33 19.59 6.57 20.40 7.04 C 21.21 7.51 21.85 8.15 22.32 8.96 C 22.79 9.77 23.03 10.64 23.03 11.58 C 23.03 12.56 22.49 14.34 21.87 14.98 L 20.82 13.44 C 21.09 12.90 21.32 12.12 21.32 11.57 C 21.32 10.60 20.97 9.77 20.28 9.09 C 19.59 8.41 18.75 8.07 17.78 8.07 C 16.82 8.07 15.98 8.50 15.32 9.10 L 13.86 8.09 C 14.88 7.01 16.29 6.33 17.78 6.33 Z"/>
<path d="M 24.70 11.58 C 24.70 11.81 24.79 12.01 24.97 12.18 C 25.15 12.36 25.35 12.45 25.58 12.45 L 27.61 12.45 C 27.84 12.45 28.04 12.36 28.21 12.19 C 28.38 12.02 28.47 11.81 28.47 11.58 C 28.47 11.35 28.39 11.15 28.22 10.99 C 28.05 10.83 27.85 10.75 27.61 10.75 L 25.58 10.75 C 25.33 10.75 25.12 10.83 24.95 10.99 C 24.78 11.15 24.70 11.35 24.70 11.58 Z"/>
<path d="M 23.25 17.93 C 23.25 18.15 23.33 18.35 23.49 18.53 L 24.15 19.16 C 24.27 19.30 24.46 19.39 24.69 19.40 L 24.70 19.41 C 24.71 19.41 24.73 19.41 24.75 19.41 C 24.77 19.41 24.78 19.41 24.80 19.41 C 24.99 19.41 25.16 19.32 25.33 19.15 C 25.50 18.99 25.59 18.79 25.59 18.55 C 25.59 18.32 25.50 18.12 25.33 17.94 L 24.68 17.33 C 24.52 17.15 24.32 17.06 24.10 17.06 C 23.87 17.06 23.67 17.14 23.50 17.31 C 23.33 17.49 23.25 17.70 23.25 17.93 Z"/>
<path d="M 22.44 6.07 C 22.44 6.31 22.53 6.51 22.70 6.67 C 22.84 6.84 23.03 6.92 23.27 6.92 C 23.51 6.92 23.71 6.84 23.87 6.67 L 25.31 5.22 C 25.48 5.06 25.57 4.87 25.57 4.63 C 25.57 4.39 25.49 4.19 25.32 4.02 C 25.15 3.85 24.95 3.77 24.71 3.77 C 24.49 3.77 24.30 3.86 24.14 4.03 L 22.70 5.47 C 22.53 5.63 22.44 5.83 22.44 6.07 Z"/>
<path d="M 16.91 3.79 C 16.91 4.02 17.00 4.22 17.17 4.39 C 17.34 4.56 17.54 4.65 17.77 4.65 C 18.01 4.65 18.20 4.57 18.36 4.40 C 18.52 4.23 18.59 4.03 18.59 3.79 L 18.59 1.73 C 18.59 1.49 18.51 1.29 18.36 1.12 C 18.21 0.95 18.01 0.87 17.77 0.87 C 17.54 0.87 17.34 0.95 17.17 1.12 C 17.00 1.29 16.91 1.49 16.91 1.73 L 16.91 3.79 Z"/>
<path d="M 9.94 4.63 C 9.94 4.87 10.02 5.06 10.19 5.22 L 10.83 5.88 C 11.00 6.05 11.20 6.13 11.44 6.14 C 11.68 6.14 11.87 6.06 12.01 5.88 C 12.20 5.73 12.29 5.53 12.29 5.28 C 12.29 5.04 12.21 4.85 12.04 4.69 L 11.41 4.03 C 11.24 3.87 11.03 3.79 10.80 3.79 C 10.55 3.79 10.34 3.87 10.18 4.03 C 10.02 4.19 9.94 4.39 9.94 4.63 Z"/>
</g>
<g id="rain">
<path d="M 13.77 23.43 C 13.77 23.55 13.81 23.67 13.88 23.81 C 14.01 24.01 14.17 24.15 14.38 24.24 C 14.45 24.27 14.55 24.29 14.68 24.29 C 14.83 24.29 14.94 24.27 15.01 24.23 C 15.21 24.15 15.35 23.95 15.42 23.65 L 16.91 18.10 C 16.97 17.86 16.95 17.65 16.84 17.45 C 16.73 17.26 16.56 17.13 16.33 17.06 C 16.10 16.99 15.88 17.01 15.69 17.13 C 15.49 17.24 15.36 17.41 15.30 17.64 L 13.80 23.20 C 13.80 23.22 13.79 23.26 13.78 23.31 C 13.77 23.37 13.77 23.40 13.77 23.43 Z"/>
<path d="M 9.61 26.48 C 9.60 26.63 9.64 26.78 9.75 26.92 C 9.86 27.06 10.01 27.17 10.21 27.25 C 10.28 27.27 10.35 27.28 10.42 27.28 C 10.59 27.28 10.76 27.23 10.93 27.13 C 11.10 27.03 11.21 26.87 11.27 26.66 L 13.56 18.09 C 13.62 17.86 13.60 17.64 13.49 17.45 C 13.38 17.25 13.22 17.12 13.00 17.05 C 12.77 16.98 12.55 17.00 12.35 17.12 C 12.15 17.23 12.01 17.40 11.95 17.63 L 9.64 26.23 C 9.62 26.30 9.61 26.39 9.61 26.48 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 4.64 16.90 C 4.64 18.23 5.10 19.37 6.03 20.33 C 6.96 21.29 8.09 21.80 9.43 21.86 C 9.54 21.86 9.60 21.80 9.60 21.69 L 9.60 20.35 C 9.60 20.24 9.54 20.18 9.43 20.18 C 8.57 20.14 7.85 19.80 7.25 19.16 C 6.65 18.52 6.35 17.77 6.35 16.90 C 6.35 16.07 6.63 15.36 7.19 14.74 C 7.75 14.13 8.45 13.77 9.28 13.67 L 9.81 13.64 C 9.94 13.64 10.01 13.58 10.01 13.45 L 10.07 12.92 C 10.18 11.84 10.63 10.93 11.44 10.21 C 12.25 9.48 13.20 9.12 14.29 9.12 C 15.38 9.12 16.33 9.48 17.14 10.21 C 17.95 10.94 18.41 11.84 18.53 12.92 L 18.61 13.50 C 18.61 13.61 18.67 13.67 18.79 13.67 L 20.40 13.67 C 21.29 13.67 22.06 13.99 22.71 14.63 C 23.36 15.27 23.69 16.02 23.69 16.90 C 23.69 17.77 23.39 18.52 22.79 19.16 C 22.19 19.80 21.46 20.14 20.61 20.18 C 20.48 20.18 20.41 20.24 20.41 20.35 L 20.41 21.69 C 20.41 21.80 20.48 21.86 20.61 21.86 C 21.48 21.84 22.28 21.60 23.01 21.15 C 23.74 20.70 24.32 20.10 24.74 19.35 C 25.16 18.60 25.37 17.78 25.37 16.91 C 25.37 16.02 25.15 15.19 24.70 14.44 C 24.26 13.69 23.65 13.09 22.89 12.66 C 22.13 12.23 21.29 12.00 20.40 12.00 L 20.08 12.00 C 19.76 10.66 19.05 9.57 17.98 8.72 C 16.91 7.87 15.68 7.44 14.30 7.44 C 12.89 7.44 11.64 7.88 10.55 8.75 C 9.46 9.62 8.76 10.74 8.45 12.10 C 7.34 12.36 6.43 12.93 5.72 13.83 C 5.01 14.73 4.64 15.75 4.64 16.90 Z"/>
<g id="rain">
<path d="M 17.47 21.23 C 17.47 21.37 17.52 21.52 17.63 21.68 C 17.74 21.84 17.89 21.95 18.08 22.01 C 18.24 22.04 18.33 22.06 18.35 22.06 C 18.44 22.06 18.57 22.03 18.72 21.96 C 18.92 21.87 19.05 21.69 19.12 21.44 L 20.02 18.10 C 20.04 17.93 20.05 17.84 20.05 17.84 C 20.05 17.68 20.00 17.53 19.90 17.38 C 19.80 17.23 19.65 17.13 19.45 17.07 C 19.36 17.05 19.27 17.04 19.19 17.04 C 19.03 17.04 18.87 17.09 18.72 17.19 C 18.57 17.29 18.47 17.44 18.41 17.64 L 17.51 21.00 L 17.47 21.23 Z"/>
<path d="M 16.74 23.80 C 16.74 23.92 16.76 24.03 16.82 24.12 C 16.90 24.31 17.05 24.46 17.26 24.56 C 17.37 24.60 17.49 24.63 17.61 24.63 C 17.67 24.63 17.77 24.61 17.91 24.57 C 18.12 24.49 18.28 24.34 18.37 24.13 C 18.44 23.91 18.44 23.70 18.36 23.50 C 18.28 23.30 18.14 23.15 17.94 23.05 C 17.71 22.94 17.50 22.93 17.29 23.02 C 17.08 23.11 16.93 23.26 16.83 23.49 C 16.77 23.59 16.74 23.69 16.74 23.80 Z"/>
<path d="M 13.31 24.26 C 13.31 24.63 13.52 24.87 13.94 24.99 C 14.05 25.02 14.13 25.03 14.18 25.03 C 14.33 25.03 14.46 25.00 14.56 24.95 C 14.77 24.87 14.91 24.68 14.98 24.38 L 16.65 18.09 C 16.71 17.85 16.69 17.64 16.59 17.44 C 16.49 17.25 16.32 17.12 16.10 17.06 C 16.02 17.04 15.93 17.03 15.83 17.03 C 15.67 17.03 15.51 17.08 15.35 17.18 C 15.19 17.28 15.09 17.43 15.05 17.62 L 13.34 23.96 C 13.32 24.10 13.31 24.20 13.31 24.26 Z"/>
<path d="M 12.58 26.87 C 12.58 26.99 12.60 27.09 12.64 27.16 C 12.73 27.38 12.88 27.53 13.09 27.61 C 13.18 27.66 13.29 27.69 13.42 27.69 C 13.48 27.69 13.58 27.67 13.72 27.63 C 13.94 27.55 14.10 27.40 14.19 27.18 C 14.29 26.96 14.29 26.74 14.19 26.52 C 14.09 26.30 13.94 26.15 13.74 26.06 C 13.54 25.97 13.34 25.97 13.12 26.06 C 12.93 26.14 12.80 26.26 12.71 26.42 C 12.62 26.58 12.58 26.73 12.58 26.87 Z"/>
<path d="M 10.72 21.28 C 10.72 21.44 10.77 21.59 10.87 21.73 C 10.97 21.88 11.13 21.98 11.33 22.05 C 11.52 22.16 11.73 22.17 11.95 22.06 C 12.17 21.96 12.32 21.76 12.39 21.46 L 13.29 18.08 C 13.35 17.83 13.33 17.61 13.21 17.41 C 13.09 17.21 12.92 17.09 12.68 17.05 C 12.60 17.03 12.52 17.02 12.44 17.02 C 12.28 17.02 12.12 17.07 11.97 17.17 C 11.82 17.27 11.71 17.42 11.65 17.61 L 10.77 21.00 C 10.73 21.16 10.72 21.25 10.72 21.28 Z"/>
<path d="M 10.09 24.10 C 10.18 24.31 10.34 24.47 10.55 24.56 C 10.75 24.66 10.96 24.67 11.17 24.58 C 11.39 24.49 11.53 24.34 11.62 24.13 C 11.72 23.91 11.73 23.70 11.64 23.49 C 11.56 23.28 11.40 23.14 11.19 23.05 C 10.99 22.94 10.79 22.93 10.58 23.02 C 10.37 23.11 10.22 23.26 10.12 23.49 C 10.01 23.66 10.01 23.86 10.09 24.10 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,3 @@
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path id="lightning" d="M7.96,24.51h0.39l6.88-10.18c0.09-0.18,0.04-0.27-0.15-0.27h-2.84l2.99-5.45c0.09-0.18,0.02-0.27-0.2-0.27h-3.81 c-0.11,0-0.2,0.06-0.29,0.18l-2.78,7.4c-0.02,0.18,0.04,0.27,0.19,0.27h2.75L7.96,24.51z M16.46,18.18h0.27l5.22-7.67 c0.05-0.08,0.06-0.15,0.04-0.2s-0.08-0.07-0.17-0.07h-2.1l2.18-4.03c0.12-0.2,0.06-0.3-0.18-0.3h-2.74c-0.13,0-0.23,0.06-0.3,0.19 l-2.08,5.48c-0.03,0.09-0.03,0.16,0.01,0.21c0.04,0.05,0.1,0.07,0.19,0.07h2.04L16.46,18.18z"/>
</svg>

After

Width:  |  Height:  |  Size: 778 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 4.14 16.90 C 4.14 15.74 4.49 14.72 5.20 13.82 C 5.91 12.92 6.82 12.35 7.94 12.10 C 8.17 11.07 8.64 10.17 9.34 9.40 C 10.04 8.63 10.89 8.08 11.87 7.75 C 12.49 7.54 13.13 7.43 13.80 7.43 C 14.61 7.43 15.40 7.59 16.15 7.91 C 16.18 7.86 17.60 8.79 17.60 8.79 C 18.66 9.70 19.32 10.76 19.57 11.97 L 19.89 11.97 C 21.13 11.97 22.19 12.36 23.06 13.15 C 23.11 13.11 24.31 14.31 24.17 14.43 C 24.57 15.13 24.79 15.88 24.82 16.67 C 24.83 16.72 24.83 16.79 24.83 16.90 C 24.83 17.79 24.61 18.62 24.16 19.38 C 23.72 20.14 23.11 20.74 22.36 21.18 C 21.60 21.62 20.77 21.85 19.88 21.85 L 9.07 21.85 C 8.18 21.85 7.35 21.63 6.59 21.18 C 5.83 20.73 5.24 20.13 4.80 19.38 C 4.36 18.63 4.14 17.80 4.14 16.90 L 4.14 16.90 Z M 5.85 16.90 C 5.85 17.79 6.17 18.56 6.81 19.21 C 7.45 19.86 8.20 20.19 9.07 20.19 L 19.88 20.19 C 20.77 20.19 21.53 19.87 22.16 19.22 C 22.79 18.57 23.11 17.80 23.11 16.90 C 23.11 16.02 22.79 15.27 22.15 14.64 C 21.51 14.01 20.75 13.69 19.87 13.69 L 18.09 13.69 L 17.99 12.94 C 17.89 11.93 17.47 11.06 16.73 10.35 C 15.99 9.64 15.11 9.24 14.10 9.15 C 14.07 9.15 14.02 9.15 13.95 9.14 C 13.88 9.13 13.84 9.13 13.80 9.13 C 13.29 9.13 12.78 9.23 12.26 9.42 L 12.26 9.40 C 11.53 9.68 10.91 10.14 10.42 10.77 C 9.92 11.40 9.62 12.12 9.52 12.94 L 9.45 13.66 L 8.77 13.69 C 7.93 13.79 7.23 14.14 6.67 14.75 C 6.11 15.36 5.85 16.07 5.85 16.90 Z"/>
<path id="moon" d="M 17.16 6.67 C 17.54 6.33 17.93 6.06 18.33 5.88 C 18.73 5.70 19.13 5.56 19.51 5.47 C 19.89 5.38 20.27 5.34 20.63 5.34 C 21.01 5.34 21.42 5.39 21.86 5.50 L 22.68 5.75 C 22.82 5.81 22.86 5.88 22.82 5.97 L 22.68 6.57 C 22.61 6.88 22.58 7.17 22.58 7.43 C 22.58 7.74 22.63 8.06 22.73 8.38 C 22.83 8.70 22.97 9.01 23.17 9.32 C 23.36 9.63 23.63 9.90 23.97 10.15 C 24.31 10.40 24.69 10.59 25.12 10.72 L 25.74 10.94 C 25.84 10.97 25.89 11.02 25.89 11.10 C 25.89 11.12 25.88 11.14 25.87 11.17 L 25.69 11.84 C 25.47 12.72 24.70 13.84 24.18 14.42 L 23.09 13.17 C 23.35 12.89 23.68 12.40 23.84 12.06 C 22.93 11.63 22.21 10.97 21.68 10.09 C 21.16 9.21 20.89 8.28 20.89 7.31 L 20.89 7.07 C 20.84 7.06 20.76 7.06 20.65 7.06 C 20.07 7.05 19.50 7.19 18.95 7.50 C 18.40 7.80 17.93 8.26 17.60 8.80 L 16.15 7.92 C 16.41 7.51 16.82 6.97 17.16 6.67 L 17.16 6.67 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 5.19 13.80 C 5.91 12.90 6.82 12.33 7.93 12.07 C 8.24 10.70 8.94 9.58 10.03 8.71 C 11.12 7.84 12.38 7.40 13.79 7.40 C 14.62 7.40 15.41 7.57 16.16 7.89 C 16.20 7.82 17.63 8.81 17.62 8.83 C 18.66 9.69 19.30 10.75 19.56 12.00 L 19.88 12.00 C 21.17 12.00 22.25 12.39 23.13 13.18 C 23.17 13.15 24.34 14.37 24.21 14.49 C 24.64 15.24 24.86 16.04 24.86 16.90 C 24.86 18.24 24.40 19.39 23.47 20.35 C 22.55 21.31 21.42 21.82 20.09 21.86 C 19.96 21.86 19.89 21.80 19.89 21.69 L 19.89 20.35 C 19.89 20.24 19.96 20.18 20.09 20.18 C 20.94 20.14 21.67 19.80 22.27 19.16 C 22.87 18.52 23.17 17.77 23.17 16.90 C 23.17 16.03 22.84 15.27 22.19 14.63 C 21.54 13.99 20.77 13.67 19.88 13.67 L 18.27 13.67 C 18.16 13.67 18.10 13.61 18.10 13.50 L 18.02 12.92 C 17.90 11.84 17.44 10.93 16.62 10.20 C 15.80 9.47 14.85 9.10 13.77 9.10 C 12.68 9.10 11.73 9.47 10.92 10.20 C 10.11 10.93 9.66 11.84 9.55 12.92 L 9.48 13.46 C 9.48 13.58 9.41 13.64 9.28 13.64 L 8.75 13.67 C 7.91 13.77 7.21 14.13 6.65 14.74 C 6.09 15.36 5.81 16.08 5.81 16.91 C 5.81 17.78 6.11 18.53 6.71 19.17 C 7.31 19.81 8.03 20.15 8.89 20.19 C 9.01 20.19 9.07 20.25 9.07 20.36 L 9.07 21.70 C 9.07 21.81 9.01 21.87 8.89 21.87 C 7.55 21.81 6.42 21.29 5.49 20.34 C 4.56 19.39 4.10 18.24 4.10 16.91 C 4.10 15.76 4.47 14.70 5.19 13.80 L 5.19 13.80 Z"/>
<path id="moon" d="M 17.16 6.67 C 17.55 6.32 17.94 6.05 18.35 5.87 C 18.76 5.69 19.15 5.56 19.52 5.47 C 19.89 5.38 20.25 5.34 20.60 5.34 C 21.05 5.34 21.49 5.41 21.91 5.53 L 22.71 5.78 C 22.83 5.84 22.88 5.91 22.85 6.00 L 22.71 6.58 C 22.65 6.86 22.62 7.13 22.63 7.37 C 22.63 7.82 22.71 8.26 22.88 8.69 C 23.05 9.13 23.34 9.54 23.74 9.94 C 24.15 10.34 24.63 10.62 25.20 10.79 L 25.76 10.96 C 25.87 10.99 25.92 11.05 25.92 11.12 C 25.92 11.16 25.91 11.18 25.89 11.19 L 25.76 11.87 C 25.55 12.71 24.90 13.91 24.23 14.50 L 23.15 13.22 C 23.41 12.96 23.71 12.50 23.88 12.11 C 22.96 11.67 22.23 11.02 21.71 10.16 C 21.19 9.30 20.93 8.39 20.93 7.41 L 20.93 7.10 C 20.88 7.09 20.79 7.08 20.68 7.08 C 20.06 7.07 19.47 7.22 18.92 7.52 C 18.39 7.81 17.93 8.29 17.62 8.82 L 16.16 7.92 C 16.41 7.53 16.83 6.97 17.16 6.67 L 17.16 6.67 Z"/>
<g id="rain">
<path d="M 16.97 21.08 C 16.97 21.24 17.02 21.40 17.12 21.54 C 17.22 21.68 17.37 21.79 17.57 21.85 C 17.74 21.87 17.83 21.88 17.84 21.88 C 18.25 21.88 18.50 21.68 18.61 21.27 L 19.48 18.10 C 19.54 17.86 19.52 17.65 19.41 17.45 C 19.30 17.26 19.13 17.13 18.91 17.06 C 18.68 16.99 18.46 17.01 18.27 17.13 C 18.07 17.24 17.94 17.41 17.87 17.64 L 17.00 20.81 C 16.98 20.90 16.97 20.99 16.97 21.08 Z"/>
<path d="M 16.25 23.64 C 16.25 23.77 16.27 23.87 16.32 23.95 C 16.40 24.15 16.55 24.30 16.76 24.39 C 16.88 24.44 16.99 24.47 17.11 24.47 C 17.17 24.47 17.27 24.45 17.41 24.41 C 17.63 24.32 17.78 24.18 17.86 23.97 C 17.94 23.75 17.94 23.54 17.86 23.34 C 17.78 23.14 17.64 22.99 17.44 22.89 C 17.22 22.79 17.00 22.78 16.79 22.87 C 16.57 22.95 16.42 23.11 16.32 23.33 C 16.27 23.41 16.25 23.51 16.25 23.64 Z"/>
<path d="M 12.81 24.06 C 12.81 24.44 13.02 24.70 13.45 24.84 C 13.54 24.87 13.62 24.89 13.68 24.89 C 13.79 24.89 13.91 24.86 14.03 24.81 C 14.26 24.73 14.42 24.54 14.50 24.24 L 16.15 18.12 C 16.21 17.88 16.19 17.67 16.08 17.47 C 15.97 17.28 15.80 17.15 15.58 17.08 C 15.35 17.01 15.13 17.03 14.93 17.15 C 14.73 17.26 14.59 17.43 14.53 17.66 L 12.85 23.83 C 12.82 23.92 12.81 24.00 12.81 24.06 Z"/>
<path d="M 12.07 26.71 C 12.07 26.83 12.09 26.93 12.13 27.00 C 12.22 27.22 12.37 27.37 12.58 27.45 C 12.67 27.50 12.78 27.52 12.91 27.52 C 12.97 27.52 13.07 27.50 13.21 27.46 C 13.44 27.38 13.60 27.23 13.69 27.01 C 13.79 26.79 13.79 26.57 13.69 26.35 C 13.59 26.13 13.44 25.98 13.24 25.89 C 13.04 25.80 12.84 25.80 12.63 25.89 C 12.44 25.97 12.30 26.09 12.21 26.25 C 12.11 26.42 12.07 26.57 12.07 26.71 Z"/>
<path d="M 10.20 21.11 C 10.20 21.26 10.25 21.41 10.36 21.56 C 10.47 21.71 10.62 21.82 10.82 21.88 C 11.08 21.98 11.30 21.98 11.49 21.88 C 11.68 21.78 11.81 21.59 11.89 21.31 L 12.77 18.10 C 12.84 17.85 12.81 17.63 12.69 17.43 C 12.57 17.23 12.39 17.11 12.15 17.06 C 11.93 16.99 11.72 17.01 11.52 17.13 C 11.32 17.24 11.19 17.41 11.12 17.64 L 10.24 20.86 C 10.24 20.88 10.23 20.92 10.22 20.98 C 10.21 21.03 10.20 21.08 10.20 21.11 Z"/>
<path d="M 9.58 23.94 C 9.67 24.15 9.82 24.30 10.04 24.39 C 10.23 24.49 10.44 24.50 10.66 24.41 C 10.88 24.33 11.03 24.18 11.11 23.96 C 11.21 23.74 11.22 23.53 11.13 23.31 C 11.05 23.10 10.90 22.95 10.68 22.87 C 10.48 22.77 10.27 22.76 10.06 22.85 C 9.85 22.94 9.69 23.09 9.59 23.31 C 9.50 23.48 9.49 23.69 9.58 23.94 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;">
<path id="cloud" d="M 5.23 13.85 C 5.95 12.95 6.86 12.37 7.96 12.11 C 8.27 10.75 8.97 9.63 10.06 8.76 C 11.15 7.89 12.40 7.45 13.81 7.45 C 14.66 7.45 15.43 7.60 16.14 7.90 C 16.18 7.83 17.61 8.79 17.60 8.81 C 18.63 9.66 19.29 10.71 19.58 11.97 L 19.90 11.97 C 21.13 11.97 22.20 12.36 23.12 13.15 C 23.16 13.11 24.32 14.31 24.18 14.44 C 24.63 15.20 24.86 16.03 24.86 16.92 C 24.86 18.26 24.40 19.41 23.48 20.37 C 22.56 21.33 21.43 21.84 20.10 21.88 C 19.97 21.88 19.90 21.82 19.90 21.71 L 19.90 20.34 C 19.90 20.22 19.97 20.16 20.10 20.16 C 20.97 20.09 21.70 19.74 22.29 19.12 C 22.88 18.50 23.18 17.76 23.18 16.91 C 23.18 16.02 22.86 15.26 22.22 14.63 C 21.58 14.00 20.81 13.68 19.90 13.68 L 18.29 13.68 C 18.17 13.68 18.11 13.62 18.11 13.50 L 18.04 12.92 C 17.92 11.83 17.46 10.92 16.65 10.20 C 15.84 9.47 14.89 9.11 13.80 9.11 C 12.71 9.11 11.76 9.47 10.95 10.20 C 10.14 10.92 9.69 11.83 9.58 12.91 L 9.51 13.43 C 9.51 13.54 9.44 13.60 9.31 13.60 L 8.78 13.67 C 7.94 13.74 7.25 14.09 6.68 14.71 C 6.11 15.33 5.83 16.06 5.83 16.90 C 5.83 17.75 6.13 18.49 6.72 19.11 C 7.31 19.74 8.04 20.08 8.91 20.15 C 9.02 20.15 9.08 20.21 9.08 20.33 L 9.08 21.70 C 9.08 21.81 9.02 21.87 8.91 21.87 C 7.57 21.81 6.44 21.29 5.51 20.34 C 4.58 19.39 4.12 18.24 4.12 16.91 C 4.12 15.76 4.48 14.74 5.23 13.85 Z"/>
<path id="moon" d="M 17.14 6.68 C 17.53 6.33 17.93 6.06 18.34 5.88 C 18.75 5.69 19.15 5.56 19.53 5.47 C 19.92 5.38 20.29 5.34 20.65 5.34 C 21.05 5.34 21.46 5.39 21.90 5.50 L 22.69 5.74 C 22.81 5.79 22.86 5.87 22.83 5.96 L 22.69 6.58 C 22.63 6.86 22.60 7.13 22.60 7.37 C 22.60 7.82 22.68 8.26 22.85 8.69 C 23.02 9.12 23.30 9.53 23.70 9.92 C 24.10 10.30 24.58 10.58 25.15 10.74 L 25.73 10.91 C 25.84 10.94 25.89 11.00 25.89 11.07 C 25.89 11.11 25.89 11.13 25.88 11.14 L 25.73 11.86 C 25.54 12.69 24.83 13.98 24.17 14.49 L 23.16 13.27 C 23.48 12.99 23.73 12.40 23.87 12.08 C 22.94 11.63 22.22 10.98 21.70 10.12 C 21.18 9.26 20.92 8.35 20.92 7.39 L 20.92 7.08 C 20.87 7.07 20.78 7.06 20.65 7.06 C 20.06 7.06 19.48 7.21 18.93 7.51 C 18.39 7.80 17.93 8.29 17.60 8.82 L 16.17 7.92 C 16.42 7.53 16.81 6.98 17.14 6.68 Z"/>
<path id="lightning" d="M 11.79 21.56 C 11.74 21.70 11.79 21.78 11.93 21.78 L 14.09 21.78 L 12.78 25.92 L 13.08 25.92 L 17.25 20.33 C 17.29 20.29 17.30 20.24 17.28 20.19 C 17.26 20.14 17.22 20.12 17.15 20.12 L 14.95 20.12 L 17.26 15.91 C 17.33 15.77 17.28 15.69 17.12 15.69 L 14.18 15.69 C 14.10 15.69 14.03 15.74 13.96 15.83 L 11.79 21.56 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Some files were not shown because too many files have changed in this diff Show More