refactor(webapp): remove emoji web app logo option
Drop the emoji-logo feature (and its font picker) from the Web App. Only an uploaded/linked image logo and favicon remain; when no logo is set, the default project logo is shown. Existing emoji-logo overrides are ignored — the keys are gone from the manifest, so the override service skips them and the app falls back to the default logo. - Remove WEBAPP_LOGO_USE_EMOJI / WEBAPP_LOGO_EMOJI / WEBAPP_LOGO_EMOJI_FONT settings, validators, manifest entries and override/runtime plumbing. - Strip the animated-emoji fetch/cache subsystem, the /webapp-emoji route and emoji branches from logo/favicon resolution; leftover emoji cache files are now purged on appearance save. - Simplify BrandMark to an image-only component and drop the emoji UI from the admin Appearance section. - Regenerate the demo settings manifest and clean docs, locales, nginx and demo data of emoji-logo references.
This commit is contained in:
@@ -98,10 +98,10 @@ docker compose logs -f backend worker frontend
|
||||
|
||||
Для каталога тарифов используется `TARIFFS_CONFIG_PATH` со значением по умолчанию `data/tariffs.json`. Пример формата лежит в [data/tariffs.example.json](data/tariffs.example.json), подробности - в [docs/features/tariffs.md](docs/features/tariffs.md).
|
||||
|
||||
В compose-примерах `/app/data` монтируется из папки `./data` рядом с `docker-compose.yml`. Заранее создайте каталог и отдайте его пользователю контейнера. Это нужно для сохранения `data/tariffs.json`, каталога тем `data/themes`, кеша логотипа Web App и animated emoji:
|
||||
В compose-примерах `/app/data` монтируется из папки `./data` рядом с `docker-compose.yml`. Заранее создайте каталог и отдайте его пользователю контейнера. Это нужно для сохранения `data/tariffs.json`, каталога тем `data/themes` и кеша логотипа Web App:
|
||||
|
||||
```bash
|
||||
mkdir -p data/themes data/webapp-logo data/webapp-emoji
|
||||
mkdir -p data/themes data/webapp-logo
|
||||
touch data/locales-overrides.json
|
||||
chown -R 10001:10001 data
|
||||
chmod -R u+rwX data
|
||||
|
||||
@@ -136,10 +136,6 @@ def _favicon_digest(url: str) -> Optional[str]:
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def _emoji_to_codepoints(value: str) -> str:
|
||||
return "_".join(f"{ord(char):x}" for char in str(value or "").strip())
|
||||
|
||||
|
||||
def prune_unused_appearance_assets(settings: Settings) -> None:
|
||||
keep_logos = {
|
||||
filename
|
||||
@@ -156,15 +152,6 @@ def prune_unused_appearance_assets(settings: Settings) -> None:
|
||||
]
|
||||
if digest
|
||||
}
|
||||
keep_emoji_prefixes = set()
|
||||
if (
|
||||
getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False)
|
||||
and str(getattr(settings, "WEBAPP_LOGO_EMOJI_FONT", "") or "").strip()
|
||||
== "noto-color-animated"
|
||||
):
|
||||
codepoints = _emoji_to_codepoints(getattr(settings, "WEBAPP_LOGO_EMOJI", ""))
|
||||
if codepoints:
|
||||
keep_emoji_prefixes.add(f"{codepoints}.512.")
|
||||
|
||||
for path in WEBAPP_UPLOADED_LOGO_DIR.glob("logo-*"):
|
||||
if path.is_file() and path.name not in keep_logos:
|
||||
@@ -184,10 +171,9 @@ def prune_unused_appearance_assets(settings: Settings) -> None:
|
||||
except OSError:
|
||||
logger.warning("Failed to remove unused webapp favicon set %s", path, exc_info=True)
|
||||
|
||||
# Emoji logos were removed; purge any leftover animated-emoji cache files.
|
||||
for path in WEBAPP_EMOJI_CACHE_DIR.glob("*.512.*"):
|
||||
if path.is_file() and not any(
|
||||
path.name.startswith(prefix) for prefix in keep_emoji_prefixes
|
||||
):
|
||||
if path.is_file():
|
||||
try:
|
||||
path.unlink()
|
||||
except OSError:
|
||||
@@ -389,7 +375,6 @@ async def admin_appearance_logo_upload_route(request: web.Request) -> web.Respon
|
||||
request,
|
||||
{
|
||||
"WEBAPP_LOGO_URL": logo_url,
|
||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
||||
**(
|
||||
{"WEBAPP_LOGO_FAVICON_URL": favicon_payload["favicon_url"]}
|
||||
if favicon_payload.get("favicon_url")
|
||||
|
||||
@@ -13,9 +13,6 @@ WEBAPP_APPEARANCE_SETTING_KEYS = frozenset(
|
||||
{
|
||||
"WEBAPP_TITLE",
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
|
||||
@@ -181,27 +181,7 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
SettingField(
|
||||
"WEBAPP_PRIMARY_COLOR", "color", "appearance", "Основной цвет", placeholder="#00fe7a"
|
||||
),
|
||||
SettingField("WEBAPP_LOGO_USE_EMOJI", "bool", "appearance", "Использовать эмоджи-логотип"),
|
||||
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_FAVICON_USE_CUSTOM",
|
||||
"bool",
|
||||
|
||||
@@ -82,7 +82,6 @@ WEBAPP_DEFAULT_LOGO_PATH = "/webapp-default-logo.webp"
|
||||
WEBAPP_DEFAULT_FAVICON_DIGEST = "19b2a242e5b7bc2d"
|
||||
WEBAPP_DEFAULT_FAVICON_DIR = WEBAPP_DEFAULT_BRAND_DIR / "favicons" / WEBAPP_DEFAULT_FAVICON_DIGEST
|
||||
WEBAPP_DEFAULT_FAVICON_URL = f"{WEBAPP_FAVICON_PATH}/{WEBAPP_DEFAULT_FAVICON_DIGEST}/icon-180.png"
|
||||
WEBAPP_EMOJI_CACHE_DIR = APP_ROOT / "data" / "webapp-emoji"
|
||||
WEBAPP_CONFIG_PLACEHOLDER = "<!-- WEBAPP_CONFIG_SCRIPT -->"
|
||||
WEBAPP_I18N_PLACEHOLDER = "<!-- WEBAPP_I18N_SCRIPT -->"
|
||||
WEBAPP_JS_PLACEHOLDER = "<!-- WEBAPP_JS_SCRIPT -->"
|
||||
@@ -92,7 +91,6 @@ DEV_MOCK_END_MARKER = "<!-- WEBAPP_DEV_MOCK_END -->"
|
||||
WEBAPP_RATE_LIMIT_WINDOW_SECONDS = 60
|
||||
WEBAPP_RATE_LIMIT_MAX_REQUESTS = 30
|
||||
WEBAPP_LOGO_MAX_BYTES = 2 * 1024 * 1024
|
||||
WEBAPP_EMOJI_MAX_BYTES = 4 * 1024 * 1024
|
||||
WEBAPP_THEME_CSS_MAX_BYTES = 512 * 1024
|
||||
WEBAPP_THEME_ASSET_MAX_BYTES = 1024 * 1024
|
||||
WEBAPP_THEME_ASSET_CONTENT_TYPES = {
|
||||
|
||||
@@ -33,7 +33,6 @@ def create_subscription_webapp_application(
|
||||
async def _startup(app_obj: web.Application) -> None:
|
||||
await _ensure_shared_http_session()
|
||||
await _warm_webapp_logo_cache(app_obj)
|
||||
await _warm_webapp_animated_emoji_cache(app_obj)
|
||||
await warm_subscription_guides_config(app_obj)
|
||||
|
||||
async def _shutdown(app_obj: web.Application) -> None:
|
||||
|
||||
@@ -206,9 +206,6 @@ async def theme_asset_route(request: web.Request) -> web.Response:
|
||||
|
||||
|
||||
def _resolve_webapp_logo_url(settings: Settings) -> str:
|
||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
return ""
|
||||
|
||||
raw_logo_url = (getattr(settings, "WEBAPP_LOGO_URL", None) or "").strip()
|
||||
if not raw_logo_url:
|
||||
return WEBAPP_DEFAULT_LOGO_PATH
|
||||
@@ -303,29 +300,8 @@ def _uploaded_webapp_logo_response(filename: str) -> web.Response:
|
||||
return response
|
||||
|
||||
|
||||
def _emoji_to_codepoints(value: str) -> str:
|
||||
return "_".join(f"{ord(char):x}" for char in str(value or "").strip())
|
||||
|
||||
|
||||
def _webapp_emoji_disk_path(codepoints: str, ext: str) -> Path:
|
||||
return WEBAPP_EMOJI_CACHE_DIR / f"{codepoints}.512.{ext}"
|
||||
|
||||
|
||||
def _webapp_animated_emoji_source_url(codepoints: str, ext: str) -> str:
|
||||
return f"https://fonts.gstatic.com/s/e/notoemoji/latest/{codepoints}/512.{ext}"
|
||||
|
||||
|
||||
def _webapp_animated_emoji_asset_path(emoji: str, ext: str = "gif") -> str:
|
||||
codepoints = _emoji_to_codepoints(emoji)
|
||||
if not codepoints or ext not in {"gif", "webp"}:
|
||||
return ""
|
||||
return f"/webapp-emoji/{codepoints}/512.{ext}"
|
||||
|
||||
|
||||
async def webapp_logo_route(request: web.Request) -> web.Response:
|
||||
settings: Settings = request.app["settings"]
|
||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
raise web.HTTPNotFound(text="webapp_logo_disabled")
|
||||
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||
if not raw_logo_url:
|
||||
raise web.HTTPNotFound(text="webapp_logo_not_configured")
|
||||
@@ -521,37 +497,8 @@ def _webapp_default_brand_file_response(path: Path, content_type: str) -> web.Re
|
||||
return web.Response(body=body, content_type=content_type)
|
||||
|
||||
|
||||
async def webapp_animated_emoji_route(request: web.Request) -> web.Response:
|
||||
codepoints = str(request.match_info.get("codepoints") or "").strip().lower()
|
||||
ext = str(request.match_info.get("ext") or "").strip().lower()
|
||||
if not re.fullmatch(r"[0-9a-f]+(?:_[0-9a-f]+)*", codepoints) or ext not in {"gif", "webp"}:
|
||||
raise web.HTTPNotFound(text="webapp_emoji_not_found")
|
||||
|
||||
emoji_cache_key = f"{codepoints}:{ext}"
|
||||
emoji_caches: Dict[str, Tuple[bytes, str]] = request.app.setdefault("webapp_emoji_cache", {})
|
||||
emoji_cache = emoji_caches.get(emoji_cache_key)
|
||||
if emoji_cache is None:
|
||||
cache_lock: asyncio.Lock = request.app.setdefault("webapp_emoji_cache_lock", asyncio.Lock())
|
||||
async with cache_lock:
|
||||
emoji_cache = emoji_caches.get(emoji_cache_key)
|
||||
if emoji_cache is None:
|
||||
emoji_cache = await _load_or_fetch_webapp_animated_emoji(codepoints, ext)
|
||||
if emoji_cache:
|
||||
emoji_caches[emoji_cache_key] = emoji_cache
|
||||
|
||||
if not emoji_cache:
|
||||
raise web.HTTPNotFound(text="webapp_emoji_unavailable")
|
||||
|
||||
body, content_type = emoji_cache
|
||||
response = web.Response(body=body, content_type=content_type)
|
||||
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||
return response
|
||||
|
||||
|
||||
async def _warm_webapp_logo_cache(app: web.Application) -> None:
|
||||
settings: Settings = app["settings"]
|
||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
return
|
||||
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||
if not raw_logo_url or not _is_proxyable_webapp_logo_url(raw_logo_url):
|
||||
return
|
||||
@@ -573,111 +520,6 @@ async def _warm_webapp_logo_cache(app: web.Application) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _warm_webapp_animated_emoji_cache(app: web.Application) -> None:
|
||||
settings: Settings = app["settings"]
|
||||
if not getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
return
|
||||
if str(settings.WEBAPP_LOGO_EMOJI_FONT or "").strip() != "noto-color-animated":
|
||||
return
|
||||
|
||||
codepoints = _emoji_to_codepoints(settings.WEBAPP_LOGO_EMOJI)
|
||||
if not codepoints:
|
||||
return
|
||||
|
||||
app.setdefault("webapp_emoji_cache", {})
|
||||
app.setdefault("webapp_emoji_cache_lock", asyncio.Lock())
|
||||
emoji_caches: Dict[str, Tuple[bytes, str]] = app["webapp_emoji_cache"]
|
||||
|
||||
for ext in ("gif", "webp"):
|
||||
emoji_cache_key = f"{codepoints}:{ext}"
|
||||
if emoji_cache_key in emoji_caches:
|
||||
continue
|
||||
loaded_emoji = await _load_or_fetch_webapp_animated_emoji(codepoints, ext)
|
||||
if loaded_emoji:
|
||||
emoji_caches[emoji_cache_key] = loaded_emoji
|
||||
if ext == "gif":
|
||||
return
|
||||
|
||||
|
||||
async def _load_or_fetch_webapp_animated_emoji(
|
||||
codepoints: str, ext: str
|
||||
) -> Optional[Tuple[bytes, str]]:
|
||||
disk_emoji = await asyncio.to_thread(_read_webapp_animated_emoji_from_disk, codepoints, ext)
|
||||
if disk_emoji:
|
||||
return disk_emoji
|
||||
|
||||
fetched_emoji = await _fetch_webapp_animated_emoji(codepoints, ext)
|
||||
if fetched_emoji:
|
||||
await asyncio.to_thread(
|
||||
_write_webapp_animated_emoji_to_disk, codepoints, ext, fetched_emoji
|
||||
)
|
||||
return fetched_emoji
|
||||
|
||||
|
||||
def _read_webapp_animated_emoji_from_disk(codepoints: str, ext: str) -> Optional[Tuple[bytes, str]]:
|
||||
path = _webapp_emoji_disk_path(codepoints, ext)
|
||||
try:
|
||||
body = path.read_bytes()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if not body or len(body) > WEBAPP_EMOJI_MAX_BYTES:
|
||||
return None
|
||||
return body, "image/gif" if ext == "gif" else "image/webp"
|
||||
|
||||
|
||||
def _write_webapp_animated_emoji_to_disk(
|
||||
codepoints: str, ext: str, emoji: Tuple[bytes, str]
|
||||
) -> None:
|
||||
body, _content_type = emoji
|
||||
if not body or len(body) > WEBAPP_EMOJI_MAX_BYTES:
|
||||
return
|
||||
|
||||
path = _webapp_emoji_disk_path(codepoints, ext)
|
||||
try:
|
||||
WEBAPP_EMOJI_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(body)
|
||||
except OSError as exc:
|
||||
logger.warning("Failed to write WEBAPP animated emoji cache: %s", exc)
|
||||
|
||||
|
||||
async def _fetch_webapp_animated_emoji(codepoints: str, ext: str) -> Optional[Tuple[bytes, str]]:
|
||||
try:
|
||||
session = await _get_shared_http_session()
|
||||
timeout = ClientTimeout(total=4)
|
||||
source_url = _webapp_animated_emoji_source_url(codepoints, ext)
|
||||
async with session.get(
|
||||
source_url,
|
||||
allow_redirects=False,
|
||||
headers={"Accept": "image/gif,image/webp,image/*,*/*;q=0.8"},
|
||||
timeout=timeout,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
return None
|
||||
|
||||
content_type = (
|
||||
(response.headers.get("Content-Type") or "").split(";", 1)[0].strip().lower()
|
||||
)
|
||||
expected_content_type = "image/gif" if ext == "gif" else "image/webp"
|
||||
if content_type and content_type != expected_content_type:
|
||||
return None
|
||||
|
||||
body = bytearray()
|
||||
async for chunk in response.content.iter_chunked(64 * 1024):
|
||||
body.extend(chunk)
|
||||
if len(body) > WEBAPP_EMOJI_MAX_BYTES:
|
||||
logger.warning("WEBAPP animated emoji exceeded the 4 MiB limit.")
|
||||
return None
|
||||
|
||||
if not body:
|
||||
return None
|
||||
|
||||
return bytes(body), expected_content_type
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to fetch WEBAPP animated emoji: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
async def _load_or_fetch_webapp_logo(logo_url: str) -> Optional[Tuple[bytes, str]]:
|
||||
disk_logo = await asyncio.to_thread(_read_webapp_logo_from_disk, logo_url)
|
||||
if disk_logo:
|
||||
@@ -1144,9 +986,6 @@ def _build_webapp_bootstrap_payload(request: web.Request) -> Dict[str, Any]:
|
||||
"themesDir": settings.WEBAPP_THEMES_DIR,
|
||||
"themePreviewKey": preview_key,
|
||||
"logoUrl": cached["logo_url"],
|
||||
"logoUseEmoji": bool(settings.WEBAPP_LOGO_USE_EMOJI),
|
||||
"logoEmoji": settings.WEBAPP_LOGO_EMOJI,
|
||||
"logoEmojiFont": settings.WEBAPP_LOGO_EMOJI_FONT,
|
||||
"faviconUrl": cached["favicon_url"],
|
||||
"faviconUseCustom": bool(settings.WEBAPP_FAVICON_USE_CUSTOM),
|
||||
"apiBase": "/api",
|
||||
@@ -1308,12 +1147,6 @@ async def index_route(request: web.Request) -> web.Response:
|
||||
f'<script src="/{_resolve_webapp_js_asset_name()}" defer></script>',
|
||||
)
|
||||
brand_asset_url = cached["logo_url"]
|
||||
if (
|
||||
not brand_asset_url
|
||||
and settings.WEBAPP_LOGO_USE_EMOJI
|
||||
and settings.WEBAPP_LOGO_EMOJI_FONT == "noto-color-animated"
|
||||
):
|
||||
brand_asset_url = _webapp_animated_emoji_asset_path(settings.WEBAPP_LOGO_EMOJI)
|
||||
if brand_asset_url:
|
||||
html = html.replace(
|
||||
"</head>",
|
||||
|
||||
@@ -46,10 +46,6 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
||||
rf"{WEBAPP_FAVICON_PATH}/{{digest:[0-9a-f]{{16}}}}/{{filename:[A-Za-z0-9_.-]+}}",
|
||||
webapp_favicon_route,
|
||||
)
|
||||
app.router.add_get(
|
||||
r"/webapp-emoji/{codepoints:[0-9a-f_]+}/512.{ext:gif|webp}",
|
||||
webapp_animated_emoji_route,
|
||||
)
|
||||
app.router.add_get("/subscription_webapp.{asset_hash:[0-9a-f]{8}}.css", css_asset_route)
|
||||
app.router.add_get("/subscription_webapp.css", css_asset_route)
|
||||
app.router.add_get(
|
||||
|
||||
@@ -48,8 +48,6 @@ def _safe_color(value: Optional[str]) -> str:
|
||||
def _public_logo_url(settings: Settings) -> Optional[str]:
|
||||
"""Email recipients can't reach the in-app /webapp-logo proxy, so only a
|
||||
stored public https URL can be used directly. Anything else is dropped."""
|
||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
return None
|
||||
raw = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
@@ -28,10 +28,7 @@ from db.dal import app_settings_dal
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APPEARANCE_OVERRIDE_KEYS = {
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
@@ -167,12 +164,6 @@ def _appearance_snapshot(settings: Settings) -> Dict[str, Any]:
|
||||
snapshot["WEBAPP_FAVICON_URL"] = favicon_url
|
||||
if getattr(settings, "WEBAPP_FAVICON_USE_CUSTOM", False):
|
||||
snapshot["WEBAPP_FAVICON_USE_CUSTOM"] = True
|
||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
||||
snapshot["WEBAPP_LOGO_USE_EMOJI"] = True
|
||||
snapshot["WEBAPP_LOGO_EMOJI"] = getattr(settings, "WEBAPP_LOGO_EMOJI", "")
|
||||
emoji_font = getattr(settings, "WEBAPP_LOGO_EMOJI_FONT", "")
|
||||
if emoji_font and emoji_font != "system":
|
||||
snapshot["WEBAPP_LOGO_EMOJI_FONT"] = emoji_font
|
||||
primary_color = getattr(settings, "WEBAPP_PRIMARY_COLOR", None)
|
||||
if primary_color and primary_color != "#00fe7a":
|
||||
snapshot["WEBAPP_PRIMARY_COLOR"] = primary_color
|
||||
|
||||
@@ -59,9 +59,6 @@ class WebAppSettings(BaseModel):
|
||||
title: str
|
||||
primary_color: str
|
||||
logo_url: Optional[str]
|
||||
logo_use_emoji: bool
|
||||
logo_emoji: str
|
||||
logo_emoji_font: str
|
||||
favicon_use_custom: bool
|
||||
favicon_url: Optional[str]
|
||||
logo_favicon_url: Optional[str]
|
||||
@@ -395,15 +392,6 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
WEBAPP_LOGO_URL: Optional[str] = Field(default=None)
|
||||
WEBAPP_LOGO_USE_EMOJI: bool = Field(default=False)
|
||||
WEBAPP_LOGO_EMOJI: str = Field(default="🫥")
|
||||
WEBAPP_LOGO_EMOJI_FONT: str = Field(
|
||||
default="system",
|
||||
description=(
|
||||
"Emoji font for logo fallback: system, noto-color, noto-color-animated, "
|
||||
"noto-emoji, twemoji, openmoji, apple, segoe, noto-local"
|
||||
),
|
||||
)
|
||||
WEBAPP_FAVICON_USE_CUSTOM: bool = Field(default=False)
|
||||
WEBAPP_FAVICON_URL: Optional[str] = Field(default=None)
|
||||
WEBAPP_LOGO_FAVICON_URL: Optional[str] = Field(default=None)
|
||||
@@ -566,9 +554,6 @@ class Settings(BaseSettings):
|
||||
title=self.WEBAPP_TITLE,
|
||||
primary_color=self.WEBAPP_PRIMARY_COLOR,
|
||||
logo_url=self.WEBAPP_LOGO_URL,
|
||||
logo_use_emoji=self.WEBAPP_LOGO_USE_EMOJI,
|
||||
logo_emoji=self.WEBAPP_LOGO_EMOJI,
|
||||
logo_emoji_font=self.WEBAPP_LOGO_EMOJI_FONT,
|
||||
favicon_use_custom=self.WEBAPP_FAVICON_USE_CUSTOM,
|
||||
favicon_url=self.WEBAPP_FAVICON_URL,
|
||||
logo_favicon_url=self.WEBAPP_LOGO_FAVICON_URL,
|
||||
@@ -810,21 +795,6 @@ class Settings(BaseSettings):
|
||||
def ignore_deprecated_webapp_logo_url_env(cls, _value):
|
||||
return None
|
||||
|
||||
@field_validator("WEBAPP_LOGO_USE_EMOJI", mode="before")
|
||||
@classmethod
|
||||
def ignore_deprecated_webapp_logo_use_emoji_env(cls, _value):
|
||||
return False
|
||||
|
||||
@field_validator("WEBAPP_LOGO_EMOJI", mode="before")
|
||||
@classmethod
|
||||
def ignore_deprecated_webapp_logo_emoji_env(cls, _value):
|
||||
return "🫥"
|
||||
|
||||
@field_validator("WEBAPP_LOGO_EMOJI_FONT", mode="before")
|
||||
@classmethod
|
||||
def ignore_deprecated_webapp_logo_emoji_font_env(cls, _value):
|
||||
return "system"
|
||||
|
||||
@field_validator("WEBAPP_FAVICON_USE_CUSTOM", mode="before")
|
||||
@classmethod
|
||||
def ignore_deprecated_webapp_favicon_use_custom_env(cls, _value):
|
||||
|
||||
@@ -84,7 +84,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/(webapp-logo|webapp-uploaded-logo|webapp-favicon|webapp-emoji|webapp-theme-css|webapp-theme-assets)/ {
|
||||
location ~ ^/(webapp-logo|webapp-uploaded-logo|webapp-favicon|webapp-theme-css|webapp-theme-assets)/ {
|
||||
proxy_pass http://backend:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -64,7 +64,6 @@ def settings():
|
||||
DEFAULT_LANGUAGE=LANGUAGE,
|
||||
EMAIL_CODE_TTL_SECONDS=600,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||
WEBAPP_TITLE="remnawave-minishop",
|
||||
)
|
||||
|
||||
@@ -161,9 +161,6 @@
|
||||
| `TELEGRAM_OAUTH_REQUEST_ACCESS` | `.env` | Дополнительные разрешения, например `write`. |
|
||||
| `WEBAPP_PRIMARY_COLOR` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_LOGO_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_LOGO_USE_EMOJI` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_LOGO_EMOJI` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_LOGO_EMOJI_FONT` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_FAVICON_USE_CUSTOM` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
| `WEBAPP_LOGO_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- блокировка пользователей, входящий список тикетов поддержки, рассылки, промокоды и просмотр логов;
|
||||
- ручная синхронизация с Remnawave;
|
||||
- редактор разрешенных настроек приложения из manifest-файла;
|
||||
- раздел **Внешний вид** для логотипа, emoji-логотипа, выбора темы, accent-цвета, масштаба логотипа и предпросмотра тем;
|
||||
- раздел **Внешний вид** для логотипа, выбора темы, accent-цвета, масштаба логотипа и предпросмотра тем;
|
||||
- раздел **Инструкции подключения** для встроенной страницы установки, поведения кнопок бота и Remnawave Subscription Page config;
|
||||
- раздел **Бэкапы** для просмотра локальных ZIP-архивов, загрузки архива и восстановления БД/compose-папки;
|
||||
- редактор JSON-каталога тарифов;
|
||||
@@ -49,7 +49,7 @@
|
||||
В manifest сейчас входят:
|
||||
|
||||
- общие параметры: язык, валюта, ссылки поддержки, документы, обязательный канал, Remnawave-доступы и поведение `/start`;
|
||||
- внешний вид и доступность Web App: название, цвет, логотип, emoji-логотип и `WEBAPP_ENABLED`;
|
||||
- внешний вид и доступность Web App: название, цвет, логотип и `WEBAPP_ENABLED`;
|
||||
- инструкции подключения: `SUBSCRIPTION_GUIDES_ENABLED`, `SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED`, чтение конфига из Remnawave Panel, JSON-переопределение и резервный путь к файлу;
|
||||
- legacy-тарифы без JSON-каталога: периоды подписки, RUB/Stars цены, реферальные бонусы и пакеты трафика;
|
||||
- платежные провайдеры: включение методов, порядок кнопок, публичные параметры и секреты YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket и Stars, а также текст и иконки кнопок оплаты;
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
## Внешний вид
|
||||
|
||||
Раздел **Внешний вид** объединяет настройки бренда и темы Web App. Логотип можно загрузить файлом или по HTTPS-ссылке; backend сохраняет файл в `data/webapp-logo/uploads` и подставляет локальный URL. Если включен emoji-логотип, картинка скрывается, а для emoji можно выбрать системный, Twemoji, Noto Color, animated Noto и другие варианты отрисовки.
|
||||
Раздел **Внешний вид** объединяет настройки бренда и темы Web App. Логотип можно загрузить файлом или по HTTPS-ссылке; backend сохраняет файл в `data/webapp-logo/uploads` и подставляет локальный URL. Если логотип не задан, показывается логотип проекта по умолчанию. Favicon генерируется из логотипа или загружается отдельно.
|
||||
|
||||
В блоке тем админка читает каталог из `WEBAPP_THEMES_DIR`, показывает встроенные и кастомные темы, позволяет выбрать текущую тему, изменить accent, включить или выключить тему для админки и настроить масштаб логотипа на главной и экране входа. Кнопка предпросмотра открывает `/home?theme_preview=<key>` и не меняет глобальную тему до сохранения.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ SUPPORT_TICKET_RATE_LIMIT_PER_HOUR=5
|
||||
|
||||
Если `WEBAPP_ENABLED=False`, пользовательское веб-приложение и админ-панель не регистрируются. Чтобы снова попасть в админку, включите `WEBAPP_ENABLED=True` в `.env` и перезапустите backend/frontend контейнеры.
|
||||
|
||||
Внешний вид настраивается в админке: раздел **Внешний вид** управляет логотипом, emoji-логотипом, accent-цветом, выбранной темой и масштабом логотипа. Кастомные темы читаются из `WEBAPP_THEMES_DIR`, а `WEBAPP_DEFAULT_THEME` может принудительно выбрать тему по ключу. Подробный контракт `theme.json`, CSS/asset-роуты и пайплайн создания темы описаны в [webapp-themes.md](webapp-themes.md).
|
||||
Внешний вид настраивается в админке: раздел **Внешний вид** управляет логотипом, accent-цветом, выбранной темой и масштабом логотипа. Кастомные темы читаются из `WEBAPP_THEMES_DIR`, а `WEBAPP_DEFAULT_THEME` может принудительно выбрать тему по ключу. Подробный контракт `theme.json`, CSS/asset-роуты и пайплайн создания темы описаны в [webapp-themes.md](webapp-themes.md).
|
||||
|
||||
## Авторизация
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ Web App поддерживает файловые темы, предпросмо
|
||||
- включить или выключить применение темы в админ-панели;
|
||||
- настроить масштаб логотипа на главной и экране входа;
|
||||
- загрузить логотип файлом или по HTTPS-ссылке;
|
||||
- включить emoji-логотип и выбрать способ его отрисовки;
|
||||
- открыть предпросмотр темы через `/home?theme_preview=<key>`.
|
||||
|
||||
Через файлы темы можно менять намного больше:
|
||||
@@ -51,7 +50,7 @@ WEBAPP_DEFAULT_THEME=
|
||||
|
||||
В compose-примерах `data/themes` - это локальная папка рядом с выбранным `docker-compose.yml`; она монтируется в контейнер как `/app/data/themes`. Правки в `backend/bot/app/web/themes` попадают в прод только при сборке собственного образа; опубликованный образ их не видит.
|
||||
|
||||
Важно: `WEBAPP_PRIMARY_COLOR`, `WEBAPP_LOGO_URL`, `WEBAPP_LOGO_USE_EMOJI`, `WEBAPP_LOGO_EMOJI` и `WEBAPP_LOGO_EMOJI_FONT` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent.
|
||||
Важно: `WEBAPP_PRIMARY_COLOR` и `WEBAPP_LOGO_URL` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent.
|
||||
|
||||
## Контракт `theme.json`
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ openssl rand -hex 32
|
||||
Перед первым запуском создайте каталоги и отдайте их пользователю контейнера:
|
||||
|
||||
```bash
|
||||
mkdir -p data/themes data/webapp-logo data/webapp-emoji data/tariffs
|
||||
mkdir -p data/themes data/webapp-logo data/tariffs
|
||||
touch data/locales-overrides.json
|
||||
chown -R 10001:10001 data
|
||||
chmod -R u+rwX data
|
||||
|
||||
@@ -377,7 +377,7 @@ distributed lock; код подготовлен к нескольким репл
|
||||
Перед первым запуском на сервере заранее дайте права пользователю контейнера `10001`:
|
||||
|
||||
```bash
|
||||
mkdir -p data/themes data/webapp-logo data/webapp-emoji data/tariffs
|
||||
mkdir -p data/themes data/webapp-logo data/tariffs
|
||||
touch data/locales-overrides.json
|
||||
chown -R 10001:10001 data
|
||||
chmod -R u+rwX data
|
||||
|
||||
@@ -358,13 +358,9 @@
|
||||
} = $accountStore);
|
||||
|
||||
$: brandTitle = CFG.title || "/minishop";
|
||||
$: brandEmoji = CFG.logoEmoji || "🫥";
|
||||
$: brandEmojiFont = CFG.logoEmojiFont || "system";
|
||||
$: brand = normalizeBrand({
|
||||
title: brandTitle,
|
||||
logoUrl: CFG.logoUseEmoji ? "" : CFG.logoUrl,
|
||||
emoji: brandEmoji,
|
||||
emojiFont: brandEmojiFont,
|
||||
logoUrl: CFG.logoUrl,
|
||||
});
|
||||
$: faviconBrand = normalizeBrand({
|
||||
...brand,
|
||||
@@ -2077,9 +2073,6 @@
|
||||
]);
|
||||
return [
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
export let mockData = {};
|
||||
|
||||
const title = config.title || "/minishop";
|
||||
const logoEmoji = config.logoEmoji || "🫥";
|
||||
const logoUrl = config.logoUrl || "/webapp-default-logo.webp";
|
||||
const plans = mockData.plans || [];
|
||||
const sub = mockData.subscription || {};
|
||||
const methods = mockData.payment_methods || [];
|
||||
@@ -78,7 +78,7 @@
|
||||
<PhoneFrame number="1" label="Главный экран">
|
||||
<main class="home-layout">
|
||||
<div class="login-brand home-brand">
|
||||
<div class="brand-mark brand-mark-xl"><span>{logoEmoji}</span></div>
|
||||
<div class="brand-mark brand-mark-xl"><img class="loaded" src={logoUrl} alt="" /></div>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
<div class="home-bottom">
|
||||
@@ -113,7 +113,7 @@
|
||||
<PhoneFrame number="2" label="Выбор тарифа">
|
||||
<div class="preview-header">
|
||||
<div class="brand-row">
|
||||
<div class="brand-mark"><span>{logoEmoji}</span></div>
|
||||
<div class="brand-mark"><img class="loaded" src={logoUrl} alt="" /></div>
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,7 +198,7 @@
|
||||
<PhoneFrame number="6" label="Пригласить друга">
|
||||
<div class="preview-header">
|
||||
<div class="brand-row">
|
||||
<div class="brand-mark"><span>{logoEmoji}</span></div>
|
||||
<div class="brand-mark"><img class="loaded" src={logoUrl} alt="" /></div>
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@
|
||||
<PhoneFrame number="7" label="Настройки">
|
||||
<div class="preview-header">
|
||||
<div class="brand-row">
|
||||
<div class="brand-mark"><span>{logoEmoji}</span></div>
|
||||
<div class="brand-mark"><img class="loaded" src={logoUrl} alt="" /></div>
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@
|
||||
|
||||
<PhoneFrame number="8" label="Логин" wide>
|
||||
<div class="login-brand small">
|
||||
<div class="brand-mark brand-mark-xl"><span>{logoEmoji}</span></div>
|
||||
<div class="brand-mark brand-mark-xl"><img class="loaded" src={logoUrl} alt="" /></div>
|
||||
<h1>{title}</h1>
|
||||
<p>Войдите в свой аккаунт</p>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script>
|
||||
import { Check, ExternalLink, FileText, RefreshCw, Save } from "$components/ui/icons.js";
|
||||
import {
|
||||
AdminBadge,
|
||||
AdminButton,
|
||||
AdminEmptyState,
|
||||
AdminSelect,
|
||||
} from "$components/patterns/admin/index.js";
|
||||
import { AdminBadge, AdminButton, AdminEmptyState } from "$components/patterns/admin/index.js";
|
||||
import { Checkbox, ColorInput, FileInput, Input, RangeInput } from "$components/ui/index.js";
|
||||
import { Switch } from "$components/ui/primitives.js";
|
||||
import { getContext, onDestroy, onMount } from "svelte";
|
||||
|
||||
import BrandMark from "$lib/webapp/BrandMark.svelte";
|
||||
import { localizedThemeName } from "$lib/webapp/themeStyle.js";
|
||||
|
||||
export let at;
|
||||
@@ -26,9 +20,6 @@
|
||||
"SUBSCRIPTION_MINI_APP_URL",
|
||||
"WEBAPP_PRIMARY_COLOR",
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
@@ -42,8 +33,7 @@
|
||||
$: fieldMap = new Map(appearanceFields.map((field) => [field.key, field]));
|
||||
$: activeKey = themesCatalog.default_theme;
|
||||
$: logoUrl = valueForKey("WEBAPP_LOGO_URL");
|
||||
$: useEmojiLogo = boolValue(valueForKey("WEBAPP_LOGO_USE_EMOJI"));
|
||||
$: currentLogoUrl = !useEmojiLogo ? pendingLogoPreviewUrl || logoUrl || brand?.logoUrl || "" : "";
|
||||
$: currentLogoUrl = pendingLogoPreviewUrl || logoUrl || brand?.logoUrl || "";
|
||||
$: previewLogoUrl =
|
||||
logoPreviewNonce && currentLogoUrl ? withLogoCacheBust(currentLogoUrl) : currentLogoUrl;
|
||||
$: persistedUseCustomFavicon = boolValue(
|
||||
@@ -59,28 +49,12 @@
|
||||
$: useCustomFavicon = faviconUseCustomDraft;
|
||||
$: faviconUrl = valueForKey("WEBAPP_FAVICON_URL", appFaviconUrl);
|
||||
$: logoFaviconUrl = valueForKey("WEBAPP_LOGO_FAVICON_URL");
|
||||
$: generatedFaviconUrl = !useEmojiLogo
|
||||
? logoFaviconUrl || appFaviconUrl || previewLogoUrl || ""
|
||||
: "";
|
||||
$: generatedFaviconUrl = logoFaviconUrl || appFaviconUrl || previewLogoUrl || "";
|
||||
$: currentFaviconUrl = useCustomFavicon
|
||||
? pendingFaviconPreviewUrl || faviconUrl || ""
|
||||
: generatedFaviconUrl;
|
||||
$: previewFaviconUrl =
|
||||
faviconPreviewNonce && currentFaviconUrl ? withCacheBust(currentFaviconUrl) : currentFaviconUrl;
|
||||
$: logoEmoji = valueForKey("WEBAPP_LOGO_EMOJI");
|
||||
$: logoEmojiInput = useEmojiLogo ? logoEmoji : "";
|
||||
$: logoEmojiPreview = logoEmoji || "🫥";
|
||||
$: logoEmojiFont = valueForKey("WEBAPP_LOGO_EMOJI_FONT") || "system";
|
||||
$: logoBrand = {
|
||||
title: "",
|
||||
logoUrl: useEmojiLogo ? "" : previewLogoUrl,
|
||||
emoji: logoEmojiPreview,
|
||||
emojiFont: logoEmojiFont,
|
||||
};
|
||||
$: emojiFontItems = (fieldMap.get("WEBAPP_LOGO_EMOJI_FONT")?.choices || []).map((item) => ({
|
||||
value: item.value,
|
||||
label: item.i18n_label_key ? adminText(item.i18n_label_key, {}, item.label) : item.label,
|
||||
}));
|
||||
$: dirtyCount = Object.keys(settingsDirty || {}).filter((key) =>
|
||||
isAppearanceSettingKey(key)
|
||||
).length;
|
||||
@@ -138,15 +112,6 @@
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
function adminLocaleKey(key) {
|
||||
const raw = String(key || "");
|
||||
return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw;
|
||||
}
|
||||
|
||||
function adminText(key, params = {}, fallback = "") {
|
||||
return key ? at(adminLocaleKey(key), params, fallback) : fallback;
|
||||
}
|
||||
|
||||
function withLogoCacheBust(url) {
|
||||
return withCacheBust(url, logoPreviewNonce);
|
||||
}
|
||||
@@ -241,7 +206,6 @@
|
||||
if (uploaded?.faviconUrl) {
|
||||
settingsStore.setFieldValue("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
||||
}
|
||||
settingsStore.setFieldValue("WEBAPP_LOGO_USE_EMOJI", false);
|
||||
if (logoFileInput) logoFileInput.value = "";
|
||||
});
|
||||
}
|
||||
@@ -256,7 +220,6 @@
|
||||
if (uploaded?.faviconUrl) {
|
||||
settingsStore.setFieldValue("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
||||
}
|
||||
settingsStore.setFieldValue("WEBAPP_LOGO_USE_EMOJI", false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,32 +266,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setEmojiLogo(enabled) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_USE_EMOJI", Boolean(enabled));
|
||||
if (!enabled) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_EMOJI", "");
|
||||
} else {
|
||||
pendingLogoPreviewUrl = "";
|
||||
clearPendingObjectUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function setAppearanceValue(key, value) {
|
||||
settingsStore.markDirty(key, value);
|
||||
}
|
||||
|
||||
async function saveAppearance() {
|
||||
const keysToSave = new Set(appearanceDirtyKeys);
|
||||
if (!useEmojiLogo && logoEmoji) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_EMOJI", "");
|
||||
keysToSave.add("WEBAPP_LOGO_EMOJI");
|
||||
}
|
||||
const shouldReloadFrontend = Array.from(keysToSave).some((key) =>
|
||||
[
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
@@ -395,13 +337,7 @@
|
||||
<header class="admin-card-head">
|
||||
<div>
|
||||
<h3>{at("appearance_brand_title", {}, "Логотип")}</h3>
|
||||
<small
|
||||
>{at(
|
||||
"appearance_brand_sub",
|
||||
{},
|
||||
"Файл, ссылка или подтвержденный emoji-логотип"
|
||||
)}</small
|
||||
>
|
||||
<small>{at("appearance_brand_sub", {}, "Загрузите логотип файлом или по ссылке")}</small>
|
||||
</div>
|
||||
<div class="admin-editor-section-actions">
|
||||
{#if dirtyCount}
|
||||
@@ -424,7 +360,7 @@
|
||||
</header>
|
||||
<div class="admin-card-body appearance-logo-grid">
|
||||
<div class="appearance-logo-preview">
|
||||
{#if !useEmojiLogo && previewLogoUrl && !logoPreviewFailed}
|
||||
{#if previewLogoUrl && !logoPreviewFailed}
|
||||
<img
|
||||
class="appearance-logo-image"
|
||||
src={previewLogoUrl}
|
||||
@@ -435,8 +371,6 @@
|
||||
logoPreviewFailed = true;
|
||||
}}
|
||||
/>
|
||||
{:else if useEmojiLogo}
|
||||
<BrandMark brand={logoBrand} size="lg" />
|
||||
{:else}
|
||||
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
||||
{/if}
|
||||
@@ -476,39 +410,6 @@
|
||||
</AdminButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="appearance-control-card">
|
||||
<label class="appearance-switch">
|
||||
<Switch.Root
|
||||
checked={useEmojiLogo}
|
||||
onCheckedChange={setEmojiLogo}
|
||||
class="admin-switch-root"
|
||||
>
|
||||
<Switch.Thumb class="admin-switch-thumb" />
|
||||
</Switch.Root>
|
||||
<span>{at("appearance_use_emoji_logo", {}, "Использовать emoji-логотип")}</span>
|
||||
</label>
|
||||
<div class="appearance-emoji-grid">
|
||||
<Input
|
||||
class="input appearance-control"
|
||||
type="text"
|
||||
maxlength="8"
|
||||
value={logoEmojiInput}
|
||||
disabled={!useEmojiLogo}
|
||||
oninput={(event) =>
|
||||
setAppearanceValue("WEBAPP_LOGO_EMOJI", event.currentTarget.value)}
|
||||
/>
|
||||
<AdminSelect
|
||||
class="appearance-control"
|
||||
value={logoEmojiFont}
|
||||
items={emojiFontItems}
|
||||
disabled={!useEmojiLogo}
|
||||
ariaLabel={at("appearance_emoji_font", {}, "Шрифт emoji")}
|
||||
placeholder={at("appearance_emoji_font", {}, "Шрифт emoji")}
|
||||
onValueChange={(value) => setAppearanceValue("WEBAPP_LOGO_EMOJI_FONT", value)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -525,8 +426,6 @@
|
||||
faviconPreviewFailed = true;
|
||||
}}
|
||||
/>
|
||||
{:else if !useCustomFavicon && useEmojiLogo}
|
||||
<BrandMark brand={logoBrand} size="lg" />
|
||||
{:else}
|
||||
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
||||
{/if}
|
||||
@@ -815,22 +714,14 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.appearance-url-row,
|
||||
.appearance-emoji-grid {
|
||||
.appearance-url-row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.appearance-url-row {
|
||||
grid-template-columns: minmax(0, 1fr) max-content;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.appearance-emoji-grid {
|
||||
grid-template-columns: minmax(0, 360px) max-content;
|
||||
}
|
||||
|
||||
:global(.appearance-control.input),
|
||||
:global(.appearance-control.admin-btn),
|
||||
:global(.appearance-control.admin-select-trigger) {
|
||||
@@ -1012,8 +903,7 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.appearance-url-row,
|
||||
.appearance-emoji-grid {
|
||||
.appearance-url-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{#if isInternalNote}
|
||||
<Lock size={15} />
|
||||
{:else if showSupportAvatar}
|
||||
<BrandMark brand={supportBrand} size="sm" fallbackEmoji={true} />
|
||||
<BrandMark brand={supportBrand} size="sm" />
|
||||
{:else if showUserAvatar && userAvatarUrl}
|
||||
<img src={userAvatarUrl} alt="" loading="lazy" referrerpolicy="no-referrer" />
|
||||
{:else if showUserAvatar && userInitials}
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
import { cn } from "../utils.js";
|
||||
import { animatedEmojiAssetUrls, normalizeBrand } from "./browser.js";
|
||||
import { normalizeBrand } from "./browser.js";
|
||||
|
||||
const LOGO_LOAD_TIMEOUT_MS = 10000;
|
||||
|
||||
const EMOJI_FONT_OPTIONS = {
|
||||
"noto-color": {
|
||||
cssFamily: "Noto Color Emoji",
|
||||
stylesheet: (text) =>
|
||||
`https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap&text=${encodeURIComponent(text)}`,
|
||||
},
|
||||
"noto-emoji": {
|
||||
cssFamily: "Noto Emoji",
|
||||
stylesheet: (text) =>
|
||||
`https://fonts.googleapis.com/css2?family=Noto+Emoji:wght@700&display=swap&text=${encodeURIComponent(text)}`,
|
||||
},
|
||||
twemoji: {
|
||||
cssFamily: "Twemoji Mozilla",
|
||||
stylesheet: () => "https://cdn.jsdelivr.net/npm/twemoji-colr-font@15.0.3/twemoji.css",
|
||||
},
|
||||
openmoji: {
|
||||
cssFamily: "OpenMoji Color",
|
||||
stylesheet: () => "https://cdn.jsdelivr.net/npm/@openmoji/font@15.1.0/css/openmoji-color.css",
|
||||
},
|
||||
};
|
||||
|
||||
export let brand = {};
|
||||
export let logoUrl = "";
|
||||
export let emoji = "";
|
||||
export let emojiFont = "";
|
||||
export let size = "sm";
|
||||
export let animate = false;
|
||||
export let fallbackEmoji = true;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
@@ -49,30 +25,13 @@
|
||||
let lastLogoUrl = "";
|
||||
let logoLoadTimer = null;
|
||||
let logoLoadTimerUrl = "";
|
||||
let fontLoaded = false;
|
||||
let loadedFontKey = "";
|
||||
let animatedEmojiError = false;
|
||||
let animatedEmojiStaticFallback = false;
|
||||
let lastAnimatedEmoji = "";
|
||||
|
||||
$: normalizedBrand = normalizeBrand({
|
||||
...brand,
|
||||
logoUrl: logoUrl || brand?.logoUrl,
|
||||
emoji: emoji || brand?.emoji || brand?.logoEmoji,
|
||||
emojiFont: emojiFont || brand?.emojiFont || brand?.logoEmojiFont,
|
||||
});
|
||||
$: normalizedLogoUrl = normalizedBrand.logoUrl;
|
||||
$: normalizedEmoji = normalizedBrand.emoji;
|
||||
$: normalizedEmojiFont = normalizedBrand.emojiFont;
|
||||
$: sizeClass = SIZE_CLASSES[size] || "";
|
||||
$: animatedEmojiAssets = animatedEmojiAssetUrls(normalizedEmoji);
|
||||
$: animatedEmojiSrc = animatedEmojiAssets.gif;
|
||||
$: animatedEmojiFallbackSrc = animatedEmojiAssets.webp;
|
||||
$: useAnimatedEmoji =
|
||||
!normalizedLogoUrl &&
|
||||
normalizedEmojiFont === "noto-color-animated" &&
|
||||
animatedEmojiSrc &&
|
||||
!animatedEmojiError;
|
||||
|
||||
$: if (normalizedLogoUrl !== lastLogoUrl) {
|
||||
lastLogoUrl = normalizedLogoUrl;
|
||||
@@ -81,82 +40,11 @@
|
||||
}
|
||||
$: if (normalizedLogoUrl && !loaded && !failed) armLogoLoadTimeout();
|
||||
$: if (!normalizedLogoUrl || loaded || failed) clearLogoLoadTimeout();
|
||||
$: if (`${normalizedEmojiFont}:${normalizedEmoji}` !== lastAnimatedEmoji) {
|
||||
lastAnimatedEmoji = `${normalizedEmojiFont}:${normalizedEmoji}`;
|
||||
animatedEmojiError = false;
|
||||
animatedEmojiStaticFallback = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearLogoLoadTimeout();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
loadEmojiFont(normalizedEmojiFont, normalizedEmoji);
|
||||
});
|
||||
|
||||
$: if (normalizedEmojiFont && normalizedEmoji) {
|
||||
loadEmojiFont(normalizedEmojiFont, normalizedEmoji);
|
||||
}
|
||||
|
||||
function loadEmojiFont(font, text) {
|
||||
if (typeof document === "undefined") return;
|
||||
if (font === "system" || font === "noto-color-animated" || !font) {
|
||||
fontLoaded = true;
|
||||
loadedFontKey = "system";
|
||||
return;
|
||||
}
|
||||
|
||||
const fontOption = EMOJI_FONT_OPTIONS[font];
|
||||
if (!fontOption) {
|
||||
fontLoaded = true;
|
||||
loadedFontKey = font;
|
||||
return;
|
||||
}
|
||||
|
||||
const fontUrl = fontOption.stylesheet(text);
|
||||
const fontKey = `${font}:${text}`;
|
||||
if (loadedFontKey === fontKey) return;
|
||||
|
||||
fontLoaded = false;
|
||||
loadedFontKey = fontKey;
|
||||
|
||||
const existing = document.querySelector(`link[data-brand-emoji-font="${fontKey}"]`);
|
||||
if (existing) {
|
||||
fontLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = fontUrl;
|
||||
link.dataset.brandEmojiFont = fontKey;
|
||||
link.onload = () => {
|
||||
fontLoaded = true;
|
||||
if (document.fonts && fontOption.cssFamily) {
|
||||
document.fonts.load(`1em "${fontOption.cssFamily}"`, text).finally(() => {
|
||||
fontLoaded = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
link.onerror = () => {
|
||||
fontLoaded = true;
|
||||
};
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function getEmojiFontClass(font) {
|
||||
if (font === "noto-color") return "emoji-font-noto-color";
|
||||
if (font === "noto-emoji") return "emoji-font-noto-emoji";
|
||||
if (font === "twemoji") return "emoji-font-twemoji";
|
||||
if (font === "openmoji") return "emoji-font-openmoji";
|
||||
if (font === "apple") return "emoji-font-apple";
|
||||
if (font === "segoe") return "emoji-font-segoe";
|
||||
if (font === "noto-local") return "emoji-font-noto-local";
|
||||
return "";
|
||||
}
|
||||
|
||||
function clearLogoLoadTimeout() {
|
||||
if (logoLoadTimer) {
|
||||
window.clearTimeout(logoLoadTimer);
|
||||
@@ -207,29 +95,6 @@
|
||||
clearLogoLoadTimeout();
|
||||
}}
|
||||
/>
|
||||
{:else if fallbackEmoji && useAnimatedEmoji}
|
||||
<img
|
||||
class="brand-mark-animated-emoji loaded"
|
||||
src={animatedEmojiStaticFallback ? animatedEmojiFallbackSrc : animatedEmojiSrc}
|
||||
alt=""
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchpriority="high"
|
||||
on:error={() => {
|
||||
if (!animatedEmojiStaticFallback && animatedEmojiFallbackSrc) {
|
||||
animatedEmojiStaticFallback = true;
|
||||
} else {
|
||||
animatedEmojiError = true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if fallbackEmoji}
|
||||
<span
|
||||
class={cn("brand-mark-emoji", getEmojiFontClass(normalizedEmojiFont))}
|
||||
style="opacity: {fontLoaded ? 1 : 0}; transition: opacity 0.2s ease;"
|
||||
>
|
||||
{normalizedEmoji}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -270,11 +135,6 @@
|
||||
font-size: 4.375rem;
|
||||
}
|
||||
|
||||
.brand-mark img.brand-mark-animated-emoji {
|
||||
object-fit: contain;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.brand-mark.brand-mark-animate {
|
||||
animation: brand-mark-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@@ -316,55 +176,4 @@
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-mark-emoji {
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateY(0.02em);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.brand-mark-xl .brand-mark-emoji {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.brand-mark-lg .brand-mark-emoji {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.emoji-font-noto-color {
|
||||
font-family: "Noto Color Emoji", sans-serif;
|
||||
}
|
||||
|
||||
.emoji-font-noto-emoji {
|
||||
color: var(--accent);
|
||||
font-family: "Noto Emoji", sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.emoji-font-twemoji {
|
||||
font-family: "Twemoji Mozilla", sans-serif;
|
||||
}
|
||||
|
||||
.emoji-font-openmoji {
|
||||
font-family: "OpenMoji Color", sans-serif;
|
||||
}
|
||||
|
||||
.emoji-font-apple {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
}
|
||||
|
||||
.emoji-font-segoe {
|
||||
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
|
||||
}
|
||||
|
||||
.emoji-font-noto-local {
|
||||
font-family: "Noto Color Emoji", "Noto Emoji", sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,41 +26,18 @@ export function escapeHtml(value) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
// Default project logo, served by the backend when no custom logo is set.
|
||||
export const DEFAULT_LOGO_URL = "/webapp-default-logo.webp";
|
||||
|
||||
export function normalizeBrand(brand = {}) {
|
||||
return {
|
||||
title: String(brand.title || "/minishop").trim() || "/minishop",
|
||||
logoUrl: String(brand.logoUrl || "").trim(),
|
||||
emoji: String(brand.emoji || brand.logoEmoji || "🫥").trim() || "🫥",
|
||||
emojiFont: String(brand.emojiFont || brand.logoEmojiFont || "system").trim() || "system",
|
||||
};
|
||||
}
|
||||
|
||||
export function emojiToCodepoints(value) {
|
||||
return Array.from(String(value || "").trim())
|
||||
.map((char) => char.codePointAt(0)?.toString(16))
|
||||
.filter(Boolean)
|
||||
.join("_");
|
||||
}
|
||||
|
||||
export function animatedEmojiAssetUrls(emoji) {
|
||||
const codepoints = emojiToCodepoints(emoji);
|
||||
if (!codepoints) return { gif: "", webp: "" };
|
||||
return {
|
||||
gif: `/webapp-emoji/${codepoints}/512.gif`,
|
||||
webp: `/webapp-emoji/${codepoints}/512.webp`,
|
||||
logoUrl: String(brand.logoUrl || "").trim() || DEFAULT_LOGO_URL,
|
||||
};
|
||||
}
|
||||
|
||||
export function brandFaviconHref(brand = {}) {
|
||||
const normalizedBrand = normalizeBrand(brand);
|
||||
if (normalizedBrand.logoUrl) return normalizedBrand.logoUrl;
|
||||
|
||||
if (normalizedBrand.emojiFont === "noto-color-animated") {
|
||||
return animatedEmojiAssetUrls(normalizedBrand.emoji).gif;
|
||||
}
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="52">${escapeHtml(normalizedBrand.emoji)}</text></svg>`;
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
return normalizeBrand(brand).logoUrl;
|
||||
}
|
||||
|
||||
export function applyFavicon(brand = {}) {
|
||||
@@ -70,9 +47,7 @@ export function applyFavicon(brand = {}) {
|
||||
|
||||
const href = brandFaviconHref(brand);
|
||||
favicon.setAttribute("href", href);
|
||||
if (href.startsWith("data:image/svg+xml")) {
|
||||
favicon.setAttribute("type", "image/svg+xml");
|
||||
} else if (href.endsWith(".gif")) {
|
||||
if (href.endsWith(".gif")) {
|
||||
favicon.setAttribute("type", "image/gif");
|
||||
} else if (href.endsWith(".webp")) {
|
||||
favicon.setAttribute("type", "image/webp");
|
||||
|
||||
@@ -86404,26 +86404,6 @@ export const DEMO_DATASET = {
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_USE_EMOJI",
|
||||
type: "bool",
|
||||
section: "appearance",
|
||||
section_order: 2,
|
||||
subsection: null,
|
||||
label: "Использовать эмоджи-логотип",
|
||||
description: "",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_use_emoji_label",
|
||||
i18n_description_key: null,
|
||||
i18n_subsection_key: null,
|
||||
i18n_placeholder_key: null,
|
||||
placeholder: "",
|
||||
optional: true,
|
||||
secret: false,
|
||||
value: false,
|
||||
overridden: false,
|
||||
updated_at: "2026-05-16T01:03:23Z",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_URL",
|
||||
type: "url",
|
||||
@@ -86444,94 +86424,6 @@ export const DEMO_DATASET = {
|
||||
overridden: false,
|
||||
updated_at: "2026-05-16T01:03:23Z",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_EMOJI",
|
||||
type: "string",
|
||||
section: "appearance",
|
||||
section_order: 2,
|
||||
subsection: null,
|
||||
label: "Эмоджи-логотип",
|
||||
description: "",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_label",
|
||||
i18n_description_key: null,
|
||||
i18n_subsection_key: null,
|
||||
i18n_placeholder_key: "admin_settings_field_webapp_logo_emoji_placeholder",
|
||||
placeholder: "🫥",
|
||||
optional: true,
|
||||
secret: false,
|
||||
value: "🫥",
|
||||
overridden: false,
|
||||
updated_at: "2026-05-27T09:34:28Z",
|
||||
default: "🫥",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_EMOJI_FONT",
|
||||
type: "string",
|
||||
section: "appearance",
|
||||
section_order: 2,
|
||||
subsection: null,
|
||||
label: "Шрифт эмоджи-логотипа",
|
||||
description: "Выберите шрифт для отображения эмодзи-логотипа",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_label",
|
||||
i18n_description_key: "admin_settings_field_webapp_logo_emoji_font_description",
|
||||
i18n_subsection_key: null,
|
||||
i18n_placeholder_key: null,
|
||||
placeholder: "",
|
||||
optional: true,
|
||||
secret: false,
|
||||
choices: [
|
||||
{
|
||||
value: "system",
|
||||
label: "Системный (по умолчанию)",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_system",
|
||||
},
|
||||
{
|
||||
value: "noto-color",
|
||||
label: "Noto Color Emoji",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_color",
|
||||
},
|
||||
{
|
||||
value: "noto-color-animated",
|
||||
label: "Noto Color Emoji Animated",
|
||||
i18n_label_key:
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated",
|
||||
},
|
||||
{
|
||||
value: "noto-emoji",
|
||||
label: "Noto Emoji",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji",
|
||||
},
|
||||
{
|
||||
value: "twemoji",
|
||||
label: "Twitter Emoji",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_twemoji",
|
||||
},
|
||||
{
|
||||
value: "openmoji",
|
||||
label: "OpenMoji",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_openmoji",
|
||||
},
|
||||
{
|
||||
value: "apple",
|
||||
label: "Apple Color Emoji (local)",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_apple",
|
||||
},
|
||||
{
|
||||
value: "segoe",
|
||||
label: "Segoe UI Emoji (local)",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_segoe",
|
||||
},
|
||||
{
|
||||
value: "noto-local",
|
||||
label: "Noto Emoji (local)",
|
||||
i18n_label_key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_local",
|
||||
},
|
||||
],
|
||||
value: "system",
|
||||
overridden: false,
|
||||
updated_at: "2026-05-13T15:37:02Z",
|
||||
default: "system",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_FAVICON_USE_CUSTOM",
|
||||
type: "bool",
|
||||
@@ -113282,18 +113174,18 @@ export const DEMO_DATASET = {
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Файл, ссылка или подтвержденный emoji-логотип",
|
||||
fallback: "Файл, ссылка или подтвержденный emoji-логотип",
|
||||
effective: "Файл, ссылка или подтвержденный emoji-логотип",
|
||||
base: "Загрузите логотип файлом или по ссылке",
|
||||
fallback: "Загрузите логотип файлом или по ссылке",
|
||||
effective: "Загрузите логотип файлом или по ссылке",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "File, URL, or confirmed emoji logo",
|
||||
fallback: "Файл, ссылка или подтвержденный emoji-логотип",
|
||||
effective: "File, URL, or confirmed emoji logo",
|
||||
base: "Upload a logo as a file or from a URL",
|
||||
fallback: "Загрузите логотип файлом или по ссылке",
|
||||
effective: "Upload a logo as a file or from a URL",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
@@ -113325,30 +113217,6 @@ export const DEMO_DATASET = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_appearance_emoji_font",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Шрифт emoji",
|
||||
fallback: "Шрифт emoji",
|
||||
effective: "Шрифт emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Emoji font",
|
||||
fallback: "Шрифт emoji",
|
||||
effective: "Emoji font",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_appearance_favicon_upload_failed",
|
||||
audience: "internal",
|
||||
@@ -113733,30 +113601,6 @@ export const DEMO_DATASET = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_appearance_use_emoji_logo",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Использовать emoji-логотип",
|
||||
fallback: "Использовать emoji-логотип",
|
||||
effective: "Использовать emoji-логотип",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Use emoji logo",
|
||||
fallback: "Использовать emoji-логотип",
|
||||
effective: "Use emoji logo",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_privacy_policy_url_label",
|
||||
audience: "internal",
|
||||
@@ -114021,294 +113865,6 @@ export const DEMO_DATASET = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_apple",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Apple Color Emoji (local)",
|
||||
fallback: "Apple Color Emoji (local)",
|
||||
effective: "Apple Color Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Apple Color Emoji (local)",
|
||||
fallback: "Apple Color Emoji (local)",
|
||||
effective: "Apple Color Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_color",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Noto Color Emoji",
|
||||
fallback: "Noto Color Emoji",
|
||||
effective: "Noto Color Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Noto Color Emoji",
|
||||
fallback: "Noto Color Emoji",
|
||||
effective: "Noto Color Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Noto Color Emoji Animated",
|
||||
fallback: "Noto Color Emoji Animated",
|
||||
effective: "Noto Color Emoji Animated",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Noto Color Emoji Animated",
|
||||
fallback: "Noto Color Emoji Animated",
|
||||
effective: "Noto Color Emoji Animated",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Noto Emoji",
|
||||
fallback: "Noto Emoji",
|
||||
effective: "Noto Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Noto Emoji",
|
||||
fallback: "Noto Emoji",
|
||||
effective: "Noto Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_noto_local",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Noto Emoji (local)",
|
||||
fallback: "Noto Emoji (local)",
|
||||
effective: "Noto Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Noto Emoji (local)",
|
||||
fallback: "Noto Emoji (local)",
|
||||
effective: "Noto Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_openmoji",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "OpenMoji",
|
||||
fallback: "OpenMoji",
|
||||
effective: "OpenMoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "OpenMoji",
|
||||
fallback: "OpenMoji",
|
||||
effective: "OpenMoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_segoe",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Segoe UI Emoji (local)",
|
||||
fallback: "Segoe UI Emoji (local)",
|
||||
effective: "Segoe UI Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Segoe UI Emoji (local)",
|
||||
fallback: "Segoe UI Emoji (local)",
|
||||
effective: "Segoe UI Emoji (local)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_system",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Системный (по умолчанию)",
|
||||
fallback: "Системный (по умолчанию)",
|
||||
effective: "Системный (по умолчанию)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "System (default)",
|
||||
fallback: "Системный (по умолчанию)",
|
||||
effective: "System (default)",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_choice_twemoji",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Twitter Emoji",
|
||||
fallback: "Twitter Emoji",
|
||||
effective: "Twitter Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Twitter Emoji",
|
||||
fallback: "Twitter Emoji",
|
||||
effective: "Twitter Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_description",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Выберите шрифт для отображения эмодзи-логотипа.",
|
||||
fallback: "Выберите шрифт для отображения эмодзи-логотипа.",
|
||||
effective: "Выберите шрифт для отображения эмодзи-логотипа.",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Choose the font used to render the emoji logo.",
|
||||
fallback: "Выберите шрифт для отображения эмодзи-логотипа.",
|
||||
effective: "Choose the font used to render the emoji logo.",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_font_label",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Шрифт эмоджи-логотипа",
|
||||
fallback: "Шрифт эмоджи-логотипа",
|
||||
effective: "Шрифт эмоджи-логотипа",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Emoji logo font",
|
||||
fallback: "Шрифт эмоджи-логотипа",
|
||||
effective: "Emoji logo font",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_emoji_label",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Эмоджи-логотип",
|
||||
fallback: "Эмоджи-логотип",
|
||||
effective: "Эмоджи-логотип",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "WebApp Logo Emoji",
|
||||
fallback: "Эмоджи-логотип",
|
||||
effective: "WebApp Logo Emoji",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_favicon_url_label",
|
||||
audience: "internal",
|
||||
@@ -114357,30 +113913,6 @@ export const DEMO_DATASET = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_logo_use_emoji_label",
|
||||
audience: "internal",
|
||||
values: {
|
||||
ru: {
|
||||
base: "Использовать эмоджи-логотип",
|
||||
fallback: "Использовать эмоджи-логотип",
|
||||
effective: "Использовать эмоджи-логотип",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
en: {
|
||||
base: "Use emoji logo",
|
||||
fallback: "Использовать эмоджи-логотип",
|
||||
effective: "Use emoji logo",
|
||||
override: "",
|
||||
overridden: false,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "admin_settings_field_webapp_primary_color_label",
|
||||
audience: "internal",
|
||||
|
||||
@@ -652,9 +652,6 @@ function demoSettingsSections(clone) {
|
||||
function applyDemoSettingToMock(key, value) {
|
||||
if (key === "WEBAPP_TITLE") DEV_MOCK.config.title = value || "";
|
||||
if (key === "WEBAPP_LOGO_URL") DEV_MOCK.config.logoUrl = value || "";
|
||||
if (key === "WEBAPP_LOGO_USE_EMOJI") DEV_MOCK.config.logoUseEmoji = Boolean(value);
|
||||
if (key === "WEBAPP_LOGO_EMOJI") DEV_MOCK.config.logoEmoji = value || "";
|
||||
if (key === "WEBAPP_LOGO_EMOJI_FONT") DEV_MOCK.config.logoEmojiFont = value || "system";
|
||||
if (key === "WEBAPP_FAVICON_URL" || key === "WEBAPP_LOGO_FAVICON_URL") {
|
||||
DEV_MOCK.config.faviconUrl = value || DEV_MOCK.config.faviconUrl || "";
|
||||
}
|
||||
@@ -1609,13 +1606,6 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_LOGO_URL")) {
|
||||
DEV_MOCK.config.logoUrl = updates.WEBAPP_LOGO_URL || "";
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_LOGO_USE_EMOJI")) {
|
||||
DEV_MOCK.config.logoUseEmoji = Boolean(updates.WEBAPP_LOGO_USE_EMOJI);
|
||||
}
|
||||
if (updates.WEBAPP_LOGO_EMOJI) DEV_MOCK.config.logoEmoji = updates.WEBAPP_LOGO_EMOJI;
|
||||
if (updates.WEBAPP_LOGO_EMOJI_FONT) {
|
||||
DEV_MOCK.config.logoEmojiFont = updates.WEBAPP_LOGO_EMOJI_FONT;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_FAVICON_URL")) {
|
||||
DEV_MOCK.config.faviconUrl = updates.WEBAPP_FAVICON_URL || "";
|
||||
}
|
||||
@@ -1742,13 +1732,6 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
id: "appearance",
|
||||
order: 2,
|
||||
fields: [
|
||||
{
|
||||
key: "WEBAPP_LOGO_USE_EMOJI",
|
||||
type: "bool",
|
||||
section: "appearance",
|
||||
label: "Emoji logo",
|
||||
value: Boolean(DEV_MOCK.config.logoUseEmoji),
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_URL",
|
||||
type: "url",
|
||||
@@ -1756,25 +1739,6 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
label: "URL логотипа",
|
||||
value: DEV_MOCK.config.logoUrl || "",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_EMOJI",
|
||||
type: "string",
|
||||
section: "appearance",
|
||||
label: "Emoji",
|
||||
value: DEV_MOCK.config.logoEmoji || "🫥",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_EMOJI_FONT",
|
||||
type: "string",
|
||||
section: "appearance",
|
||||
label: "Emoji font",
|
||||
value: DEV_MOCK.config.logoEmojiFont || "system",
|
||||
choices: [
|
||||
{ value: "system", label: "Системный" },
|
||||
{ value: "noto-color", label: "Noto Color Emoji" },
|
||||
{ value: "noto-color-animated", label: "Noto Color Emoji Animated" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_FAVICON_USE_CUSTOM",
|
||||
type: "bool",
|
||||
|
||||
@@ -217,9 +217,6 @@ export const DEV_MOCK = {
|
||||
title: "/minishop",
|
||||
primaryColor: "#00fe7a",
|
||||
logoUrl: "/webapp-default-logo.webp",
|
||||
logoUseEmoji: false,
|
||||
logoEmoji: "🫥",
|
||||
logoEmojiFont: "system",
|
||||
faviconUrl: "/webapp-favicon/19b2a242e5b7bc2d/icon-180.png",
|
||||
faviconUseCustom: false,
|
||||
trialEnabled: true,
|
||||
|
||||
@@ -275,25 +275,6 @@
|
||||
"overridden": false,
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"key": "WEBAPP_LOGO_USE_EMOJI",
|
||||
"type": "bool",
|
||||
"section": "appearance",
|
||||
"section_order": 2,
|
||||
"subsection": null,
|
||||
"label": "Использовать эмоджи-логотип",
|
||||
"description": "",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_use_emoji_label",
|
||||
"i18n_description_key": null,
|
||||
"i18n_subsection_key": null,
|
||||
"i18n_placeholder_key": null,
|
||||
"placeholder": "",
|
||||
"optional": true,
|
||||
"secret": false,
|
||||
"value": "",
|
||||
"overridden": false,
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"key": "WEBAPP_LOGO_URL",
|
||||
"type": "url",
|
||||
@@ -313,91 +294,6 @@
|
||||
"overridden": false,
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"key": "WEBAPP_LOGO_EMOJI",
|
||||
"type": "string",
|
||||
"section": "appearance",
|
||||
"section_order": 2,
|
||||
"subsection": null,
|
||||
"label": "Эмоджи-логотип",
|
||||
"description": "",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_label",
|
||||
"i18n_description_key": null,
|
||||
"i18n_subsection_key": null,
|
||||
"i18n_placeholder_key": "admin_settings_field_webapp_logo_emoji_placeholder",
|
||||
"placeholder": "🫥",
|
||||
"optional": true,
|
||||
"secret": false,
|
||||
"value": "",
|
||||
"overridden": false,
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"key": "WEBAPP_LOGO_EMOJI_FONT",
|
||||
"type": "string",
|
||||
"section": "appearance",
|
||||
"section_order": 2,
|
||||
"subsection": null,
|
||||
"label": "Шрифт эмоджи-логотипа",
|
||||
"description": "Выберите шрифт для отображения эмодзи-логотипа",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_label",
|
||||
"i18n_description_key": "admin_settings_field_webapp_logo_emoji_font_description",
|
||||
"i18n_subsection_key": null,
|
||||
"i18n_placeholder_key": null,
|
||||
"placeholder": "",
|
||||
"optional": true,
|
||||
"secret": false,
|
||||
"choices": [
|
||||
{
|
||||
"value": "system",
|
||||
"label": "Системный (по умолчанию)",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_system"
|
||||
},
|
||||
{
|
||||
"value": "noto-color",
|
||||
"label": "Noto Color Emoji",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_noto_color"
|
||||
},
|
||||
{
|
||||
"value": "noto-color-animated",
|
||||
"label": "Noto Color Emoji Animated",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated"
|
||||
},
|
||||
{
|
||||
"value": "noto-emoji",
|
||||
"label": "Noto Emoji",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji"
|
||||
},
|
||||
{
|
||||
"value": "twemoji",
|
||||
"label": "Twitter Emoji",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_twemoji"
|
||||
},
|
||||
{
|
||||
"value": "openmoji",
|
||||
"label": "OpenMoji",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_openmoji"
|
||||
},
|
||||
{
|
||||
"value": "apple",
|
||||
"label": "Apple Color Emoji (local)",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_apple"
|
||||
},
|
||||
{
|
||||
"value": "segoe",
|
||||
"label": "Segoe UI Emoji (local)",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_segoe"
|
||||
},
|
||||
{
|
||||
"value": "noto-local",
|
||||
"label": "Noto Emoji (local)",
|
||||
"i18n_label_key": "admin_settings_field_webapp_logo_emoji_font_choice_noto_local"
|
||||
}
|
||||
],
|
||||
"value": "",
|
||||
"overridden": false,
|
||||
"updated_at": null
|
||||
},
|
||||
{
|
||||
"key": "WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"type": "bool",
|
||||
|
||||
+1
-16
@@ -1574,20 +1574,7 @@
|
||||
"admin_settings_field_subscription_mini_app_url_label": "Public Mini App URL",
|
||||
"admin_settings_field_subscription_mini_app_url_description": "For example, https://app.example.com/.",
|
||||
"admin_settings_field_webapp_primary_color_label": "WebApp Primary Color",
|
||||
"admin_settings_field_webapp_logo_use_emoji_label": "Use emoji logo",
|
||||
"admin_settings_field_webapp_logo_url_label": "WebApp Logo URL",
|
||||
"admin_settings_field_webapp_logo_emoji_label": "WebApp Logo Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_label": "Emoji logo font",
|
||||
"admin_settings_field_webapp_logo_emoji_font_description": "Choose the font used to render the emoji logo.",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_system": "System (default)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_color": "Noto Color Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated": "Noto Color Emoji Animated",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji": "Noto Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_twemoji": "Twitter Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_openmoji": "OpenMoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_apple": "Apple Color Emoji (local)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_segoe": "Segoe UI Emoji (local)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_local": "Noto Emoji (local)",
|
||||
"admin_settings_field_webapp_favicon_use_custom_label": "Use separate favicon",
|
||||
"admin_settings_field_webapp_favicon_url_label": "Separate favicon URL",
|
||||
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon from logo",
|
||||
@@ -1962,9 +1949,8 @@
|
||||
"admin_actions": "Actions",
|
||||
"admin_ad_created": "Campaign created",
|
||||
"admin_ad_deleted": "Campaign deleted",
|
||||
"admin_appearance_brand_sub": "File, URL, or confirmed emoji logo",
|
||||
"admin_appearance_brand_sub": "Upload a logo as a file or from a URL",
|
||||
"admin_appearance_brand_title": "Logo",
|
||||
"admin_appearance_emoji_font": "Emoji font",
|
||||
"admin_appearance_favicon_upload_failed": "Failed to upload favicon",
|
||||
"admin_appearance_favicon_upload_file": "Upload favicon",
|
||||
"admin_appearance_favicon_upload_url": "From URL",
|
||||
@@ -1980,7 +1966,6 @@
|
||||
"admin_appearance_themes_sub": "Global theme, accent color, and preview",
|
||||
"admin_appearance_themes_title": "Themes",
|
||||
"admin_appearance_use_custom_favicon": "Use a separate favicon",
|
||||
"admin_appearance_use_emoji_logo": "Use emoji logo",
|
||||
"admin_backups_create": "Create backup",
|
||||
"admin_backups_create_done": "Backup created",
|
||||
"admin_backups_create_failed": "Failed to create backup",
|
||||
|
||||
+1
-16
@@ -1574,20 +1574,7 @@
|
||||
"admin_settings_field_subscription_mini_app_url_label": "Публичный URL Mini App",
|
||||
"admin_settings_field_subscription_mini_app_url_description": "Например, https://app.example.com/.",
|
||||
"admin_settings_field_webapp_primary_color_label": "Основной цвет",
|
||||
"admin_settings_field_webapp_logo_use_emoji_label": "Использовать эмоджи-логотип",
|
||||
"admin_settings_field_webapp_logo_url_label": "URL логотипа",
|
||||
"admin_settings_field_webapp_logo_emoji_label": "Эмоджи-логотип",
|
||||
"admin_settings_field_webapp_logo_emoji_font_label": "Шрифт эмоджи-логотипа",
|
||||
"admin_settings_field_webapp_logo_emoji_font_description": "Выберите шрифт для отображения эмодзи-логотипа.",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_system": "Системный (по умолчанию)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_color": "Noto Color Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_color_animated": "Noto Color Emoji Animated",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_emoji": "Noto Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_twemoji": "Twitter Emoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_openmoji": "OpenMoji",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_apple": "Apple Color Emoji (local)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_segoe": "Segoe UI Emoji (local)",
|
||||
"admin_settings_field_webapp_logo_emoji_font_choice_noto_local": "Noto Emoji (local)",
|
||||
"admin_settings_field_webapp_favicon_use_custom_label": "Использовать отдельную favicon",
|
||||
"admin_settings_field_webapp_favicon_url_label": "URL отдельной favicon",
|
||||
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon из логотипа",
|
||||
@@ -1962,9 +1949,8 @@
|
||||
"admin_actions": "Действия",
|
||||
"admin_ad_created": "Кампания создана",
|
||||
"admin_ad_deleted": "Кампания удалена",
|
||||
"admin_appearance_brand_sub": "Файл, ссылка или подтвержденный emoji-логотип",
|
||||
"admin_appearance_brand_sub": "Загрузите логотип файлом или по ссылке",
|
||||
"admin_appearance_brand_title": "Логотип",
|
||||
"admin_appearance_emoji_font": "Шрифт emoji",
|
||||
"admin_appearance_favicon_upload_failed": "Не удалось загрузить favicon",
|
||||
"admin_appearance_favicon_upload_file": "Загрузить favicon",
|
||||
"admin_appearance_favicon_upload_url": "По ссылке",
|
||||
@@ -1980,7 +1966,6 @@
|
||||
"admin_appearance_themes_sub": "Глобальная тема, accent color и предпросмотр",
|
||||
"admin_appearance_themes_title": "Темы",
|
||||
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
|
||||
"admin_appearance_use_emoji_logo": "Использовать emoji-логотип",
|
||||
"admin_backups_create": "Создать бэкап",
|
||||
"admin_backups_create_done": "Бэкап создан",
|
||||
"admin_backups_create_failed": "Не удалось создать бэкап",
|
||||
|
||||
@@ -35,7 +35,6 @@ def _settings(default_language: str = "ru"):
|
||||
DEFAULT_LANGUAGE=default_language,
|
||||
EMAIL_CODE_TTL_SECONDS=600,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||
WEBAPP_TITLE="Mini Shop",
|
||||
)
|
||||
|
||||
@@ -206,7 +206,6 @@ def test_email_templates_use_override_only_language_without_base_locale_file(tmp
|
||||
DEFAULT_LANGUAGE="ru",
|
||||
EMAIL_CODE_TTL_SECONDS=600,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||
WEBAPP_TITLE="Remnawave",
|
||||
)
|
||||
|
||||
@@ -342,7 +342,6 @@ class WebAppSecurityTests(unittest.IsolatedAsyncioTestCase):
|
||||
DEFAULT_LANGUAGE="ru",
|
||||
EMAIL_CODE_TTL_SECONDS=600,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||
WEBAPP_TITLE="Remnawave",
|
||||
)
|
||||
|
||||
@@ -127,9 +127,6 @@ class SettingsTests(unittest.TestCase):
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
WEBAPP_PRIMARY_COLOR="#ff0000",
|
||||
WEBAPP_LOGO_URL="https://cdn.example.com/logo.png",
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🔥",
|
||||
WEBAPP_LOGO_EMOJI_FONT="twemoji",
|
||||
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||
WEBAPP_FAVICON_URL="https://cdn.example.com/favicon.png",
|
||||
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.png",
|
||||
@@ -137,9 +134,6 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(settings.WEBAPP_PRIMARY_COLOR, "#00fe7a")
|
||||
self.assertIsNone(settings.WEBAPP_LOGO_URL)
|
||||
self.assertFalse(settings.WEBAPP_LOGO_USE_EMOJI)
|
||||
self.assertEqual(settings.WEBAPP_LOGO_EMOJI, "🫥")
|
||||
self.assertEqual(settings.WEBAPP_LOGO_EMOJI_FONT, "system")
|
||||
self.assertFalse(settings.WEBAPP_FAVICON_USE_CUSTOM)
|
||||
self.assertIsNone(settings.WEBAPP_FAVICON_URL)
|
||||
self.assertIsNone(settings.WEBAPP_LOGO_FAVICON_URL)
|
||||
@@ -202,7 +196,6 @@ class SettingsTests(unittest.TestCase):
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
)
|
||||
settings.WEBAPP_LOGO_URL = "/webapp-uploaded-logo/logo-1111111111111111.png"
|
||||
settings.WEBAPP_LOGO_USE_EMOJI = False
|
||||
settings.WEBAPP_LOGO_FAVICON_URL = "/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png"
|
||||
settings.WEBAPP_FAVICON_USE_CUSTOM = True
|
||||
settings.WEBAPP_FAVICON_URL = "/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png"
|
||||
|
||||
@@ -74,7 +74,6 @@ def _settings(**overrides):
|
||||
SUBSCRIPTION_MINI_APP_URL="https://app.example.test/",
|
||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||
WEBAPP_TITLE="Minishop",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_LOGO_URL="",
|
||||
email_auth_configured=True,
|
||||
**overrides,
|
||||
|
||||
@@ -38,7 +38,6 @@ def _settings(**overrides):
|
||||
"WEBAPP_PRIMARY_COLOR": "#00fe7a",
|
||||
"WEBAPP_TITLE": "Mini Shop",
|
||||
"WEBAPP_LOGO_URL": "",
|
||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
@@ -351,7 +351,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
|
||||
def test_default_webapp_logo_is_used_without_admin_upload(self):
|
||||
settings = SimpleNamespace(WEBAPP_LOGO_USE_EMOJI=False, WEBAPP_LOGO_URL="")
|
||||
settings = SimpleNamespace(WEBAPP_LOGO_URL="")
|
||||
|
||||
self.assertEqual(
|
||||
subscription_webapp._resolve_webapp_logo_url(settings),
|
||||
@@ -360,7 +360,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_webapp_logo_route_serves_configured_uploaded_logo(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
)
|
||||
request = SimpleNamespace(app={"settings": settings})
|
||||
@@ -375,14 +374,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(response.content_type, "image/png")
|
||||
self.assertEqual(response.body, b"logo")
|
||||
|
||||
def test_webapp_logo_is_hidden_when_emoji_logo_is_enabled(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-abcdef1234567890.png",
|
||||
)
|
||||
|
||||
self.assertEqual(subscription_webapp._resolve_webapp_logo_url(settings), "")
|
||||
|
||||
def test_custom_webapp_favicon_takes_precedence(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||
@@ -470,7 +461,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_ENABLED=True,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||
WEBAPP_FAVICON_URL=f"/webapp-favicon/{digest}/icon-180.png",
|
||||
WEBAPP_LOGO_FAVICON_URL="",
|
||||
@@ -491,7 +481,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_ENABLED=True,
|
||||
WEBAPP_LOGO_URL="",
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_FAVICON_USE_CUSTOM=False,
|
||||
WEBAPP_FAVICON_URL="",
|
||||
WEBAPP_LOGO_FAVICON_URL="",
|
||||
@@ -592,16 +581,14 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
(favicons / "aaaaaaaaaaaaaaaa" / "icon-180.png").write_bytes(b"keep")
|
||||
(favicons / "bbbbbbbbbbbbbbbb" / "icon-180.png").write_bytes(b"keep")
|
||||
(favicons / "cccccccccccccccc" / "icon-180.png").write_bytes(b"remove")
|
||||
(emoji / "1f929.512.gif").write_bytes(b"keep")
|
||||
(emoji / "1f929.512.webp").write_bytes(b"keep")
|
||||
# Emoji logos were removed; any leftover emoji cache files are purged.
|
||||
(emoji / "1f929.512.gif").write_bytes(b"remove")
|
||||
(emoji / "1f929.512.webp").write_bytes(b"remove")
|
||||
(emoji / "1f525.512.gif").write_bytes(b"remove")
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
WEBAPP_FAVICON_URL="/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png",
|
||||
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png",
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🤩",
|
||||
WEBAPP_LOGO_EMOJI_FONT="noto-color-animated",
|
||||
)
|
||||
|
||||
with (
|
||||
@@ -616,8 +603,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertTrue((favicons / "aaaaaaaaaaaaaaaa").exists())
|
||||
self.assertTrue((favicons / "bbbbbbbbbbbbbbbb").exists())
|
||||
self.assertFalse((favicons / "cccccccccccccccc").exists())
|
||||
self.assertTrue((emoji / "1f929.512.gif").exists())
|
||||
self.assertTrue((emoji / "1f929.512.webp").exists())
|
||||
self.assertFalse((emoji / "1f929.512.gif").exists())
|
||||
self.assertFalse((emoji / "1f929.512.webp").exists())
|
||||
self.assertFalse((emoji / "1f525.512.gif").exists())
|
||||
|
||||
async def test_persist_appearance_upload_writes_overrides_and_clears_caches(self):
|
||||
@@ -632,7 +619,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
updates = {
|
||||
"WEBAPP_LOGO_URL": "/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
||||
}
|
||||
|
||||
with (
|
||||
@@ -709,12 +695,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(bumped.theme_by_key("custom").assets_version, 4)
|
||||
|
||||
def test_animated_emoji_asset_path_uses_same_origin_route(self):
|
||||
self.assertEqual(
|
||||
subscription_webapp._webapp_animated_emoji_asset_path("🤩"),
|
||||
"/webapp-emoji/1f929/512.gif",
|
||||
)
|
||||
|
||||
def test_telegram_avatar_url_uses_same_origin_account_route(self):
|
||||
avatar = SimpleNamespace(
|
||||
user_id=123,
|
||||
@@ -1323,7 +1303,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
logo_url = "https://cdn.example.com/logo.png"
|
||||
logo = (b"cached-logo", "image/png")
|
||||
app = {
|
||||
"settings": SimpleNamespace(WEBAPP_LOGO_URL=logo_url, WEBAPP_LOGO_USE_EMOJI=False),
|
||||
"settings": SimpleNamespace(WEBAPP_LOGO_URL=logo_url),
|
||||
"webapp_logo_cache": None,
|
||||
"webapp_logo_cache_lock": asyncio.Lock(),
|
||||
}
|
||||
@@ -1343,44 +1323,3 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
fetch_logo.assert_not_called()
|
||||
self.assertEqual(app["webapp_logo_cache"], (logo_url, logo[0], logo[1]))
|
||||
|
||||
def test_webapp_animated_emoji_disk_cache_roundtrip(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with patch.object(webapp_assets, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)):
|
||||
subscription_webapp._write_webapp_animated_emoji_to_disk(
|
||||
"1f929",
|
||||
"gif",
|
||||
(b"gif-bytes", "image/gif"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
subscription_webapp._read_webapp_animated_emoji_from_disk("1f929", "gif"),
|
||||
(b"gif-bytes", "image/gif"),
|
||||
)
|
||||
|
||||
async def test_warm_webapp_animated_emoji_cache_uses_disk_cache(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
app = {
|
||||
"settings": SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🤩",
|
||||
WEBAPP_LOGO_EMOJI_FONT="noto-color-animated",
|
||||
),
|
||||
"webapp_emoji_cache": {},
|
||||
"webapp_emoji_cache_lock": asyncio.Lock(),
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(webapp_assets, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)),
|
||||
patch.object(webapp_assets, "_fetch_webapp_animated_emoji") as fetch_emoji,
|
||||
):
|
||||
subscription_webapp._write_webapp_animated_emoji_to_disk(
|
||||
"1f929",
|
||||
"gif",
|
||||
(b"cached-gif", "image/gif"),
|
||||
)
|
||||
|
||||
await subscription_webapp._warm_webapp_animated_emoji_cache(app)
|
||||
|
||||
fetch_emoji.assert_not_called()
|
||||
self.assertEqual(app["webapp_emoji_cache"]["1f929:gif"], (b"cached-gif", "image/gif"))
|
||||
|
||||
@@ -90,7 +90,6 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("GET", "/robots.txt"): "robots_txt_route",
|
||||
("GET", "/webapp-logo"): "webapp_logo_route",
|
||||
("GET", "/webapp-uploaded-logo/{filename}"): "webapp_uploaded_logo_route",
|
||||
("GET", "/webapp-emoji/{codepoints}/512.{ext}"): "webapp_animated_emoji_route",
|
||||
("GET", "/subscription_webapp.css"): "css_asset_route",
|
||||
("GET", "/subscription_webapp_admin.css"): "admin_css_asset_route",
|
||||
("GET", "/webapp-theme-css/{path}"): "theme_css_asset_route",
|
||||
|
||||
Reference in New Issue
Block a user