Skip to content

Commit 9e9899c

Browse files
committed
feat: update CMS adapter configuration and enhance playlist handling
1 parent 7cd5f5b commit 9e9899c

16 files changed

+810
-113
lines changed

.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ VITE_PLAYER_VERSION=0.0.1
22
VITE_ENABLE_PLAYBACK_TRACKER=false
33
VITE_TIMEZONE=America/New_York
44
# Available adapters: NetworkFile, Screenlite, GarlicHub
5-
VITE_CMS_ADAPTER=GarlicHub
5+
VITE_CMS_ADAPTER=NetworkFile
66
# URL examples: /demo/playlist_data.json, http://localhost:3000, https://garlic-hub.com
7-
VITE_CMS_ADAPTER_URL=https://garlic-hub.com
7+
VITE_CMS_ADAPTER_URL=/demo/playlist_data.json

public/demo/playlist_data.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"id": "demo_playlist_1",
44
"start_date": "2025-05-01",
5-
"end_date": "2025-05-31",
5+
"end_date": "2025-07-31",
66
"start_time": "00:00:00",
77
"end_time": "23:59:59",
88
"width": 1280,

src/App.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
1-
import { useMemo } from 'react'
1+
import { useEffect, useMemo } from 'react'
22
import { useCMSAdapter } from './hooks/useCMSAdapter'
33
import { getCMSAdapter } from './utils/getCMSAdapter'
44
import { Player } from './Player'
55
import { useCachedData } from './hooks/useCachedData'
6+
import { useConfigStore } from './stores/configStore'
7+
import { ConfigOverlay } from './components/ConfigOverlay'
68

