feat: separate webapp favicon configuration in appearance admin panel section
This commit is contained in:
@@ -251,6 +251,10 @@
|
||||
emoji: brandEmoji,
|
||||
emojiFont: brandEmojiFont,
|
||||
});
|
||||
$: faviconBrand = normalizeBrand({
|
||||
...brand,
|
||||
logoUrl: String(CFG.faviconUrl || "").trim() || brand.logoUrl,
|
||||
});
|
||||
$: plans = data?.plans?.length ? data.plans : DEV_MOCK.data.plans;
|
||||
$: methods = data?.payment_methods?.length ? data.payment_methods : [];
|
||||
$: appSettings = data?.settings || DEV_MOCK.data.settings;
|
||||
@@ -377,7 +381,7 @@
|
||||
: telegramLoginUnavailable
|
||||
? t("wa_auth_telegram_not_configured")
|
||||
: "";
|
||||
$: applyFavicon(brand);
|
||||
$: applyFavicon(faviconBrand);
|
||||
$: syncBodyScrollLock(
|
||||
paymentModalOpen ||
|
||||
changeModalOpen ||
|
||||
@@ -885,6 +889,9 @@
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
].some((key) => keys.has(key));
|
||||
}
|
||||
|
||||
@@ -987,6 +994,8 @@
|
||||
onThemesSaved={handleAdminPersistedSaved}
|
||||
{brandTitle}
|
||||
{brand}
|
||||
appFaviconUrl={CFG.faviconUrl}
|
||||
appFaviconUseCustom={CFG.faviconUseCustom}
|
||||
appVersion={CFG.appVersion}
|
||||
appRepositoryUrl={CFG.appRepositoryUrl}
|
||||
{currentLang}
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
export let onThemesSaved = () => {};
|
||||
export let brand = {};
|
||||
export let brandTitle = "/minishop";
|
||||
export let appFaviconUrl = "";
|
||||
export let appFaviconUseCustom = false;
|
||||
export let appVersion = "dev+local";
|
||||
export let appRepositoryUrl = "https://github.com/3252a8/remnawave-minishop";
|
||||
export let currentLang = "ru";
|
||||
@@ -647,7 +649,14 @@
|
||||
{/if}
|
||||
|
||||
{#if active === "appearance"}
|
||||
<AppearanceSection {at} {currentLang} {onSettingsSaved} {brand} />
|
||||
<AppearanceSection
|
||||
{at}
|
||||
{currentLang}
|
||||
{onSettingsSaved}
|
||||
{brand}
|
||||
{appFaviconUrl}
|
||||
{appFaviconUseCustom}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if active === "settings"}
|
||||
|
||||
@@ -16,9 +16,23 @@
|
||||
export let currentLang = "ru";
|
||||
export let onSettingsSaved = () => {};
|
||||
export let brand = {};
|
||||
export let appFaviconUrl = "";
|
||||
export let appFaviconUseCustom = false;
|
||||
|
||||
const settingsStore = getContext("settingsStore");
|
||||
const themesStore = getContext("themesStore");
|
||||
const APPEARANCE_SETTING_KEYS = new Set([
|
||||
"WEBAPP_TITLE",
|
||||
"WEBAPP_PRIMARY_COLOR",
|
||||
"WEBAPP_LOGO_URL",
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
"WEBAPP_ENABLED",
|
||||
]);
|
||||
|
||||
$: ({ settingsSections, settingsLoading, settingsDirty, settingsSaving } = $settingsStore);
|
||||
$: ({ themesCatalog, themesLoading, themesDir, themesSaving } = $themesStore);
|
||||
@@ -31,6 +45,25 @@
|
||||
$: currentLogoUrl = !useEmojiLogo ? pendingLogoPreviewUrl || logoUrl || brand?.logoUrl || "" : "";
|
||||
$: previewLogoUrl =
|
||||
logoPreviewNonce && currentLogoUrl ? withLogoCacheBust(currentLogoUrl) : currentLogoUrl;
|
||||
$: persistedUseCustomFavicon = boolValue(
|
||||
valueForKey("WEBAPP_FAVICON_USE_CUSTOM", appFaviconUseCustom)
|
||||
);
|
||||
$: if (
|
||||
!Object.prototype.hasOwnProperty.call(settingsDirty, "WEBAPP_FAVICON_USE_CUSTOM") &&
|
||||
lastPersistedUseCustomFavicon !== persistedUseCustomFavicon
|
||||
) {
|
||||
faviconUseCustomDraft = persistedUseCustomFavicon;
|
||||
lastPersistedUseCustomFavicon = persistedUseCustomFavicon;
|
||||
}
|
||||
$: useCustomFavicon = faviconUseCustomDraft;
|
||||
$: faviconUrl = valueForKey("WEBAPP_FAVICON_URL", appFaviconUrl);
|
||||
$: logoFaviconUrl = valueForKey("WEBAPP_LOGO_FAVICON_URL");
|
||||
$: generatedFaviconUrl = !useEmojiLogo ? logoFaviconUrl || previewLogoUrl || "" : "";
|
||||
$: currentFaviconUrl = useCustomFavicon
|
||||
? pendingFaviconPreviewUrl || faviconUrl || ""
|
||||
: generatedFaviconUrl;
|
||||
$: previewFaviconUrl =
|
||||
faviconPreviewNonce && currentFaviconUrl ? withCacheBust(currentFaviconUrl) : currentFaviconUrl;
|
||||
$: logoEmoji = valueForKey("WEBAPP_LOGO_EMOJI");
|
||||
$: logoEmojiInput = useEmojiLogo ? logoEmoji : "";
|
||||
$: logoEmojiPreview = logoEmoji || "🫥";
|
||||
@@ -46,36 +79,51 @@
|
||||
label: item.label,
|
||||
}));
|
||||
$: dirtyCount = Object.keys(settingsDirty || {}).filter((key) =>
|
||||
appearanceFields.some((field) => field.key === key)
|
||||
isAppearanceSettingKey(key)
|
||||
).length;
|
||||
$: appearanceDirtyKeys = Object.keys(settingsDirty || {}).filter((key) =>
|
||||
appearanceFields.some((field) => field.key === key)
|
||||
isAppearanceSettingKey(key)
|
||||
);
|
||||
|
||||
let logoFileInput;
|
||||
let faviconFileInput;
|
||||
let logoSourceUrl = "";
|
||||
let faviconSourceUrl = "";
|
||||
let logoPreviewNonce = 0;
|
||||
let faviconPreviewNonce = 0;
|
||||
let logoPreviewFailed = false;
|
||||
let faviconPreviewFailed = false;
|
||||
let lastPreviewLogoUrl = "";
|
||||
let lastPreviewFaviconUrl = "";
|
||||
let lastPersistedUseCustomFavicon;
|
||||
let faviconUseCustomDraft = false;
|
||||
let pendingLogoPreviewUrl = "";
|
||||
let pendingFaviconPreviewUrl = "";
|
||||
let pendingObjectUrl = "";
|
||||
let pendingFaviconObjectUrl = "";
|
||||
|
||||
$: if (previewLogoUrl !== lastPreviewLogoUrl) {
|
||||
lastPreviewLogoUrl = previewLogoUrl;
|
||||
logoPreviewFailed = false;
|
||||
}
|
||||
|
||||
function valueFor(field) {
|
||||
if (!field) return "";
|
||||
if (settingsDirty[field.key]?.deleted) return "";
|
||||
if (Object.prototype.hasOwnProperty.call(settingsDirty, field.key)) {
|
||||
return settingsDirty[field.key].value;
|
||||
}
|
||||
return field.value ?? "";
|
||||
$: if (previewFaviconUrl !== lastPreviewFaviconUrl) {
|
||||
lastPreviewFaviconUrl = previewFaviconUrl;
|
||||
faviconPreviewFailed = false;
|
||||
}
|
||||
|
||||
function valueForKey(key) {
|
||||
return valueFor(fieldMap.get(key));
|
||||
function valueForKey(key, fallback = "") {
|
||||
if (settingsDirty[key]?.deleted) return "";
|
||||
if (Object.prototype.hasOwnProperty.call(settingsDirty, key)) {
|
||||
return settingsDirty[key].value;
|
||||
}
|
||||
const field = fieldMap.get(key);
|
||||
if (!field) return fallback;
|
||||
return field.value ?? fallback;
|
||||
}
|
||||
|
||||
function isAppearanceSettingKey(key) {
|
||||
return APPEARANCE_SETTING_KEYS.has(key) || appearanceFields.some((field) => field.key === key);
|
||||
}
|
||||
|
||||
function boolValue(value) {
|
||||
@@ -88,9 +136,13 @@
|
||||
}
|
||||
|
||||
function withLogoCacheBust(url) {
|
||||
return withCacheBust(url, logoPreviewNonce);
|
||||
}
|
||||
|
||||
function withCacheBust(url, nonce) {
|
||||
if (!url || url.startsWith("data:") || url.startsWith("blob:")) return url;
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
return `${url}${separator}v=${logoPreviewNonce}`;
|
||||
return `${url}${separator}v=${nonce}`;
|
||||
}
|
||||
|
||||
function clearPendingObjectUrl() {
|
||||
@@ -100,6 +152,13 @@
|
||||
pendingObjectUrl = "";
|
||||
}
|
||||
|
||||
function clearPendingFaviconObjectUrl() {
|
||||
if (pendingFaviconObjectUrl && typeof URL !== "undefined") {
|
||||
URL.revokeObjectURL(pendingFaviconObjectUrl);
|
||||
}
|
||||
pendingFaviconObjectUrl = "";
|
||||
}
|
||||
|
||||
function setPendingLogoPreview(url, objectUrl = "") {
|
||||
clearPendingObjectUrl();
|
||||
pendingObjectUrl = objectUrl;
|
||||
@@ -108,6 +167,14 @@
|
||||
logoPreviewNonce = Date.now();
|
||||
}
|
||||
|
||||
function setPendingFaviconPreview(url, objectUrl = "") {
|
||||
clearPendingFaviconObjectUrl();
|
||||
pendingFaviconObjectUrl = objectUrl;
|
||||
pendingFaviconPreviewUrl = url;
|
||||
faviconPreviewFailed = false;
|
||||
faviconPreviewNonce = Date.now();
|
||||
}
|
||||
|
||||
function themeTitle(theme) {
|
||||
return localizedThemeName(theme, currentLang) || "—";
|
||||
}
|
||||
@@ -151,28 +218,77 @@
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setPendingLogoPreview(objectUrl, objectUrl);
|
||||
}
|
||||
themesStore.uploadLogoFile(file).then((uploadedUrl) => {
|
||||
themesStore.uploadLogoFile(file).then((uploaded) => {
|
||||
const uploadedUrl = uploaded?.logoUrl || "";
|
||||
if (!uploadedUrl) {
|
||||
pendingLogoPreviewUrl = "";
|
||||
clearPendingObjectUrl();
|
||||
return;
|
||||
}
|
||||
settingsStore.markDirty("WEBAPP_LOGO_URL", uploadedUrl);
|
||||
if (uploaded?.faviconUrl) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
||||
}
|
||||
settingsStore.markDirty("WEBAPP_LOGO_USE_EMOJI", false);
|
||||
if (logoFileInput) logoFileInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
function uploadLogoFromUrl() {
|
||||
themesStore.uploadLogoUrl(logoSourceUrl).then((uploadedUrl) => {
|
||||
themesStore.uploadLogoUrl(logoSourceUrl).then((uploaded) => {
|
||||
const uploadedUrl = uploaded?.logoUrl || "";
|
||||
if (!uploadedUrl) return;
|
||||
setPendingLogoPreview(uploadedUrl);
|
||||
logoSourceUrl = "";
|
||||
settingsStore.markDirty("WEBAPP_LOGO_URL", uploadedUrl);
|
||||
if (uploaded?.faviconUrl) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_FAVICON_URL", uploaded.faviconUrl);
|
||||
}
|
||||
settingsStore.markDirty("WEBAPP_LOGO_USE_EMOJI", false);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFaviconFileChange(event) {
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (!file) return;
|
||||
if (typeof URL !== "undefined") {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setPendingFaviconPreview(objectUrl, objectUrl);
|
||||
}
|
||||
themesStore.uploadFaviconFile(file).then((uploaded) => {
|
||||
const uploadedUrl = uploaded?.faviconUrl || "";
|
||||
if (!uploadedUrl) {
|
||||
pendingFaviconPreviewUrl = "";
|
||||
clearPendingFaviconObjectUrl();
|
||||
return;
|
||||
}
|
||||
settingsStore.markDirty("WEBAPP_FAVICON_URL", uploadedUrl);
|
||||
setCustomFavicon(true);
|
||||
if (faviconFileInput) faviconFileInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFaviconFromUrl() {
|
||||
themesStore.uploadFaviconUrl(faviconSourceUrl).then((uploaded) => {
|
||||
const uploadedUrl = uploaded?.faviconUrl || "";
|
||||
if (!uploadedUrl) return;
|
||||
setPendingFaviconPreview(uploadedUrl);
|
||||
faviconSourceUrl = "";
|
||||
settingsStore.markDirty("WEBAPP_FAVICON_URL", uploadedUrl);
|
||||
setCustomFavicon(true);
|
||||
});
|
||||
}
|
||||
|
||||
function setCustomFavicon(enabled) {
|
||||
const nextEnabled = Boolean(enabled);
|
||||
faviconUseCustomDraft = nextEnabled;
|
||||
settingsStore.markDirty("WEBAPP_FAVICON_USE_CUSTOM", nextEnabled);
|
||||
if (!nextEnabled) {
|
||||
pendingFaviconPreviewUrl = "";
|
||||
clearPendingFaviconObjectUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function setEmojiLogo(enabled) {
|
||||
settingsStore.markDirty("WEBAPP_LOGO_USE_EMOJI", Boolean(enabled));
|
||||
if (!enabled) {
|
||||
@@ -199,6 +315,9 @@
|
||||
"WEBAPP_LOGO_USE_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI",
|
||||
"WEBAPP_LOGO_EMOJI_FONT",
|
||||
"WEBAPP_FAVICON_URL",
|
||||
"WEBAPP_FAVICON_USE_CUSTOM",
|
||||
"WEBAPP_LOGO_FAVICON_URL",
|
||||
].includes(key)
|
||||
);
|
||||
let settingsSaved = true;
|
||||
@@ -251,6 +370,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
clearPendingObjectUrl();
|
||||
clearPendingFaviconObjectUrl();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -379,6 +499,74 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card-body appearance-logo-grid appearance-favicon-grid">
|
||||
<div class="appearance-logo-preview appearance-favicon-preview">
|
||||
{#if previewFaviconUrl && !faviconPreviewFailed}
|
||||
<img
|
||||
class="appearance-logo-image"
|
||||
src={previewFaviconUrl}
|
||||
alt=""
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
onerror={() => {
|
||||
faviconPreviewFailed = true;
|
||||
}}
|
||||
/>
|
||||
{:else if !useCustomFavicon && useEmojiLogo}
|
||||
<BrandMark brand={logoBrand} size="lg" />
|
||||
{:else}
|
||||
<span class="appearance-logo-empty" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="appearance-controls">
|
||||
<section class="appearance-control-card">
|
||||
<label class="appearance-switch">
|
||||
<Switch.Root
|
||||
bind:checked={faviconUseCustomDraft}
|
||||
onCheckedChange={setCustomFavicon}
|
||||
class="admin-switch-root"
|
||||
>
|
||||
<Switch.Thumb class="admin-switch-thumb" />
|
||||
</Switch.Root>
|
||||
<span>{at("appearance_use_custom_favicon", {}, "Использовать отдельную favicon")}</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={faviconFileInput}
|
||||
class="appearance-file-input"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/x-icon,.ico"
|
||||
onchange={handleFaviconFileChange}
|
||||
/>
|
||||
<AdminButton
|
||||
class="appearance-control"
|
||||
size="sm"
|
||||
onclick={() => faviconFileInput?.click()}
|
||||
disabled={themesSaving}
|
||||
>
|
||||
<FileText size={13} />
|
||||
{at("appearance_favicon_upload_file", {}, "Загрузить favicon")}
|
||||
</AdminButton>
|
||||
<div class="appearance-url-row">
|
||||
<input
|
||||
class="input appearance-control"
|
||||
type="url"
|
||||
placeholder="https://example.com/icon.png"
|
||||
bind:value={faviconSourceUrl}
|
||||
/>
|
||||
<AdminButton
|
||||
class="appearance-control"
|
||||
size="sm"
|
||||
onclick={uploadFaviconFromUrl}
|
||||
disabled={themesSaving || !faviconSourceUrl.trim()}
|
||||
>
|
||||
{at("appearance_favicon_upload_url", {}, "По ссылке")}
|
||||
</AdminButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="admin-card">
|
||||
@@ -548,6 +736,11 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.appearance-favicon-grid {
|
||||
grid-template-columns: minmax(132px, 140px) minmax(0, 520px);
|
||||
border-top: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.appearance-logo-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -564,6 +757,12 @@
|
||||
background: color-mix(in srgb, var(--admin-surface-2) 54%, var(--admin-surface));
|
||||
}
|
||||
|
||||
.appearance-favicon-preview {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.appearance-logo-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -801,6 +1000,11 @@
|
||||
width: min(164px, 100%);
|
||||
}
|
||||
|
||||
.appearance-favicon-preview {
|
||||
width: min(140px, 100%);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.appearance-url-row,
|
||||
.appearance-emoji-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -76,7 +76,7 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||
"Логотип загружен. Сохраните изменения, чтобы применить его."
|
||||
)
|
||||
);
|
||||
return data.logo_url || "";
|
||||
return { logoUrl: data.logo_url || "", faviconUrl: data.favicon_url || "" };
|
||||
}
|
||||
flash(
|
||||
data?.message ||
|
||||
@@ -106,7 +106,7 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||
"Логотип загружен. Сохраните изменения, чтобы применить его."
|
||||
)
|
||||
);
|
||||
return data.logo_url || "";
|
||||
return { logoUrl: data.logo_url || "", faviconUrl: data.favicon_url || "" };
|
||||
}
|
||||
flash(
|
||||
data?.message ||
|
||||
@@ -119,6 +119,67 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFaviconFile(file) {
|
||||
if (!file) return null;
|
||||
state.update((s) => ({ ...s, themesSaving: true }));
|
||||
try {
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
const data = await api("/admin/appearance/favicon", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
if (data?.ok) {
|
||||
flash(
|
||||
at(
|
||||
"appearance_favicon_uploaded_pending",
|
||||
{},
|
||||
"Favicon загружена. Сохраните изменения, чтобы применить ее."
|
||||
)
|
||||
);
|
||||
return { faviconUrl: data.favicon_url || "", variants: data.variants || {} };
|
||||
}
|
||||
flash(
|
||||
data?.message ||
|
||||
data?.error ||
|
||||
at("appearance_favicon_upload_failed", {}, "Не удалось загрузить favicon")
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, themesSaving: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFaviconUrl(url) {
|
||||
const sourceUrl = String(url || "").trim();
|
||||
if (!sourceUrl) return null;
|
||||
state.update((s) => ({ ...s, themesSaving: true }));
|
||||
try {
|
||||
const data = await api("/admin/appearance/favicon", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ url: sourceUrl }),
|
||||
});
|
||||
if (data?.ok) {
|
||||
flash(
|
||||
at(
|
||||
"appearance_favicon_uploaded_pending",
|
||||
{},
|
||||
"Favicon загружена. Сохраните изменения, чтобы применить ее."
|
||||
)
|
||||
);
|
||||
return { faviconUrl: data.favicon_url || "", variants: data.variants || {} };
|
||||
}
|
||||
flash(
|
||||
data?.message ||
|
||||
data?.error ||
|
||||
at("appearance_favicon_upload_failed", {}, "Не удалось загрузить favicon")
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
state.update((s) => ({ ...s, themesSaving: false }));
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentTheme(key) {
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
@@ -213,5 +274,7 @@ export function createThemesStore({ api, onThemesSaved, flash, at }) {
|
||||
toggleAdminUse,
|
||||
uploadLogoFile,
|
||||
uploadLogoUrl,
|
||||
uploadFaviconFile,
|
||||
uploadFaviconUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,7 +220,21 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
};
|
||||
}
|
||||
if (path === "/admin/appearance/logo") {
|
||||
return { ok: true, logo_url: "/webapp-uploaded-logo/logo-0000000000000000.png" };
|
||||
return {
|
||||
ok: true,
|
||||
logo_url: "/webapp-uploaded-logo/logo-0000000000000000.png",
|
||||
favicon_url: "/webapp-favicon/0000000000000000/icon-180.png",
|
||||
};
|
||||
}
|
||||
if (path === "/admin/appearance/favicon") {
|
||||
return {
|
||||
ok: true,
|
||||
favicon_url: "/webapp-favicon/1111111111111111/icon-180.png",
|
||||
variants: {
|
||||
"32": "/webapp-favicon/1111111111111111/icon-32.png",
|
||||
apple_touch: "/webapp-favicon/1111111111111111/apple-touch-icon.png",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === "/admin/settings" && String(options.method || "GET").toUpperCase() === "PATCH") {
|
||||
try {
|
||||
@@ -236,6 +250,15 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
if (updates.WEBAPP_LOGO_EMOJI_FONT) {
|
||||
DEV_MOCK.config.logoEmojiFont = updates.WEBAPP_LOGO_EMOJI_FONT;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_FAVICON_URL")) {
|
||||
DEV_MOCK.config.faviconUrl = updates.WEBAPP_FAVICON_URL || "";
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_LOGO_FAVICON_URL")) {
|
||||
DEV_MOCK.config.faviconUrl = updates.WEBAPP_LOGO_FAVICON_URL || DEV_MOCK.config.faviconUrl || "";
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, "WEBAPP_FAVICON_USE_CUSTOM")) {
|
||||
DEV_MOCK.config.faviconUseCustom = Boolean(updates.WEBAPP_FAVICON_USE_CUSTOM);
|
||||
}
|
||||
} catch (_e) {
|
||||
void _e;
|
||||
}
|
||||
@@ -282,6 +305,27 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
{ value: "noto-color-animated", label: "Noto Color Emoji Animated" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_FAVICON_USE_CUSTOM",
|
||||
type: "bool",
|
||||
section: "appearance",
|
||||
label: "Custom favicon",
|
||||
value: Boolean(DEV_MOCK.config.faviconUseCustom),
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_FAVICON_URL",
|
||||
type: "url",
|
||||
section: "appearance",
|
||||
label: "Favicon URL",
|
||||
value: DEV_MOCK.config.faviconUrl || "",
|
||||
},
|
||||
{
|
||||
key: "WEBAPP_LOGO_FAVICON_URL",
|
||||
type: "url",
|
||||
section: "appearance",
|
||||
label: "Logo favicon URL",
|
||||
value: DEV_MOCK.config.faviconUrl || "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,6 +30,8 @@ export const DEV_MOCK = {
|
||||
logoUseEmoji: false,
|
||||
logoEmoji: "🫥",
|
||||
logoEmojiFont: "system",
|
||||
faviconUrl: "",
|
||||
faviconUseCustom: false,
|
||||
apiBase: "/api",
|
||||
supportUrl: "https://t.me/support",
|
||||
privacyPolicyUrl: "https://example.com/privacy",
|
||||
|
||||
Reference in New Issue
Block a user