Commit graph

17 commits

Author SHA1 Message Date
ScreenTinker 3159f94107 Make unblock stick, and say so when a device is refused
A customer blocked a screen once to see what the button did, then spent an evening
unable to get it back. Three separate faults stacked up.

1. Unblock did not stick. applyToDevice() restores `blocked` on re-pair — deliberately,
so a block cannot be shrugged off by deleting the device — which makes the SAVED copy
the real authority. Unblock only ever wrote `devices`, so the saved row stayed 1 and the
next delete + re-pair silently re-blocked. There was no way out from the dashboard at
all: unblock, re-pair, refused, repeat. Block and unblock now both mirror to the saved
copy, so the survives-a-re-pair property is deliberate rather than a leftover.

2. The refusal was invisible. handleServerRejection() clears credentials and calls
onUnpaired, but only ProvisioningActivity ever assigned that callback — and it is long
gone by the time playback is running. So the screen sat on "Connecting to server" and
the player eventually blamed the URL, sending the operator off checking their network
while the server had already said exactly what was wrong. MainActivity now handles it.

(This half was mine: clearing those leaked callbacks to stop the relaunch loop removed
the only thing that surfaced a rejection. It was a broken path — it fired into a
destroyed Activity — but it was the only one, and MainActivity should have owned it.)

