mirror of
https://github.com/usetrmnl/inker.git
synced 2026-04-29 13:45:07 -07:00
0.3.3 — Emoji/CJK rendering, refresh rate & UX fixes
This commit is contained in:
+5
-7
@@ -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,6 +1,6 @@
|
||||
[](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,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": {
|
||||
|
||||
@@ -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
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "inker-frontend",
|
||||
"private": true,
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bunx --bun vite",
|
||||
|
||||
@@ -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
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB |
Reference in New Issue
Block a user