better theme for tv

This commit is contained in:
2026-09-10 19:46:35 +03:30
parent d4e3a99e61
commit 0da9f68353
7 changed files with 80 additions and 32 deletions
+5
View File
@@ -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()
+38 -3
View File
@@ -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<string>('manifest_url')
if (explicit) return [explicit]
const saManifest = getSuperAdminManifestUrl()
if (saManifest) {
return [saManifest]
}
return sortConfigUrls(getConfigUrls()).map(configUrlToManifestUrl)
}
+1
View File
@@ -86,6 +86,7 @@ import {
productsFirstItems,
productsItems,
productsPageLoading,
productsTypeItems,
changeProductsTab,
initProductsPageData,
onProductsPageProviderMounted,
@@ -73,6 +73,7 @@ import {
productsFirstItems,
productsItems,
productsPageLoading,
productsTypeItems,
changeProductsTab,
} from '@/views/base/products/composables/useProductsPageData'
+9 -21
View File
@@ -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<string, boolean>)
const listEls: Record<string, HTMLElement | null> = Object.fromEntries(keys.map((key) => [key, null]))
const resumeTimers: Record<string, ReturnType<typeof setTimeout> | 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,
}
}
+4 -1
View File
@@ -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) {
+22 -7
View File
@@ -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)"
>
<div
v-if="col.items.length"
@@ -251,7 +251,7 @@ const {
toggleFullscreen,
} = useTvMonitorPage()
const { setColListRef, pauseColAuto, scheduleColAuto } = useTvColAutoScroll(['parsian', 'bank'])
const { setColListRef, pauseColAuto, resumeColAuto } = useTvColAutoScroll(['parsian', 'bank'])
function prefersReducedMotion() {
return typeof window !== 'undefined'
@@ -259,12 +259,13 @@ function prefersReducedMotion() {
}
function marqueeCopies(count: number) {
return count >= 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;
}