diff --git a/Dockerfile b/Dockerfile index 46341c9..a931468 100644 --- a/Dockerfile +++ b/Dockerfile @@ -91,7 +91,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ nginx \ # Chrome headless shell dependencies wget ca-certificates openssl unzip \ - fonts-liberation fontconfig \ + fonts-liberation fonts-noto-color-emoji fonts-noto-cjk fontconfig \ libnss3 libatk-bridge2.0-0t64 libdrm2 libxkbcommon0 \ libgbm1 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ libasound2t64 libcups2t64 libatk1.0-0t64 libnspr4 libdbus-1-3 \ @@ -104,15 +104,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ unzip /tmp/chrome.zip -d /opt/ && \ chmod +x /opt/chrome-headless-shell-linux64/chrome-headless-shell && \ rm /tmp/chrome.zip && \ - # Strip Chrome: remove GPU libs (--disable-gpu), keep only en-US locale, remove hyphen data + # Strip Chrome: remove GPU libs (--disable-gpu), keep locale packs for Unicode/CJK shaping rm -f /opt/chrome-headless-shell-linux64/libEGL.so \ /opt/chrome-headless-shell-linux64/libGLESv2.so \ /opt/chrome-headless-shell-linux64/libvk_swiftshader.so \ /opt/chrome-headless-shell-linux64/libvulkan.so.1 \ /opt/chrome-headless-shell-linux64/vk_swiftshader_icd.json \ /opt/chrome-headless-shell-linux64/LICENSE.headless_shell && \ - find /opt/chrome-headless-shell-linux64/locales -type f ! -name 'en-US.pak' -delete && \ - rm -rf /opt/chrome-headless-shell-linux64/hyphen-data && \ # Install s6-overlay wget -q "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" -O /tmp/s6-noarch.tar.xz && \ tar -C / -Jxpf /tmp/s6-noarch.tar.xz && \ @@ -133,11 +131,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ dpkg --purge --force-depends \ libllvm19 libz3-4 libperl5.40 perl perl-modules-5.40 \ libavahi-client3 libavahi-common-data libavahi-common3 \ - libelf1t64 libc-l10n locales \ + libelf1t64 \ 2>/dev/null || true && \ rm -rf /var/lib/apt/lists/* /var/log/dpkg.log /var/log/apt && \ - # Remove system locales and unused data (keep only C/POSIX and en_US) - rm -rf /usr/share/locale /usr/share/i18n /usr/share/doc /usr/share/man \ + # Remove unused data (keep locales and i18n for Unicode/CJK support) + rm -rf /usr/share/doc /usr/share/man \ /usr/share/info /usr/share/lintian /usr/share/X11/xkb \ /var/cache/debconf/*-old && \ # Remove auto-created PostgreSQL cluster (will be initialized on first run) diff --git a/README.md b/README.md index a4b1e23..5c3a431 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://buymeacoffee.com/wojo_o) -# Inker v0.3.2 +# Inker v0.3.3 Self-hosted e-ink device management server built for the homelab community. Works with [TRMNL](https://usetrmnl.com/) devices (supports firmware 1.7.8) and any BYOD e-ink display. Design screens, create custom widgets with live data from your local network, and manage your displays from a modern web interface. diff --git a/backend/package.json b/backend/package.json index 8d6a820..a93be69 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "inker-backend", - "version": "0.3.2", + "version": "0.3.3", "description": "Inker Server Backend - E-ink Device Management", "main": "dist/main.js", "scripts": { diff --git a/backend/src/api/display/display.service.ts b/backend/src/api/display/display.service.ts index da345e0..835cb58 100644 --- a/backend/src/api/display/display.service.ts +++ b/backend/src/api/display/display.service.ts @@ -303,12 +303,10 @@ export class DisplayService { }); } - // For multi-screen playlists, cap refresh rate at current screen's duration - // so the device calls back in time for rotation + // Use the playlist item's configured duration as the refresh rate + // so the device checks back at the interval the user actually set const screenDuration = currentScreen.duration || 60; - const effectiveDeviceRate = device.playlist.items.length > 1 - ? Math.min(device.refreshRate, screenDuration) - : device.refreshRate; + const effectiveDeviceRate = screenDuration; // Calculate refresh rate based on current screen content // Clock widgets get minute-synced refresh; otherwise uses device's configured rate diff --git a/backend/src/main.ts b/backend/src/main.ts index cf5f291..e98dffb 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -81,7 +81,7 @@ async function bootstrap() { const config = new DocumentBuilder() .setTitle('Inker API') .setDescription('API documentation for Inker e-ink device management server') - .setVersion('0.3.2') + .setVersion('0.3.3') .addBearerAuth() .addApiKey({ type: 'apiKey', name: 'X-Device-Key', in: 'header' }, 'device-key') .build(); diff --git a/backend/src/screen-designer/services/screen-renderer.service.ts b/backend/src/screen-designer/services/screen-renderer.service.ts index d3626a6..ff7279e 100644 --- a/backend/src/screen-designer/services/screen-renderer.service.ts +++ b/backend/src/screen-designer/services/screen-renderer.service.ts @@ -1971,7 +1971,7 @@ export class ScreenRendererService implements OnModuleDestroy, OnModuleInit { // List items already have prefix (•, -, 1.) from renderList(), so no list-style needed // Use full fontSize to match frontend WidgetRenderer.tsx // Allow text wrapping (no white-space: nowrap) to match designer behavior - const listItems = renderedContent.slice(0, 10).map(item => + const listItems = renderedContent.map(item => `
  • ${this.escapeHtml(String(item))}
  • ` ).join(''); innerHtml = ``; @@ -3207,7 +3207,7 @@ export class ScreenRendererService implements OnModuleDestroy, OnModuleInit { } if (Array.isArray(renderedContent)) { - const items = renderedContent.slice(0, 10).map(item => + const items = renderedContent.map(item => `
  • ${this.escapeHtml(String(item))}
  • ` ).join(''); return `
    `; diff --git a/frontend/package.json b/frontend/package.json index 35fb7be..69c1649 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "inker-frontend", "private": true, - "version": "0.3.2", + "version": "0.3.3", "type": "module", "scripts": { "dev": "bunx --bun vite", diff --git a/frontend/src/pages/playlists/PlaylistForm.tsx b/frontend/src/pages/playlists/PlaylistForm.tsx index 12c8e7e..9bea895 100644 --- a/frontend/src/pages/playlists/PlaylistForm.tsx +++ b/frontend/src/pages/playlists/PlaylistForm.tsx @@ -40,7 +40,7 @@ export function PlaylistForm() { }); const [selectedScreenId, setSelectedScreenId] = useState(''); - const [duration, setDuration] = useState(60); + const [durationInput, setDurationInput] = useState('60'); const { data: playlist, isLoading: isLoadingPlaylist } = useApi( async () => { @@ -220,9 +220,10 @@ export function PlaylistForm() { if (!selectedScreenId) return; const currentScreens = formData.screens || []; + const parsedDuration = Math.max(60, Number(durationInput) || 60); const newScreen: PlaylistScreen = { screenId: selectedScreenId, - duration, + duration: parsedDuration, order: currentScreens.length, }; @@ -232,7 +233,7 @@ export function PlaylistForm() { })); setSelectedScreenId(''); - setDuration(60); + setDurationInput('60'); }; const handleRemoveScreen = (index: number) => { @@ -264,12 +265,21 @@ export function PlaylistForm() { })); }; - const handleUpdateScreenDuration = (index: number, newDuration: number) => { - const validDuration = Math.max(60, newDuration); + const handleUpdateScreenDuration = (index: number, value: string) => { + const num = value === '' ? 0 : Number(value); setFormData((prev) => ({ ...prev, screens: (prev.screens || []).map((screen, i) => - i === index ? { ...screen, duration: validDuration } : screen + i === index ? { ...screen, duration: num } : screen + ), + })); + }; + + const handleBlurScreenDuration = (index: number) => { + setFormData((prev) => ({ + ...prev, + screens: (prev.screens || []).map((screen, i) => + i === index ? { ...screen, duration: Math.max(60, screen.duration || 60) } : screen ), })); }; @@ -425,8 +435,9 @@ export function PlaylistForm() { setDuration(Math.max(60, Number(e.target.value)))} + value={durationInput} + onChange={(e) => setDurationInput(e.target.value)} + onBlur={() => setDurationInput(String(Math.max(60, Number(durationInput) || 60)))} min={60} /> @@ -503,8 +514,9 @@ export function PlaylistForm() {
    handleUpdateScreenDuration(index, Number(e.target.value))} + value={screen.duration || ''} + onChange={(e) => handleUpdateScreenDuration(index, e.target.value)} + onBlur={() => handleBlurScreenDuration(index)} min={60} className="w-16 px-2 py-1 text-sm text-center border border-border-light rounded focus:outline-none focus:ring-1 focus:ring-accent" /> diff --git a/preview.png b/preview.png deleted file mode 100644 index 715eda1..0000000 Binary files a/preview.png and /dev/null differ