cineby-tv-mod 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.
Files changed (3) hide show
  1. package/inject.js +281 -0
  2. package/package.json +24 -0
  3. package/style.css +95 -0
package/inject.js ADDED
@@ -0,0 +1,281 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ // ── CSS Injection ─────────────────────────────────────────────────────────
5
+ // TizenBrew only serves the single `main` JS file — no native CSS field exists.
6
+ // All styles must be injected programmatically.
7
+ const injectStyles = () => {
8
+ if (document.getElementById('tizen-cineby-styles')) return; // guard re-injection
9
+ const style = document.createElement('style');
10
+ style.id = 'tizen-cineby-styles';
11
+ style.textContent = `
12
+ /* TV Overscan & Base Reset */
13
+ :root {
14
+ --bg-color: #000;
15
+ --text-color: #fff;
16
+ --focus-color: #ffcc00;
17
+ --safe-area: 40px;
18
+ }
19
+
20
+ body {
21
+ background-color: var(--bg-color) !important;
22
+ color: var(--text-color) !important;
23
+ padding: var(--safe-area) !important;
24
+ overflow-x: hidden;
25
+ font-family: sans-serif;
26
+ }
27
+
28
+ /* Hide Junk & Ads */
29
+ iframe, .ads, .ad-container, .popunder, .overlay, #footer, .sidebar,
30
+ [id*="google_ads"], [class*="adsbygoogle"] {
31
+ display: none !important;
32
+ visibility: hidden !important;
33
+ pointer-events: none !important;
34
+ }
35
+
36
+ /* Layout Transformation: Grid View */
37
+ #main, .movie-container, .grid {
38
+ display: grid !important;
39
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important;
40
+ gap: 20px !important;
41
+ padding: 20px 0 !important;
42
+ }
43
+
44
+ /* Movie Card Styling */
45
+ .movie, .movie-card, .item {
46
+ background: #1a1a1a !important;
47
+ border-radius: 8px !important;
48
+ overflow: hidden;
49
+ transition: transform 0.2s ease-in-out;
50
+ }
51
+
52
+ .movie img, .poster {
53
+ width: 100% !important;
54
+ height: auto !important;
55
+ display: block;
56
+ }
57
+
58
+ .movie-info, .details {
59
+ padding: 10px !important;
60
+ font-size: 1.2rem !important;
61
+ font-weight: bold;
62
+ }
63
+
64
+ /* Spatial Navigation Focus State */
65
+ .tizen-focus {
66
+ outline: 6px solid var(--focus-color) !important;
67
+ outline-offset: 4px;
68
+ transform: scale(1.08) !important;
69
+ z-index: 999;
70
+ box-shadow: 0 0 20px rgba(255, 204, 0, 0.6);
71
+ }
72
+
73
+ /* Header/Navbar Adjustments */
74
+ header, #navbar, .header {
75
+ height: 80px !important;
76
+ display: flex !important;
77
+ align-items: center !important;
78
+ justify-content: space-between !important;
79
+ padding: 0 20px !important;
80
+ background: #111 !important;
81
+ margin-bottom: 20px;
82
+ }
83
+
84
+ .nav-links a {
85
+ font-size: 1.5rem !important;
86
+ margin: 0 15px !important;
87
+ text-decoration: none;
88
+ color: #ccc;
89
+ }
90
+
91
+ /* Search Bar TV-Friendly */
92
+ #search, .search {
93
+ background: #333 !important;
94
+ color: #fff !important;
95
+ border: none !important;
96
+ padding: 10px 20px !important;
97
+ font-size: 1.2rem !important;
98
+ border-radius: 20px !important;
99
+ }
100
+
101
+ /* Video Player Fullscreen Focus */
102
+ video, iframe[src*="embed"] {
103
+ width: 100vw !important;
104
+ height: 56.25vw !important; /* 16:9 */
105
+ max-height: 80vh;
106
+ }
107
+ `;
108
+ (document.head || document.documentElement).appendChild(style);
109
+ };
110
+
111
+ // ── Fix 2: whitelist-based window.open override ───────────────────────────
112
+ const AD_DOMAINS = [
113
+ 'doubleclick.net', 'googlesyndication.com', 'adservice.google.com',
114
+ 'ads.yahoo.com', 'adnxs.com', 'rubiconproject.com', 'openx.net',
115
+ 'pubmatic.com', 'casalemedia.com', 'advertising.com'
116
+ ];
117
+
118
+ const isAdUrl = (url) => {
119
+ if (!url) return true;
120
+ try {
121
+ const hostname = new URL(url).hostname;
122
+ return AD_DOMAINS.some(domain => hostname.includes(domain));
123
+ } catch (e) {
124
+ return false;
125
+ }
126
+ };
127
+
128
+ // 1. DOM Sanitization & Anti-Ad Logic
129
+ const sanitize = () => {
130
+ const adSelectors = [
131
+ 'iframe:not([src*="cineby"])', '.ads', '.popunder', '#footer',
132
+ '[id*="google_ads"]', '[class*="adsbygoogle"]', '.backdrop'
133
+ ];
134
+ adSelectors.forEach(sel => {
135
+ document.querySelectorAll(sel).forEach(el => el.remove());
136
+ });
137
+
138
+ if (!window._tizenOpenPatched) {
139
+ const originalOpen = window.open;
140
+ window.open = function(url, ...args) {
141
+ if (isAdUrl(url)) return null;
142
+ return originalOpen.call(window, url, ...args);
143
+ };
144
+ window._tizenOpenPatched = true;
145
+ }
146
+ };
147
+
148
+ // 2. Spatial Navigation Logic
149
+ let focusableElements = [];
150
+ let currentIndex = 0;
151
+ let focusedElement = null;
152
+
153
+ const updateFocusable = () => {
154
+ focusableElements = Array.from(document.querySelectorAll(
155
+ 'a, button, input, textarea, .movie, .movie-card, [tabindex="0"]'
156
+ )).filter(el => {
157
+ const rect = el.getBoundingClientRect();
158
+ return rect.width > 0 && rect.height > 0 &&
159
+ window.getComputedStyle(el).display !== 'none';
160
+ });
161
+
162
+ if (focusedElement) {
163
+ const newIndex = focusableElements.indexOf(focusedElement);
164
+ if (newIndex !== -1) {
165
+ currentIndex = newIndex;
166
+ } else {
167
+ currentIndex = Math.min(currentIndex, Math.max(0, focusableElements.length - 1));
168
+ focusedElement = focusableElements[currentIndex] || null;
169
+ }
170
+ }
171
+ };
172
+
173
+ const observer = new MutationObserver(() => {
174
+ sanitize();
175
+ updateFocusable();
176
+ });
177
+
178
+ const setFocus = (index) => {
179
+ if (focusableElements.length === 0) return;
180
+
181
+ focusableElements.forEach(el => el.classList.remove('tizen-focus'));
182
+
183
+ currentIndex = Math.max(0, Math.min(index, focusableElements.length - 1));
184
+
185
+ const target = focusableElements[currentIndex];
186
+ focusedElement = target;
187
+ target.classList.add('tizen-focus');
188
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
189
+ target.focus();
190
+ };
191
+
192
+ const getDistance = (rect1, rect2) => {
193
+ return Math.sqrt(
194
+ Math.pow(rect1.left - rect2.left, 2) +
195
+ Math.pow(rect1.top - rect2.top, 2)
196
+ );
197
+ };
198
+
199
+ const move = (direction) => {
200
+ if (!focusableElements.length) return;
201
+
202
+ const currentRect = focusableElements[currentIndex].getBoundingClientRect();
203
+ let closestIndex = -1;
204
+ let minDistance = Infinity;
205
+
206
+ focusableElements.forEach((el, index) => {
207
+ if (index === currentIndex) return;
208
+ const rect = el.getBoundingClientRect();
209
+
210
+ let isCandidate = false;
211
+ if (direction === 'ArrowUp')
212
+ isCandidate = rect.bottom <= currentRect.top + 5 && rect.top <= currentRect.bottom;
213
+ if (direction === 'ArrowDown')
214
+ isCandidate = rect.top >= currentRect.bottom - 5 && rect.bottom >= currentRect.top;
215
+ if (direction === 'ArrowLeft')
216
+ isCandidate = rect.right <= currentRect.left + 5 && rect.left <= currentRect.right;
217
+ if (direction === 'ArrowRight')
218
+ isCandidate = rect.left >= currentRect.right - 5 && rect.right >= currentRect.left;
219
+
220
+ if (isCandidate) {
221
+ const dist = getDistance(currentRect, rect);
222
+ if (dist < minDistance) {
223
+ minDistance = dist;
224
+ closestIndex = index;
225
+ }
226
+ }
227
+ });
228
+
229
+ if (closestIndex !== -1) setFocus(closestIndex);
230
+ };
231
+
232
+ // 3. Remote Control Key Mapping
233
+ window.addEventListener('keydown', (e) => {
234
+ const key = e.key;
235
+ const code = e.keyCode;
236
+
237
+ switch (key) {
238
+ case 'ArrowUp':
239
+ case 'ArrowDown':
240
+ case 'ArrowLeft':
241
+ case 'ArrowRight':
242
+ e.preventDefault();
243
+ move(key);
244
+ break;
245
+ case 'Enter':
246
+ e.preventDefault();
247
+ if (focusableElements[currentIndex]) {
248
+ focusableElements[currentIndex].click();
249
+ }
250
+ break;
251
+ // Fix: Tizen back button sends e.key === 'XF86Back' (keyCode 10009).
252
+ // 'GoBack', 'Backspace', 'Select', 'Escape' do not map to real Tizen remote keys.
253
+ case 'XF86Back':
254
+ e.preventDefault();
255
+ window.history.back();
256
+ break;
257
+ default:
258
+ // Fallback: handle back by keyCode for older Tizen firmware
259
+ if (code === 10009) {
260
+ e.preventDefault();
261
+ window.history.back();
262
+ }
263
+ break;
264
+ }
265
+ });
266
+
267
+ // Fix 5: reliable content-ready check instead of hard-coded setTimeout(1000)
268
+ const init = () => {
269
+ injectStyles();
270
+ observer.observe(document.body, { childList: true, subtree: true });
271
+ sanitize();
272
+ updateFocusable();
273
+ setFocus(0);
274
+ };
275
+
276
+ if (document.readyState === 'complete') {
277
+ init();
278
+ } else {
279
+ window.addEventListener('load', init);
280
+ }
281
+ })();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "cineby-tv-mod",
3
+ "appName": "Cineby TV",
4
+ "version": "1.0.0",
5
+ "description": "TV-optimized interface for Cineby with spatial navigation and ad-blocking.",
6
+ "packageType": "mods",
7
+ "websiteURL": "https://cineby.app",
8
+ "main": "inject.js",
9
+ "files": [
10
+ "inject.js",
11
+ "style.css"
12
+ ],
13
+ "keywords": [
14
+ "tizenbrew",
15
+ "tizen",
16
+ "samsung",
17
+ "smart-tv",
18
+ "cineby",
19
+ "spatial-navigation"
20
+ ],
21
+ "keys": [],
22
+ "author": "Senior TizenBrew Developer",
23
+ "license": "MIT"
24
+ }
package/style.css ADDED
@@ -0,0 +1,95 @@
1
+ /* TV Overscan & Base Reset */
2
+ :root {
3
+ --bg-color: #000;
4
+ --text-color: #fff;
5
+ --focus-color: #ffcc00;
6
+ --safe-area: 40px;
7
+ }
8
+
9
+ body {
10
+ background-color: var(--bg-color) !important;
11
+ color: var(--text-color) !important;
12
+ padding: var(--safe-area) !important;
13
+ overflow-x: hidden;
14
+ font-family: sans-serif;
15
+ }
16
+
17
+ /* Hide Junk & Ads */
18
+ iframe, .ads, .ad-container, .popunder, .overlay, #footer, .sidebar,
19
+ [id*="google_ads"], [class*="adsbygoogle"] {
20
+ display: none !important;
21
+ visibility: hidden !important;
22
+ pointer-events: none !important;
23
+ }
24
+
25
+ /* Layout Transformation: Grid View */
26
+ #main, .movie-container, .grid {
27
+ display: grid !important;
28
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important;
29
+ gap: 20px !important;
30
+ padding: 20px 0 !important;
31
+ }
32
+
33
+ /* Movie Card Styling */
34
+ .movie, .movie-card, .item {
35
+ background: #1a1a1a !important;
36
+ border-radius: 8px !important;
37
+ overflow: hidden;
38
+ transition: transform 0.2s ease-in-out;
39
+ }
40
+
41
+ .movie img, .poster {
42
+ width: 100% !important;
43
+ height: auto !important;
44
+ display: block;
45
+ }
46
+
47
+ .movie-info, .details {
48
+ padding: 10px !important;
49
+ font-size: 1.2rem !important;
50
+ font-weight: bold;
51
+ }
52
+
53
+ /* Spatial Navigation Focus State */
54
+ .tizen-focus {
55
+ outline: 6px solid var(--focus-color) !important;
56
+ outline-offset: 4px;
57
+ transform: scale(1.08) !important;
58
+ z-index: 999;
59
+ box-shadow: 0 0 20px rgba(255, 204, 0, 0.6);
60
+ }
61
+
62
+ /* Header/Navbar Adjustments */
63
+ header, #navbar, .header {
64
+ height: 80px !important;
65
+ display: flex !important;
66
+ align-items: center !important;
67
+ justify-content: space-between !important;
68
+ padding: 0 20px !important;
69
+ background: #111 !important;
70
+ margin-bottom: 20px;
71
+ }
72
+
73
+ .nav-links a {
74
+ font-size: 1.5rem !important;
75
+ margin: 0 15px !important;
76
+ text-decoration: none;
77
+ color: #ccc;
78
+ }
79
+
80
+ /* Search Bar TV-Friendly */
81
+ #search, .search {
82
+ background: #333 !important;
83
+ color: #fff !important;
84
+ border: none !important;
85
+ padding: 10px 20px !important;
86
+ font-size: 1.2rem !important;
87
+ border-radius: 20px !important;
88
+ }
89
+
90
+ /* Video Player Fullscreen Focus */
91
+ video, iframe[src*="embed"] {
92
+ width: 100vw !important;
93
+ height: 56.25vw !important; /* 16:9 */
94
+ max-height: 80vh;
95
+ }