diff --git a/HyperDRM.manifest.json b/HyperDRM.manifest.json index ae1255a..631943d 100644 --- a/HyperDRM.manifest.json +++ b/HyperDRM.manifest.json @@ -1,15 +1,15 @@ { "id": "org.webosbrew.histogramhyperion", - "version": "0.6.2", + "version": "0.6.3", "type": "web", "title": "HyperDRM", "appDescription": "Ambient lighting for webOS and Hyperion", "iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM", "rootRequired": true, - "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.6.2_arm.ipk", + "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.6.3_arm.ipk", "ipkHash": { - "sha256": "9de20a046176168428378346872934c1fbbee79f33f99451addd880f193892ed" + "sha256": "02f0ed2ff450573ad35807d6c75dc61840031826513c8cf2e71f7fb93332e0b1" }, - "ipkSize": 1163072 + "ipkSize": 1163846 } diff --git a/HyperDRM_0.6.2_arm.ipk b/HyperDRM_0.6.3_arm.ipk similarity index 91% rename from HyperDRM_0.6.2_arm.ipk rename to HyperDRM_0.6.3_arm.ipk index a057c47..6f97a02 100644 Binary files a/HyperDRM_0.6.2_arm.ipk and b/HyperDRM_0.6.3_arm.ipk differ diff --git a/README.md b/README.md index b25a96e..343f3b7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Only the own service is modified. No Hue, LG library, driver or firmware binarie ## Installation -The current package is [HyperDRM_0.6.2_arm.ipk](HyperDRM_0.6.2_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. +The current package is [HyperDRM_0.6.3_arm.ipk](HyperDRM_0.6.3_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. Elevate the installed own service through Homebrew Channel: @@ -35,7 +35,7 @@ luna-send -n 1 luna://org.webosbrew.hbchannel.service/elevateService '{"id":"org Perform elevation before first opening the app or calling its service. If the service was already started without root, elevation updates its launch permissions but does not restart the existing process: terminate only the own `histogram-service` process (verify its executable path first), then reopen the app or call start again. Closing the UI alone does not terminate the native service. -Open **HyperDRM** on the TV and select **Start**. Hyperion's JSON server must be enabled on port 19444. The existing Hyperion LED device/output configuration remains responsible for controlling the hardware. +Open **HyperDRM** on the TV and select **Start**. Use the remote arrow buttons to move between controls and OK to activate the selected control. Magic Remote pointer input is also supported. Hyperion's JSON server must be enabled on port 19444. The existing Hyperion LED device/output configuration remains responsible for controlling the hardware. The internal app/service IDs remain `org.webosbrew.histogramhyperion` and `org.webosbrew.histogramhyperion.service` so existing installations and the autostart hook remain compatible. Hyperion displays the source as **HyperDRM**. After changing the LED layout, restart the service. @@ -83,3 +83,5 @@ The logo master is `app/logo.png`, with 80 px and 130 px installation icons. ## Publishing status See [the publishing review](docs/publishing-review.md) for the requirements audit and unresolved submission conditions. This repository is not a claim that webOSbrew has approved the app. + +Remote navigation regression tests: `node tests/remote-navigation.test.js`. diff --git a/app/app.js b/app/app.js index 07f663d..01a0820 100644 --- a/app/app.js +++ b/app/app.js @@ -26,3 +26,5 @@ call('status',display);setInterval(function(){if(!pending.length)call('status',d document.getElementById('start').focus(); document.getElementById('autostart').onclick=function(){var button=this;var enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true;call('autostart',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:enabled});}; + +setupRemoteNavigation(['start','stop','refresh','autostart'].map(function(id){return document.getElementById(id);}),document,window); diff --git a/app/appinfo.json b/app/appinfo.json index 65143aa..70b7d32 100644 --- a/app/appinfo.json +++ b/app/appinfo.json @@ -1 +1 @@ -{"id": "org.webosbrew.histogramhyperion", "version": "0.6.2", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} \ No newline at end of file +{"id": "org.webosbrew.histogramhyperion", "version": "0.6.3", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS and Hyperion", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} \ No newline at end of file diff --git a/app/index.html b/app/index.html index 2a87315..f9e67a3 100644 --- a/app/index.html +++ b/app/index.html @@ -1,8 +1,8 @@ HyperDRM

HyperDRM

Ambient lighting for webOS and Hyperion.

Checking service …

- -

Starts the service when the TV boots. Stop ends only the current session.

+
+

Use the arrow buttons to select a control and OK to activate it.

Starts the service when the TV boots. Stop ends only the current session.

127.0.0.1:19444 · Priority 149 · 10 updates per second

The Hyperion layout determines the number and positions of LEDs. Colors come from 16 LG histogram regions with interpolated edge colors. Restart the service after changing the LED layout.

