commit ea271573c1342276f63f13b9ee04aa5cadd5d54f Author: Dmitry Lyzo Date: Sat Oct 12 17:25:42 2019 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ecd118 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.wgt +www +.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e7d526 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +

Jellyfin Tizen

+

Part of the Jellyfin Project

+ +## Build Process + +### Getting Started + +1. Download and install Tizen Studio (https://developer.tizen.org/development/tizen-studio/download). +2. Setup Samsung certificate (need Samsung account). +3. Clone or download this repository. + ```sh + git clone https://github.com/jellyfin/jellyfin-tizen.git + ``` +4. Clone or download Jellyfin Web repository. + ```sh + git clone https://github.com/jellyfin/jellyfin-web.git + ``` +5. Go to Jellyfin Tizen directory. + ```sh + cd jellyfin-tizen + ``` + +### Prepare Interface + +If any changes are made to `jellyfin-web/`, the `www/` directory will need to be rebuilt using the following command. + +```sh +JELLYFIN_WEB_DIR=../jellyfin-web/src npx gulp +``` + +> If `NODE_ENV=development` is set in the environment, then the source files will be copied without being minified. + +> The `JELLYFIN_WEB_DIR` environment variable can be used to override the location of `jellyfin-web`. + +### Build WGT + +```sh +tizen build-web -e ".*" -e gulpfile.js +tizen package -t wgt -o . -- .buildResult +``` + +### Deploy to Emulator + +1. Run emulator. +2. Install package. + ```sh + tizen install -n *.wgt -t T-samsung-5.0-x86 + ``` + > Specify target with `-t` option. + +### Deploy to TV + +1. Run TV. +2. Activate Developer Mode on TV (https://developer.samsung.com/tv/develop/getting-started/using-sdk/tv-device). +3. Connect to TV with Device Manager from Tizen Studio. Or with sdb. + ```sh + sdb connect YOUR_TV_IP + ``` +4. Install package. + ```sh + tizen install -n *.wgt -t UE65NU7400 + ``` + > Specify target with `-t` option. diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..2fadcb0 --- /dev/null +++ b/config.xml @@ -0,0 +1,14 @@ + + + + + Jellyfin + + Jellyfin for Samsung Smart TV (Tizen). + + + Jellyfin + + + + diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..abcba1c --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,100 @@ +var gulp = require('gulp'); +var gulpif = require('gulp-if'); +var del = require('del'); +var dom = require('gulp-dom'); +var uglifyes = require('uglify-es'); +var composer = require('gulp-uglify/composer'); +var uglify = composer(uglifyes, console); + +// Check the NODE_ENV environment variable +var isDev = process.env.NODE_ENV === 'development'; +// Allow overriding of jellyfin-web directory +var WEB_DIR = process.env.JELLYFIN_WEB_DIR || 'node_modules/jellyfin-web/dist'; +console.info('Using jellyfin-web from', WEB_DIR); + +// Skip minification for development builds or minified files +var compress = !isDev && [ + '**/*', + '!**/*min.*', + '!**/*hls.js', + // Temporarily exclude apiclient until updated + '!bower_components/emby-apiclient/**/*.js' +]; + +var uglifyOptions = { + compress: { + drop_console: true + } +}; + +var paths = { + assets: { + src: [ + WEB_DIR + '/**/*', + '!' + WEB_DIR + '/index.html' + ], + dest: 'www/' + }, + index: { + src: WEB_DIR + '/index.html', + dest: 'www/' + } +}; + +// Clean the www directory +function clean() { + return del([ + 'www' + ]); +} + +// Copy unmodified assets +function copy() { + return gulp.src(paths.assets.src) + .pipe(gulp.dest(paths.assets.dest)); +} + +// Add required tags to index.html +function modifyIndex() { + return gulp.src(paths.index.src) + .pipe(dom(function() { + // inject CSP meta tag + var meta = this.createElement('meta'); + meta.setAttribute('http-equiv', 'Content-Security-Policy'); + meta.setAttribute('content', 'default-src * \'self\' \'unsafe-inline\' \'unsafe-eval\' data: gap: file: filesystem: ws: wss:;'); + this.head.appendChild(meta); + + // inject appMode script + var appMode = this.createElement('script'); + appMode.text = 'window.appMode=\'cordova\';'; + this.body.appendChild(appMode); + + // inject tizen.js + var tizen = this.createElement('script'); + tizen.setAttribute('src', '../tizen.js'); + tizen.setAttribute('defer', ''); + this.body.appendChild(tizen); + + // inject apploader.js + var apploader = this.createElement('script'); + apploader.setAttribute('src', 'scripts/apploader.js'); + apploader.setAttribute('defer', ''); + this.body.appendChild(apploader); + + return this; + })) + .pipe(gulp.dest(paths.index.dest)) +} + +// Default build task +var build = gulp.series( + clean, + gulp.parallel(copy, modifyIndex) +); + +// Export tasks so they can be run individually +exports.clean = clean; +exports.copy = copy; +exports.modifyIndex = modifyIndex; +// Export default task +exports.default = build; diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..954bc25 Binary files /dev/null and b/icon.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..3eb0f48 --- /dev/null +++ b/index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/tizen.js b/tizen.js new file mode 100644 index 0000000..aaf8b56 --- /dev/null +++ b/tizen.js @@ -0,0 +1,179 @@ +'use strict'; + +console.log('Tizen adapter'); + +window.addEventListener('load', function() { + + //console.log(JSON.stringify(tizen.tvinputdevice.getSupportedKeys())); + + tizen.tvinputdevice.registerKey('MediaPlay'); + tizen.tvinputdevice.registerKey('MediaTrackPrevious'); + tizen.tvinputdevice.registerKey('MediaTrackNext'); + tizen.tvinputdevice.registerKey('MediaRewind'); + tizen.tvinputdevice.registerKey('MediaFastForward'); + + require(['inputManager', 'focusManager', 'viewManager', 'appRouter', 'actionsheet'], function(inputManager, focusManager, viewManager, appRouter, actionsheet) { + + const commands = { + '10009': 'back', + '415': 'playpause', + '10232': 'previoustrack', + '10233': 'nexttrack', + '412': 'rewind', + '417': 'fastforward' + }; + + var isRestored; + var lastActiveElement; + var historyStartup; + var historyDepth = 0; + var exitPromise; + + //document.addEventListener('keypress', function(e) { + // console.log('keypress'); + //}); + + //document.addEventListener('keyup', function(e) { + // console.log('keyup'); + //}); + + document.addEventListener('keydown', function(e) { + //console.log('keydown: keyCode: ' + e.keyCode + ' key: ' + e.key + ' location: ' + e.location); + + var command = commands[e.keyCode]; + + if (command) { + //console.log('command: ' + command); + + if (command === 'back' && historyDepth < 2 && !exitPromise) { + exitPromise = actionsheet.show({ + title: Globalize.translate('Exit?'), + items: [ + {id: 'yes', name: Globalize.translate('Yes')}, + {id: 'no', name: Globalize.translate('No')} + ] + }).then(function (value) { + exitPromise = null; + + if (value === 'yes') { + try { + tizen.application.getCurrentApplication().exit(); + } catch (ignore) {} + } + }, + function () { + exitPromise = null; + }); + return; + } + + inputManager.trigger(command); + } + }); + + document.addEventListener('click', function() { + lastActiveElement = document.activeElement; + }); + + document.addEventListener('viewhide', function() { + lastActiveElement = document.activeElement; + }); + + function onPageLoad() { + console.debug('onPageLoad ' + window.location.href + ' isRestored=' + isRestored); + + if (isRestored) { + return; + } + + var view = viewManager.currentView() || document.body; + + var element = lastActiveElement; + lastActiveElement = null; + + // These elements are recreated + if (element) { + if (element.classList.contains('btnPreviousPage')) { + element = view.querySelector('.btnPreviousPage'); + } else if (element.classList.contains('btnNextPage')) { + element = view.querySelector('.btnNextPage'); + } + } + + if (element && focusManager.isCurrentlyFocusable(element)) { + focusManager.focus(element); + } else { + element = focusManager.autoFocus(view); + } + } + + // Starts listening for changes in the '.loading-spinner' HTML element + function installMutationObserver() { + var mutationObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + console.debug(mutation.type); + if (mutation.target.classList.contains('hide')) { + onPageLoad(); + } + }); + }); + + var spinner = document.querySelector('.loading-spinner'); + + if (spinner) { + mutationObserver.observe(spinner, { attributes : true }); + document.removeEventListener('viewshow', installMutationObserver); + } + } + document.addEventListener('viewshow', installMutationObserver); + + window.addEventListener('pushState', function(e) { + + // Reset history on some pages + + var path = e.arguments && e.arguments[2] ? e.arguments[2] : ''; + var pos = path.indexOf('?'); + path = path.substring(0, pos !== -1 ? pos : path.length); + + switch (path) { + case '#!/home.html': + if (!historyStartup || historyStartup !== path) { + historyStartup = path; + historyDepth = 0; + } + break; + case '#!/selectserver.html': + case '#!/login.html': + historyStartup = path; + historyDepth = 0; + break; + } + + historyDepth++; + + isRestored = false; + + //console.log('history: ' + historyDepth + ', ' + historyStartup); + }); + + window.addEventListener('popstate', function() { + historyDepth--; + isRestored = true; + //console.log('history: ' + historyDepth + ', ' + historyStartup); + }); + + // Add 'pushState' and 'replaceState' events + var _wr = function(type) { + var orig = history[type]; + return function() { + var rv = orig.apply(this, arguments); + var e = new Event(type); + e.arguments = arguments; + window.dispatchEvent(e); + return rv; + }; + }; + history.pushState = _wr('pushState'); + history.replaceState = _wr('replaceState'); + }); +});