0.3.3 — Emoji/CJK rendering, refresh rate & UX fixes

This commit is contained in:
wojo
2026-04-16 21:51:45 +00:00
parent bf274b0edc
commit 31b98b89b8
9 changed files with 36 additions and 28 deletions
+5 -7
View File
@@ -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)
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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": {
+3 -5
View File
@@ -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
+1 -1
View File
@@ -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();
@@ -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 =>
`<li style="font-size: ${fontSize}px; line-height: 1.2; margin-bottom: 4px;">${this.escapeHtml(String(item))}</li>`
).join('');
innerHtml = `<ul style="list-style: none; width: 100%; text-align: ${textAlign}; margin: 0; padding: 0;">${listItems}</ul>`;
@@ -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 =>
`<li style="margin-bottom: 4px;">${this.escapeHtml(String(item))}</li>`
).join('');
return `<div style="${baseStyle}"><ul style="list-style: none; margin: 0; padding: 0; width: 100%; text-align: ${textAlign};">${items}</ul></div>`;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "inker-frontend",
"private": true,
"version": "0.3.2",
"version": "0.3.3",
"type": "module",
"scripts": {
"dev": "bunx --bun vite",
+22 -10
View File
@@ -40,7 +40,7 @@ export function PlaylistForm() {
});
const [selectedScreenId, setSelectedScreenId] = useState<string>('');
const [duration, setDuration] = useState<number>(60);
const [durationInput, setDurationInput] = useState<string>('60');
const { data: playlist, isLoading: isLoadingPlaylist } = useApi<Playlist>(
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() {
<Input
label="Duration (s)"
type="number"
value={duration.toString()}
onChange={(e) => 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}
/>
</div>
@@ -503,8 +514,9 @@ export function PlaylistForm() {
<div className="flex items-center gap-1 flex-shrink-0">
<input
type="number"
value={screen.duration}
onChange={(e) => 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"
/>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB