diff --git a/README.md b/README.md
index d619f58..a23d9c4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Remnawave Minishop
+
+
Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи и управления подписками Remnawave. Бот обрабатывает регистрацию, оплату, продление, пробный период, промокоды, рефералов и поддержку в чате. Web App показывает ссылку подключения, срок действия, трафик, оплату, устройства и вход по Telegram Mini Apps `initData`, Telegram OAuth / OpenID Connect и одноразовому email-коду.
Проект является переработанным форком [kavore/remnawave-tg-shop](https://github.com/kavore/remnawave-tg-shop). Для переноса данных из прежнего стека используйте [инструкцию по миграции](docs/migration-to-minishop.md).
diff --git a/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte b/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte
index 758592e..b5b7dab 100644
--- a/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte
+++ b/bot/app/web/frontend/src/admin/sections/AppearanceSection.svelte
@@ -134,6 +134,12 @@
return `#${hex}`;
}
+ function homeLogoScale(theme) {
+ const scale = Number(theme.tokens?.home_logo_scale || 100);
+ if (!Number.isFinite(scale)) return 100;
+ return Math.min(300, Math.max(50, Math.round(scale)));
+ }
+
function openThemeAccentPicker(theme) {
themesStore.setThemeAccent(theme.key, pickerHex(theme.tokens?.accent || "#00fe7a"));
}
@@ -216,6 +222,10 @@
themesStore.setThemeAccent(theme.key, value);
}
+ function setThemeHomeLogoScale(theme, value) {
+ themesStore.setThemeHomeLogoScale(theme.key, value);
+ }
+
function selectTheme(theme, event = null) {
if (event?.target?.closest?.("button,input,label")) return;
if (!themesSaving) themesStore.setCurrentTheme(theme.key);
@@ -472,6 +482,37 @@
/>
{at("themes_use_in_admin", {}, "Использовать в админке")}
+
({
+ ...s,
+ themesCatalog: {
+ ...s.themesCatalog,
+ themes: (s.themesCatalog.themes || []).map((theme) =>
+ theme.key === key
+ ? {
+ ...theme,
+ tokens: {
+ ...(theme.tokens || {}),
+ home_logo_scale: nextScale === 100 ? null : nextScale,
+ },
+ }
+ : theme
+ ),
+ },
+ }));
+ }
+
return {
subscribe: state.subscribe,
loadThemes,
saveThemes,
setCurrentTheme,
setThemeAccent,
+ setThemeHomeLogoScale,
togglePrimaryAccent,
toggleAdminUse,
uploadLogoFile,
diff --git a/bot/app/web/frontend/src/lib/webapp/themeStyle.js b/bot/app/web/frontend/src/lib/webapp/themeStyle.js
index e312c37..c415cd0 100644
--- a/bot/app/web/frontend/src/lib/webapp/themeStyle.js
+++ b/bot/app/web/frontend/src/lib/webapp/themeStyle.js
@@ -17,6 +17,7 @@ const TOKEN_TO_CSS_VAR = {
font_sans: "--font-sans",
font_logo: "--font-logo",
font_mono: "--font-mono",
+ home_logo_scale: "--home-logo-scale",
admin_bg: "--admin-bg",
admin_surface: "--admin-surface",
admin_surface_2: "--admin-surface-2",
@@ -38,6 +39,12 @@ export function themeTokensToInlineStyle(tokens, primaryFallback = "#00fe7a", op
if (key === "accent") continue;
const value = t[key];
if (value === undefined || value === null || value === "") continue;
+ if (key === "home_logo_scale") {
+ const scale = Number(value);
+ if (!Number.isFinite(scale) || scale <= 0) continue;
+ parts.push(`${cssVar}:${scale / 100}`);
+ continue;
+ }
parts.push(`${cssVar}:${String(value)}`);
}
return parts.join(";");
diff --git a/bot/app/web/frontend/src/styles/webapp.css b/bot/app/web/frontend/src/styles/webapp.css
index 18c1416..9b7d6dd 100644
--- a/bot/app/web/frontend/src/styles/webapp.css
+++ b/bot/app/web/frontend/src/styles/webapp.css
@@ -30,12 +30,18 @@ a {
display: grid;
min-height: 100dvh;
place-items: center;
- align-content: center;
+ align-content: safe center;
gap: 14px;
color: var(--muted);
font-weight: 800;
}
+.loader .brand-mark.brand-mark-lg {
+ width: calc(4.125rem * var(--home-logo-scale, 1));
+ height: calc(4.125rem * var(--home-logo-scale, 1));
+ font-size: calc(2.875rem * var(--home-logo-scale, 1));
+}
+
.app-header,
.preview-header,
.screen-head {
@@ -1530,17 +1536,29 @@ a {
.home-layout {
display: grid;
min-height: calc(100dvh - 34px);
- grid-template-rows: minmax(0, 1fr) auto;
+ grid-template-rows: minmax(min-content, 1fr) auto;
gap: 18px;
padding-bottom: 86px;
animation: section-enter 0.24s ease-out both;
}
.home-brand {
- align-self: center;
+ align-self: safe center;
padding-top: 0;
}
+.home-brand .brand-mark.brand-mark-xl {
+ width: calc(6rem * var(--home-logo-scale, 1));
+ height: calc(6rem * var(--home-logo-scale, 1));
+ font-size: calc(4.375rem * var(--home-logo-scale, 1));
+}
+
+.login-brand-auth .brand-mark.brand-mark-xl {
+ width: calc(116px * var(--home-logo-scale, 1));
+ height: calc(116px * var(--home-logo-scale, 1));
+ font-size: calc(84px * var(--home-logo-scale, 1));
+}
+
.home-bottom {
display: grid;
gap: 10px;
@@ -1549,7 +1567,7 @@ a {
.auth-screen {
display: grid;
- align-content: center;
+ align-content: safe center;
}
.auth-card-wrap {
@@ -1568,12 +1586,6 @@ a {
gap: 5px;
}
-.login-brand-auth .brand-mark.brand-mark-xl {
- width: 116px;
- height: 116px;
- font-size: 84px;
-}
-
.login-brand h1 {
margin: 0;
overflow-wrap: anywhere;
@@ -2129,9 +2141,9 @@ a {
.home-brand .brand-mark,
.home-brand .brand-mark-lg,
.home-brand .brand-mark-xl {
- width: clamp(180px, 18vw, 260px);
- height: clamp(180px, 18vw, 260px);
- font-size: clamp(124px, 13vw, 184px);
+ width: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale, 1));
+ height: calc(clamp(180px, 18vw, 260px) * var(--home-logo-scale, 1));
+ font-size: calc(clamp(124px, 13vw, 184px) * var(--home-logo-scale, 1));
}
.home-bottom {
diff --git a/bot/app/web/templates/subscription_webapp.html b/bot/app/web/templates/subscription_webapp.html
index cf213f6..492c5c8 100644
--- a/bot/app/web/templates/subscription_webapp.html
+++ b/bot/app/web/templates/subscription_webapp.html
@@ -9,7 +9,7 @@
/minishop
-
+
diff --git a/bot/app/web/themes/windows95/style.css b/bot/app/web/themes/windows95/style.css
index 68a5709..bdbdccc 100644
--- a/bot/app/web/themes/windows95/style.css
+++ b/bot/app/web/themes/windows95/style.css
@@ -1,3 +1,5 @@
+@import url("https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap&subset=cyrillic");
+
.theme-key-windows95 {
color-scheme: light;
image-rendering: pixelated;
@@ -15,7 +17,7 @@
--blue: #000080;
--radius: 0px;
--font-sans: "MS Sans Serif", Tahoma, "Segoe UI", sans-serif;
- --font-logo: "MS Sans Serif", Tahoma, "Segoe UI", sans-serif;
+ --font-logo: "Press Start 2P", "Pixel Cyr", "MS Sans Serif", Tahoma, "Segoe UI", sans-serif;
--font-mono: Consolas, "Courier New", monospace;
--accent-contrast: #ffffff;
--surface-sheen: transparent;
@@ -89,6 +91,8 @@
.theme-key-windows95 svg.lucide-bitcoin,
.theme-key-windows95 svg.lucide-check,
.theme-key-windows95 svg.lucide-check-circle-2,
+.theme-key-windows95 svg.lucide-circle-check,
+.theme-key-windows95 svg.lucide-circle-check-big,
.theme-key-windows95 svg.lucide-chevrons-up-down,
.theme-key-windows95 svg.lucide-circle-x,
.theme-key-windows95 svg.lucide-coins,
@@ -142,6 +146,7 @@
background-repeat: no-repeat;
background-size: contain;
image-rendering: pixelated;
+ filter: none !important;
}
.theme-key-windows95 svg.lucide-arrow-left {
@@ -547,6 +552,10 @@
color: var(--accent);
}
+.theme-key-windows95 .traffic-top strong {
+ color: var(--muted);
+}
+
.theme-key-windows95 .btn-primary,
.theme-key-windows95 .bottom-nav button.active {
color: #ffffff;
diff --git a/bot/app/web/webapp/assets.py b/bot/app/web/webapp/assets.py
index 198ba08..c0916fd 100644
--- a/bot/app/web/webapp/assets.py
+++ b/bot/app/web/webapp/assets.py
@@ -857,12 +857,12 @@ async def index_route(request: web.Request) -> web.Response:
brand_asset_url = _webapp_animated_emoji_asset_path(settings.WEBAPP_LOGO_EMOJI)
if brand_asset_url:
html = html.replace(
- '', # noqa: E501
- f'', # noqa: E501
+ '',
+ f'',
)
else:
html = html.replace(
- '', # noqa: E501
+ '',
"",
)
return web.Response(text=html, content_type="text/html", charset="utf-8")
diff --git a/config/webapp_themes_config.py b/config/webapp_themes_config.py
index 4f713bc..495cdeb 100644
--- a/config/webapp_themes_config.py
+++ b/config/webapp_themes_config.py
@@ -38,6 +38,7 @@ class ThemeTokens(BaseModel):
font_sans: Optional[str] = None
font_logo: Optional[str] = None
font_mono: Optional[str] = None
+ home_logo_scale: Optional[int] = None
admin_bg: Optional[str] = None
admin_surface: Optional[str] = None
admin_surface_2: Optional[str] = None
@@ -64,6 +65,16 @@ class ThemeTokens(BaseModel):
hex_value = "".join(char * 2 for char in hex_value)
return f"#{hex_value}"
+ @field_validator("home_logo_scale")
+ @classmethod
+ def _normalize_home_logo_scale(cls, value: Optional[int]) -> Optional[int]:
+ if value is None:
+ return None
+ scale = int(value)
+ if scale < 50 or scale > 300:
+ raise ValueError("home_logo_scale must be between 50 and 300 percent")
+ return scale
+
class WebappTheme(BaseModel):
"""Single theme descriptor loaded from WEBAPP_THEMES_DIR//theme.json."""
diff --git a/tests/test_webapp_themes_config.py b/tests/test_webapp_themes_config.py
index f8a9d43..1cb81fe 100644
--- a/tests/test_webapp_themes_config.py
+++ b/tests/test_webapp_themes_config.py
@@ -33,7 +33,7 @@ class WebappThemesConfigTests(unittest.TestCase):
self.assertEqual(win95.tokens.style_preset, "win95")
self.assertFalse(win95.use_primary_accent)
self.assertTrue(win95.use_in_admin)
- self.assertEqual(win95.assets_version, 1)
+ self.assertEqual(win95.assets_version, 3)
ascii_theme = cfg.theme_by_key("ascii")
self.assertIsNotNone(ascii_theme)
self.assertEqual(ascii_theme.css_file, "style.css")
@@ -294,6 +294,25 @@ class WebappThemesConfigTests(unittest.TestCase):
self.assertEqual(cfg.theme_by_key("custom").tokens.accent, "#00ff88")
+ def test_theme_home_logo_scale_is_public_token(self):
+ cfg = WebappThemesConfig(
+ default_theme="custom",
+ themes=[
+ {
+ "key": "custom",
+ "enabled": True,
+ "default": True,
+ "tokens": {"color_scheme": "dark", "home_logo_scale": 135},
+ }
+ ],
+ )
+
+ payload = public_themes_catalog_payload(cfg, "#abc123")
+ custom = payload["themes"][0]
+
+ self.assertEqual(cfg.theme_by_key("custom").tokens.home_logo_scale, 135)
+ self.assertEqual(custom["tokens"]["home_logo_scale"], 135)
+
def test_theme_accent_rejects_non_hex_values(self):
with self.assertRaises(ValueError):
WebappThemesConfig(
@@ -361,11 +380,14 @@ class WebappThemesConfigTests(unittest.TestCase):
descriptor["assets_version"],
cfg.theme_by_key("windows95").assets_version,
)
- self.assertEqual(descriptor["assets_version"], 1)
+ self.assertEqual(descriptor["assets_version"], 3)
self.assertIn("lucide-house", css)
self.assertIn("lucide-earth", css)
self.assertIn("lucide-circle-check", css)
self.assertIn("lucide-circle-check-big", css)
+ self.assertIn("filter: none !important", css)
+ self.assertIn("Press Start 2P", css)
+ self.assertIn(".theme-key-windows95 .traffic-top strong", css)
self.assertTrue((stale_theme_dir / "icons" / "dashboard.png").exists())
def test_resolved_refreshes_stale_builtin_light_assets(self):