nuvio-tizen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Nuvio for Tizen (UN50MU6100 / Tizen 3.0)
2
+
3
+ A **from-scratch Tizen web-app reimplementation** of [Nuvio](https://github.com/paytonlowe/NuvioStreaming)'s
4
+ core behaviour, targeting 2017 Samsung TVs (Tizen 3.0, ~Chromium 47).
5
+
6
+ > **Why a rewrite and not a port?** The upstream Nuvio apps are React Native / Expo
7
+ > (Android TV, iOS). Samsung Tizen TVs run *web apps*, not React Native and not Android
8
+ > APKs. So this project reuses Nuvio's **data model** (Stremio addons for catalogs,
9
+ > metadata and streams) but is written as plain, old-Chromium-safe HTML/JS/CSS with
10
+ > Samsung's **AVPlay** API for video.
11
+
12
+ ## What works (v1)
13
+
14
+ - Loads user-configured **Stremio addons** (defaults to Cinemeta for catalogs + metadata).
15
+ - Home screen with catalog rows + **Continue Watching**.
16
+ - Detail pages: movies (Play) and series (season/episode browser).
17
+ - **Stream aggregation** across all installed stream addons.
18
+ - Playback via **AVPlay** (HLS/DASH/MP4/progressive) with an HTML5 `<video>` fallback
19
+ when run in a desktop browser for development.
20
+ - OSD with seek (±10s / ±30s), resume, and per-title progress saved to `localStorage`.
21
+ - Full **D-pad + media-key** remote navigation (custom spatial focus engine).
22
+ - Settings screen to add/remove addon manifest URLs.
23
+
24
+ ## Content / legal note
25
+
26
+ This app hosts **no** media. It is a client for the open Stremio addon protocol, exactly
27
+ like Nuvio: catalogs and metadata come from **Cinemeta** (official), and playable sources
28
+ come from whatever **stream addons you add yourself** in Settings. Streams that only expose
29
+ a torrent `infoHash` (no HTTP URL) are listed but not playable — AVPlay needs a direct
30
+ HTTP(S)/HLS/DASH URL, and this app embeds no torrent/debrid engine.
31
+
32
+ ---
33
+
34
+ ## Install path A — TizenBrew (recommended, no cert signing)
35
+
36
+ TizenBrew supports **Tizen 3.0–6.0** (your MU6100 is exactly at the 3.0 floor).
37
+ TizenBrew app modules are just web pages served to the TV; `package.json` here is already
38
+ a valid module manifest (`packageType: "app"`, `appPath: "index.html"`).
39
+
40
+ Two ways to load it:
41
+
42
+ **1. Publish to npm** and add it as a module in the TizenBrew MMENU:
43
+ ```bash
44
+ npm publish # from this folder (name: nuvio-tizen)
45
+ ```
46
+ Then in TizenBrew on the TV: MMENU → add module → `nuvio-tizen`.
47
+
48
+ **2. Local dev without publishing** — serve this folder and point a small wrapper package
49
+ at it, or use the [`@tizenbrew-kit/cli`](https://github.com/thichcode/tizenbrew-kit) dev
50
+ server. Simplest smoke test is Install path B's `chrome://` / desktop preview below.
51
+
52
+ ## Install path B — Tizen Studio `.wgt` (sideload)
53
+
54
+ `config.xml` + `icon.png` make this a standalone Tizen TV widget.
55
+
56
+ ```bash
57
+ # From this project folder, with Tizen Studio CLI on PATH:
58
+ tizen build-web -- .
59
+ tizen package -t wgt -s <your-cert-profile> -- .buildResult
60
+ # Put the TV in Developer Mode (Apps → 12345 → set host PC IP), then:
61
+ tizen install -n NuvioTizen.wgt -t <TV-device-name>
62
+ ```
63
+ `required_version="2.4"` matches the Tizen 3.0 TV web API level.
64
+
65
+ ## Develop / test on desktop first
66
+
67
+ Because everything is plain web tech, you can run the UI in any browser
68
+ (AVPlay is absent, so it uses the `<video>` fallback — good for testing navigation,
69
+ addon loading, and playable HTTP streams):
70
+
71
+ ```bash
72
+ cd "NuvioTizen"
73
+ python3 -m http.server 8080
74
+ # open http://localhost:8080 — use arrow keys + Enter, Backspace = RETURN/back
75
+ ```
76
+ > Note: browser CORS may block some addon hosts on desktop; on the TV the widget's
77
+ > `<access origin="*">` (and TizenBrew's environment) allow the cross-origin calls.
78
+
79
+ ---
80
+
81
+ ## Architecture
82
+
83
+ | File | Responsibility |
84
+ |------|----------------|
85
+ | `index.html` | Layout, video planes (`<object avplayer>` + `<video>` fallback), OSD |
86
+ | `js/polyfills.js` | Feature-guarded shims for Chromium 47 |
87
+ | `js/util.js` | DOM helpers, XHR-JSON, `localStorage`, toast/spinner, concurrency |
88
+ | `js/stremio.js` | Stremio addon client: manifests, catalog/meta/stream, resource matching |
89
+ | `js/keys.js` | Registers TV media keys; maps keyCodes → symbolic actions |
90
+ | `js/focus.js` | Geometry-based spatial (D-pad) focus engine |
91
+ | `js/player.js` | AVPlay lifecycle + HTML5 fallback, OSD, resume/progress |
92
+ | `js/views.js` | Home / Detail / Settings rendering |
93
+ | `js/app.js` | Bootstrap + navigation stack + key routing |
94
+
95
+ ### JS compatibility rules (Tizen 3.0 ≈ Chromium 47)
96
+ No `async`/`await`, no ES6 classes, no destructuring, no default params, no object spread.
97
+ `Promise`, `fetch`-era XHR, template literals, and arrow functions are fine.
98
+
99
+ ## Known limitations / roadmap
100
+
101
+ - **Untested on the physical TV** — AVPlay quirks (some HLS variants, DRM) will need
102
+ device iteration. Start with the desktop `<video>` smoke test, then a known-good HTTP
103
+ MP4/HLS stream addon on the set.
104
+ - No Trakt sync, no search UI, no subtitles yet (Stremio subtitle resource + AVPlay
105
+ `setSubtitleFile` is the next addition).
106
+ - On-screen keyboard for adding addons relies on the TV IME focusing the `<input>`.
107
+ - Catalog pagination ("load more") not yet wired (fetches first page only).
package/config.xml ADDED
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
3
+ id="http://nuvio.tizen/NuvioTizen" version="1.0.0" viewmodes="maximized">
4
+ <tizen:application id="Nuvio00001.Nuvio" package="Nuvio00001" required_version="2.4"/>
5
+ <content src="index.html"/>
6
+ <feature name="http://tizen.org/feature/screen.size.all"/>
7
+ <icon src="icon.png"/>
8
+ <name>Nuvio</name>
9
+ <tizen:privilege name="http://tizen.org/privilege/internet"/>
10
+ <tizen:privilege name="http://tizen.org/privilege/tv.inputdevice"/>
11
+ <tizen:privilege name="http://tizen.org/privilege/application.launch"/>
12
+ <!-- Cross-origin XHR to any addon host -->
13
+ <access origin="*" subdomains="true"/>
14
+ <tizen:profile name="tv"/>
15
+ <tizen:setting screen-orientation="landscape" context-menu="disable"
16
+ background-support="disable" encryption="disable"
17
+ install-location="auto" hwkey-event="enable"/>
18
+ </widget>
package/css/style.css ADDED
@@ -0,0 +1,106 @@
1
+ /* Nuvio for Tizen 3.0 — conservative CSS (no CSS grid gap edge cases, flexbox is fine on Chromium 47) */
2
+ * { box-sizing: border-box; margin: 0; padding: 0; }
3
+
4
+ html, body {
5
+ width: 1920px;
6
+ height: 1080px;
7
+ overflow: hidden;
8
+ background: #0e1117;
9
+ color: #e6e9ef;
10
+ font-family: Arial, "Tizen Sans", sans-serif;
11
+ -webkit-user-select: none;
12
+ user-select: none;
13
+ }
14
+
15
+ /* --- Video planes --- */
16
+ #av-player, #html5-player {
17
+ position: absolute;
18
+ top: 0; left: 0;
19
+ width: 1920px; height: 1080px;
20
+ background: #000;
21
+ display: none;
22
+ z-index: 1;
23
+ }
24
+ #av-player.active, #html5-player.active { display: block; }
25
+
26
+ #app { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 10; }
27
+ .screen { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; }
28
+ .hidden { display: none !important; }
29
+
30
+ /* --- Header --- */
31
+ .header { height: 96px; padding: 30px 60px 0 60px; display: flex; align-items: center; }
32
+ .brand { font-size: 40px; font-weight: bold; color: #818cf8; letter-spacing: 1px; }
33
+ .brand .dot { color: #e6e9ef; }
34
+ .header .spacer { flex: 1; }
35
+ .header .gear { font-size: 30px; opacity: 0.7; }
36
+
37
+ /* --- Rows --- */
38
+ .rows { position: absolute; top: 110px; left: 0; right: 0; bottom: 0; overflow: hidden; }
39
+ .row { margin: 0 0 34px 0; }
40
+ .row-title { font-size: 26px; font-weight: bold; margin: 0 0 12px 60px; color: #c7cbd4; }
41
+ .row-scroller { white-space: nowrap; padding: 0 60px; overflow: hidden; }
42
+
43
+ /* --- Cards --- */
44
+ .card {
45
+ display: inline-block;
46
+ width: 200px;
47
+ height: 300px;
48
+ margin-right: 22px;
49
+ border-radius: 10px;
50
+ background: #1a1f2b;
51
+ vertical-align: top;
52
+ position: relative;
53
+ overflow: hidden;
54
+ border: 4px solid transparent;
55
+ }
56
+ .card .poster { width: 100%; height: 100%; display: block; background-size: cover; background-position: center; }
57
+ .card .fallback { display: flex; align-items: center; justify-content: center; height: 100%; padding: 10px; font-size: 20px; color: #8b93a5; text-align: center; white-space: normal; }
58
+ .card .cap { position: absolute; bottom: 0; left: 0; right: 0; padding: 26px 10px 8px 10px; font-size: 18px; background: linear-gradient(transparent, rgba(0,0,0,0.85)); white-space: normal; line-height: 1.2; max-height: 70px; overflow: hidden; }
59
+ .card .prog { position: absolute; bottom: 0; left: 0; height: 6px; background: #818cf8; }
60
+
61
+ /* --- Focus state (TV) --- */
62
+ .focusable.focused { border-color: #818cf8; box-shadow: 0 0 0 2px rgba(129,140,248,0.6); }
63
+ .card.focusable.focused { transform: scale(1.06); z-index: 5; }
64
+ .pill.focusable.focused, .stream.focusable.focused, .ep.focusable.focused { background: #818cf8; color: #0e1117; }
65
+ .gear.focusable.focused { opacity: 1; color: #818cf8; }
66
+
67
+ /* --- Detail screen --- */
68
+ .detail-hero { position: absolute; top: 0; left: 0; right: 0; height: 620px; background-size: cover; background-position: center top; }
69
+ .detail-hero .shade { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(14,17,23,0.96) 30%, rgba(14,17,23,0.35) 100%), linear-gradient(0deg, #0e1117 4%, transparent 40%); }
70
+ .detail-body { position: absolute; top: 150px; left: 60px; width: 1000px; }
71
+ .detail-title { font-size: 54px; font-weight: bold; margin-bottom: 14px; }
72
+ .detail-meta { font-size: 22px; color: #a7adba; margin-bottom: 18px; }
73
+ .detail-desc { font-size: 22px; line-height: 1.45; color: #cfd3dc; max-height: 200px; overflow: hidden; margin-bottom: 26px; }
74
+ .btn-row { margin-top: 10px; }
75
+ .pill { display: inline-block; padding: 14px 30px; margin-right: 16px; border-radius: 30px; background: #1a1f2b; font-size: 22px; border: 4px solid transparent; }
76
+
77
+ /* Episode + stream lists */
78
+ .list-panel { position: absolute; top: 640px; left: 60px; right: 60px; bottom: 40px; overflow: hidden; }
79
+ .season-bar { margin-bottom: 14px; white-space: nowrap; }
80
+ .ep, .stream { display: block; padding: 16px 22px; margin-bottom: 10px; background: #161b26; border-radius: 8px; font-size: 22px; border: 4px solid transparent; white-space: normal; }
81
+ .ep .ep-sub, .stream .s-sub { display: block; font-size: 17px; color: #9aa2b1; margin-top: 4px; }
82
+ .list-scroll { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; }
83
+
84
+ /* Settings */
85
+ .settings-wrap { position: absolute; top: 120px; left: 60px; right: 60px; }
86
+ .settings-wrap h2 { font-size: 30px; margin-bottom: 20px; }
87
+ .addon-item { padding: 16px 20px; margin-bottom: 10px; background: #161b26; border-radius: 8px; font-size: 20px; border: 4px solid transparent; word-break: break-all; white-space: normal; }
88
+ .addon-item .a-name { font-weight: bold; }
89
+ .addon-item .a-url { font-size: 15px; color: #8b93a5; }
90
+ .hint { color: #8b93a5; font-size: 18px; margin-top: 20px; line-height: 1.5; }
91
+
92
+ /* --- Player OSD --- */
93
+ #player-osd { position: absolute; left: 0; right: 0; bottom: 0; height: 220px; z-index: 20; padding: 40px 60px; background: linear-gradient(transparent, rgba(0,0,0,0.9)); }
94
+ #osd-title { font-size: 30px; font-weight: bold; margin-bottom: 20px; }
95
+ #osd-bar { position: relative; height: 8px; background: rgba(255,255,255,0.2); border-radius: 4px; }
96
+ #osd-buffer { position: absolute; top: 0; left: 0; height: 8px; background: rgba(255,255,255,0.35); border-radius: 4px; width: 0; }
97
+ #osd-progress { position: absolute; top: 0; left: 0; height: 8px; background: #818cf8; border-radius: 4px; width: 0; z-index: 2; }
98
+ #osd-times { display: flex; justify-content: space-between; margin-top: 10px; font-size: 20px; color: #cfd3dc; }
99
+ #osd-hint { margin-top: 14px; font-size: 18px; color: #8b93a5; }
100
+
101
+ /* --- Toast + spinner --- */
102
+ #toast { position: absolute; bottom: 60px; left: 50%; margin-left: -300px; width: 600px; text-align: center; z-index: 40; background: rgba(0,0,0,0.85); padding: 18px 24px; border-radius: 10px; font-size: 22px; }
103
+ #spinner { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; z-index: 50; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.3); }
104
+ .spin { width: 70px; height: 70px; border: 7px solid rgba(255,255,255,0.2); border-top-color: #818cf8; border-radius: 50%; -webkit-animation: sp 0.9s linear infinite; animation: sp 0.9s linear infinite; }
105
+ @-webkit-keyframes sp { to { -webkit-transform: rotate(360deg); } }
106
+ @keyframes sp { to { transform: rotate(360deg); } }
package/icon.png ADDED
Binary file
package/index.html ADDED
@@ -0,0 +1,42 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=1920, height=1080, user-scalable=no">
6
+ <title>Nuvio</title>
7
+ <link rel="stylesheet" href="css/style.css">
8
+ </head>
9
+ <body>
10
+ <!-- AVPlay video plane (Tizen). Sits behind the UI; shown only during playback. -->
11
+ <object type="application/avplayer" id="av-player"></object>
12
+ <!-- HTML5 fallback for desktop/dev testing when webapis.avplay is absent -->
13
+ <video id="html5-player" playsinline></video>
14
+
15
+ <div id="app">
16
+ <div id="screen-home" class="screen"></div>
17
+ <div id="screen-detail" class="screen hidden"></div>
18
+ <div id="screen-settings" class="screen hidden"></div>
19
+ </div>
20
+
21
+ <!-- Player OSD overlay (over the video plane) -->
22
+ <div id="player-osd" class="hidden">
23
+ <div id="osd-title"></div>
24
+ <div id="osd-bar"><div id="osd-progress"></div><div id="osd-buffer"></div></div>
25
+ <div id="osd-times"><span id="osd-cur">00:00</span><span id="osd-dur">00:00</span></div>
26
+ <div id="osd-hint">OK: Play/Pause &nbsp; &larr;/&rarr;: Seek &nbsp; RETURN: Back</div>
27
+ </div>
28
+
29
+ <div id="toast" class="hidden"></div>
30
+ <div id="spinner" class="hidden"><div class="spin"></div></div>
31
+
32
+ <!-- Load order matters: polyfills -> util -> data -> input -> player -> views -> app -->
33
+ <script src="js/polyfills.js"></script>
34
+ <script src="js/util.js"></script>
35
+ <script src="js/stremio.js"></script>
36
+ <script src="js/keys.js"></script>
37
+ <script src="js/focus.js"></script>
38
+ <script src="js/player.js"></script>
39
+ <script src="js/views.js"></script>
40
+ <script src="js/app.js"></script>
41
+ </body>
42
+ </html>
package/js/app.js ADDED
@@ -0,0 +1,78 @@
1
+ /* Bootstrap + navigation router. Global namespace: App */
2
+ var App = (function () {
3
+ 'use strict';
4
+
5
+ var _current = 'home';
6
+ var _stack = []; // frames: { screen, focus }
7
+
8
+ function screenEl(name) { return U.byId('screen-' + name); }
9
+
10
+ function showScreen(name) {
11
+ ['home', 'detail', 'settings'].forEach(function (n) {
12
+ screenEl(n).className = 'screen' + (n === name ? '' : ' hidden');
13
+ });
14
+ _current = name;
15
+ }
16
+
17
+ function pushFrame() { _stack.push({ screen: _current, focus: Focus.current() }); }
18
+
19
+ function openDetail(type, id) {
20
+ if (!id) { return; }
21
+ pushFrame();
22
+ showScreen('detail');
23
+ Views.renderDetail(type, id);
24
+ }
25
+
26
+ function openSettings() {
27
+ pushFrame();
28
+ showScreen('settings');
29
+ Views.renderSettings();
30
+ }
31
+
32
+ function back() {
33
+ if (Player.isActive()) { Player.exit(); return; }
34
+ if (_current === 'detail' && Views.backInDetail()) { return; }
35
+ var f = _stack.pop();
36
+ if (!f) { tryExitApp(); return; }
37
+ showScreen(f.screen);
38
+ Focus.setScope(screenEl(f.screen), f.focus);
39
+ }
40
+
41
+ function tryExitApp() {
42
+ try {
43
+ if (window.tizen && tizen.application) {
44
+ tizen.application.getCurrentApplication().exit();
45
+ return;
46
+ }
47
+ } catch (e) {}
48
+ U.toast('Press RETURN again to exit'); // fallback on non-Tizen
49
+ }
50
+
51
+ function onKey(action) {
52
+ if (Player.isActive()) { return Player.handleKey(action); }
53
+ switch (action) {
54
+ case 'LEFT': Focus.move('LEFT'); return true;
55
+ case 'RIGHT': Focus.move('RIGHT'); return true;
56
+ case 'UP': Focus.move('UP'); return true;
57
+ case 'DOWN': Focus.move('DOWN'); return true;
58
+ case 'OK': Focus.clickCurrent(); return true;
59
+ case 'BACK': back(); return true;
60
+ default: return true;
61
+ }
62
+ }
63
+
64
+ function start() {
65
+ Player.init();
66
+ Keys.init();
67
+ Keys.setHandler(onKey);
68
+ showScreen('home');
69
+ Views.renderHome();
70
+ }
71
+
72
+ return {
73
+ start: start, back: back,
74
+ openDetail: openDetail, openSettings: openSettings
75
+ };
76
+ })();
77
+
78
+ window.onload = function () { App.start(); };
package/js/focus.js ADDED
@@ -0,0 +1,96 @@
1
+ /* Spatial focus navigation for the D-pad. Global namespace: Focus
2
+ Any element with class "focusable" participates. */
3
+ var Focus = (function () {
4
+ 'use strict';
5
+
6
+ var _scope = null;
7
+ var _current = null;
8
+
9
+ function items() {
10
+ if (!_scope) { return []; }
11
+ var nodes = _scope.querySelectorAll('.focusable');
12
+ var out = [];
13
+ for (var i = 0; i < nodes.length; i++) {
14
+ var n = nodes[i];
15
+ var r = n.getBoundingClientRect();
16
+ if (r.width > 0 && r.height > 0) { out.push(n); }
17
+ }
18
+ return out;
19
+ }
20
+
21
+ function setFocus(node) {
22
+ if (!node) { return; }
23
+ if (_current && _current !== node && _current.classList) { _current.classList.remove('focused'); }
24
+ _current = node;
25
+ if (node.classList) { node.classList.add('focused'); }
26
+ ensureVisible(node);
27
+ }
28
+
29
+ function ensureVisible(node) {
30
+ var p = node.parentNode;
31
+ while (p && p.nodeType === 1 && p !== document.body) {
32
+ var canX = p.scrollWidth > p.clientWidth + 2;
33
+ var canY = p.scrollHeight > p.clientHeight + 2;
34
+ if (canX || canY) {
35
+ var pr = p.getBoundingClientRect();
36
+ var nr = node.getBoundingClientRect();
37
+ if (canX) {
38
+ var mx = 90;
39
+ if (nr.left < pr.left + mx) { p.scrollLeft -= (pr.left + mx - nr.left); }
40
+ else if (nr.right > pr.right - mx) { p.scrollLeft += (nr.right - (pr.right - mx)); }
41
+ }
42
+ if (canY) {
43
+ var my = 70;
44
+ if (nr.top < pr.top + my) { p.scrollTop -= (pr.top + my - nr.top); }
45
+ else if (nr.bottom > pr.bottom - my) { p.scrollTop += (nr.bottom - (pr.bottom - my)); }
46
+ }
47
+ }
48
+ p = p.parentNode;
49
+ }
50
+ }
51
+
52
+ function move(dir) {
53
+ var list = items();
54
+ if (!list.length) { return; }
55
+ if (!_current || list.indexOf(_current) === -1) { setFocus(list[0]); return; }
56
+
57
+ var r = _current.getBoundingClientRect();
58
+ var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
59
+ var best = null, bestScore = Infinity;
60
+
61
+ for (var i = 0; i < list.length; i++) {
62
+ var node = list[i];
63
+ if (node === _current) { continue; }
64
+ var b = node.getBoundingClientRect();
65
+ var bx = b.left + b.width / 2, by = b.top + b.height / 2;
66
+ var dx = bx - cx, dy = by - cy;
67
+ var primary, cross;
68
+ if (dir === 'LEFT') { if (dx > -1) { continue; } primary = -dx; cross = Math.abs(dy); }
69
+ else if (dir === 'RIGHT') { if (dx < 1) { continue; } primary = dx; cross = Math.abs(dy); }
70
+ else if (dir === 'UP') { if (dy > -1) { continue; } primary = -dy; cross = Math.abs(dx); }
71
+ else { if (dy < 1) { continue; } primary = dy; cross = Math.abs(dx); }
72
+ var score = primary + cross * 2.2;
73
+ if (score < bestScore) { bestScore = score; best = node; }
74
+ }
75
+ if (best) { setFocus(best); }
76
+ }
77
+
78
+ // scope: root element to search within. preferred: optional element to focus first.
79
+ function setScope(scope, preferred) {
80
+ _scope = scope;
81
+ _current = null;
82
+ var list = items();
83
+ if (preferred && list.indexOf(preferred) !== -1) { setFocus(preferred); }
84
+ else if (list.length) { setFocus(list[0]); }
85
+ }
86
+
87
+ function current() { return _current; }
88
+ function clickCurrent() { if (_current) { fireClick(_current); } }
89
+ function fireClick(node) {
90
+ if (node.__onselect) { node.__onselect(node); return; }
91
+ if (node.click) { node.click(); }
92
+ }
93
+
94
+ return { setScope: setScope, move: move, setFocus: setFocus, current: current,
95
+ clickCurrent: clickCurrent, refresh: function () { return items(); } };
96
+ })();
package/js/keys.js ADDED
@@ -0,0 +1,56 @@
1
+ /* Remote control input. Global namespace: Keys
2
+ Registers TV media keys and normalizes keyCodes into symbolic actions. */
3
+ var Keys = (function () {
4
+ 'use strict';
5
+
6
+ var CODE = {
7
+ 37: 'LEFT', 38: 'UP', 39: 'RIGHT', 40: 'DOWN',
8
+ 13: 'OK',
9
+ 10009: 'BACK', // RETURN
10
+ 415: 'PLAY',
11
+ 19: 'PAUSE',
12
+ 10252: 'PLAYPAUSE',
13
+ 413: 'STOP',
14
+ 417: 'FF',
15
+ 412: 'REW',
16
+ 427: 'CHUP', 428: 'CHDOWN'
17
+ };
18
+
19
+ // Names to register via the TVInputDevice API (arrows/OK/RETURN are always on).
20
+ var REGISTER = [
21
+ 'MediaPlay', 'MediaPause', 'MediaPlayPause', 'MediaStop',
22
+ 'MediaRewind', 'MediaFastForward',
23
+ 'MediaTrackPrevious', 'MediaTrackNext'
24
+ ];
25
+
26
+ function register() {
27
+ try {
28
+ if (window.tizen && tizen.tvinputdevice && tizen.tvinputdevice.registerKey) {
29
+ REGISTER.forEach(function (name) {
30
+ try { tizen.tvinputdevice.registerKey(name); } catch (e) {}
31
+ });
32
+ }
33
+ } catch (e) {}
34
+ }
35
+
36
+ var _handler = null;
37
+ function setHandler(fn) { _handler = fn; }
38
+
39
+ function onKeyDown(ev) {
40
+ var action = CODE[ev.keyCode];
41
+ if (!action) { return; }
42
+ if (_handler) {
43
+ var handled = _handler(action, ev);
44
+ if (handled !== false) {
45
+ if (ev.preventDefault) { ev.preventDefault(); }
46
+ }
47
+ }
48
+ }
49
+
50
+ function init() {
51
+ register();
52
+ document.addEventListener('keydown', onKeyDown, false);
53
+ }
54
+
55
+ return { init: init, setHandler: setHandler, CODE: CODE };
56
+ })();