diff --git a/.gitignore b/.gitignore index 04676b9..d57223c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,6 @@ __pycache__/ locales/ru_backup.json locales/en_backup.json db/models_old.py -data/tariffs.json +data/* +!data/tariffs.example.json docker-compose-dev.yml diff --git a/bot/app/web/admin_settings_manifest.py b/bot/app/web/admin_settings_manifest.py index d21f637..1b4b578 100644 --- a/bot/app/web/admin_settings_manifest.py +++ b/bot/app/web/admin_settings_manifest.py @@ -81,6 +81,24 @@ SETTINGS_MANIFEST: List[SettingField] = [ ), SettingField("WEBAPP_LOGO_URL", "url", "appearance", "URL логотипа"), SettingField("WEBAPP_LOGO_EMOJI", "string", "appearance", "Эмоджи-логотип", placeholder="🫥"), + SettingField( + "WEBAPP_LOGO_EMOJI_FONT", + "string", + "appearance", + "Шрифт эмоджи-логотипа", + "Выберите шрифт для отображения эмодзи-логотипа", + choices=( + ("system", "Системный (по умолчанию)"), + ("noto-color", "Noto Color Emoji"), + ("noto-color-animated", "Noto Color Emoji Animated"), + ("noto-emoji", "Noto Emoji"), + ("twemoji", "Twitter Emoji"), + ("openmoji", "OpenMoji"), + ("apple", "Apple Color Emoji (local)"), + ("segoe", "Segoe UI Emoji (local)"), + ("noto-local", "Noto Emoji (local)"), + ), + ), SettingField("WEBAPP_ENABLED", "bool", "appearance", "Web App включён"), # ─── Subscription periods & pricing ──────────────────────────── SettingField("MONTH_1_ENABLED", "bool", "pricing", "Тариф 1 месяц"), @@ -488,21 +506,22 @@ def manifest_payload() -> List[dict]: for field in SETTINGS_MANIFEST: auto_label_i18n_key = f"settings_field_{field.key.lower()}_label" auto_description_i18n_key = f"settings_field_{field.key.lower()}_description" - items.append( - { - "key": field.key, - "type": field.type, - "section": field.section, - "section_order": sections_order.get(field.section, 99), - "subsection": field.subsection, - "label": field.label, - "description": field.description, - "i18n_label_key": field.i18n_label_key or auto_label_i18n_key, - "i18n_description_key": field.i18n_description_key - or (auto_description_i18n_key if field.description else None), - "placeholder": field.placeholder, - "optional": field.optional, - "secret": field.secret, - } - ) + item = { + "key": field.key, + "type": field.type, + "section": field.section, + "section_order": sections_order.get(field.section, 99), + "subsection": field.subsection, + "label": field.label, + "description": field.description, + "i18n_label_key": field.i18n_label_key or auto_label_i18n_key, + "i18n_description_key": field.i18n_description_key + or (auto_description_i18n_key if field.description else None), + "placeholder": field.placeholder, + "optional": field.optional, + "secret": field.secret, + } + if field.choices: + item["choices"] = [{"value": v, "label": lbl} for v, lbl in field.choices] + items.append(item) return items diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte index 85cc505..99d8657 100644 --- a/bot/app/web/frontend/src/App.svelte +++ b/bot/app/web/frontend/src/App.svelte @@ -6,7 +6,7 @@ import { createAccountStore } from "./lib/webapp/stores/accountStore.js"; import { Tooltip } from "$components/ui/primitives.js"; - import BrandMark from "./BrandMark.svelte"; + import BrandMark from "$lib/webapp/BrandMark.svelte"; import PreviewBoard from "./PreviewBoard.svelte"; import AdminPanel from "./admin/AdminPanel.svelte"; import WebAppShell from "./webapp/WebAppShell.svelte"; @@ -29,7 +29,12 @@ WEBAPP_LANGUAGE_ORDER, } from "./lib/webapp/constants.js"; - import { applyFavicon, readJsonScript, structuredCloneSafe } from "./lib/webapp/browser.js"; + import { + applyFavicon, + normalizeBrand, + readJsonScript, + structuredCloneSafe, + } from "./lib/webapp/browser.js"; import { createApiClient } from "./lib/webapp/publicApi.js"; import { createI18n } from "./lib/webapp/i18n.js"; import { normalizedEmail, telegramName } from "./lib/webapp/formatters.js"; @@ -241,6 +246,13 @@ $: brandTitle = CFG.title || "/minishop"; $: brandEmoji = CFG.logoEmoji || "🫥"; + $: brandEmojiFont = CFG.logoEmojiFont || "system"; + $: brand = normalizeBrand({ + title: brandTitle, + logoUrl: CFG.logoUrl, + emoji: brandEmoji, + emojiFont: brandEmojiFont, + }); $: accent = CFG.primaryColor || "#00fe7a"; $: plans = data?.plans?.length ? data.plans : DEV_MOCK.data.plans; $: methods = data?.payment_methods?.length ? data.payment_methods : []; @@ -344,7 +356,7 @@ : telegramLoginUnavailable ? t("wa_auth_telegram_not_configured") : ""; - $: applyFavicon(CFG.logoUrl, brandEmoji); + $: applyFavicon(brand); $: syncBodyScrollLock( paymentModalOpen || changeModalOpen || @@ -903,7 +915,7 @@
{#if mode === "loading"}
- +
{t("wa_loading")}
{:else if mode === "login"} @@ -911,7 +923,7 @@ {screen} {CFG} {brandTitle} - {brandEmoji} + {brand} bind:email={$authStore.email} bind:emailCode={$authStore.emailCode} {pendingEmail} @@ -952,8 +964,7 @@ onSettingsSaved={handleAdminPersistedSaved} onTariffsSaved={handleAdminPersistedSaved} {brandTitle} - logoUrl={CFG.logoUrl} - logoEmoji={brandEmoji} + {brand} appVersion={CFG.appVersion} appRepositoryUrl={CFG.appRepositoryUrl} {currentLang} @@ -966,9 +977,8 @@ {#if screen === "home"} - import { onDestroy } from "svelte"; - - import { cn } from "./lib/utils.js"; - - const LOGO_LOAD_TIMEOUT_MS = 2600; - - export let logoUrl = ""; - export let emoji = "🫥"; - let className = ""; - export { className as class }; - - let loaded = false; - let failed = false; - let lastLogoUrl = ""; - let logoLoadTimer = null; - let logoLoadTimerUrl = ""; - - $: normalizedLogoUrl = String(logoUrl || "").trim(); - $: normalizedEmoji = String(emoji || "🫥").trim() || "🫥"; - $: if (normalizedLogoUrl !== lastLogoUrl) { - lastLogoUrl = normalizedLogoUrl; - loaded = false; - failed = false; - } - $: if (normalizedLogoUrl && !loaded && !failed) armLogoLoadTimeout(); - $: if (!normalizedLogoUrl || loaded || failed) clearLogoLoadTimeout(); - - onDestroy(clearLogoLoadTimeout); - - function clearLogoLoadTimeout() { - if (logoLoadTimer) { - window.clearTimeout(logoLoadTimer); - logoLoadTimer = null; - } - logoLoadTimerUrl = ""; - } - - function armLogoLoadTimeout() { - if (typeof window === "undefined") return; - if (logoLoadTimer && logoLoadTimerUrl === normalizedLogoUrl) return; - clearLogoLoadTimeout(); - logoLoadTimerUrl = normalizedLogoUrl; - logoLoadTimer = window.setTimeout(() => { - if (logoLoadTimerUrl === normalizedLogoUrl && !loaded) failed = true; - }, LOGO_LOAD_TIMEOUT_MS); - } - - -
- {#if normalizedLogoUrl && !failed} - {#if !loaded} - - {/if} - (loaded = true)} - on:error={() => (failed = true)} - /> - {:else} - {normalizedEmoji} - {/if} -
diff --git a/bot/app/web/frontend/src/admin/AdminPanel.svelte b/bot/app/web/frontend/src/admin/AdminPanel.svelte index b7e64ac..698f205 100644 --- a/bot/app/web/frontend/src/admin/AdminPanel.svelte +++ b/bot/app/web/frontend/src/admin/AdminPanel.svelte @@ -24,7 +24,7 @@ import { Select } from "$components/ui/primitives.js"; import { AdminBadge, AdminButton } from "$components/patterns/admin/index.js"; - import BrandMark from "../BrandMark.svelte"; + import BrandMark from "$lib/webapp/BrandMark.svelte"; import AdsSection from "./sections/AdsSection.svelte"; import BroadcastSection from "./sections/BroadcastSection.svelte"; import LogsSection from "./sections/LogsSection.svelte"; @@ -71,9 +71,8 @@ export let onSectionChange = () => {}; export let onSettingsSaved = () => {}; export let onTariffsSaved = () => {}; + export let brand = {}; export let brandTitle = "/minishop"; - export let logoUrl = ""; - export let logoEmoji = "🫥"; export let appVersion = "dev+local"; export let appRepositoryUrl = "https://github.com/3252a8/remnawave-minishop"; export let currentLang = "ru"; @@ -180,8 +179,7 @@ function readReduceMotion() { return ( - typeof window !== "undefined" && - window.matchMedia("(prefers-reduced-motion: reduce)").matches + typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches ); } @@ -423,7 +421,7 @@