2 Commits

Author SHA1 Message Date
bad5dff2b2 ci(version): 0.4.2 2026-07-22 21:52:30 +03:00
e2af31b812 fix(gismeteo): fix empty wind direction value in parser 2026-07-22 21:52:15 +03:00
60 changed files with 824 additions and 2135 deletions

View File

@@ -12,7 +12,7 @@
<link rel="stylesheet" <link rel="stylesheet"
href="/static/gallery.css?v={{version}}"> href="/static/gallery.css?v={{version}}">
<script type="module" <script type="module"
src="/static/gallery.js?v={{version}}"></script> src="/static/gallery.es.js?v={{version}}"></script>
<link rel="icon" <link rel="icon"
href="/favicon.ico?v={{version}}" href="/favicon.ico?v={{version}}"
type="image/x-icon"> type="image/x-icon">

View File

@@ -148,6 +148,7 @@ class WindDirectionParser(RowParser[WindDirection]):
WIND_DIRECTION_MAP: dict[str, WindDirection] = { WIND_DIRECTION_MAP: dict[str, WindDirection] = {
"": WindDirection.CALM, "": WindDirection.CALM,
"": WindDirection.CALM,
"штиль": WindDirection.CALM, "штиль": WindDirection.CALM,
"с": WindDirection.N, "с": WindDirection.N,
"св": WindDirection.NE, "св": WindDirection.NE,

View File

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

2476
static/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
{ {
"name": "gallery", "name": "gallery",
"version": "0.4.1", "version": "0.4.2",
"type": "module",
"scripts": { "scripts": {
"build": "webpack --mode production", "build": "vite build",
"dev": "webpack --watch --mode development" "dev": "vite build --watch"
}, },
"author": "shmyga <shmyga.z@gmail.com>", "author": "shmyga <shmyga.z@gmail.com>",
"license": "ISC", "license": "ISC",
@@ -13,22 +12,10 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"flag-icons": "^7.5.0" "flag-icons": "^7.5.0",
"sass": "^1.99.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.11", "vite": "^8.0.9"
"@types/node": "^26.1.0",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^7.1.4",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.10.2",
"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"
} }
} }

View File

@@ -1,6 +0,0 @@
import { Tooltip } from "bootstrap";
document.addEventListener("DOMContentLoaded", (event) => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map((tooltipTriggerEl) => new Tooltip(tooltipTriggerEl));
});

View File

@@ -1,6 +1,12 @@
import "./app/common/language"; import * as bootstrap from "bootstrap";
import "./app/common/theme"; import "./components";
import "./app/common/tooltips"; import "./language";
import "./app/components/app-link"; import "./main.scss";
import "./app/schedule"; import "./theme";
import "./app/weather"; import "./weather/weather";
import "./schedule/schedule";
document.addEventListener("DOMContentLoaded", (event) => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl));
});

View File

@@ -19,7 +19,7 @@
setLanguage(getPreferredLanguage()); setLanguage(getPreferredLanguage());
const showActiveLanguage = (language: string, focus = false) => { const showActiveLanguage = (language: string, focus = false) => {
const languageSwitcher = document.querySelector<HTMLElement>("#bd-language"); const languageSwitcher = document.querySelector("#bd-language");
if (!languageSwitcher) { if (!languageSwitcher) {
return; return;
@@ -27,26 +27,24 @@
const languageSwitcherText = document.querySelector("#bd-language-text"); const languageSwitcherText = document.querySelector("#bd-language-text");
const activeLanguageIcon = document.querySelector(".language-icon-active"); const activeLanguageIcon = document.querySelector(".language-icon-active");
const btnToActive = document.querySelector<HTMLElement>(`[data-bs-language-value="${language}"]`); const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
const activeLanguageIconClass = (btnToActive?.querySelector(".fi")?.className.match(/fi-[\w-]+/) || [])[0] || ""; const activeLanguageIconClass = btnToActive.querySelector(".fi").className.match(/fi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-language-value]").forEach((element) => { document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
element.classList.remove("active"); element.classList.remove("active");
element.setAttribute("aria-pressed", "false"); element.setAttribute("aria-pressed", "false");
}); });
btnToActive?.classList.add("active"); btnToActive.classList.add("active");
btnToActive?.setAttribute("aria-pressed", "true"); btnToActive.setAttribute("aria-pressed", "true");
const classesToRemove = Array.from(activeLanguageIcon?.classList || []).filter((className) => const classesToRemove = Array.from(activeLanguageIcon.classList).filter((className) => className.startsWith("fi-"));
className.startsWith("fi-"), activeLanguageIcon.classList.remove(...classesToRemove);
); activeLanguageIcon.classList.add(activeLanguageIconClass);
activeLanguageIcon?.classList.remove(...classesToRemove); const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
activeLanguageIcon?.classList.add(activeLanguageIconClass);
const languageSwitcherLabel = `${languageSwitcherText?.textContent} (${btnToActive?.dataset.bsLanguageValue})`;
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel); languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);
if (focus) { if (focus) {
languageSwitcher?.focus(); languageSwitcher.focus();
} }
}; };

