From 0da9f68353cc01dc145d73383c9f19edbf3bb311 Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 10 Sep 2026 19:46:35 +0330 Subject: [PATCH] better theme for tv --- index.html | 5 +++ src/modules/pwa/manifest.ts | 41 +++++++++++++++++-- src/views/base/products/index.vue | 1 + .../products/mobile/MobileProductsView.vue | 1 + .../tv/composables/useTvColAutoScroll.ts | 30 ++++---------- src/views/tv/composables/useTvMonitorPage.ts | 5 ++- src/views/tv/index.vue | 29 +++++++++---- 7 files changed, 80 insertions(+), 32 deletions(-) diff --git a/index.html b/index.html index 08c3019..4f8a2dd 100644 --- a/index.html +++ b/index.html @@ -295,6 +295,11 @@ if (!logoUrl) return localStorage.setItem(BOOT_LOGO_KEY, logoUrl) if (logoUrl.indexOf('data:') === 0 || typeof fetch !== 'function') return + try { + if (new URL(logoUrl, window.location.href).origin !== window.location.origin) return + } catch (e) { + return + } fetch(logoUrl, { mode: 'cors', credentials: 'omit' }) .then(function (res) { return res && res.ok ? res.blob() : Promise.reject() diff --git a/src/modules/pwa/manifest.ts b/src/modules/pwa/manifest.ts index 721dfce..ed80951 100644 --- a/src/modules/pwa/manifest.ts +++ b/src/modules/pwa/manifest.ts @@ -1,6 +1,11 @@ import type { AppConfig } from '@/config/types' import { getConfig, getRawAppConfig } from '@/config/app-config' import { getConfigUrls, sortConfigUrls } from '@/config/load-config' +import { + getSuperAdminBase, + isSuperAdminUrl, + resolveTenantDomain, +} from '@/config/superadmin' import { clearCachedPwaManifest, readCachedPwaManifest, @@ -23,7 +28,24 @@ function sleep(ms: number) { } function normalizeBaseUrl(url: string) { - return url.replace(/\/get_user\/?$/i, '').replace(/\/+$/, '') + try { + const parsed = new URL(url) + parsed.search = '' + parsed.hash = '' + parsed.pathname = parsed.pathname + .replace(/\/get_user\/?$/i, '') + .replace(/\/manifest\.json$/i, '') + if (!parsed.pathname || parsed.pathname === '/') { + return parsed.origin + } + return `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}` + } catch { + return url + .replace(/[?#].*$/, '') + .replace(/\/get_user\/?$/i, '') + .replace(/\/manifest\.json$/i, '') + .replace(/\/+$/, '') + } } /** همان baseای که get_user از آن لود شده (کش شده در localStorage) */ @@ -33,15 +55,28 @@ export function getConfigBaseFromStorage() { return normalizeBaseUrl(lastSuccess) } +function getSuperAdminManifestUrl(): string | null { + const saBase = getSuperAdminBase() + if (!saBase) return null + return `${saBase}/manifest.json?domain=${encodeURIComponent(resolveTenantDomain())}` +} + function configUrlToManifestUrl(configUrl: string) { - return `${configUrl.replace(/\/+$/, '')}/manifest.json` + if (isSuperAdminUrl(configUrl)) { + return getSuperAdminManifestUrl() || `${normalizeBaseUrl(configUrl)}/manifest.json` + } + return `${normalizeBaseUrl(configUrl)}/manifest.json` } -/** همان ۵ URL پایهٔ get_user — اولویت با CONFIG_Success_URL */ export function resolveManifestUrls(config?: AppConfig | null): string[] { const explicit = getConfig('manifest_url') if (explicit) return [explicit] + const saManifest = getSuperAdminManifestUrl() + if (saManifest) { + return [saManifest] + } + return sortConfigUrls(getConfigUrls()).map(configUrlToManifestUrl) } diff --git a/src/views/base/products/index.vue b/src/views/base/products/index.vue index 2aaa689..6917662 100644 --- a/src/views/base/products/index.vue +++ b/src/views/base/products/index.vue @@ -86,6 +86,7 @@ import { productsFirstItems, productsItems, productsPageLoading, + productsTypeItems, changeProductsTab, initProductsPageData, onProductsPageProviderMounted, diff --git a/src/views/base/products/mobile/MobileProductsView.vue b/src/views/base/products/mobile/MobileProductsView.vue index 2f9a68d..65a9250 100644 --- a/src/views/base/products/mobile/MobileProductsView.vue +++ b/src/views/base/products/mobile/MobileProductsView.vue @@ -73,6 +73,7 @@ import { productsFirstItems, productsItems, productsPageLoading, + productsTypeItems, changeProductsTab, } from '@/views/base/products/composables/useProductsPageData' diff --git a/src/views/tv/composables/useTvColAutoScroll.ts b/src/views/tv/composables/useTvColAutoScroll.ts index 7316a31..33523d8 100644 --- a/src/views/tv/composables/useTvColAutoScroll.ts +++ b/src/views/tv/composables/useTvColAutoScroll.ts @@ -1,12 +1,10 @@ import { onMounted, onUnmounted, reactive } from 'vue' -const AUTO_RESUME_MS = 6000 -const AUTO_PX_PER_FRAME = 0.45 +const AUTO_PX_PER_FRAME = 0.55 export function useTvColAutoScroll(keys: string[]) { const paused = reactive(Object.fromEntries(keys.map((key) => [key, false])) as Record) const listEls: Record = Object.fromEntries(keys.map((key) => [key, null])) - const resumeTimers: Record | undefined> = {} let rafId = 0 function reduceMotion() { @@ -20,20 +18,10 @@ export function useTvColAutoScroll(keys: string[]) { function pauseColAuto(key: string) { paused[key] = true - if (resumeTimers[key]) { - clearTimeout(resumeTimers[key]) - resumeTimers[key] = undefined - } } - function scheduleColAuto(key: string) { - if (resumeTimers[key]) { - clearTimeout(resumeTimers[key]) - } - resumeTimers[key] = setTimeout(() => { - paused[key] = false - resumeTimers[key] = undefined - }, AUTO_RESUME_MS) + function resumeColAuto(key: string) { + paused[key] = false } function tick() { @@ -42,11 +30,14 @@ export function useTvColAutoScroll(keys: string[]) { if (paused[key]) continue const el = listEls[key] if (!el) continue + const overflow = el.scrollHeight - el.clientHeight + if (overflow <= 1) continue const half = el.scrollHeight / 2 - if (half <= el.clientHeight + 8) continue el.scrollTop += AUTO_PX_PER_FRAME - if (el.scrollTop >= half) { + if (half > el.clientHeight && el.scrollTop >= half) { el.scrollTop -= half + } else if (el.scrollTop >= overflow) { + el.scrollTop = 0 } } } @@ -59,15 +50,12 @@ export function useTvColAutoScroll(keys: string[]) { onUnmounted(() => { cancelAnimationFrame(rafId) - for (const key of keys) { - if (resumeTimers[key]) clearTimeout(resumeTimers[key]) - } }) return { pausedCols: paused, setColListRef, pauseColAuto, - scheduleColAuto, + resumeColAuto, } } diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts index bce0708..c1d61bd 100644 --- a/src/views/tv/composables/useTvMonitorPage.ts +++ b/src/views/tv/composables/useTvMonitorPage.ts @@ -38,7 +38,10 @@ function isParsianCoin(product: Product) { function isBankCoin(product: Product) { if (isParsianCoin(product)) return false const group = (product.group_type ?? '').trim() - return group === 'سکه' || group === 'سکه بانکی' || (group.includes('سکه') && !group.includes('پارسیان')) + const title = (product.title ?? '').trim() + if (group === 'سکه' || group === 'سکه بانکی') return true + if (group.includes('سکه') && !group.includes('پارسیان')) return true + return title.includes('بانکی') } function bankCoinRank(product: Product) { diff --git a/src/views/tv/index.vue b/src/views/tv/index.vue index 98b4ac0..d6564b4 100644 --- a/src/views/tv/index.vue +++ b/src/views/tv/index.vue @@ -90,7 +90,7 @@ :ref="(el) => setColListRef(col.key, el)" class="tv-col__list" @mouseenter="pauseColAuto(col.key)" - @mouseleave="scheduleColAuto(col.key)" + @mouseleave="resumeColAuto(col.key)" >
= 2 && !prefersReducedMotion() ? [0, 1] : [0] + return count >= 1 && !prefersReducedMotion() ? [0, 1] : [0] } function loopTicker(items: Product[]) { if (!items.length) return [] - return [0, 1].flatMap((copy) => items.map((product) => ({ product, copy }))) + const copies = items.length >= 2 && !prefersReducedMotion() ? [0, 1] : [0] + return copies.flatMap((copy) => items.map((product) => ({ product, copy }))) } const shamsTickerLoop = computed(() => loopTicker(tickerProducts.value)) @@ -281,7 +282,7 @@ const coinColumns = computed(() => [ }, { key: 'bank', - title: 'سکه بانکی', + title: 'سکه', hint: 'دسته سکه', empty: 'سکه‌ای برای نمایش نیست', items: bankCoins.value, @@ -592,10 +593,20 @@ const coinColumns = computed(() => [ flex-direction: column; overflow-x: hidden; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: color-mix(in srgb, var(--tv-primary) 42%, transparent) transparent; + overscroll-behavior: contain; + container-type: size; + scrollbar-width: none; + + &:hover { + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--tv-primary) 42%, transparent) transparent; + } &::-webkit-scrollbar { + width: 0; + } + + &:hover::-webkit-scrollbar { width: 6px; } @@ -611,13 +622,17 @@ const coinColumns = computed(() => [ .tv-col__track { display: flex; + flex-shrink: 0; flex-direction: column; } .tv-col__group { display: flex; + flex-shrink: 0; flex-direction: column; gap: 8px; + box-sizing: border-box; + min-height: 100cqh; padding-bottom: 8px; }