- + diff --git a/app/remote.js b/app/remote.js new file mode 100644 index 0000000..5a4c93f --- /dev/null +++ b/app/remote.js @@ -0,0 +1,30 @@ +/* Directional control for webOS remotes and standard keyboards. */ +function setupRemoteNavigation(buttons, doc, win) { + var lastFocused=buttons[0], enterDown=false; + buttons.forEach(function(button){ + button.addEventListener('focus',function(){lastFocused=button;}); + button.addEventListener('mouseenter',function(){if(!button.disabled)button.focus();}); + }); + doc.addEventListener('keydown',function(event){ + var code=event.keyCode||event.which; + var direction=event.key==='ArrowLeft'||event.key==='ArrowUp'||code===37||code===38?-1: + event.key==='ArrowRight'||event.key==='ArrowDown'||code===39||code===40?1:0; + var enter=event.key==='Enter'||code===13; + if(!direction&&!enter)return; + event.preventDefault(); + var enabled=buttons.filter(function(button){return !button.disabled;}); + if(!enabled.length)return; + var focused=doc.activeElement,index=enabled.indexOf(focused); + if(direction){ + if(index<0){index=enabled.indexOf(lastFocused);if(index<0)index=0;enabled[index].focus();} + else enabled[(index+direction+enabled.length)%enabled.length].focus(); + }else{ + if(enterDown||event.repeat)return; + enterDown=true; + if(index<0){index=enabled.indexOf(lastFocused);enabled[index<0?0:index].focus();} + else focused.click(); + } + }); + doc.addEventListener('keyup',function(event){if(event.key==='Enter'||event.keyCode===13||event.which===13)enterDown=false;}); + win.addEventListener('blur',function(){enterDown=false;}); +} diff --git a/native/service.c b/native/service.c index ee5ee9a..f165e78 100644 --- a/native/service.c +++ b/native/service.c @@ -150,7 +150,7 @@ static bool autostart(void *h,void *m,void *ctx){ static bool status(void *h,void *m,void *ctx){ (void)ctx;char json[4096],safe[512];size_t n=0; for(size_t i=0;last_error[i] && n+2=32)safe[n++]=c;}safe[n]=0; - int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.2\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe); + int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.6.3\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"endpoint\":\"127.0.0.1:19444\",\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",capture?"true":"false",color_valid?"true":"false",frames,PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe); for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]); used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false"); for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]); diff --git a/package/packageinfo.json b/package/packageinfo.json index 13434bc..71e1269 100644 --- a/package/packageinfo.json +++ b/package/packageinfo.json @@ -1 +1 @@ -{"id": "org.webosbrew.histogramhyperion", "version": "0.6.2", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} \ No newline at end of file +{"id": "org.webosbrew.histogramhyperion", "version": "0.6.3", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} \ No newline at end of file diff --git a/service/histogram-service b/service/histogram-service index abb86e0..3031dbc 100644 Binary files a/service/histogram-service and b/service/histogram-service differ diff --git a/tests/remote-navigation.test.js b/tests/remote-navigation.test.js new file mode 100644 index 0000000..f8d0eec --- /dev/null +++ b/tests/remote-navigation.test.js @@ -0,0 +1,29 @@ +const assert=require('assert'); +const fs=require('fs'); +const path=require('path'); +const vm=require('vm'); +const handlers={},windowHandlers={}; +const doc={activeElement:null,addEventListener(type,handler){handlers[type]=handler;}}; +const buttons=['start','stop','refresh','autostart'].map(id=>({ + id,disabled:false,clicks:0,handlers:{}, + addEventListener(type,handler){this.handlers[type]=handler;}, + focus(){doc.activeElement=this;this.handlers.focus();}, + click(){this.clicks++;} +})); +const context={};vm.createContext(context); +vm.runInContext(fs.readFileSync(path.join(__dirname,'../app/remote.js'),'utf8'),context); +context.setupRemoteNavigation(buttons,doc,{addEventListener(type,handler){windowHandlers[type]=handler;}}); +function key(code,type='keydown',repeat=false){let prevented=false;handlers[type]({keyCode:code,repeat,preventDefault(){prevented=true;}});return prevented;} +buttons[0].focus();assert(key(39));assert.equal(doc.activeElement.id,'stop'); +key(37);assert.equal(doc.activeElement.id,'start');key(37);assert.equal(doc.activeElement.id,'autostart'); +key(39);assert.equal(doc.activeElement.id,'start'); +buttons[1].disabled=true;key(39);assert.equal(doc.activeElement.id,'refresh'); +key(13);assert.equal(buttons[2].clicks,1);key(13);key(13,'keydown',true);assert.equal(buttons[2].clicks,1); +key(13,'keyup');key(13);assert.equal(buttons[2].clicks,2);key(13,'keyup'); +buttons[3].handlers.mouseenter();assert.equal(doc.activeElement.id,'autostart');key(37);assert.equal(doc.activeElement.id,'refresh'); +doc.activeElement=null;key(39);assert.equal(doc.activeElement.id,'refresh'); +assert(!key(461));assert(!key(65)); +key(40);assert.equal(doc.activeElement.id,'autostart');key(38);assert.equal(doc.activeElement.id,'refresh'); +key(13);windowHandlers.blur();key(13);assert.equal(buttons[2].clicks,4); +buttons.forEach(button=>button.disabled=true);assert(key(39)); +console.log('PASS: arrow navigation, wraparound, disabled controls, OK once per press, pointer handoff, focus recovery, Back preserved');