79
export const App = () => {
8-
const searchParams = new URLSearchParams(window.location.search)
9-
10-
const adapterParam = searchParams.get('adapter')
10+
const { config, loadConfig } = useConfigStore()
11+
const adapterParam = config.cmsAdapter
12+
const adapterUrl = config.cmsAdapterUrl
13+
const timezone = config.timezone
14+
15+
useEffect(() => {
16+
loadConfig()
17+
}, [loadConfig])
1118

12-
const adapter = useMemo(() => getCMSAdapter(adapterParam), [adapterParam])
19+
const adapter = useMemo(() => getCMSAdapter(adapterParam, adapterUrl), [adapterParam, adapterUrl])
1320

1421
const data = useCMSAdapter({ adapter })
1522

16-
const timezone = import.meta.env.VITE_TIMEZONE
17-
1823
const { cachedData, isCaching } = useCachedData(data)
1924

20-
if(cachedData.length > 0) return <Player data={ data } timezone={timezone} />
21-
22-
if (!isCaching) {
23-
return (
24-
<div className='bg-black w-screen h-screen overflow-hidden'>
25-
<h1 className='text-white text-3xl font-bold'>Schedule is empty</h1>
26-
</div>
27-
)
28-
}
29-
30-
if (isCaching) {
31-
return (
32-
<div className='bg-black w-screen h-screen overflow-hidden'>
33-
<h1 className='text-white text-3xl font-bold'>Loading...</h1>
34-
</div>
35-
)
36-
}
25+
return (
26+
<>
27+
<ConfigOverlay />
28+
{cachedData.length > 0 ? (
29+
<Player data={data} timezone={timezone} />
30+
) : !isCaching ? (
31+
<div className='bg-black w-screen h-screen overflow-hidden'>
32+
<h1 className='text-white text-3xl font-bold'>Schedule is empty</h1>
33+
</div>
34+
) : (
35+
<div className='bg-black w-screen h-screen overflow-hidden'>
36+
<h1 className='text-white text-3xl font-bold'>Loading...</h1>
37+
</div>
38+
)}
39+
</>
40+
)
3741
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { MediaItem, MediaCacheAdapter, CacheProgress } from '../types/cache'
2+
3+
const CACHE_NAME = 'screenlite-media-cache-v1'
4+
const METADATA_KEY = 'screenlite-media-metadata'
5+
6+
interface CacheMetadata {
7+
[url: string]: {
8+
type: 'image' | 'video'
9+
contentType: string
10+
size: number
11+
cachedAt: number
12+
}
13+
}
14+
15+
export class BrowserMediaCacheAdapter implements MediaCacheAdapter {
16+
private async getMetadata(): Promise<CacheMetadata> {
17+
const data = localStorage.getItem(METADATA_KEY)
18+
return data ? JSON.parse(data) : {}
19+
}
20+
21+
private async setMetadata(metadata: CacheMetadata): Promise<void> {
22+
localStorage.setItem(METADATA_KEY, JSON.stringify(metadata))
23+
}
24+
25+
private async updateMetadata(url: string, data: CacheMetadata[string]): Promise<void> {
26+
const metadata = await this.getMetadata()
27+
metadata[url] = data
28+
await this.setMetadata(metadata)
29+
}
30+
31+
async cacheItem(item: MediaItem): Promise<void> {
32+
const cache = await caches.open(CACHE_NAME)
33+
34+
// First check if we already have this item
35+
if (await this.hasItem(item.url)) {
36+
return
37+
}
38+
39+
try {
40+
const response = await fetch(item.url)
41+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
42+
43+
const contentType = response.headers.get('content-type') || item.contentType || 'application/octet-stream'
44+
const size = parseInt(response.headers.get('content-length') || '0', 10)
45+
46+
// Store the response in the cache
47+
await cache.put(item.url, response.clone())
48+
49+
// Store metadata
50+
await this.updateMetadata(item.url, {
51+
type: item.type,
52+
contentType,
53+
size,
54+
cachedAt: Date.now()
55+
})
56+
} catch (error) {
57+
console.error(`Failed to cache item: ${item.url}`, error)
58+
throw error
59+
}
60+
}
61+
62+
async cacheItems(items: MediaItem[], onProgress?: (progress: CacheProgress) => void): Promise<void> {
63+
const total = items.length
64+
let completed = 0
65+
66+
for (const item of items) {
67+
try {
68+
onProgress?.({
69+
url: item.url,
70+
progress: (completed / total) * 100,
71+
status: 'downloading'
72+
})
73+
74+
await this.cacheItem(item)
75+
completed++
76+
77+
onProgress?.({
78+
url: item.url,
79+
progress: (completed / total) * 100,
80+
status: 'completed'
81+
})
82+
} catch (error) {
83+
onProgress?.({
84+
url: item.url,
85+
progress: (completed / total) * 100,
86+
status: 'error',
87+
error: error instanceof Error ? error.message : 'Unknown error'
88+
})
89+
}
90+
}
91+
}
92+
93+
async getItem(url: string): Promise<string | null> {
94+
const cache = await caches.open(CACHE_NAME)
95+
const response = await cache.match(url)
96+
97+
if (!response) return null
98+
99+
// For browsers that support it, create a blob URL
100+
if (window.URL && response.blob) {
101+
const blob = await response.blob()
102+
return URL.createObjectURL(blob)
103+
}
104+
105+
// Fallback to the original URL if we can't create a blob URL
106+
return url
107+
}
108+
109+
async hasItem(url: string): Promise<boolean> {
110+
const cache = await caches.open(CACHE_NAME)
111+
const response = await cache.match(url)
112+
return !!response
113+
}
114+
115+
async removeItem(url: string): Promise<void> {
116+
const cache = await caches.open(CACHE_NAME)
117+
await cache.delete(url)
118+
119+
// Remove metadata
120+
const metadata = await this.getMetadata()
121+
delete metadata[url]
122+
await this.setMetadata(metadata)
123+
}
124+
125+
async clear(): Promise<void> {
126+
await caches.delete(CACHE_NAME)
127+
localStorage.removeItem(METADATA_KEY)
128+
}
129+
130+
async getSize(): Promise<number> {
131+
const metadata = await this.getMetadata()
132+
return Object.values(metadata).reduce((total, item) => total + item.size, 0)
133+
}
134+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { ConfigData, ConfigStorageAdapter } from '../types/config'
2+
import { DEFAULT_CONFIG } from '../config/defaults'
3+
4+
const CONFIG_STORAGE_KEY = 'screenlite_config'
5+
6+
export class LocalStorageConfigAdapter implements ConfigStorageAdapter {
7+
async get(): Promise<ConfigData> {
8+
try {
9+
// Get environment variables
10+
const envConfig = {
11+
cmsAdapter: import.meta.env.VITE_CMS_ADAPTER || undefined,
12+
cmsAdapterUrl: import.meta.env.VITE_CMS_ADAPTER_URL || undefined,
13+
timezone: import.meta.env.VITE_TIMEZONE || undefined,
14+
playbackTrackerEnabled: import.meta.env.VITE_ENABLE_PLAYBACK_TRACKER === 'true' ? true : undefined
15+
}
16+
17+
// Filter out undefined env values
18+
const filteredEnvConfig = Object.fromEntries(
19+
Object.entries(envConfig).filter(([_, value]) => value !== undefined)
20+
) as Partial<ConfigData>
21+
22+
// Get stored config
23+
const storedConfig = localStorage.getItem(CONFIG_STORAGE_KEY)
24+
const parsedStoredConfig: Partial<ConfigData> = storedConfig ? JSON.parse(storedConfig) : {}
25+
26+
// Merge in order: defaults <- env <- stored (user settings)
27+
// This allows user settings to override environment variables
28+
return {
29+
...DEFAULT_CONFIG,
30+
...filteredEnvConfig,
31+
...parsedStoredConfig
32+
} as ConfigData
33+
} catch (error) {
34+
console.error('Failed to get config from localStorage:', error)
35+
return DEFAULT_CONFIG
36+
}
37+
}
38+
39+
async set(config: Partial<ConfigData>): Promise<void> {
40+
try {
41+
const existingConfig = await this.get()
42+
const newConfig = { ...existingConfig, ...config }
43+
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(newConfig))
44+
} catch (error) {
45+
console.error('Failed to set config in localStorage:', error)
46+
throw error
47+
}
48+
}
49+
50+
async clear(): Promise<void> {
51+
try {
52+
localStorage.removeItem(CONFIG_STORAGE_KEY)
53+
} catch (error) {
54+
console.error('Failed to clear config from localStorage:', error)
55+
throw error
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)