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).
|
Для каталога тарифов используется `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
|
```bash
|
||||||
mkdir -p data/themes data/webapp-logo data/webapp-emoji
|
mkdir -p data/themes data/webapp-logo
|
||||||
touch data/locales-overrides.json
|
touch data/locales-overrides.json
|
||||||
chown -R 10001:10001 data
|
chown -R 10001:10001 data
|
||||||
chmod -R u+rwX 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
|
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:
|
def prune_unused_appearance_assets(settings: Settings) -> None:
|
||||||
keep_logos = {
|
keep_logos = {
|
||||||
filename
|
filename
|
||||||
@@ -156,15 +152,6 @@ def prune_unused_appearance_assets(settings: Settings) -> None:
|
|||||||
]
|
]
|
||||||
if digest
|
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-*"):
|
for path in WEBAPP_UPLOADED_LOGO_DIR.glob("logo-*"):
|
||||||
if path.is_file() and path.name not in keep_logos:
|
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:
|
except OSError:
|
||||||
logger.warning("Failed to remove unused webapp favicon set %s", path, exc_info=True)
|
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.*"):
|
for path in WEBAPP_EMOJI_CACHE_DIR.glob("*.512.*"):
|
||||||
if path.is_file() and not any(
|
if path.is_file():
|
||||||
path.name.startswith(prefix) for prefix in keep_emoji_prefixes
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
path.unlink()
|
path.unlink()
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -389,7 +375,6 @@ async def admin_appearance_logo_upload_route(request: web.Request) -> web.Respon
|
|||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
"WEBAPP_LOGO_URL": logo_url,
|
"WEBAPP_LOGO_URL": logo_url,
|
||||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
|
||||||
**(
|
**(
|
||||||
{"WEBAPP_LOGO_FAVICON_URL": favicon_payload["favicon_url"]}
|
{"WEBAPP_LOGO_FAVICON_URL": favicon_payload["favicon_url"]}
|
||||||
if favicon_payload.get("favicon_url")
|
if favicon_payload.get("favicon_url")
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ WEBAPP_APPEARANCE_SETTING_KEYS = frozenset(
|
|||||||
{
|
{
|
||||||
"WEBAPP_TITLE",
|
"WEBAPP_TITLE",
|
||||||
"WEBAPP_LOGO_URL",
|
"WEBAPP_LOGO_URL",
|
||||||
"WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI_FONT",
|
|
||||||
"WEBAPP_FAVICON_URL",
|
"WEBAPP_FAVICON_URL",
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"WEBAPP_LOGO_FAVICON_URL",
|
"WEBAPP_LOGO_FAVICON_URL",
|
||||||
|
|||||||
@@ -181,27 +181,7 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
|||||||
SettingField(
|
SettingField(
|
||||||
"WEBAPP_PRIMARY_COLOR", "color", "appearance", "Основной цвет", placeholder="#00fe7a"
|
"WEBAPP_PRIMARY_COLOR", "color", "appearance", "Основной цвет", placeholder="#00fe7a"
|
||||||
),
|
),
|
||||||
SettingField("WEBAPP_LOGO_USE_EMOJI", "bool", "appearance", "Использовать эмоджи-логотип"),
|
|
||||||
SettingField("WEBAPP_LOGO_URL", "url", "appearance", "URL логотипа"),
|
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(
|
SettingField(
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"bool",
|
"bool",
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ WEBAPP_DEFAULT_LOGO_PATH = "/webapp-default-logo.webp"
|
|||||||
WEBAPP_DEFAULT_FAVICON_DIGEST = "19b2a242e5b7bc2d"
|
WEBAPP_DEFAULT_FAVICON_DIGEST = "19b2a242e5b7bc2d"
|
||||||
WEBAPP_DEFAULT_FAVICON_DIR = WEBAPP_DEFAULT_BRAND_DIR / "favicons" / WEBAPP_DEFAULT_FAVICON_DIGEST
|
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_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_CONFIG_PLACEHOLDER = "<!-- WEBAPP_CONFIG_SCRIPT -->"
|
||||||
WEBAPP_I18N_PLACEHOLDER = "<!-- WEBAPP_I18N_SCRIPT -->"
|
WEBAPP_I18N_PLACEHOLDER = "<!-- WEBAPP_I18N_SCRIPT -->"
|
||||||
WEBAPP_JS_PLACEHOLDER = "<!-- WEBAPP_JS_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_WINDOW_SECONDS = 60
|
||||||
WEBAPP_RATE_LIMIT_MAX_REQUESTS = 30
|
WEBAPP_RATE_LIMIT_MAX_REQUESTS = 30
|
||||||
WEBAPP_LOGO_MAX_BYTES = 2 * 1024 * 1024
|
WEBAPP_LOGO_MAX_BYTES = 2 * 1024 * 1024
|
||||||
WEBAPP_EMOJI_MAX_BYTES = 4 * 1024 * 1024
|
|
||||||
WEBAPP_THEME_CSS_MAX_BYTES = 512 * 1024
|
WEBAPP_THEME_CSS_MAX_BYTES = 512 * 1024
|
||||||
WEBAPP_THEME_ASSET_MAX_BYTES = 1024 * 1024
|
WEBAPP_THEME_ASSET_MAX_BYTES = 1024 * 1024
|
||||||
WEBAPP_THEME_ASSET_CONTENT_TYPES = {
|
WEBAPP_THEME_ASSET_CONTENT_TYPES = {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ def create_subscription_webapp_application(
|
|||||||
async def _startup(app_obj: web.Application) -> None:
|
async def _startup(app_obj: web.Application) -> None:
|
||||||
await _ensure_shared_http_session()
|
await _ensure_shared_http_session()
|
||||||
await _warm_webapp_logo_cache(app_obj)
|
await _warm_webapp_logo_cache(app_obj)
|
||||||
await _warm_webapp_animated_emoji_cache(app_obj)
|
|
||||||
await warm_subscription_guides_config(app_obj)
|
await warm_subscription_guides_config(app_obj)
|
||||||
|
|
||||||
async def _shutdown(app_obj: web.Application) -> None:
|
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:
|
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()
|
raw_logo_url = (getattr(settings, "WEBAPP_LOGO_URL", None) or "").strip()
|
||||||
if not raw_logo_url:
|
if not raw_logo_url:
|
||||||
return WEBAPP_DEFAULT_LOGO_PATH
|
return WEBAPP_DEFAULT_LOGO_PATH
|
||||||
@@ -303,29 +300,8 @@ def _uploaded_webapp_logo_response(filename: str) -> web.Response:
|
|||||||
return 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:
|
async def webapp_logo_route(request: web.Request) -> web.Response:
|
||||||
settings: Settings = request.app["settings"]
|
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()
|
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||||
if not raw_logo_url:
|
if not raw_logo_url:
|
||||||
raise web.HTTPNotFound(text="webapp_logo_not_configured")
|
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)
|
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:
|
async def _warm_webapp_logo_cache(app: web.Application) -> None:
|
||||||
settings: Settings = app["settings"]
|
settings: Settings = app["settings"]
|
||||||
if getattr(settings, "WEBAPP_LOGO_USE_EMOJI", False):
|
|
||||||
return
|
|
||||||
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
|
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||||
if not raw_logo_url or not _is_proxyable_webapp_logo_url(raw_logo_url):
|
if not raw_logo_url or not _is_proxyable_webapp_logo_url(raw_logo_url):
|
||||||
return
|
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]]:
|
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)
|
disk_logo = await asyncio.to_thread(_read_webapp_logo_from_disk, logo_url)
|
||||||
if disk_logo:
|
if disk_logo:
|
||||||
@@ -1144,9 +986,6 @@ def _build_webapp_bootstrap_payload(request: web.Request) -> Dict[str, Any]:
|
|||||||
"themesDir": settings.WEBAPP_THEMES_DIR,
|
"themesDir": settings.WEBAPP_THEMES_DIR,
|
||||||
"themePreviewKey": preview_key,
|
"themePreviewKey": preview_key,
|
||||||
"logoUrl": cached["logo_url"],
|
"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"],
|
"faviconUrl": cached["favicon_url"],
|
||||||
"faviconUseCustom": bool(settings.WEBAPP_FAVICON_USE_CUSTOM),
|
"faviconUseCustom": bool(settings.WEBAPP_FAVICON_USE_CUSTOM),
|
||||||
"apiBase": "/api",
|
"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>',
|
f'<script src="/{_resolve_webapp_js_asset_name()}" defer></script>',
|
||||||
)
|
)
|
||||||
brand_asset_url = cached["logo_url"]
|
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:
|
if brand_asset_url:
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
"</head>",
|
"</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_.-]+}}",
|
rf"{WEBAPP_FAVICON_PATH}/{{digest:[0-9a-f]{{16}}}}/{{filename:[A-Za-z0-9_.-]+}}",
|
||||||
webapp_favicon_route,
|
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.{asset_hash:[0-9a-f]{8}}.css", css_asset_route)
|
||||||
app.router.add_get("/subscription_webapp.css", css_asset_route)
|
app.router.add_get("/subscription_webapp.css", css_asset_route)
|
||||||
app.router.add_get(
|
app.router.add_get(
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ def _safe_color(value: Optional[str]) -> str:
|
|||||||
def _public_logo_url(settings: Settings) -> Optional[str]:
|
def _public_logo_url(settings: Settings) -> Optional[str]:
|
||||||
"""Email recipients can't reach the in-app /webapp-logo proxy, so only a
|
"""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."""
|
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()
|
raw = (settings.WEBAPP_LOGO_URL or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ from db.dal import app_settings_dal
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
APPEARANCE_OVERRIDE_KEYS = {
|
APPEARANCE_OVERRIDE_KEYS = {
|
||||||
"WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
"WEBAPP_LOGO_URL",
|
"WEBAPP_LOGO_URL",
|
||||||
"WEBAPP_LOGO_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI_FONT",
|
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"WEBAPP_FAVICON_URL",
|
"WEBAPP_FAVICON_URL",
|
||||||
"WEBAPP_LOGO_FAVICON_URL",
|
"WEBAPP_LOGO_FAVICON_URL",
|
||||||
@@ -167,12 +164,6 @@ def _appearance_snapshot(settings: Settings) -> Dict[str, Any]:
|
|||||||
snapshot["WEBAPP_FAVICON_URL"] = favicon_url
|
snapshot["WEBAPP_FAVICON_URL"] = favicon_url
|
||||||
if getattr(settings, "WEBAPP_FAVICON_USE_CUSTOM", False):
|
if getattr(settings, "WEBAPP_FAVICON_USE_CUSTOM", False):
|
||||||
snapshot["WEBAPP_FAVICON_USE_CUSTOM"] = True
|
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)
|
primary_color = getattr(settings, "WEBAPP_PRIMARY_COLOR", None)
|
||||||
if primary_color and primary_color != "#00fe7a":
|
if primary_color and primary_color != "#00fe7a":
|
||||||
snapshot["WEBAPP_PRIMARY_COLOR"] = primary_color
|
snapshot["WEBAPP_PRIMARY_COLOR"] = primary_color
|
||||||
|
|||||||
@@ -59,9 +59,6 @@ class WebAppSettings(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
primary_color: str
|
primary_color: str
|
||||||
logo_url: Optional[str]
|
logo_url: Optional[str]
|
||||||
logo_use_emoji: bool
|
|
||||||
logo_emoji: str
|
|
||||||
logo_emoji_font: str
|
|
||||||
favicon_use_custom: bool
|
favicon_use_custom: bool
|
||||||
favicon_url: Optional[str]
|
favicon_url: Optional[str]
|
||||||
logo_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_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_USE_CUSTOM: bool = Field(default=False)
|
||||||
WEBAPP_FAVICON_URL: Optional[str] = Field(default=None)
|
WEBAPP_FAVICON_URL: Optional[str] = Field(default=None)
|
||||||
WEBAPP_LOGO_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,
|
title=self.WEBAPP_TITLE,
|
||||||
primary_color=self.WEBAPP_PRIMARY_COLOR,
|
primary_color=self.WEBAPP_PRIMARY_COLOR,
|
||||||
logo_url=self.WEBAPP_LOGO_URL,
|
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_use_custom=self.WEBAPP_FAVICON_USE_CUSTOM,
|
||||||
favicon_url=self.WEBAPP_FAVICON_URL,
|
favicon_url=self.WEBAPP_FAVICON_URL,
|
||||||
logo_favicon_url=self.WEBAPP_LOGO_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):
|
def ignore_deprecated_webapp_logo_url_env(cls, _value):
|
||||||
return None
|
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")
|
@field_validator("WEBAPP_FAVICON_USE_CUSTOM", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def ignore_deprecated_webapp_favicon_use_custom_env(cls, _value):
|
def ignore_deprecated_webapp_favicon_use_custom_env(cls, _value):
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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_pass http://backend:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ def settings():
|
|||||||
DEFAULT_LANGUAGE=LANGUAGE,
|
DEFAULT_LANGUAGE=LANGUAGE,
|
||||||
EMAIL_CODE_TTL_SECONDS=600,
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="remnawave-minishop",
|
WEBAPP_TITLE="remnawave-minishop",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,9 +161,6 @@
|
|||||||
| `TELEGRAM_OAUTH_REQUEST_ACCESS` | `.env` | Дополнительные разрешения, например `write`. |
|
| `TELEGRAM_OAUTH_REQUEST_ACCESS` | `.env` | Дополнительные разрешения, например `write`. |
|
||||||
| `WEBAPP_PRIMARY_COLOR` | Админка | Устаревшее env-поле, игнорируется. |
|
| `WEBAPP_PRIMARY_COLOR` | Админка | Устаревшее env-поле, игнорируется. |
|
||||||
| `WEBAPP_LOGO_URL` | Админка | Устаревшее 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_USE_CUSTOM` | Админка | Устаревшее env-поле, игнорируется. |
|
||||||
| `WEBAPP_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
| `WEBAPP_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
||||||
| `WEBAPP_LOGO_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
| `WEBAPP_LOGO_FAVICON_URL` | Админка | Устаревшее env-поле, игнорируется. |
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- блокировка пользователей, входящий список тикетов поддержки, рассылки, промокоды и просмотр логов;
|
- блокировка пользователей, входящий список тикетов поддержки, рассылки, промокоды и просмотр логов;
|
||||||
- ручная синхронизация с Remnawave;
|
- ручная синхронизация с Remnawave;
|
||||||
- редактор разрешенных настроек приложения из manifest-файла;
|
- редактор разрешенных настроек приложения из manifest-файла;
|
||||||
- раздел **Внешний вид** для логотипа, emoji-логотипа, выбора темы, accent-цвета, масштаба логотипа и предпросмотра тем;
|
- раздел **Внешний вид** для логотипа, выбора темы, accent-цвета, масштаба логотипа и предпросмотра тем;
|
||||||
- раздел **Инструкции подключения** для встроенной страницы установки, поведения кнопок бота и Remnawave Subscription Page config;
|
- раздел **Инструкции подключения** для встроенной страницы установки, поведения кнопок бота и Remnawave Subscription Page config;
|
||||||
- раздел **Бэкапы** для просмотра локальных ZIP-архивов, загрузки архива и восстановления БД/compose-папки;
|
- раздел **Бэкапы** для просмотра локальных ZIP-архивов, загрузки архива и восстановления БД/compose-папки;
|
||||||
- редактор JSON-каталога тарифов;
|
- редактор JSON-каталога тарифов;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
В manifest сейчас входят:
|
В manifest сейчас входят:
|
||||||
|
|
||||||
- общие параметры: язык, валюта, ссылки поддержки, документы, обязательный канал, Remnawave-доступы и поведение `/start`;
|
- общие параметры: язык, валюта, ссылки поддержки, документы, обязательный канал, Remnawave-доступы и поведение `/start`;
|
||||||
- внешний вид и доступность Web App: название, цвет, логотип, emoji-логотип и `WEBAPP_ENABLED`;
|
- внешний вид и доступность Web App: название, цвет, логотип и `WEBAPP_ENABLED`;
|
||||||
- инструкции подключения: `SUBSCRIPTION_GUIDES_ENABLED`, `SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED`, чтение конфига из Remnawave Panel, JSON-переопределение и резервный путь к файлу;
|
- инструкции подключения: `SUBSCRIPTION_GUIDES_ENABLED`, `SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED`, чтение конфига из Remnawave Panel, JSON-переопределение и резервный путь к файлу;
|
||||||
- legacy-тарифы без JSON-каталога: периоды подписки, RUB/Stars цены, реферальные бонусы и пакеты трафика;
|
- legacy-тарифы без JSON-каталога: периоды подписки, RUB/Stars цены, реферальные бонусы и пакеты трафика;
|
||||||
- платежные провайдеры: включение методов, порядок кнопок, публичные параметры и секреты YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket и 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>` и не меняет глобальную тему до сохранения.
|
В блоке тем админка читает каталог из `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 контейнеры.
|
Если `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-ссылке;
|
- загрузить логотип файлом или по HTTPS-ссылке;
|
||||||
- включить emoji-логотип и выбрать способ его отрисовки;
|
|
||||||
- открыть предпросмотр темы через `/home?theme_preview=<key>`.
|
- открыть предпросмотр темы через `/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` попадают в прод только при сборке собственного образа; опубликованный образ их не видит.
|
В 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`
|
## Контракт `theme.json`
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ openssl rand -hex 32
|
|||||||
Перед первым запуском создайте каталоги и отдайте их пользователю контейнера:
|
Перед первым запуском создайте каталоги и отдайте их пользователю контейнера:
|
||||||
|
|
||||||
```bash
|
```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
|
touch data/locales-overrides.json
|
||||||
chown -R 10001:10001 data
|
chown -R 10001:10001 data
|
||||||
chmod -R u+rwX data
|
chmod -R u+rwX data
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ distributed lock; код подготовлен к нескольким репл
|
|||||||
Перед первым запуском на сервере заранее дайте права пользователю контейнера `10001`:
|
Перед первым запуском на сервере заранее дайте права пользователю контейнера `10001`:
|
||||||
|
|
||||||
```bash
|
```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
|
touch data/locales-overrides.json
|
||||||
chown -R 10001:10001 data
|
chown -R 10001:10001 data
|
||||||
chmod -R u+rwX data
|
chmod -R u+rwX data
|
||||||
|
|||||||
@@ -358,13 +358,9 @@
|
|||||||
} = $accountStore);
|
} = $accountStore);
|
||||||
|
|
||||||
$: brandTitle = CFG.title || "/minishop";
|
$: brandTitle = CFG.title || "/minishop";
|
||||||
$: brandEmoji = CFG.logoEmoji || "🫥";
|
|
||||||
$: brandEmojiFont = CFG.logoEmojiFont || "system";
|
|
||||||
$: brand = normalizeBrand({
|
$: brand = normalizeBrand({
|
||||||
title: brandTitle,
|
title: brandTitle,
|
||||||
logoUrl: CFG.logoUseEmoji ? "" : CFG.logoUrl,
|
logoUrl: CFG.logoUrl,
|
||||||
emoji: brandEmoji,
|
|
||||||
emojiFont: brandEmojiFont,
|
|
||||||
});
|
});
|
||||||
$: faviconBrand = normalizeBrand({
|
$: faviconBrand = normalizeBrand({
|
||||||
...brand,
|
...brand,
|
||||||
@@ -2077,9 +2073,6 @@
|
|||||||
]);
|
]);
|
||||||
return [
|
return [
|
||||||
"WEBAPP_LOGO_URL",
|
"WEBAPP_LOGO_URL",
|
||||||
"WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI_FONT",
|
|
||||||
"WEBAPP_FAVICON_URL",
|
"WEBAPP_FAVICON_URL",
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"WEBAPP_LOGO_FAVICON_URL",
|
"WEBAPP_LOGO_FAVICON_URL",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
export let mockData = {};
|
export let mockData = {};
|
||||||
|
|
||||||
const title = config.title || "/minishop";
|
const title = config.title || "/minishop";
|
||||||
const logoEmoji = config.logoEmoji || "🫥";
|
const logoUrl = config.logoUrl || "/webapp-default-logo.webp";
|
||||||
const plans = mockData.plans || [];
|
const plans = mockData.plans || [];
|
||||||
const sub = mockData.subscription || {};
|
const sub = mockData.subscription || {};
|
||||||
const methods = mockData.payment_methods || [];
|
const methods = mockData.payment_methods || [];
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<PhoneFrame number="1" label="Главный экран">
|
<PhoneFrame number="1" label="Главный экран">
|
||||||
<main class="home-layout">
|
<main class="home-layout">
|
||||||
<div class="login-brand home-brand">
|
<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>
|
<h1>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-bottom">
|
<div class="home-bottom">
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<PhoneFrame number="2" label="Выбор тарифа">
|
<PhoneFrame number="2" label="Выбор тарифа">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<div class="brand-row">
|
<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>
|
<strong>{title}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<PhoneFrame number="6" label="Пригласить друга">
|
<PhoneFrame number="6" label="Пригласить друга">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<div class="brand-row">
|
<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>
|
<strong>{title}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
<PhoneFrame number="7" label="Настройки">
|
<PhoneFrame number="7" label="Настройки">
|
||||||
<div class="preview-header">
|
<div class="preview-header">
|
||||||
<div class="brand-row">
|
<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>
|
<strong>{title}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
|
|
||||||
<PhoneFrame number="8" label="Логин" wide>
|
<PhoneFrame number="8" label="Логин" wide>
|
||||||
<div class="login-brand small">
|
<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>
|
<h1>{title}</h1>
|
||||||
<p>Войдите в свой аккаунт</p>
|
<p>Войдите в свой аккаунт</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Check, ExternalLink, FileText, RefreshCw, Save } from "$components/ui/icons.js";
|
import { Check, ExternalLink, FileText, RefreshCw, Save } from "$components/ui/icons.js";
|
||||||
import {
|
import { AdminBadge, AdminButton, AdminEmptyState } from "$components/patterns/admin/index.js";
|
||||||
AdminBadge,
|
|
||||||
AdminButton,
|
|
||||||
AdminEmptyState,
|
|
||||||
AdminSelect,
|
|
||||||
} from "$components/patterns/admin/index.js";
|
|
||||||
import { Checkbox, ColorInput, FileInput, Input, RangeInput } from "$components/ui/index.js";
|
import { Checkbox, ColorInput, FileInput, Input, RangeInput } from "$components/ui/index.js";
|
||||||
import { Switch } from "$components/ui/primitives.js";
|
import { Switch } from "$components/ui/primitives.js";
|
||||||
import { getContext, onDestroy, onMount } from "svelte";
|
import { getContext, onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
import BrandMark from "$lib/webapp/BrandMark.svelte";
|
|
||||||
import { localizedThemeName } from "$lib/webapp/themeStyle.js";
|
import { localizedThemeName } from "$lib/webapp/themeStyle.js";
|
||||||
|
|
||||||
export let at;
|
export let at;
|
||||||
@@ -26,9 +20,6 @@
|
|||||||
"SUBSCRIPTION_MINI_APP_URL",
|
"SUBSCRIPTION_MINI_APP_URL",
|
||||||
"WEBAPP_PRIMARY_COLOR",
|
"WEBAPP_PRIMARY_COLOR",
|
||||||
"WEBAPP_LOGO_URL",
|
"WEBAPP_LOGO_URL",
|
||||||
"WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI_FONT",
|
|
||||||
"WEBAPP_FAVICON_URL",
|
"WEBAPP_FAVICON_URL",
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"WEBAPP_LOGO_FAVICON_URL",
|
"WEBAPP_LOGO_FAVICON_URL",
|
||||||
@@ -42,8 +33,7 @@
|
|||||||
$: fieldMap = new Map(appearanceFields.map((field) => [field.key, field]));
|
$: fieldMap = new Map(appearanceFields.map((field) => [field.key, field]));
|
||||||
$: activeKey = themesCatalog.default_theme;
|
$: activeKey = themesCatalog.default_theme;
|
||||||
$: logoUrl = valueForKey("WEBAPP_LOGO_URL");
|
$: logoUrl = valueForKey("WEBAPP_LOGO_URL");
|
||||||
$: useEmojiLogo = boolValue(valueForKey("WEBAPP_LOGO_USE_EMOJI"));
|
$: currentLogoUrl = pendingLogoPreviewUrl || logoUrl || brand?.logoUrl || "";
|
||||||
$: currentLogoUrl = !useEmojiLogo ? pendingLogoPreviewUrl || logoUrl || brand?.logoUrl || "" : "";
|
|
||||||
$: previewLogoUrl =
|
$: previewLogoUrl =
|
||||||
logoPreviewNonce && currentLogoUrl ? withLogoCacheBust(currentLogoUrl) : currentLogoUrl;
|
logoPreviewNonce && currentLogoUrl ? withLogoCacheBust(currentLogoUrl) : currentLogoUrl;
|
||||||
$: persistedUseCustomFavicon = boolValue(
|
$: persistedUseCustomFavicon = boolValue(
|
||||||
@@ -59,28 +49,12 @@
|
|||||||
$: useCustomFavicon = faviconUseCustomDraft;
|
$: useCustomFavicon = faviconUseCustomDraft;
|
||||||
$: faviconUrl = valueForKey("WEBAPP_FAVICON_URL", appFaviconUrl);
|
$: faviconUrl = valueForKey("WEBAPP_FAVICON_URL", appFaviconUrl);
|
||||||
$: logoFaviconUrl = valueForKey("WEBAPP_LOGO_FAVICON_URL");
|
$: logoFaviconUrl = valueForKey("WEBAPP_LOGO_FAVICON_URL");
|
||||||
$: generatedFaviconUrl = !useEmojiLogo
|
$: generatedFaviconUrl = logoFaviconUrl || appFaviconUrl || previewLogoUrl || "";
|
||||||
? logoFaviconUrl || appFaviconUrl || previewLogoUrl || ""
|
|
||||||
: "";
|
|
||||||
$: currentFaviconUrl = useCustomFavicon
|
$: currentFaviconUrl = useCustomFavicon
|
||||||
? pendingFaviconPreviewUrl || faviconUrl || ""
|
? pendingFaviconPreviewUrl || faviconUrl || ""
|
||||||
: generatedFaviconUrl;
|
: generatedFaviconUrl;
|
||||||
$: previewFaviconUrl =
|
$: previewFaviconUrl =
|
||||||
faviconPreviewNonce && currentFaviconUrl ? withCacheBust(currentFaviconUrl) : currentFaviconUrl;
|
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) =>
|
$: dirtyCount = Object.keys(settingsDirty || {}).filter((key) =>
|
||||||
isAppearanceSettingKey(key)
|
isAppearanceSettingKey(key)
|
||||||
).length;
|
).length;
|
||||||
@@ -138,15 +112,6 @@
|
|||||||
return Boolean(value);
|
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) {
|
function withLogoCacheBust(url) {
|
||||||
return withCacheBust(url, logoPreviewNonce);
|
return withCacheBust(url, logoPreviewNonce);
|
||||||
}
|
}
|
||||||
@@ -241,7 +206,6 @@
|
|||||||
if (uploaded?.faviconUrl) {
|
if (uploaded?.faviconUrl) {
|
||||||
settingsStore.setFieldValue("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
settingsStore.setFieldValue("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
||||||
}
|
}
|
||||||
settingsStore.setFieldValue("WEBAPP_LOGO_USE_EMOJI", false);
|
|
||||||
if (logoFileInput) logoFileInput.value = "";
|
if (logoFileInput) logoFileInput.value = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -256,7 +220,6 @@
|
|||||||
if (uploaded?.faviconUrl) {
|
if (uploaded?.faviconUrl) {
|
||||||
settingsStore.setFieldValue("WEBAPP_LOGO_FAVICON_URL", 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() {
|
async function saveAppearance() {
|
||||||
const keysToSave = new Set(appearanceDirtyKeys);
|
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) =>
|
const shouldReloadFrontend = Array.from(keysToSave).some((key) =>
|
||||||
[
|
[
|
||||||
"WEBAPP_LOGO_URL",
|
"WEBAPP_LOGO_URL",
|
||||||
"WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI",
|
|
||||||
"WEBAPP_LOGO_EMOJI_FONT",
|
|
||||||
"WEBAPP_FAVICON_URL",
|
"WEBAPP_FAVICON_URL",
|
||||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"WEBAPP_LOGO_FAVICON_URL",
|
"WEBAPP_LOGO_FAVICON_URL",
|
||||||
@@ -395,13 +337,7 @@
|
|||||||
<header class="admin-card-head">
|
<header class="admin-card-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>{at("appearance_brand_title", {}, "Логотип")}</h3>
|
<h3>{at("appearance_brand_title", {}, "Логотип")}</h3>
|
||||||
<small
|
<small>{at("appearance_brand_sub", {}, "Загрузите логотип файлом или по ссылке")}</small>
|
||||||
>{at(
|
|
||||||
"appearance_brand_sub",
|
|
||||||
{},
|
|
||||||
"Файл, ссылка или подтвержденный emoji-логотип"
|
|
||||||
)}</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-editor-section-actions">
|
<div class="admin-editor-section-actions">
|
||||||
{#if dirtyCount}
|
{#if dirtyCount}
|
||||||
@@ -424,7 +360,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="admin-card-body appearance-logo-grid">
|
<div class="admin-card-body appearance-logo-grid">
|
||||||
<div class="appearance-logo-preview">
|
<div class="appearance-logo-preview">
|
||||||
{#if !useEmojiLogo && previewLogoUrl && !logoPreviewFailed}
|
{#if previewLogoUrl && !logoPreviewFailed}
|
||||||
<img
|
<img
|
||||||
class="appearance-logo-image"
|
class="appearance-logo-image"
|
||||||
src={previewLogoUrl}
|
src={previewLogoUrl}
|
||||||
@@ -435,8 +371,6 @@
|
|||||||
logoPreviewFailed = true;
|
logoPreviewFailed = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if useEmojiLogo}
|
|
||||||
<BrandMark brand={logoBrand} size="lg" />
|
|
||||||
{:else}
|
{:else}
|
||||||
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -476,39 +410,6 @@
|
|||||||
</AdminButton>
|
</AdminButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -525,8 +426,6 @@
|
|||||||
faviconPreviewFailed = true;
|
faviconPreviewFailed = true;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if !useCustomFavicon && useEmojiLogo}
|
|
||||||
<BrandMark brand={logoBrand} size="lg" />
|
|
||||||
{:else}
|
{:else}
|
||||||
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -815,22 +714,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appearance-url-row,
|
.appearance-url-row {
|
||||||
.appearance-emoji-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
}
|
|
||||||
|
|
||||||
.appearance-url-row {
|
|
||||||
grid-template-columns: minmax(0, 1fr) max-content;
|
grid-template-columns: minmax(0, 1fr) max-content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appearance-emoji-grid {
|
|
||||||
grid-template-columns: minmax(0, 360px) max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.appearance-control.input),
|
:global(.appearance-control.input),
|
||||||
:global(.appearance-control.admin-btn),
|
:global(.appearance-control.admin-btn),
|
||||||
:global(.appearance-control.admin-select-trigger) {
|
:global(.appearance-control.admin-select-trigger) {
|
||||||
@@ -1012,8 +903,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appearance-url-row,
|
.appearance-url-row {
|
||||||
.appearance-emoji-grid {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
{#if isInternalNote}
|
{#if isInternalNote}
|
||||||
<Lock size={15} />
|
<Lock size={15} />
|
||||||
{:else if showSupportAvatar}
|
{:else if showSupportAvatar}
|
||||||
<BrandMark brand={supportBrand} size="sm" fallbackEmoji={true} />
|
<BrandMark brand={supportBrand} size="sm" />
|
||||||
{:else if showUserAvatar && userAvatarUrl}
|
{:else if showUserAvatar && userAvatarUrl}
|
||||||
<img src={userAvatarUrl} alt="" loading="lazy" referrerpolicy="no-referrer" />
|
<img src={userAvatarUrl} alt="" loading="lazy" referrerpolicy="no-referrer" />
|
||||||
{:else if showUserAvatar && userInitials}
|
{:else if showUserAvatar && userInitials}
|
||||||
|
|||||||
@@ -1,39 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
import { cn } from "../utils.js";
|
import { cn } from "../utils.js";
|
||||||
import { animatedEmojiAssetUrls, normalizeBrand } from "./browser.js";
|
import { normalizeBrand } from "./browser.js";
|
||||||
|
|
||||||
const LOGO_LOAD_TIMEOUT_MS = 10000;
|
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 brand = {};
|
||||||
export let logoUrl = "";
|
export let logoUrl = "";
|
||||||
export let emoji = "";
|
|
||||||
export let emojiFont = "";
|
|
||||||
export let size = "sm";
|
export let size = "sm";
|
||||||
export let animate = false;
|
export let animate = false;
|
||||||
export let fallbackEmoji = true;
|
|
||||||
let className = "";
|
let className = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
@@ -49,30 +25,13 @@
|
|||||||
let lastLogoUrl = "";
|
let lastLogoUrl = "";
|
||||||
let logoLoadTimer = null;
|
let logoLoadTimer = null;
|
||||||
let logoLoadTimerUrl = "";
|
let logoLoadTimerUrl = "";
|
||||||
let fontLoaded = false;
|
|
||||||
let loadedFontKey = "";
|
|
||||||
let animatedEmojiError = false;
|
|
||||||
let animatedEmojiStaticFallback = false;
|
|
||||||
let lastAnimatedEmoji = "";
|
|
||||||
|
|
||||||
$: normalizedBrand = normalizeBrand({
|
$: normalizedBrand = normalizeBrand({
|
||||||
...brand,
|
...brand,
|
||||||
logoUrl: logoUrl || brand?.logoUrl,
|
logoUrl: logoUrl || brand?.logoUrl,
|
||||||
emoji: emoji || brand?.emoji || brand?.logoEmoji,
|
|
||||||
emojiFont: emojiFont || brand?.emojiFont || brand?.logoEmojiFont,
|
|
||||||
});
|
});
|
||||||
$: normalizedLogoUrl = normalizedBrand.logoUrl;
|
$: normalizedLogoUrl = normalizedBrand.logoUrl;
|
||||||
$: normalizedEmoji = normalizedBrand.emoji;
|
|
||||||
$: normalizedEmojiFont = normalizedBrand.emojiFont;
|
|
||||||
$: sizeClass = SIZE_CLASSES[size] || "";
|
$: sizeClass = SIZE_CLASSES[size] || "";
|
||||||
$: animatedEmojiAssets = animatedEmojiAssetUrls(normalizedEmoji);
|
|
||||||
$: animatedEmojiSrc = animatedEmojiAssets.gif;
|
|
||||||
$: animatedEmojiFallbackSrc = animatedEmojiAssets.webp;
|
|
||||||
$: useAnimatedEmoji =
|
|
||||||
!normalizedLogoUrl &&
|
|
||||||
normalizedEmojiFont === "noto-color-animated" &&
|
|
||||||
animatedEmojiSrc &&
|
|
||||||
!animatedEmojiError;
|
|
||||||
|
|
||||||
$: if (normalizedLogoUrl !== lastLogoUrl) {
|
$: if (normalizedLogoUrl !== lastLogoUrl) {
|
||||||
lastLogoUrl = normalizedLogoUrl;
|
lastLogoUrl = normalizedLogoUrl;
|
||||||
@@ -81,82 +40,11 @@
|
|||||||
}
|
}
|
||||||
$: if (normalizedLogoUrl && !loaded && !failed) armLogoLoadTimeout();
|
$: if (normalizedLogoUrl && !loaded && !failed) armLogoLoadTimeout();
|
||||||
$: if (!normalizedLogoUrl || loaded || failed) clearLogoLoadTimeout();
|
$: if (!normalizedLogoUrl || loaded || failed) clearLogoLoadTimeout();
|
||||||
$: if (`${normalizedEmojiFont}:${normalizedEmoji}` !== lastAnimatedEmoji) {
|
|
||||||
lastAnimatedEmoji = `${normalizedEmojiFont}:${normalizedEmoji}`;
|
|
||||||
animatedEmojiError = false;
|
|
||||||
animatedEmojiStaticFallback = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearLogoLoadTimeout();
|
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() {
|
function clearLogoLoadTimeout() {
|
||||||
if (logoLoadTimer) {
|
if (logoLoadTimer) {
|
||||||
window.clearTimeout(logoLoadTimer);
|
window.clearTimeout(logoLoadTimer);
|
||||||
@@ -207,29 +95,6 @@
|
|||||||
clearLogoLoadTimeout();
|
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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -270,11 +135,6 @@
|
|||||||
font-size: 4.375rem;
|
font-size: 4.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark img.brand-mark-animated-emoji {
|
|
||||||
object-fit: contain;
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-mark.brand-mark-animate {
|
.brand-mark.brand-mark-animate {
|
||||||
animation: brand-mark-pulse 2s ease-in-out infinite;
|
animation: brand-mark-pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -316,55 +176,4 @@
|
|||||||
transform: scale(1.05);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -26,41 +26,18 @@ export function escapeHtml(value) {
|
|||||||
.replaceAll("'", "'");
|
.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 = {}) {
|
export function normalizeBrand(brand = {}) {
|
||||||
return {
|
return {
|
||||||
title: String(brand.title || "/minishop").trim() || "/minishop",
|
title: String(brand.title || "/minishop").trim() || "/minishop",
|
||||||
logoUrl: String(brand.logoUrl || "").trim(),
|
logoUrl: String(brand.logoUrl || "").trim() || DEFAULT_LOGO_URL,
|
||||||
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`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function brandFaviconHref(brand = {}) {
|
export function brandFaviconHref(brand = {}) {
|
||||||
const normalizedBrand = normalizeBrand(brand);
|
return normalizeBrand(brand).logoUrl;
|
||||||
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)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyFavicon(brand = {}) {
|
export function applyFavicon(brand = {}) {
|
||||||
@@ -70,9 +47,7 @@ export function applyFavicon(brand = {}) {
|
|||||||
|
|
||||||
const href = brandFaviconHref(brand);
|
const href = brandFaviconHref(brand);
|
||||||
favicon.setAttribute("href", href);
|
favicon.setAttribute("href", href);
|
||||||
if (href.startsWith("data:image/svg+xml")) {
|
if (href.endsWith(".gif")) {
|
||||||
favicon.setAttribute("type", "image/svg+xml");
|
|
||||||
} else if (href.endsWith(".gif")) {
|
|
||||||
favicon.setAttribute("type", "image/gif");
|
favicon.setAttribute("type", "image/gif");
|
||||||
} else if (href.endsWith(".webp")) {
|
} else if (href.endsWith(".webp")) {
|
||||||
favicon.setAttribute("type", "image/webp");
|
favicon.setAttribute("type", "image/webp");
|
||||||
|
|||||||
@@ -86404,26 +86404,6 @@ export const DEMO_DATASET = {
|
|||||||
overridden: false,
|
overridden: false,
|
||||||
updated_at: null,
|
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",
|
key: "WEBAPP_LOGO_URL",
|
||||||
type: "url",
|
type: "url",
|
||||||
@@ -86444,94 +86424,6 @@ export const DEMO_DATASET = {
|
|||||||
overridden: false,
|
overridden: false,
|
||||||
updated_at: "2026-05-16T01:03:23Z",
|
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",
|
key: "WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
type: "bool",
|
type: "bool",
|
||||||
@@ -113282,18 +113174,18 @@ export const DEMO_DATASET = {
|
|||||||
audience: "internal",
|
audience: "internal",
|
||||||
values: {
|
values: {
|
||||||
ru: {
|
ru: {
|
||||||
base: "Файл, ссылка или подтвержденный emoji-логотип",
|
base: "Загрузите логотип файлом или по ссылке",
|
||||||
fallback: "Файл, ссылка или подтвержденный emoji-логотип",
|
fallback: "Загрузите логотип файлом или по ссылке",
|
||||||
effective: "Файл, ссылка или подтвержденный emoji-логотип",
|
effective: "Загрузите логотип файлом или по ссылке",
|
||||||
override: "",
|
override: "",
|
||||||
overridden: false,
|
overridden: false,
|
||||||
updated_at: null,
|
updated_at: null,
|
||||||
updated_by: null,
|
updated_by: null,
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
base: "File, URL, or confirmed emoji logo",
|
base: "Upload a logo as a file or from a URL",
|
||||||
fallback: "Файл, ссылка или подтвержденный emoji-логотип",
|
fallback: "Загрузите логотип файлом или по ссылке",
|
||||||
effective: "File, URL, or confirmed emoji logo",
|
effective: "Upload a logo as a file or from a URL",
|
||||||
override: "",
|
override: "",
|
||||||
overridden: false,
|
overridden: false,
|
||||||
updated_at: null,
|
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",
|
key: "admin_appearance_favicon_upload_failed",
|
||||||
audience: "internal",
|
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",
|
key: "admin_settings_field_privacy_policy_url_label",
|
||||||
audience: "internal",
|
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",
|
key: "admin_settings_field_webapp_logo_favicon_url_label",
|
||||||
audience: "internal",
|
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",
|
key: "admin_settings_field_webapp_primary_color_label",
|
||||||
audience: "internal",
|
audience: "internal",
|
||||||
|
|||||||
@@ -652,9 +652,6 @@ function demoSettingsSections(clone) {
|
|||||||
function applyDemoSettingToMock(key, value) {
|
function applyDemoSettingToMock(key, value) {
|
||||||
if (key === "WEBAPP_TITLE") DEV_MOCK.config.title = value || "";
|
if (key === "WEBAPP_TITLE") DEV_MOCK.config.title = value || "";
|
||||||
if (key === "WEBAPP_LOGO_URL") DEV_MOCK.config.logoUrl = 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") {
|
if (key === "WEBAPP_FAVICON_URL" || key === "WEBAPP_LOGO_FAVICON_URL") {
|
||||||
DEV_MOCK.config.faviconUrl = value || DEV_MOCK.config.faviconUrl || "";
|
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")) {
|
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_LOGO_URL")) {
|
||||||
DEV_MOCK.config.logoUrl = 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")) {
|
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_FAVICON_URL")) {
|
||||||
DEV_MOCK.config.faviconUrl = 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",
|
id: "appearance",
|
||||||
order: 2,
|
order: 2,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
|
||||||
key: "WEBAPP_LOGO_USE_EMOJI",
|
|
||||||
type: "bool",
|
|
||||||
section: "appearance",
|
|
||||||
label: "Emoji logo",
|
|
||||||
value: Boolean(DEV_MOCK.config.logoUseEmoji),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "WEBAPP_LOGO_URL",
|
key: "WEBAPP_LOGO_URL",
|
||||||
type: "url",
|
type: "url",
|
||||||
@@ -1756,25 +1739,6 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
label: "URL логотипа",
|
label: "URL логотипа",
|
||||||
value: DEV_MOCK.config.logoUrl || "",
|
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",
|
key: "WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
type: "bool",
|
type: "bool",
|
||||||
|
|||||||
@@ -217,9 +217,6 @@ export const DEV_MOCK = {
|
|||||||
title: "/minishop",
|
title: "/minishop",
|
||||||
primaryColor: "#00fe7a",
|
primaryColor: "#00fe7a",
|
||||||
logoUrl: "/webapp-default-logo.webp",
|
logoUrl: "/webapp-default-logo.webp",
|
||||||
logoUseEmoji: false,
|
|
||||||
logoEmoji: "🫥",
|
|
||||||
logoEmojiFont: "system",
|
|
||||||
faviconUrl: "/webapp-favicon/19b2a242e5b7bc2d/icon-180.png",
|
faviconUrl: "/webapp-favicon/19b2a242e5b7bc2d/icon-180.png",
|
||||||
faviconUseCustom: false,
|
faviconUseCustom: false,
|
||||||
trialEnabled: true,
|
trialEnabled: true,
|
||||||
|
|||||||
@@ -275,25 +275,6 @@
|
|||||||
"overridden": false,
|
"overridden": false,
|
||||||
"updated_at": null
|
"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",
|
"key": "WEBAPP_LOGO_URL",
|
||||||
"type": "url",
|
"type": "url",
|
||||||
@@ -313,91 +294,6 @@
|
|||||||
"overridden": false,
|
"overridden": false,
|
||||||
"updated_at": null
|
"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",
|
"key": "WEBAPP_FAVICON_USE_CUSTOM",
|
||||||
"type": "bool",
|
"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_label": "Public Mini App URL",
|
||||||
"admin_settings_field_subscription_mini_app_url_description": "For example, https://app.example.com/.",
|
"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_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_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_use_custom_label": "Use separate favicon",
|
||||||
"admin_settings_field_webapp_favicon_url_label": "Separate favicon URL",
|
"admin_settings_field_webapp_favicon_url_label": "Separate favicon URL",
|
||||||
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon from logo",
|
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon from logo",
|
||||||
@@ -1962,9 +1949,8 @@
|
|||||||
"admin_actions": "Actions",
|
"admin_actions": "Actions",
|
||||||
"admin_ad_created": "Campaign created",
|
"admin_ad_created": "Campaign created",
|
||||||
"admin_ad_deleted": "Campaign deleted",
|
"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_brand_title": "Logo",
|
||||||
"admin_appearance_emoji_font": "Emoji font",
|
|
||||||
"admin_appearance_favicon_upload_failed": "Failed to upload favicon",
|
"admin_appearance_favicon_upload_failed": "Failed to upload favicon",
|
||||||
"admin_appearance_favicon_upload_file": "Upload favicon",
|
"admin_appearance_favicon_upload_file": "Upload favicon",
|
||||||
"admin_appearance_favicon_upload_url": "From URL",
|
"admin_appearance_favicon_upload_url": "From URL",
|
||||||
@@ -1980,7 +1966,6 @@
|
|||||||
"admin_appearance_themes_sub": "Global theme, accent color, and preview",
|
"admin_appearance_themes_sub": "Global theme, accent color, and preview",
|
||||||
"admin_appearance_themes_title": "Themes",
|
"admin_appearance_themes_title": "Themes",
|
||||||
"admin_appearance_use_custom_favicon": "Use a separate favicon",
|
"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": "Create backup",
|
||||||
"admin_backups_create_done": "Backup created",
|
"admin_backups_create_done": "Backup created",
|
||||||
"admin_backups_create_failed": "Failed to create backup",
|
"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_label": "Публичный URL Mini App",
|
||||||
"admin_settings_field_subscription_mini_app_url_description": "Например, https://app.example.com/.",
|
"admin_settings_field_subscription_mini_app_url_description": "Например, https://app.example.com/.",
|
||||||
"admin_settings_field_webapp_primary_color_label": "Основной цвет",
|
"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_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_use_custom_label": "Использовать отдельную favicon",
|
||||||
"admin_settings_field_webapp_favicon_url_label": "URL отдельной favicon",
|
"admin_settings_field_webapp_favicon_url_label": "URL отдельной favicon",
|
||||||
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon из логотипа",
|
"admin_settings_field_webapp_logo_favicon_url_label": "Favicon из логотипа",
|
||||||
@@ -1962,9 +1949,8 @@
|
|||||||
"admin_actions": "Действия",
|
"admin_actions": "Действия",
|
||||||
"admin_ad_created": "Кампания создана",
|
"admin_ad_created": "Кампания создана",
|
||||||
"admin_ad_deleted": "Кампания удалена",
|
"admin_ad_deleted": "Кампания удалена",
|
||||||
"admin_appearance_brand_sub": "Файл, ссылка или подтвержденный emoji-логотип",
|
"admin_appearance_brand_sub": "Загрузите логотип файлом или по ссылке",
|
||||||
"admin_appearance_brand_title": "Логотип",
|
"admin_appearance_brand_title": "Логотип",
|
||||||
"admin_appearance_emoji_font": "Шрифт emoji",
|
|
||||||
"admin_appearance_favicon_upload_failed": "Не удалось загрузить favicon",
|
"admin_appearance_favicon_upload_failed": "Не удалось загрузить favicon",
|
||||||
"admin_appearance_favicon_upload_file": "Загрузить favicon",
|
"admin_appearance_favicon_upload_file": "Загрузить favicon",
|
||||||
"admin_appearance_favicon_upload_url": "По ссылке",
|
"admin_appearance_favicon_upload_url": "По ссылке",
|
||||||
@@ -1980,7 +1966,6 @@
|
|||||||
"admin_appearance_themes_sub": "Глобальная тема, accent color и предпросмотр",
|
"admin_appearance_themes_sub": "Глобальная тема, accent color и предпросмотр",
|
||||||
"admin_appearance_themes_title": "Темы",
|
"admin_appearance_themes_title": "Темы",
|
||||||
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
|
"admin_appearance_use_custom_favicon": "Использовать отдельную favicon",
|
||||||
"admin_appearance_use_emoji_logo": "Использовать emoji-логотип",
|
|
||||||
"admin_backups_create": "Создать бэкап",
|
"admin_backups_create": "Создать бэкап",
|
||||||
"admin_backups_create_done": "Бэкап создан",
|
"admin_backups_create_done": "Бэкап создан",
|
||||||
"admin_backups_create_failed": "Не удалось создать бэкап",
|
"admin_backups_create_failed": "Не удалось создать бэкап",
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ def _settings(default_language: str = "ru"):
|
|||||||
DEFAULT_LANGUAGE=default_language,
|
DEFAULT_LANGUAGE=default_language,
|
||||||
EMAIL_CODE_TTL_SECONDS=600,
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="Mini Shop",
|
WEBAPP_TITLE="Mini Shop",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ def test_email_templates_use_override_only_language_without_base_locale_file(tmp
|
|||||||
DEFAULT_LANGUAGE="ru",
|
DEFAULT_LANGUAGE="ru",
|
||||||
EMAIL_CODE_TTL_SECONDS=600,
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="Remnawave",
|
WEBAPP_TITLE="Remnawave",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -342,7 +342,6 @@ class WebAppSecurityTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
DEFAULT_LANGUAGE="ru",
|
DEFAULT_LANGUAGE="ru",
|
||||||
EMAIL_CODE_TTL_SECONDS=600,
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="Remnawave",
|
WEBAPP_TITLE="Remnawave",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ class SettingsTests(unittest.TestCase):
|
|||||||
POSTGRES_PASSWORD="app_password",
|
POSTGRES_PASSWORD="app_password",
|
||||||
WEBAPP_PRIMARY_COLOR="#ff0000",
|
WEBAPP_PRIMARY_COLOR="#ff0000",
|
||||||
WEBAPP_LOGO_URL="https://cdn.example.com/logo.png",
|
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_USE_CUSTOM=True,
|
||||||
WEBAPP_FAVICON_URL="https://cdn.example.com/favicon.png",
|
WEBAPP_FAVICON_URL="https://cdn.example.com/favicon.png",
|
||||||
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.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.assertEqual(settings.WEBAPP_PRIMARY_COLOR, "#00fe7a")
|
||||||
self.assertIsNone(settings.WEBAPP_LOGO_URL)
|
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.assertFalse(settings.WEBAPP_FAVICON_USE_CUSTOM)
|
||||||
self.assertIsNone(settings.WEBAPP_FAVICON_URL)
|
self.assertIsNone(settings.WEBAPP_FAVICON_URL)
|
||||||
self.assertIsNone(settings.WEBAPP_LOGO_FAVICON_URL)
|
self.assertIsNone(settings.WEBAPP_LOGO_FAVICON_URL)
|
||||||
@@ -202,7 +196,6 @@ class SettingsTests(unittest.TestCase):
|
|||||||
POSTGRES_PASSWORD="app_password",
|
POSTGRES_PASSWORD="app_password",
|
||||||
)
|
)
|
||||||
settings.WEBAPP_LOGO_URL = "/webapp-uploaded-logo/logo-1111111111111111.png"
|
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_LOGO_FAVICON_URL = "/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png"
|
||||||
settings.WEBAPP_FAVICON_USE_CUSTOM = True
|
settings.WEBAPP_FAVICON_USE_CUSTOM = True
|
||||||
settings.WEBAPP_FAVICON_URL = "/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png"
|
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/",
|
SUBSCRIPTION_MINI_APP_URL="https://app.example.test/",
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="Minishop",
|
WEBAPP_TITLE="Minishop",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
email_auth_configured=True,
|
email_auth_configured=True,
|
||||||
**overrides,
|
**overrides,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ def _settings(**overrides):
|
|||||||
"WEBAPP_PRIMARY_COLOR": "#00fe7a",
|
"WEBAPP_PRIMARY_COLOR": "#00fe7a",
|
||||||
"WEBAPP_TITLE": "Mini Shop",
|
"WEBAPP_TITLE": "Mini Shop",
|
||||||
"WEBAPP_LOGO_URL": "",
|
"WEBAPP_LOGO_URL": "",
|
||||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
|
||||||
}
|
}
|
||||||
values.update(overrides)
|
values.update(overrides)
|
||||||
return SimpleNamespace(**values)
|
return SimpleNamespace(**values)
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_default_webapp_logo_is_used_without_admin_upload(self):
|
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(
|
self.assertEqual(
|
||||||
subscription_webapp._resolve_webapp_logo_url(settings),
|
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):
|
async def test_webapp_logo_route_serves_configured_uploaded_logo(self):
|
||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||||
)
|
)
|
||||||
request = SimpleNamespace(app={"settings": settings})
|
request = SimpleNamespace(app={"settings": settings})
|
||||||
@@ -375,14 +374,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(response.content_type, "image/png")
|
self.assertEqual(response.content_type, "image/png")
|
||||||
self.assertEqual(response.body, b"logo")
|
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):
|
def test_custom_webapp_favicon_takes_precedence(self):
|
||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
WEBAPP_FAVICON_USE_CUSTOM=True,
|
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||||
@@ -470,7 +461,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
WEBAPP_ENABLED=True,
|
WEBAPP_ENABLED=True,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_FAVICON_USE_CUSTOM=True,
|
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||||
WEBAPP_FAVICON_URL=f"/webapp-favicon/{digest}/icon-180.png",
|
WEBAPP_FAVICON_URL=f"/webapp-favicon/{digest}/icon-180.png",
|
||||||
WEBAPP_LOGO_FAVICON_URL="",
|
WEBAPP_LOGO_FAVICON_URL="",
|
||||||
@@ -491,7 +481,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
WEBAPP_ENABLED=True,
|
WEBAPP_ENABLED=True,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL="",
|
||||||
WEBAPP_LOGO_USE_EMOJI=False,
|
|
||||||
WEBAPP_FAVICON_USE_CUSTOM=False,
|
WEBAPP_FAVICON_USE_CUSTOM=False,
|
||||||
WEBAPP_FAVICON_URL="",
|
WEBAPP_FAVICON_URL="",
|
||||||
WEBAPP_LOGO_FAVICON_URL="",
|
WEBAPP_LOGO_FAVICON_URL="",
|
||||||
@@ -592,16 +581,14 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
(favicons / "aaaaaaaaaaaaaaaa" / "icon-180.png").write_bytes(b"keep")
|
(favicons / "aaaaaaaaaaaaaaaa" / "icon-180.png").write_bytes(b"keep")
|
||||||
(favicons / "bbbbbbbbbbbbbbbb" / "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")
|
(favicons / "cccccccccccccccc" / "icon-180.png").write_bytes(b"remove")
|
||||||
(emoji / "1f929.512.gif").write_bytes(b"keep")
|
# Emoji logos were removed; any leftover emoji cache files are purged.
|
||||||
(emoji / "1f929.512.webp").write_bytes(b"keep")
|
(emoji / "1f929.512.gif").write_bytes(b"remove")
|
||||||
|
(emoji / "1f929.512.webp").write_bytes(b"remove")
|
||||||
(emoji / "1f525.512.gif").write_bytes(b"remove")
|
(emoji / "1f525.512.gif").write_bytes(b"remove")
|
||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||||
WEBAPP_FAVICON_URL="/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png",
|
WEBAPP_FAVICON_URL="/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png",
|
||||||
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/bbbbbbbbbbbbbbbb/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 (
|
with (
|
||||||
@@ -616,8 +603,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertTrue((favicons / "aaaaaaaaaaaaaaaa").exists())
|
self.assertTrue((favicons / "aaaaaaaaaaaaaaaa").exists())
|
||||||
self.assertTrue((favicons / "bbbbbbbbbbbbbbbb").exists())
|
self.assertTrue((favicons / "bbbbbbbbbbbbbbbb").exists())
|
||||||
self.assertFalse((favicons / "cccccccccccccccc").exists())
|
self.assertFalse((favicons / "cccccccccccccccc").exists())
|
||||||
self.assertTrue((emoji / "1f929.512.gif").exists())
|
self.assertFalse((emoji / "1f929.512.gif").exists())
|
||||||
self.assertTrue((emoji / "1f929.512.webp").exists())
|
self.assertFalse((emoji / "1f929.512.webp").exists())
|
||||||
self.assertFalse((emoji / "1f525.512.gif").exists())
|
self.assertFalse((emoji / "1f525.512.gif").exists())
|
||||||
|
|
||||||
async def test_persist_appearance_upload_writes_overrides_and_clears_caches(self):
|
async def test_persist_appearance_upload_writes_overrides_and_clears_caches(self):
|
||||||
@@ -632,7 +619,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
updates = {
|
updates = {
|
||||||
"WEBAPP_LOGO_URL": "/webapp-uploaded-logo/logo-1111111111111111.png",
|
"WEBAPP_LOGO_URL": "/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@@ -709,12 +695,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
self.assertEqual(bumped.theme_by_key("custom").assets_version, 4)
|
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):
|
def test_telegram_avatar_url_uses_same_origin_account_route(self):
|
||||||
avatar = SimpleNamespace(
|
avatar = SimpleNamespace(
|
||||||
user_id=123,
|
user_id=123,
|
||||||
@@ -1323,7 +1303,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
logo_url = "https://cdn.example.com/logo.png"
|
logo_url = "https://cdn.example.com/logo.png"
|
||||||
logo = (b"cached-logo", "image/png")
|
logo = (b"cached-logo", "image/png")
|
||||||
app = {
|
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": None,
|
||||||
"webapp_logo_cache_lock": asyncio.Lock(),
|
"webapp_logo_cache_lock": asyncio.Lock(),
|
||||||
}
|
}
|
||||||
@@ -1343,44 +1323,3 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
fetch_logo.assert_not_called()
|
fetch_logo.assert_not_called()
|
||||||
self.assertEqual(app["webapp_logo_cache"], (logo_url, logo[0], logo[1]))
|
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", "/robots.txt"): "robots_txt_route",
|
||||||
("GET", "/webapp-logo"): "webapp_logo_route",
|
("GET", "/webapp-logo"): "webapp_logo_route",
|
||||||
("GET", "/webapp-uploaded-logo/{filename}"): "webapp_uploaded_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.css"): "css_asset_route",
|
||||||
("GET", "/subscription_webapp_admin.css"): "admin_css_asset_route",
|
("GET", "/subscription_webapp_admin.css"): "admin_css_asset_route",
|
||||||
("GET", "/webapp-theme-css/{path}"): "theme_css_asset_route",
|
("GET", "/webapp-theme-css/{path}"): "theme_css_asset_route",
|
||||||
|
|||||||
Reference in New Issue
Block a user