View File

@@ -1,5 +1,12 @@
@use "flag-icons/sass/flag-icons" with ( @use "flag-icons/sass/flag-icons" with (
$flag-icons-path: "flag-icons/flags", $flag-icons-path: "flag-icons/flags",
$flag-icons-included-countries: (
"gb",
"ru",
"by",
"ua",
"kz",
)
); );
.fir { .fir {

View File

@@ -1,3 +0,0 @@
@use "./weather-icons/sass/weather-icons" with (
$weather-icons-path: "weather-icons/icons"
);

View File

@@ -0,0 +1,22 @@
from pathlib import Path
from string import Template
LIST_ITEM = Template(
""".$name {
mask-image: url(./svg/$name.svg);
}
"""
)
def generate():
work_dir = Path(__file__).parent
data = ""
for item in (work_dir / "svg").glob("*.svg"):
data += LIST_ITEM.substitute({"name": item.stem})
target = work_dir / "classes-list.scss"
target.write_text(data)
if __name__ == "__main__":
generate()

View File

@@ -0,0 +1,93 @@
.wi-day-snow {
mask-image: url(./svg/wi-day-snow.svg);
}
.wi-rain-mix {
mask-image: url(./svg/wi-rain-mix.svg);
}
.wi-thunderstorm {
mask-image: url(./svg/wi-thunderstorm.svg);
}
.wi-hail {
mask-image: url(./svg/wi-hail.svg);
}
.wi-day-rain {
mask-image: url(./svg/wi-day-rain.svg);
}
.wi-cloudy {
mask-image: url(./svg/wi-cloudy.svg);
}
.wi-day-hail {
mask-image: url(./svg/wi-day-hail.svg);
}
.wi-night-clear {
mask-image: url(./svg/wi-night-clear.svg);
}
.wi-day-cloudy {
mask-image: url(./svg/wi-day-cloudy.svg);
}
.wi-day-storm-showers {
mask-image: url(./svg/wi-day-storm-showers.svg);
}
.wi-lightning {
mask-image: url(./svg/wi-lightning.svg);
}
.wi-day-rain-mix {
mask-image: url(./svg/wi-day-rain-mix.svg);
}
.wi-day-showers {
mask-image: url(./svg/wi-day-showers.svg);
}
.wi-storm-showers {
mask-image: url(./svg/wi-storm-showers.svg);
}
.wi-snow {
mask-image: url(./svg/wi-snow.svg);
}
.wi-cloud {
mask-image: url(./svg/wi-cloud.svg);
}
.wi-night-alt-snow {
mask-image: url(./svg/wi-night-alt-snow.svg);
}
.wi-night-alt-lightning {
mask-image: url(./svg/wi-night-alt-lightning.svg);
}
.wi-day-sunny {
mask-image: url(./svg/wi-day-sunny.svg);
}
.wi-night-alt-hail {
mask-image: url(./svg/wi-night-alt-hail.svg);
}
.wi-night-alt-cloudy {
mask-image: url(./svg/wi-night-alt-cloudy.svg);
}
.wi-night-alt-storm-showers {
mask-image: url(./svg/wi-night-alt-storm-showers.svg);
}
.wi-day-thunderstorm {
mask-image: url(./svg/wi-day-thunderstorm.svg);
}
.wi-wind-deg {
mask-image: url(./svg/wi-wind-deg.svg);
}
.wi-showers {
mask-image: url(./svg/wi-showers.svg);
}
.wi-night-alt-rain-mix {
mask-image: url(./svg/wi-night-alt-rain-mix.svg);
}
.wi-rain {
mask-image: url(./svg/wi-rain.svg);
}
.wi-night-alt-thunderstorm {
mask-image: url(./svg/wi-night-alt-thunderstorm.svg);
}
.wi-night-alt-showers {
mask-image: url(./svg/wi-night-alt-showers.svg);
}
.wi-day-lightning {
mask-image: url(./svg/wi-day-lightning.svg);
}
.wi-night-alt-rain {
mask-image: url(./svg/wi-night-alt-rain.svg);
}

View File

@@ -1,34 +0,0 @@
$weather-icons-path: "../icons" !default;
$weather-icons-included: (
"wi-cloud",
"wi-cloudy",
"wi-day-cloudy",
"wi-day-hail",
"wi-day-lightning",
"wi-day-rain-mix",
"wi-day-rain",
"wi-day-showers",
"wi-day-snow",
"wi-day-storm-showers",
"wi-day-sunny",
"wi-day-thunderstorm",
"wi-hail",
"wi-lightning",
"wi-night-alt-cloudy",
"wi-night-alt-hail",
"wi-night-alt-lightning",
"wi-night-alt-rain-mix",
"wi-night-alt-rain",
"wi-night-alt-showers",
"wi-night-alt-snow",
"wi-night-alt-storm-showers",
"wi-night-alt-thunderstorm",
"wi-night-clear",
"wi-rain-mix",
"wi-rain",
"wi-showers",
"wi-snow",
"wi-storm-showers",
"wi-thunderstorm",
"wi-wind-deg"
) !default;

View File

@@ -1,6 +0,0 @@
@use "variables";
@use "weather-icons-base";
@each $icon in variables.$weather-icons-included {
@include weather-icons-base.weather-icon($icon);
}

View File

@@ -1,4 +0,0 @@
@forward "variables";
@use "weather-icons-base";
@use "weather-icons-list";
@use "weather-wind-aliases";

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 806 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,4 +1,5 @@
@use "variables"; @import "./classes-list";
@import "./classes-wind-aliases";
.wi { .wi {
display: inline-block; display: inline-block;
@@ -20,9 +21,3 @@
font-size: 3.5rem; font-size: 3.5rem;
} }
} }
@mixin weather-icon($icon) {
.#{$icon} {
mask-image: url(#{variables.$weather-icons-path}/#{$icon}.svg);
}
}

