Version: 4.0
Date: February 14, 2026
Status: Declarative-First Architecture
TizenPortal exposes a unified API via window.TizenPortal. The same runtime runs on both the portal page and target sites.
// Check if TizenPortal is available
if (window.TizenPortal) {
console.log('TizenPortal', window.TizenPortal.version);
}
// Example usage
TizenPortal.log('Hello from bundle');
TizenPortal.config.read('pointerMode');
Version format is 4-digit numeric for easy TV remote entry:
| Format | Example | Meaning |
|---|---|---|
XXYY |
0447 |
Major 04, Minor 47 |
// Check version
if (parseInt(TizenPortal.version) >= 300) {
// Use 3.x features
}
interface TizenPortal {
// Metadata
version: string; // Current version from package.json (e.g., "1018")
// Sub-APIs
config: ConfigAPI;
input: InputAPI;
focus: FocusAPI;
keys: KeyConstants;
cards: CardsAPI;
elements: ElementsAPI; // Element registration API (v1.0+)
features: FeaturesAPI; // Feature management & navigable selector registry
bundles: BundlesAPI;
polyfills: PolyfillAPI;
// Navigation
loadSite: (card: Card) => void;
closeSite: () => void;
returnToPortal: () => void;
getCurrentCard: () => Card | null;
// UI
showToast: (message: string, duration?: number) => void;
showLoading: (text: string) => void;
hideLoading: () => void;
toggleSiteAddressBar: () => void;
toggleSiteDiagnostics: () => void;
// Diagnostics
isDiagnosticsPanelVisible: () => boolean;
clearDiagnosticsLogs: () => void;
scrollDiagnosticsLogs: (amount: number) => void;
cycleDiagnosticsLogFilter: (direction: 1 | -1) => void;
// Logging
log: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
// State
getState: () => StateObject;
}
interface CardsAPI {
register: (config: { selector: string; type?: 'single' | 'multi'; container?: string }) => void;
unregister: (selector: string) => void;
clear: () => void;
process: () => number;
getRegistrations: () => Array<{ selector: string; type: string; container?: string | null }>;
}
interface BundlesAPI {
list: () => string[];
getActive: () => any | null;
getActiveName: () => string | null;
}
interface FeaturesAPI {
apply: (doc?: Document) => void; // Apply all enabled features to document
remove: (doc?: Document) => void; // Remove all features from document
getAll: () => Array<{ key: string; name: string; displayName: string }>;
getConfig: () => Record<string, any>; // Current feature toggle values
getDefaults: () => Record<string, any>; // Default feature toggle values
addNavigableSelector: (selector: string) => void;
registry: RegistryAPI; // Shared unified registry
}
interface PolyfillAPI {
has: (name: string) => boolean;
loaded: () => string[];
}
Navigate to a site with bundle payload.
TizenPortal.loadSite({
id: 'abc123',
name: 'My Server',
url: 'https://abs.example.com',
featureBundle: 'audiobookshelf',
userAgent: 'mobile',
});
What happens:
bundleName and cardNamecard.url#tp=BASE64Return to the portal launcher.
TizenPortal.returnToPortal();
// Navigates to: https://axelnanol.github.io/tizenportal/dist/index.html
Display a toast notification.
TizenPortal.showToast('Card saved');
TizenPortal.showToast('Error occurred', 5000); // 5 second duration
Logging functions that output to both console and diagnostics panel.
TizenPortal.log('Site loaded');
TizenPortal.warn('Deprecated feature used');
TizenPortal.error('Failed to load bundle');
Log Levels:
log - General information (white in diagnostics)warn - Warnings (yellow in diagnostics)error - Errors (red in diagnostics)Logs are captured in a circular buffer and displayed in the diagnostics panel.
Check if the diagnostics panel is currently visible.
if (TizenPortal.isDiagnosticsPanelVisible()) {
TizenPortal.log('Diagnostics is open');
}
Clear all logs from the diagnostics panel.
TizenPortal.clearDiagnosticsLogs();
TizenPortal.log('Logs cleared');
Note: Yellow button in diagnostics also clears logs.
Scroll the diagnostics log view by the specified amount.
// Scroll down 5 lines
TizenPortal.scrollDiagnosticsLogs(5);
// Scroll up 5 lines
TizenPortal.scrollDiagnosticsLogs(-5);
Cycle through log level filters (All → Log → Warn → Error → All).
// Cycle forward
TizenPortal.cycleDiagnosticsLogFilter(1);
// Cycle backward
TizenPortal.cycleDiagnosticsLogFilter(-1);
Filter Levels:
log() entrieswarn() entrieserror() entriesGet current runtime state.
const state = TizenPortal.getState();
// {
// initialized: true,
// isPortalPage: false,
// currentCard: { name: 'My Server', ... },
// currentBundle: 'audiobookshelf',
// siteActive: true
// }
Configuration management with localStorage persistence.
interface ConfigAPI {
read: (key: string) => any;
write: (key: string, value: any) => void;
get: (key: string) => any; // Alias for read
set: (key: string, value: any) => void; // Alias for write
getAll: () => object; // Get entire config object
reset: () => void; // Restore default settings
onChange: (callback: (event: ConfigChangeEvent) => void) => void;
}
const pointerMode = TizenPortal.config.read('pointerMode');
const customSetting = TizenPortal.config.read('myBundleSetting');
TizenPortal.config.write('pointerMode', true);
TizenPortal.config.write('myBundleSetting', { enabled: true });
TizenPortal.config.onChange(function(event) {
console.log('Config changed:', event.key, event.value);
});
Get the entire configuration object.
const allConfig = TizenPortal.config.getAll();
console.log('All settings:', allConfig);
Restore all settings to their default values.
TizenPortal.config.reset();
TizenPortal.log('Settings reset to defaults');
| Key | Type | Default | Description |
|---|---|---|---|
pointerMode |
boolean | false | On-screen mouse enabled |
focusHighlight |
boolean | true | Focus indicators visible |
safeMode |
boolean | false | Safe mode enabled |
diagnosticsEnabled |
boolean | false | Debug overlay enabled |
lastVisitedUrl |
string | null | Last visited site URL |
tp_portal |
object | — | Portal preferences (theme, HUD, color hints) |
tp_features |
object | — | Global site feature toggles |
tp_userscripts |
object | — | Global userscript configuration |
tp_apps |
array | [] | Site card definitions |
tp_portal)interface PortalPreferences {
theme: 'light' | 'dark' | 'auto' | 'portal' | 'backdrop' | 'custom';
customColor1?: string; // Hex color for custom theme
customColor2?: string; // Hex color for custom theme
backdropUrl?: string; // Background image URL
portalFilter?: 'glow' | 'crisp' | 'flat' | 'vignette';
portalAccentPosition?: 'corners' | 'opposite' | 'top' | 'bottom' | 'sides';
hudPosition: 'off' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
hintsPosition: 'off' | 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
}
tp_features)interface FeatureToggles {
focusStyling: boolean; // Global focus styling control
focusOutlineMode: 'off' | 'on' | 'high' | 'portal' | 'white';
// off = subtle blue ring (browser default-like)
// on = solid blue ring (default)
// high = yellow ring (maximum contrast on dark backgrounds)
// portal = layered box-shadow glow ring matching the portal card style
// white = white ring (maximum contrast on colourful backgrounds)
tabindexInjection: boolean; // Auto-add tabindex to elements
scrollIntoView: boolean; // Auto-scroll on focus
safeArea: boolean; // Apply TV safe area inset (5%)
gpuHints: boolean; // Apply GPU acceleration hints
cssReset: boolean; // Apply CSS normalization
hideScrollbars: boolean; // Visually hide scrollbars
wrapTextInputs: boolean; // Protect text inputs from auto-focus
viewportMode: 'auto' | 'locked' | 'unlocked'; // Viewport control
uaMode: 'tizen' | 'desktop' | 'mobile'; // User agent mode
}
Input state and handler registration.
interface InputAPI {
isPointerMode: () => boolean;
togglePointer: () => boolean;
isIMEActive: () => boolean;
registerKeyHandler: (handler: KeyHandler) => void;
wrapTextInputs: (selector: string) => number;
unwrapTextInputs: (selector: string) => void;
activateInput: (el: HTMLElement) => void;
deactivateInput: (el: HTMLElement) => void;
}
type KeyHandler = (event: KeyboardEvent) => boolean;
Check if pointer (mouse) mode is active.
if (TizenPortal.input.isPointerMode()) {
// Pointer mode active - user can click
}
Check if TV keyboard/IME is active.
if (TizenPortal.input.isIMEActive()) {
// Keyboard active - disable spatial navigation
}
Register a custom key handler that runs BEFORE core handlers.
TizenPortal.input.registerKeyHandler(function(event) {
if (event.keyCode === 415) { // Play key
playCurrentItem();
return true; // Consumed - stop propagation
}
return false; // Let core handle
});
Return Values:
true - Event consumed, stop further handlingfalse - Pass to next handler / core handlingProtect text input elements from auto-opening the on-screen keyboard. User must press Enter to activate.
// Wrap all text inputs
var count = TizenPortal.input.wrapTextInputs('input[type="text"], textarea');
TizenPortal.log('Protected ' + count + ' text inputs');
Returns: Number of inputs wrapped.
Remove protection from text inputs.
TizenPortal.input.unwrapTextInputs('input[type="text"]');
Manually activate a text input (opens on-screen keyboard).
var input = document.querySelector('#search');
TizenPortal.input.activateInput(input);
Manually deactivate a text input (closes on-screen keyboard).
TizenPortal.input.deactivateInput(input);
Additional pointer mode utilities (not in main API):
// Get cursor position
var pos = TizenPortal.pointer.getPosition();
// { x: 960, y: 540 }
// Set cursor position
TizenPortal.pointer.setPosition(100, 200);
// Clear hover highlight
TizenPortal.pointer.clearHoverHighlight();
Focus management utilities.
interface FocusAPI {
set: (element: HTMLElement) => void;
get: () => HTMLElement | null;
enableScrollIntoView: (options?: ScrollOptions) => void;
disableScrollIntoView: () => void;
setScrollEnabled: (enabled: boolean) => void;
setInitialFocus: (selectors: string[]) => void;
lockViewport: () => void;
unlockViewport: () => void;
observeDOM: (callback: () => void) => void;
stopObservingDOM: () => void;
}
// Set focus
TizenPortal.focus.set(document.querySelector('.my-element'));
// Get currently focused element
const focused = TizenPortal.focus.get();
Enable automatic scroll when focus changes.
TizenPortal.focus.enableScrollIntoView({
marginTop: 100,
marginBottom: 100,
marginLeft: 50,
marginRight: 50,
behavior: 'smooth' // or 'auto'
});
Set initial focus using selector priority list.
// Tries each selector in order, focuses first match
TizenPortal.focus.setInitialFocus([
'.currently-playing',
'.book-card:first-child',
'a:first-of-type'
]);
Lock viewport to 1920px width.
TizenPortal.focus.lockViewport();
Watch for DOM changes (useful for SPAs).
TizenPortal.focus.observeDOM(function() {
// Called when DOM changes
makeFocusable();
});
Register a CSS selector whose matching elements should receive tabindex="0" for TV
navigation. Call this from your bundle’s onDOMReady (or onActivate) to teach the
global tabindex injection system about site-specific interactive elements.
applyFeatures() call or
the next SPA route change.MutationObserver, which always reads from the live selector list.// bundles/my-bundle/main.js – onDOMReady or onActivate
onDOMReady: function() {
var add = window.TizenPortal.features.addNavigableSelector
.bind(window.TizenPortal.features);
// Site-specific interactive elements that are not covered by
// the global selector list (a[href], button, [role="button"], …)
add('.my-custom-card');
add('[data-clickable]');
add('.sidebar-item');
},
The global list already includes all native interactive elements and ARIA roles, so only add selectors for elements that are unique to the target site.
Card registration system for multi-element card interactions.
interface CardsAPI {
register: (config: CardRegistration) => void;
unregister: (selector: string) => void;
clear: () => void;
process: () => number;
getRegistrations: () => Array<CardRegistrationInfo>;
}
interface CardRegistration {
selector: string; // CSS selector for card elements
type?: 'single' | 'multi'; // Interaction type (auto-detected if omitted)
container?: string; // CSS selector for focus container
}
interface CardRegistrationInfo {
selector: string;
type: 'single' | 'multi';
container: string | null;
}
Register a card pattern for multi-element handling.
// Register book cards that can be entered
TizenPortal.cards.register({
selector: '.book-card',
type: 'multi',
container: '.book-card-inner'
});
// Register with auto-detection
TizenPortal.cards.register({
selector: '.media-item'
});
Card Types:
single: Cards with one interactive element (Enter activates immediately)multi: Cards with multiple interactive elements (Enter enters card, Back exits)Remove a card registration.
TizenPortal.cards.unregister('.book-card');
Remove all card registrations.
TizenPortal.cards.clear();
Process registered cards and make them focusable. Returns the number of cards processed.
const count = TizenPortal.cards.process();
TizenPortal.log('Processed ' + count + ' cards');
The card system provides a two-level navigation model:
Single-Action Cards:
Multi-Action Cards:
// Register album cards with multiple actions
TizenPortal.cards.register({
selector: '.album-card',
type: 'multi'
});
// Process cards after page load
setTimeout(function() {
var count = TizenPortal.cards.process();
TizenPortal.log('Made ' + count + ' albums focusable');
}, 500);
// Cleanup on deactivation
userscript.cleanup = function() {
TizenPortal.cards.clear();
};
TizenPortal provides utility functions for common tasks.
interface UtilsAPI {
escapeHtml: (str: string) => string;
isValidHttpUrl: (url: string) => boolean;
sanitizeUrl: (raw: string) => string;
isValidHexColor: (value: string) => boolean;
sanitizeCss: (css: string) => string;
}
Escape HTML special characters to prevent XSS.
var safe = TizenPortal.utils.escapeHtml(userInput);
element.textContent = safe; // Safe to use
Validate if a string is a valid HTTP/HTTPS URL.
if (TizenPortal.utils.isValidHttpUrl(input)) {
window.location.href = input;
}
Sanitize and validate a URL, returning safe version or ‘#’.
var safeUrl = TizenPortal.utils.sanitizeUrl(userInput);
link.href = safeUrl;
Check if a string is a valid hex color code.
if (TizenPortal.utils.isValidHexColor(input)) {
element.style.backgroundColor = input;
}
Sanitize CSS string for safe injection.
var safeCss = TizenPortal.utils.sanitizeCss(userCss);
styleElement.textContent = safeCss;
These helpers simplify teardown so bundles do not need to store listener references manually.
Attach a one-time event listener that removes itself after the first invocation. Returns a cancel function that removes the listener before it fires.
TizenPortal.once(
element: EventTarget,
eventType: string,
handler: (event: Event) => void
): () => void
// Wait for DOMContentLoaded without storing a reference
TizenPortal.once(document, 'DOMContentLoaded', function() {
registerCards();
});
// Cancel before firing if needed
var cancel = TizenPortal.once(document, 'DOMContentLoaded', init);
if (notNeededAfterAll) cancel();
Warns to console if called with invalid arguments.
Register a cleanup callback that is called automatically when the active bundle deactivates (inside unloadBundle()), after onDeactivate. All registered callbacks are drained and called in registration order; each is isolated in its own try-catch.
TizenPortal.onCleanup(fn: () => void): void
onActivate: function(window, card) {
var observer = new MutationObserver(handleMutation);
observer.observe(document.body, { childList: true, subtree: true });
// No need to store observer in module state or manually clean up in onDeactivate
TizenPortal.onCleanup(function() {
observer.disconnect();
});
},
Combine with TizenPortal.once for one-time DOM listeners:
onActivate: function(window, card) {
var cancel = TizenPortal.once(document, 'DOMContentLoaded', init);
TizenPortal.onCleanup(cancel); // Cancels listener if page was already loaded
},
Available in: v1.0+
Declarative element manipulation API. Register selectors and operations once; the core automatically processes existing and dynamically added elements.
interface ElementsAPI {
register: (config: ElementRegistration) => string; // Returns registration ID
unregister: (id: string) => void; // Remove registration
clear: () => void; // Clear all registrations
process: () => number; // Manual processing (usually not needed)
}
interface ElementRegistration {
selector: string; // CSS selector
operation: string; // Operation: focusable, class, attribute, style, hide, show, remove
// Optional filters
container?: string; // Limit to container selector
condition?: (el: Element) => boolean; // Runtime condition function
// Operation-specific options
nav?: string; // For focusable: 'vertical' | 'horizontal'
classes?: string[]; // For class: classes to add
remove?: boolean; // For class: remove instead of add
attributes?: Object; // For attribute: key-value pairs
styles?: Object; // For style: CSS properties
important?: boolean; // For style: add !important flag
// Performance options
debounceMs?: number; // Custom debounce (default: 100ms)
immediate?: boolean; // Skip debounce for critical elements
}
1. focusable - Make elements keyboard/remote navigable
TizenPortal.elements.register({
selector: 'nav a',
operation: 'focusable',
nav: 'vertical' // Optional: vertical | horizontal
});
Sets: tabindex="0", data-tp-nav, data-tp-focusable, role="button" (if needed)
2. class - Add/remove CSS classes
// Add classes
TizenPortal.elements.register({
selector: '.card',
operation: 'class',
classes: ['tp-card', 'tp-focusable']
});
// Remove classes
TizenPortal.elements.register({
selector: '.old-style',
operation: 'class',
classes: ['legacy-class'],
remove: true
});
3. attribute - Set/remove HTML attributes
TizenPortal.elements.register({
selector: '#toolbar',
operation: 'attribute',
attributes: {
'data-region': 'toolbar',
'aria-label': 'Main toolbar',
'data-tp-nav': 'horizontal'
}
});
// Dynamic values via functions
TizenPortal.elements.register({
selector: '.item',
operation: 'attribute',
attributes: {
'aria-label': function(el) {
return 'Item: ' + el.textContent;
}
}
});
4. style - Apply inline CSS styles
TizenPortal.elements.register({
selector: '#mobile-menu',
operation: 'style',
styles: {
display: 'none',
visibility: 'hidden'
},
important: true // Add !important flag
});
// Supports camelCase (auto-converts to kebab-case)
TizenPortal.elements.register({
selector: '.toolbar',
operation: 'style',
styles: {
backgroundColor: '#333',
borderRadius: '8px'
}
});
5. hide - Hide elements (stores original display value)
TizenPortal.elements.register({
selector: '.mobile-only',
operation: 'hide'
});
6. show - Show previously hidden elements
TizenPortal.elements.register({
selector: '.tv-only',
operation: 'show'
});
7. remove - Remove elements from DOM (with safety checks)
TizenPortal.elements.register({
selector: '.ad-container',
operation: 'remove',
condition: function(el) {
// Safety check before removal
return !el.closest('.important-content');
}
});
Container Scoping - Limit processing to specific containers for performance:
TizenPortal.elements.register({
selector: 'button',
operation: 'focusable',
container: '#content-area' // Only process buttons in #content-area
});
Conditional Registration - Apply operations based on runtime conditions:
TizenPortal.elements.register({
selector: '.dynamic-content',
operation: 'focusable',
condition: function(el) {
// Only if visible
return el.offsetParent !== null;
}
});
TizenPortal.elements.register({
selector: 'button',
operation: 'focusable',
condition: function(el) {
// Only if not disabled and not hidden
return !el.disabled && el.getAttribute('aria-hidden') !== 'true';
}
});
Page-Specific Registration - Apply to specific pages:
TizenPortal.elements.register({
selector: '.item-detail-button',
operation: 'focusable',
condition: function(el) {
return window.location.pathname.indexOf('/item/') !== -1;
}
});
Element registrations automatically observe the DOM for changes. No manual observation needed:
onActivate: function(window, card) {
// Register once
TizenPortal.elements.register({
selector: 'nav a',
operation: 'focusable'
});
// Core automatically:
// 1. Processes existing elements
// 2. Observes DOM for new elements
// 3. Applies operations to dynamically added elements
// 4. Cleans up on bundle deactivation
}
Usually not needed (automatic observation handles this), but available for edge cases:
// Manually trigger processing
var count = TizenPortal.elements.process();
console.log('Processed', count, 'elements');
Registrations are automatically cleared when the bundle deactivates. Manual cleanup is rarely needed:
onDeactivate: function(window, card) {
// Automatic cleanup - no action needed
// All registrations are cleared automatically
}
// Manual cleanup (if needed)
var regId = TizenPortal.elements.register({...});
TizenPortal.elements.unregister(regId); // Remove specific registration
TizenPortal.elements.clear(); // Remove all registrations
container option for large DOMsexport default {
name: 'my-bundle',
onActivate: function(window, card) {
// Navigation
TizenPortal.elements.register({
selector: 'nav a',
operation: 'focusable',
nav: 'vertical'
});
// Hide mobile elements
TizenPortal.elements.register({
selector: '.mobile-only, .smartphone-menu',
operation: 'hide'
});
// Style toolbar for TV
TizenPortal.elements.register({
selector: '#toolbar',
operation: 'style',
styles: {
position: 'fixed',
top: '0',
right: '320px'
},
important: true
});
// Make buttons focusable (with conditions)
TizenPortal.elements.register({
selector: 'button',
operation: 'focusable',
condition: function(el) {
return !el.disabled && el.offsetParent !== null;
}
});
// That's it! Core handles everything automatically
}
};
See Bundle Authoring Guide - Section 8.5 for complete documentation and migration examples.
Samsung Tizen key code mapping.
const KEYS = TizenPortal.keys;
// Navigation
KEYS.LEFT // 37
KEYS.UP // 38
KEYS.RIGHT // 39
KEYS.DOWN // 40
KEYS.ENTER // 13
// System
KEYS.BACK // 10009
KEYS.EXIT // 10182
// Color buttons
KEYS.RED // 403
KEYS.GREEN // 404
KEYS.YELLOW // 405
KEYS.BLUE // 406
// Media keys
KEYS.PLAY // 415
KEYS.PAUSE // 19
KEYS.PLAY_PAUSE // 10252
KEYS.STOP // 413
KEYS.REWIND // 412
KEYS.FAST_FORWARD // 417
// IME
KEYS.IME_DONE // 65376
KEYS.IME_CANCEL // 65385
interface KeyHelpers {
isNavigationKey: (keyCode: number) => boolean; // Check if Left/Up/Right/Down
isColorButton: (keyCode: number) => boolean; // Check if Red/Green/Yellow/Blue
isMediaKey: (keyCode: number) => boolean; // Check if Play/Pause/Stop/etc
getKeyName: (keyCode: number) => string; // Get human-readable key name
}
// Check key types
if (TizenPortal.keys.isNavigationKey(event.keyCode)) {
// Handle directional input
}
if (TizenPortal.keys.isColorButton(event.keyCode)) {
// Handle color button
}
// Get key name for logging
var name = TizenPortal.keys.getKeyName(event.keyCode);
TizenPortal.log('Key pressed: ' + name);
Color buttons support long-press actions (hold for 500ms).
// Long-press threshold is 500ms
const LONG_PRESS_MS = 500;
// Core handler automatically detects long presses
// Your custom handlers receive the event before long-press fires
TizenPortal.input.registerKeyHandler(function(event) {
if (event.keyCode === TizenPortal.keys.RED) {
// This fires on keydown (before long-press)
// Return false to allow long-press to fire
return false;
}
});
The BACK key (10009) has special handling:
// Back key is handled by core, but you can intercept
TizenPortal.input.registerKeyHandler(function(event) {
if (event.keyCode === TizenPortal.keys.BACK) {
// Custom back behavior
customBack Handler();
return true; // Prevent default back action
}
return false;
});
document.addEventListener('keydown', function(e) {
if (e.keyCode === TizenPortal.keys.RED) {
// Show address bar
}
});
The payload passed via URL hash when navigating to sites.
interface Payload {
bundleName: string; // Feature bundle identifier
cardName: string; // Card display name
css?: string; // Bundle CSS (optional)
js?: string; // Bundle JS bootstrap (optional)
ua?: string; // User-Agent override
viewportMode?: string;
focusOutlineMode?: 'off' | 'on' | 'high' | 'portal' | 'white';
tabindexInjection?: boolean;
scrollIntoView?: boolean;
safeArea?: boolean;
gpuHints?: boolean;
cssReset?: boolean;
hideScrollbars?: boolean;
wrapTextInputs?: boolean;
bundleOptions?: Record<string, any>;
bundleOptionData?: Record<string, any>;
}
const payload = {
bundleName: 'audiobookshelf',
cardName: 'My Server',
};
const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
const url = cardUrl + '#tp=' + encoded;
const hash = window.location.hash;
const match = hash.match(/[#&]tp=([^&]+)/);
if (match) {
try {
const decoded = decodeURIComponent(escape(atob(match[1])));
const payload = JSON.parse(decoded);
} catch (e) {
console.error('Failed to decode payload:', e);
}
}
Site cards stored in localStorage.
interface Card {
id: string; // Unique identifier
name: string; // Display name
url: string; // Target URL
featureBundle: string | null; // Feature bundle name
icon?: string | null; // Base64 or URL
viewportMode?: 'auto' | 'locked' | 'unlocked' | null;
focusOutlineMode?: 'on' | 'high' | 'off' | 'portal' | 'white' | null;
userAgent?: 'tizen' | 'mobile' | 'desktop' | null;
tabindexInjection?: boolean | null;
scrollIntoView?: boolean | null;
safeArea?: boolean | null;
gpuHints?: boolean | null;
cssReset?: boolean | null;
hideScrollbars?: boolean | null;
wrapTextInputs?: boolean | null;
bundleOptions?: Record<string, any>;
bundleOptionData?: Record<string, any>;
userscripts?: Array<UserscriptEntry>; // Per-site userscripts
userscriptToggles?: Record<string, boolean>; // Global script toggles for this site
bundleUserscriptToggles?: Record<string, Record<string, boolean>>; // Bundle script toggles
order?: number; // Grid position
createdAt?: number;
updatedAt?: number;
}
interface UserscriptEntry {
id: string; // Unique script identifier
name: string; // Display name
enabled: boolean; // Whether script is enabled
source: 'inline' | 'url'; // Script source type
inline?: string; // Inline script code
url?: string; // External script URL
cached?: string; // Cached script from URL
lastFetched?: number; // Timestamp of last fetch
}
> Note: User-agent spoofing is JavaScript-only (not network-layer). Some sites may still detect the underlying browser.
Cards stored in localStorage.tp_apps as JSON array.
// Get cards
function getCards() {
try {
return JSON.parse(localStorage.getItem('tp_apps')) || [];
} catch (e) {
return [];
}
}
// Save cards
function saveCards(cards) {
localStorage.setItem('tp_apps', JSON.stringify(cards));
}
Bundle definition used in the registry.
interface Bundle {
name: string; // Internal identifier
displayName: string; // UI display name
description: string; // Bundle description
style: string; // CSS content (imported from .css file)
userscripts?: Array<UserscriptEntry>; // Bundle-provided userscripts
// Lifecycle hooks
onBeforeLoad?: (window: Window, card: Card) => void;
onAfterLoad?: (window: Window, card: Card) => void;
onActivate?: (window: Window, card: Card) => void;
onDeactivate?: (window: Window, card: Card) => void;
onNavigate?: (url: string) => void;
onKeyDown?: (event: KeyboardEvent) => boolean;
}
import { getBundle, getBundleNames, hasBundle } from './bundles/registry.js';
// Get bundle by name
const bundle = getBundle('audiobookshelf');
// List all bundle names
const names = getBundleNames();
// ['default', 'audiobookshelf', 'adblock']
// Check if bundle exists
if (hasBundle('my-bundle')) {
// Bundle available
}
Bundles can define lifecycle hooks that are called at specific points:
Called before the page’s DOM is fully loaded.
export default {
name: 'my-bundle',
onBeforeLoad(window, card) {
// Runs before DOMContentLoaded
// Use for early script injection
TizenPortal.log('Preparing site...');
}
};
Use cases: Early script injection, preload resources
Called after the page’s DOM is fully loaded.
export default {
name: 'my-bundle',
onAfterLoad(window, card) {
// Runs after DOMContentLoaded
// DOM is ready for manipulation
registerCards();
setInitialFocus();
}
};
Use cases: DOM manipulation, focus setup, card registration
Called when the bundle becomes active (user navigates to site).
export default {
name: 'my-bundle',
onActivate(window, card) {
// Bundle is now active
TizenPortal.log('Bundle activated:', card.name);
// Setup event listeners
document.addEventListener('myEvent', handleEvent);
}
};
Use cases: Event listener setup, initialization, state setup
Called when the bundle is deactivated (user navigates away).
export default {
name: 'my-bundle',
onActivate(window, card) {
// Preferred: use TizenPortal.onCleanup so cleanup is automatic
var observer = new MutationObserver(handleMutation);
observer.observe(document.body, { childList: true, subtree: true });
TizenPortal.onCleanup(function() { observer.disconnect(); });
var cancel = TizenPortal.once(document, 'DOMContentLoaded', init);
TizenPortal.onCleanup(cancel);
},
onDeactivate(window, card) {
// Bundle is being deactivated
TizenPortal.log('Cleaning up...');
// Only manual teardown that cannot use onCleanup still belongs here
// (e.g. restoring patched prototypes that require the window reference)
clearInterval(myInterval);
}
};
Use cases: Cleanup, remove listeners, clear timers. Prefer TizenPortal.onCleanup() for most teardown — see Bundle Lifecycle Helpers.
Called when URL changes within the same site (SPA navigation).
export default {
name: 'my-bundle',
onNavigate(url) {
// URL changed (SPA navigation)
TizenPortal.log('Navigated to:', url);
// Re-process cards for new content
setTimeout(function() {
TizenPortal.cards.process();
}, 500);
}
};
Use cases: SPA navigation handling, re-processing cards for new page content, resetting per-page state, setting initial focus.
Note: Core polls window.location.href every 500 ms and also listens to popstate, so bundles do not need their own URL-change watcher.
Intercept key events before core processing.
export default {
name: 'my-bundle',
onKeyDown(event) {
// Intercept Play key for custom behavior
if (event.keyCode === TizenPortal.keys.PLAY) {
playCurrentItem();
return true; // Consumed - stop propagation
}
return false; // Let core handle
}
};
Return values:
true - Event consumed, stop further handlingfalse - Pass to core handlingUse cases: Custom key bindings, media key handling
import myStyles from './style.css';
export default {
name: 'my-bundle',
displayName: 'My Bundle',
description: 'Enhances My Site for TV',
style: myStyles,
userscripts: [
{
id: 'fix-nav',
name: 'Fix Navigation',
enabled: true,
source: 'inline',
inline: 'document.querySelector(".nav").style.display = "flex";'
}
],
onBeforeLoad(window, card) {
TizenPortal.log('Preparing:', card.name);
},
onAfterLoad(window, card) {
// Register multi-action cards
TizenPortal.cards.register({
selector: '.media-card',
type: 'multi'
});
// Process cards
setTimeout(function() {
var count = TizenPortal.cards.process();
TizenPortal.log('Processed', count, 'cards');
}, 500);
// Set initial focus
TizenPortal.focus.setInitialFocus([
'.media-card:first-child',
'button:first-of-type'
]);
},
onActivate(window, card) {
TizenPortal.log('Bundle active');
},
onDeactivate(window, card) {
TizenPortal.cards.clear();
TizenPortal.log('Cleaned up');
},
onKeyDown(event) {
if (event.keyCode === TizenPortal.keys.PLAY) {
playCurrentMedia();
return true;
}
return false;
}
};
Fired when configuration changes.
interface ConfigChangeEvent {
type: 'configChange';
detail: {
key: string;
value: any;
};
}
TizenPortal.config.onChange(function(event) {
if (event.detail.key === 'pointerMode') {
updatePointerState(event.detail.value);
}
});
Standard DOM keydown events. Use e.keyCode for Chrome 47 compatibility.
document.addEventListener('keydown', function(e) {
switch (e.keyCode) {
case TizenPortal.keys.YELLOW:
TizenPortal.returnToPortal();
break;
case TizenPortal.keys.BLUE:
toggleDiagnostics();
break;
}
});
Userscripts have access to the full TizenPortal API plus a special cleanup mechanism.
// Available in userscript execution context:
// - window: The page's global window object
// - document: The page's document object
// - TizenPortal: Full TizenPortal API
// - card: Current card object (may be null)
// - bundle: Current bundle object (may be null)
// - userscript: Runtime object for script metadata
// Example userscript:
TizenPortal.log('Userscript executing:', userscript.name);
// Modify page
document.body.style.backgroundColor = '#000';
// Register cleanup
userscript.cleanup = function() {
document.body.style.backgroundColor = '';
TizenPortal.log('Cleanup complete');
};
Register a cleanup function to remove event listeners, timers, or other side effects when the script is deactivated:
var intervalId = setInterval(function() {
checkPageState();
}, 1000);
// Cleanup will be called when:
// - User navigates away
// - Script is disabled
// - Page is reloaded
userscript.cleanup = function() {
clearInterval(intervalId);
};
TizenPortal.log() for visibility in diagnostics// Add custom keyboard shortcuts
function handleCustomKey(e) {
if (e.keyCode === TizenPortal.keys.PLAY) {
var video = document.querySelector('video');
if (video) {
if (video.paused) {
video.play();
} else {
video.pause();
}
e.preventDefault();
e.stopPropagation();
}
}
}
document.addEventListener('keydown', handleCustomKey, true);
// Cleanup
userscript.cleanup = function() {
document.removeEventListener('keydown', handleCustomKey, true);
TizenPortal.log('Custom navigation cleaned up');
};
| Button | Short Press | Long Press |
|---|---|---|
| Red (403) | Address bar | Reload |
| Green (404) | Pointer toggle | Edit focused card (portal) / Focus highlight toggle (sites) |
| Yellow (405) | Preferences (portal) / Return to portal (sites) | Add Site (portal) / Return to portal (sites) |
| Blue (406) | Diagnostics | Safe mode |
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-01-12 | Initial version (APP mode) |
| 2.0 | 2026-01-20 | MOD mode architecture |
| 3.0 | 2026-01-31 | Universal runtime, focus API |
End of API Reference