3. The reason was thrown away. The server sends device:auth-error {error: "Device
blocked"} and the client discarded it. It is kept now, and a blocked screen says so
instead of implying a network fault. Localised in all six languages, matching the other
on-screen status strings.

Also ran on prod: one stale saved block cleared (fingerprint ef6540376599, the reporter's
tablet), DB backed up first. It was the only such row.

Tests pin both directions, because the two are easy to confuse: unblock must clear the
saved copy, AND a genuine block must still survive a delete + re-pair.
2026-07-29 18:44:11 -05:00
screentinker 11c2890433
fix(android): visible D-pad focus stroke on setup buttons (#218)
The setup screen buttons used a bare <ripple> which only reacts to
state_pressed (touch). D-pad navigation triggers state_focused, which the
ripple ignored, leaving the focused button nearly invisible against the
dark background from TV viewing distance.

Wrap the button shape in a <selector> (kept inside the ripple so touch
still gets the press ripple) that adds a bright 3dp white stroke on
state_focused.

Closes #209

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:34:21 -05:00
screentinker ef91f644a7
feat(system-control): Tier 0/1 controls with no device-owner dependency (#160) (#169)
* feat(system-control): Tier 0/1 controls with no device-owner dependency [#160]

Track A of the system-control split (Track B = device owner, shipped in #168). Ships the
capabilities that need NO device owner, with graceful per-tier degradation.

Capability reporting (keystone):
- DeviceInfo now reports can_write_settings / accessibility_enabled / overlay_granted
  alongside the existing tier/device_owner flags; server persists them (3 additive device
  columns, older APKs default to 0); dashboard gates controls + shows what's grantable.

Android SystemControl (new, all best-effort / no-op when unsupported):
- Tier 0 (no permission): media volume (AudioManager STREAM_MUSIC), per-window brightness
  (WindowManager.LayoutParams.screenBrightness — dims our window only).
- Tier 1 (WRITE_SETTINGS): system-wide brightness + screen-off timeout (Settings.System).
- Commands set_volume / set_brightness / set_system_brightness / set_screen_timeout wired
  in MainActivity.onCommand; ALLOWED_COMMANDS extended for the group path.
- SetupActivity gains a one-time WRITE_SETTINGS grant row (mirrors the overlay/accessibility
  grants); manifest declares WRITE_SETTINGS.

Dashboard:
- device-detail "System control" section (any Android panel): volume + this-app brightness
  sliders always; system brightness + sleep-timeout only when the panel reports
  can_write_settings, else a "grant on the panel" hint. Sends on release (not drag).

Validated live on a non-owner tier-0 panel: dashboard → set_volume 0.75/0.15 → the panel's
STREAM_MUSIC volume moved to 11/2 (of 15). 423 server tests green.

Closes #160.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(system-control): volume slider reflects real volume + device-owner brightness/timeout [#160]

Two fixes from live testing:

1. Volume "doesn't remember" — the slider hardcoded 50 because the panel never reported its
   current volume. Now DeviceInfo reports media_volume (0..1); a new lightweight device:info
   socket event lets the panel re-report right after a set_volume (no full re-register / playlist
   re-push); server stores devices.media_volume; the dashboard inits the slider from it.
   Validated: dashboard set_volume 0.60 -> panel STREAM_MUSIC 2->9 (of 15) -> stored 0.60.

2. System brightness/timeout on a DEVICE OWNER — was gated only on WRITE_SETTINGS, which an
   owner doesn't have. A device owner can set those via DevicePolicyManager.setSystemSetting
   with no grant, so SystemControl now takes that path when isDeviceOwner(), and the dashboard
   enables the Tier-1 controls when can_write_settings OR tier===2. STPolicy.setSystemSetting added.

deviceSocket device_info UPDATE extracted into applyDeviceInfo(), shared by device:register and
device:info. Migration: devices.media_volume REAL (additive). 423 server tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(system-control): brightness/timeout remember + move controls into a tab [#160]

Same "remember what it's set to" treatment as volume, now for brightness + sleep timeout, and
the System control section moves off the top into its own "Controls" tab.

Reporting (DeviceInfo -> device:info re-report -> devices columns -> dashboard slider init):
- system_brightness (read from Settings.System, no permission) + screen_off_timeout_ms.
- window_brightness: persisted in ServerConfig (survives relaunch, re-applied on MainActivity
  launch) so the per-window slider reflects it too.
- reportInfoNow() now also fires after set_brightness / set_screen_timeout.

Dashboard: new "Controls" tab (any Android panel) holding the volume/brightness/timeout controls;
every control inits from the reported value; sleep dropdown preselects the current timeout.

Server: +3 additive columns (system_brightness, window_brightness, screen_off_timeout_ms); the
device_info UPDATE stores them. Migrations all additive/re-runnable. 423 server tests green.

Validated live: set_brightness 0.40 -> stored window_brightness 0.40; volume 0.30 -> 0.33.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:48:47 -05:00
screentinker 501ffb11c1
feat(device-owner): tier foundation + QR provisioning + content-expiry & device enhancements (#168)
Device-owner tier substrate + silent install, end-to-end QR provisioning (Android 12+ compliance, APK-derived checksum, URL pre-seed, zero-touch onboarding, guided a11y screen), content-expiry (#157) with player no-restart deferral, and the device-enhancement batch (#10/#12/#13/#14). Backward-compatible with 1.9.3 clients; all autonomous behaviors opt-in. QA + security review green. Closes #161, #157, #159.
2026-07-12 19:41:07 -05:00
ScreenTinker f60f677cf0 fix(android): player provisioning + playback robustness
Client-side fixes to the Android signage player, all validated end-to-end on a
Pixel-10 emulator (Android 16) against the alpha server.

- content download: a local item with "remote_url": null was mis-tagged as a
  remote stream (org.json optString returns the STRING "null" for a JSON null),
  so it was ack'd "ready" and NEVER downloaded — stranding the screen on
  "waiting for content" and only ever playing 1 of N files. Guard with isNull().
- playback (#162): PlaylistController trusted isRunning+currentIndex as "already
  playing" and never re-called playItem, permanently stranding a panel on
  "waiting for content" after a restart/OTA/content-not-ready-at-first-start.
  Guards now require hasContentOnScreen (a genuine render) before short-circuiting.
- provisioning: revert to the URL-entry screen if a connect attempt hangs >60s
  (wrong/unreachable URL) instead of an endless "Connecting to server…".
- re-pair: a server rejection (device:unpaired / auth-error) left the device
  connected-but-unregistered with no pairing code (stuck); a naive re-register
  then stormed the #150 reclaim guard ~20x/s. Now: re-register once, debounced +
  backed off; honor the reclaim-settle window with a stable "re-pairing available
  in Xs" countdown; show the code only once the server accepts it (isPairingCodeLive).
- status: a fully-online device could sit on a stale "Connecting to server…" when
  MainActivity was relaunched (CLEAR_TASK) after the service already registered —
  it now pulls a fresh playlist on bind so the real state renders.
- setup: add a Default Launcher (HOME role) step so a kiosk can be set as the
  default launcher without adb (prevents ~45s activity-recreate churn).
- debug: new DebugLog.v() streams the deep download/playback trace only while
  live dashboard debug is enabled; silent in production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:34:47 -05:00
BlazzzPlay 58f27d56e8 fix(android): server-provisioned settings PIN replaces hardcoded 0000
- Remove stray brace that broke compilation (MainActivity line 985)
- Server generates unique 6-digit PIN per device during pairing
- PIN stored in encrypted SharedPreferences (ServerConfig.settingsPin)
- Fallback: generate random PIN locally if server doesn't send one
- Include settings_pin in device:paired on pair + reconnect
- DB migration: settings_pin column on devices table
- Hint changed from hardcoded 0000 to generic 'PIN' string
2026-07-09 19:05:24 -04:00
BlazzzPlay c7f1eed63f feat(android): PIN gate for hidden settings menu 2026-07-09 10:50:34 -04:00
BlazzzPlay 69be6e804e feat(android): hidden settings menu with multi-tap BACK/ESC detection
Add an in-app settings menu reachable via 2× BACK (or ESC) taps,
with a 1.8s window — Android TV and touch devices.

- 2 taps: settings dialog (change server, re-pair, permissions, exit)
- 3 taps: exit dialog directly (skip menu)
- Auto-banner after 10+ consecutive connection failures

Settings options:
- Change server URL (pre-fills ProvisioningActivity)
- Reconfigure device (clear credentials → re-pair)
- Permissions (Accessibility + Notifications status → system settings)
- Device info (ID, APK version, connection status)
- Exit app (finishAffinity)

Also adds EXTRA_SERVER_URL to ProvisioningActivity and a
consecutiveFailures counter to WebSocketService.
2026-07-07 16:57:49 -04:00
screentinker 7660d7433e
fix(#109): render Android PiP overlay above the YouTube WebView video plane (#135)
* fix(#109): render Android PiP overlay above the YouTube WebView video plane

The PiP overlay (#109) returned sent:1 and showed its title in `uiautomator
dump`, but nothing painted on screen while YouTube was playing. By elimination
(YouTube-specific, landscape so no off-screen transform, real on-screen bounds
in the dump) the cause is surface occlusion: pipLayout sat as the last child of
rootLayout — the SAME compositing band as R.id.youtubeWebView — so the playing
video surface drew over it.

Fix (task option 1a): reparent pipLayout out of rootLayout to the window
content (android.R.id.content) as a top-level sibling drawn after rootLayout, so
it composites above the WebView. MainActivity.mirrorTransformToPip() copies
rootView's orientation/wall transform onto it so corner positions still track
the rotated content (web/Tizen parity). show() also bringToFront()+
requestLayout()+invalidate() on attach (covers the cause-3 measure/visibility
path). Remote-view screenshots now capture the content root so the PiP is still
included.

Instrumentation (Phase 1, default OFF): PipOverlay.pipDebug paints a solid
magenta box + border with media on top (box paints even if media never loads)
and logs box/pipLayout/rootView/youtubeWebView geometry over device:log tag
"pip"; loadImageInto also logs on success. Toggled via device:command
{type:"pip_debug"} (routed through MainActivity.onCommand).

Server: POST /api/pip and the clear handler log one concise [pip] dispatch line
(target + sent/offline) so journalctl shows PiP activity.

Validated end-to-end on an emulator (pixel10/API34) paired to an isolated local
server with YouTube playing: no crash, the PiP box composites above the live
video frame (center + top-right), clear removes it, and the portrait transform
mirror rotates the overlay with the stage (no off-screen). The Fire TV
hardware-overlay punch-through still needs real hardware (emulator composites
video inline); pipDebug + docs/109-android-pip-visibility.md cover that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#109): image PiPs never painted — set slot token before decode

Emulator e2e of an image PiP (a QR PNG) found the image area always blank (box
background + title only). Pre-existing defect, also on main, independent of the
occlusion reparent.

Root cause in PipOverlay.show(): teardown() clears `current` to null, then
loadImageInto() captured `token = current` (null) as its drop-if-replaced guard,
but `current` was set to the new pip_id AFTER the media was built. The image
decode finishes on a background thread and posts back after show() returns, so
`token != current` (null != pip_id) was always true and every decoded bitmap was
dropped. Web PiPs and the box/title were unaffected, which masked it.

Fix: set `current = pip_id` before building media so loadImageInto's token
matches. Verified on emulator — a QR image PiP now renders over both a static
image and live YouTube (hardware screencap + the app's software view.draw
capture both show it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#109): record web PiP (HTML+JS) verification on emulator

Web PiP type loads its WebView and executes JS (a page stamping JS OK · <time>
rendered over live YouTube). No code change — web PiPs don't use the image path
that had the token bug. Completes the image/web/box content-type verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#109): implement PiP close_button on Android (was a documented no-op)

The server forwarded close_button (routes/pip.js) and it's in openapi.yaml, but
no player rendered it — Tizen deferred "close-button focus" as non-MVP, the web
player has none, and Android's PipOverlay never read the flag. So the documented
field did nothing on any device.

Implement it on Android: when close_button:true, a tappable ✕ floats at the box's
top-right in a FrameLayout wrapper that is a SIBLING of the box — so it isn't
clipped by the box outline or dimmed by the overlay opacity. Tapping it clears
THIS overlay (id-matched via the captured token). Only the ✕ is clickable; the
rest of the full-screen pipLayout stays touch-transparent, so taps elsewhere
fall through to the playing content (no input regression).

Verified on the emulator over live YouTube: the ✕ renders at the corner, and
tapping it removes the overlay while the video keeps playing.

Parity note: web/Tizen players still don't implement close_button; D-pad focus
of the ✕ on non-touch TV hardware is intentionally not wired (MVP = touch/pointer,
matching the Tizen focus deferral).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:49:20 -05:00
screentinker 965920cd17
PiP overlay MVP: push image/web overlays to a device or group (#109) (#127)
* PiP overlay MVP: push image/web overlays to a device or group (#109)

Implements the #109 MVP from docs proposal: a floating overlay PUSHED to a device or
group in real time, rendered above the playlist without disturbing it. Scope is the
MVP only — video/RTSP, MQTT, offline-queue, and the priority/stacking system are
deferred to follow-up PRs as the proposal specifies.

Protocol (/device socket, player-agnostic):
- device:pip-show { pip_id, type:image|web, uri, position, width, height, duration,
  title?, title_color?, background_color?, opacity?, border_radius?, close_button? }
- device:pip-clear { pip_id? }
The player fetches uri itself (same trust model as remote_url content; server never
proxies). type:web is full-trust by design, hence the 'full' token scope.

Server (server/routes/pip.js, new; mounted in config/api-surface.js PUBLIC_ROUTERS):
- POST /api/pip and POST /api/pip/clear + DELETE /api/pip, all requireScope('full').
- Resolves device_id to a device OR a group, expands a group to members, and emits
  per-device — reusing the group command route's room-size online check and
  {device_id, name, status: sent|offline} result shape. Generates pip_id.
- Validates type/position allowlists, uri http(s), numeric bounds on
  width/height/duration/opacity/border_radius, colors via the existing VALID_COLOR
  (#RRGGBB; transparency is the separate opacity field).
- Workspace-isolated: every target query is scoped to req.workspaceId, so a token
  bound to workspace A can't address workspace B (404). Offline devices are reported,
  never queued (PiP is ephemeral).

Player overlay layer (Tizen; tizen/js/pip-overlay.js, new):
- A #pip sibling ABOVE #stage that PlaylistPlayer/ZoneRenderer never touch.
- applyOrientation now applies the SAME transform to #pip as #stage, so corner
  positions track the visible CONTENT in all four orientations.
- image -> <img>, web -> <iframe> (muted by default: empty allow= denies autoplay),
  sized/positioned/styled per payload, optional title bar.
- Single overlay slot, last-show-wins; duration timer (0 = until cleared); pip-clear
  (id-aware) or timer tears down; teardown wrapped so a malformed payload can't wedge
  the layer. Reports show/clear over device:log (tag 'pip').

Dashboard: a minimal "Send overlay" / "Clear overlay" tester on the device-detail
controls (device/group via the open device, type, uri, position, duration), calling
POST /api/pip through the api helper.

Tests (server suite green, 161/161):
- api.test.js: PiP tier — authz (read/write 403, full passes), workspace isolation
  (wsA token -> wsB device 404), payload validation, device + group targeting, clear;
  plus the PUBLIC_ROUTERS snapshot-firewall updated for /api/pip.
- pip-overlay.test.js: loads the real player.js + pip-overlay.js in a vm with a DOM
  shim; proves the overlay shows, auto-dismisses on the duration timer, and never
  changes the playlist signature / touches #stage; web->iframe, last-show-wins,
  id-aware clear, malformed-payload safety.

Not in this PR (intentional):
- Android player overlay — fast-follow. Protocol + server are player-agnostic; the
  Android layer (an overlay View above the player, orientation-matched to MainActivity's
  rootView rotation) is the same shape and lands next.
- OpenAPI docs for POST /api/pip — the contract test's scope heuristic only treats
  'command' paths as full-scope, so documenting a full-scope non-command route there
  needs that heuristic extended first; deferred with the docs item (proposal §8.6).
- video/rtsp types, MQTT, offline queue-on-reconnect, priority/stacking, arbitrary
  (x,y)/selector positioning (proposal §6).

Refs #109

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PiP overlay: add Android + web players (#109)

Extends the #109 PiP MVP to the other two players so the protocol (device:pip-show /
device:pip-clear) is honored fleet-wide, not just on Tizen. No server/protocol changes —
the route and socket messages are player-agnostic; these are the two missing surfaces.

Web player (server/player/index.html):
- New #pipContainer layer above #playerContainer, pointer-transparent, that the playlist
  render never touches. The same orientation transform is applied to it as to
  #playerContainer (extended to also reset width/height on landscape so a
  portrait->landscape switch realigns), so corner positions track the visible content.
- Inline PiP logic mirroring tizen/js/pip-overlay.js: image -> <img>, web -> <iframe>
  (muted by default via empty allow=), position/size/bg/opacity/radius/title, single slot
  last-show-wins, duration timer (0 = until cleared), id-aware clear, wrapped teardown.
- device:pip-show/clear handlers; reports show/clear over device:log (tag "pip").

Android player:
- activity_main.xml: a pipLayout FrameLayout as the LAST child of rootLayout — it draws
  above the content AND inherits rootView's orientation rotation/translation, so corner
  positioning is orientation-matched for free.
- PipOverlay.kt (new): builds the overlay box into pipLayout. image -> ImageView (decoded
  off-thread via ImageLoader, dropped if torn down mid-decode); web -> WebView with
  mediaPlaybackRequiresUserGesture=true (mute-by-default). Gravity-based corner/center
  placement with a 4% inset, GradientDrawable bg + corner radius, alpha=opacity, optional
  title bar. Single slot last-show-wins; duration timer; id-aware clear; teardown wrapped
  and also run on activity destroy (WebView cleanup).
- WebSocketService: onPipShow/onPipClear callbacks + safeOn handlers posted to the main
  thread (they build Views) + a sendLog(tag, level, message) emitter for device:log.
- MainActivity: instantiate PipOverlay (log -> wsService.sendLog("pip", ...)), wire the
  callbacks, tear down on destroy.

Verified: Android assembleDebug builds clean; web player inline JS parses; server suite
still 161/161 (no server changes this commit). Not yet validated on real hardware —
four-orientation corner positioning mirrors the player container/rootView transform but
should be eyeballed on a panel.

Refs #109

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:54:44 -05:00
ScreenTinker 2ccf3264a9 feat(scheduling): per-item schedule blocks (#74 dayparting, #75 auto-expire)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
Each playlist item can carry schedule blocks (active days, start/end
time-of-day, optional start/end dates). An item plays when the screen's
local "now" matches at least one block; an item with no blocks always
plays. #74 covers time-of-day/day-of-week windows including overnight
wrap; #75 covers inclusive date ranges (auto-expiry). Evaluation is
on-device, so dayparting and expiry work offline.

- Shared evaluator contract: shared/schedule-vectors.json (39 vectors —
  DST US+AU, overnight-wrap anchoring, timezone correctness, date
  boundaries). Canonical JS evaluator in server/lib/schedule-eval.js;
  Kotlin and Tizen ports kept in lockstep by drift guards (Tizen byte-diff
  test, Kotlin JUnit reads the shared JSON, new android-test CI job).
- All three players (web, Android, Tizen) filter by schedule against their
  own clock, idle with a "Nothing scheduled" message + 30s re-check when
  everything is filtered, and fail open on any evaluator error.
- Editor: per-item schedule modal + row badge in the playlist editor;
  client validation mirrors the server; editing marks the playlist draft.
- Part B (behaviour change): device/group schedule overrides now evaluate
  in each device's effective timezone instead of server-local time.
- Device detail shows the reported timezone + a clock-skew warning.
- i18n for en/es/fr/de/pt across all new strings (namespaced itemsched.*
  to avoid colliding with the device-schedule calendar's schedule.*).
- CHANGELOG documents the feature, the Part B change, the fail-open
  guarantee, and the scheduled-single-video re-render tradeoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:46:41 -05:00
ScreenTinker d9d7a8ae0f feat(android): reliable boot-launch incl. Android TV (1.7.11)
The player has a launcher (category.HOME) + a boot receiver, but auto-start was
unreliable where you can't set a home launcher (Android TV) and on Android 14+,
where USE_FULL_SCREEN_INTENT is auto-revoked for non-calling apps so the boot
full-screen launcher silently no-ops.

Boot launch:
- BootReceiver now does a direct background startActivity when 'display over other
  apps' (SYSTEM_ALERT_WINDOW) is granted — a real exception to the bg-activity-launch
  restriction, and the one path that works on Android TV. Full-screen-intent
  notification kept as a fallback (locked screen / no overlay).
- Boot notification moved to a dedicated HIGH-importance channel (full-screen
  intents are only honored from one), and it auto-dismisses once the UI is up.

Setup screen — new permission rows so operators can grant what boot-launch needs:
- Launch on Boot (USE_FULL_SCREEN_INTENT, shown on Android 14+)
- Background Activity (battery-optimization exemption)
- Display Over Apps (SYSTEM_ALERT_WINDOW)
Made the screen scrollable and ~50% smaller text/buttons so all rows + Continue
fit on one screen (incl. landscape signage). Install-Unknown-Apps subtitle now
states updates are signature-verified, so it doesn't read as 'install anything'.

Verified end-to-end on an Android 16 emulator: after reboot the app auto-launched
(Direct launch via overlay) and the boot notice cleared itself; all rows toggle.
2026-06-09 17:44:49 -05:00
ScreenTinker 60cda97b1d fix(android): stop pairing-code glyph clip + remove duplicate instruction
- The code's bottom was still clipped: the autosize TextView used wrap_content
  height, which clips glyph bottoms. Give it a fixed 96dp box (autosize 24-64sp,
  gravity center) so the text is centered inside a bounded box and never clipped.
- The "Enter this code…" line appeared twice (static label + statusText). Clear
  statusText when paired so it shows only once, with the code.
2026-06-08 19:53:44 -05:00
ScreenTinker 86340caf9d fix(android): keep pairing code fully on-screen (was clipped at bottom)
Follow-up to the provisioning layout fix - on a Pixel the code's bottom half was
cut off. Tightened the screen so the whole block fits:
- "RemoteDisplay" title 36sp -> 22sp, smaller subtitle + margins.
- Anchor content to the top (gravity center_horizontal|top) so the code sits
  high instead of being pushed below the fold by vertical centering.
- Pairing code autosize cap 96sp -> 56sp + vertical padding, so tall digits
  aren't clipped and the block stays on screen on short/landscape phones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:46:30 -05:00
ScreenTinker 06c6c3214b fix(android): make pairing code fit/visible on all screen sizes
Reported on a Pixel 10: the pairing code wasn't visible. The provisioning screen
was a non-scrolling vertical stack, and when the pairing section appeared below
the server-URL + Connect controls, the fixed 64sp code got pushed off-screen on
short/landscape phones (and could clip horizontally on narrow widths).

- Wrap the screen in a ScrollView (fillViewport) so content is always reachable.
- pairingCodeText now auto-sizes (autoSizeTextType=uniform, 24-96sp, single line,
  match_parent width) so it fills the width and never clips - phones, TVs, sticks.
- Hide the server-URL section + Connect button once paired so the code gets the
  full screen.

Compile-checked + signed APK builds. Needs on-device confirmation (Pixel 10 /
onn stick) that the code is now visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:19:55 -05:00
ScreenTinker dec56506f9 i18n: add Android localized string resources
Adds values-{es,fr,de,pt,hi}/strings.xml mirroring values/strings.xml.
Two strings: app_name (kept as RemoteDisplay across all locales) and
the accessibility service description (translated).

Hindi is a copy of English by design — same approach as the web's
empty hi.js. Native review can replace the en text in place once
done; Android picks the right file based on device language.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:20:14 -05:00
ScreenTinker 1594a9d4a4 Initial open source release
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 12:14:53 -05:00