View File

@@ -1,9 +1,40 @@
@import "./lib/flag-icons"; @import "./lib/flag-icons";
@import "./lib/bootstrap"; @import "./lib/bootstrap";
@import "./lib/bootstrap-icons"; @import "./lib/bootstrap-icons";
@import "./lib/weather-icons"; @import "./lib/weather-icons/weather-icons";
@import "./styles/weather.scss"; @import "./widget.scss";
@import "./styles/app"; @import "./weather/weather.scss";
@import "./styles/widget.scss";
@import "./styles/mobile"; .table.table-compact {
td {
padding: 0.1rem 0.4rem;
}
}
.icon-header {
@include font-size($h4-font-size);
}
.app-header {
display: flex;
flex-direction: row;
> .link-list {
display: flex;
flex-direction: row;
gap: 1rem;
app-link {
display: flex;
align-items: center;
&:not(:last-child)::after {
margin-left: 1rem;
content: "|";
}
}
}
}
@import "./mobile";

View File

@@ -1,4 +1,4 @@
@import "../lib/bootstrap"; @import "./lib/bootstrap";
$default-space: 2; $default-space: 2;

View File

@@ -108,3 +108,4 @@ declare global {
window.scheduleChannelStorage = new ScheduleChannelStorage(); window.scheduleChannelStorage = new ScheduleChannelStorage();
window.scheduleChannelManager = new ScheduleChannelManager(); window.scheduleChannelManager = new ScheduleChannelManager();
console.log("ok");

View File

@@ -1,32 +0,0 @@
@import "../lib/bootstrap";
.table.table-compact {
td {
padding: 0.1rem 0.4rem;
}
}
.icon-header {
@include font-size($h4-font-size);
}
.app-header {
display: flex;
flex-direction: row;
> .link-list {
display: flex;
flex-direction: row;
gap: 1rem;
app-link {
display: flex;
align-items: center;
&:not(:last-child)::after {
margin-left: 1rem;
content: "|";
}
}
}
}

View File

@@ -25,7 +25,7 @@
setTheme(getPreferredTheme()); setTheme(getPreferredTheme());
const showActiveTheme = (theme: string, focus = false) => { const showActiveTheme = (theme: string, focus = false) => {
const themeSwitcher = document.querySelector<HTMLElement>("#bd-theme"); const themeSwitcher = document.querySelector("#bd-theme");
if (!themeSwitcher) { if (!themeSwitcher) {
return; return;
@@ -33,26 +33,24 @@
const themeSwitcherText = document.querySelector("#bd-theme-text"); const themeSwitcherText = document.querySelector("#bd-theme-text");
const activeThemeIcon = document.querySelector(".theme-icon-active"); const activeThemeIcon = document.querySelector(".theme-icon-active");
const btnToActive = document.querySelector<HTMLElement>(`[data-bs-theme-value="${theme}"]`); const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
const activeThemeIconClass = (btnToActive?.querySelector(".bi")?.className.match(/bi-[\w-]+/) || [])[0] || ""; const activeThemeIconClass = btnToActive.querySelector(".bi").className.match(/bi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => { document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
element.classList.remove("active"); element.classList.remove("active");
element.setAttribute("aria-pressed", "false"); element.setAttribute("aria-pressed", "false");
}); });
btnToActive?.classList.add("active"); btnToActive.classList.add("active");
btnToActive?.setAttribute("aria-pressed", "true"); btnToActive.setAttribute("aria-pressed", "true");
const classesToRemove = Array.from(activeThemeIcon?.classList || []).filter((className) => const classesToRemove = Array.from(activeThemeIcon.classList).filter((className) => className.startsWith("bi-"));
className.startsWith("bi-"), activeThemeIcon.classList.remove(...classesToRemove);
); activeThemeIcon.classList.add(activeThemeIconClass);
activeThemeIcon?.classList.remove(...classesToRemove); const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
activeThemeIcon?.classList.add(activeThemeIconClass);
const themeSwitcherLabel = `${themeSwitcherText?.textContent} (${btnToActive?.dataset.bsThemeValue})`;
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel); themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
if (focus) { if (focus) {
themeSwitcher?.focus(); themeSwitcher.focus();
} }
}; };
@@ -68,7 +66,7 @@
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => { document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
toggle.addEventListener("click", () => { toggle.addEventListener("click", () => {
const theme = toggle.getAttribute("data-bs-theme-value") || ""; const theme = toggle.getAttribute("data-bs-theme-value") || '';
setStoredTheme(theme); setStoredTheme(theme);
setTheme(theme); setTheme(theme);
showActiveTheme(theme, true); showActiveTheme(theme, true);

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/",
"noImplicitAny": true,
"module": "esnext",
"moduleResolution": "bundler",
"target": "esnext",
"jsx": "react-jsx",
"allowJs": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

14
static/vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import packageJson from "./package.json";
export default defineConfig({
base: "./",
build: {
outDir: "./dist",
lib: {
entry: "./src/index.ts",
name: packageJson.name,
fileName: (format) => `${packageJson.name}.${format}.js`,
},
},
});

View File

@@ -1,77 +0,0 @@
import CopyPlugin from "copy-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import path from "node:path";
import { fileURLToPath } from "url";
import webpack from "webpack";
import packageInfo from "./package.json" with { type: "json" };
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const config: webpack.Configuration = {
entry: ["./src/index.ts", "./src/main.scss"],
module: {
rules: [
{ test: /\.svg$/, use: ["file-loader"] },
{
test: /\.tsx?$/,
use: {
loader: "ts-loader",
},
exclude: /node_modules/,
},
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
{ loader: "css-loader", options: { sourceMap: true, url: false } },
{
loader: "sass-loader",
options: {
sourceMap: true,
sassOptions: { quietDeps: true, silenceDeprecations: ["color-functions", "global-builtin", "import"] },
},
},
],
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: `${packageInfo.name}.js`,
path: path.resolve(__dirname, "dist"),
clean: true,
},
plugins: [
new MiniCssExtractPlugin({ filename: `${packageInfo.name}.css` }),
new CopyPlugin({
patterns: [
{
from: "./src/lib/weather-icons/icons/*.svg",
to: "weather-icons/icons/[name][ext]",
},
{
from: "./node_modules/bootstrap-icons/icons/*.svg",
to: "bootstrap-icons/icons/[name][ext]",
},
{
from: "./node_modules/flag-icons/flags/1x1/*.svg",
to: "flag-icons/flags/1x1/[name][ext]",
},
{
from: "./node_modules/flag-icons/flags/4x3/*.svg",
to: "flag-icons/flags/4x3/[name][ext]",
},
],
}),
],
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
};
export default config;