diff --git a/config.xml b/config.xml
index 7d0a89e..f1a2095 100644
--- a/config.xml
+++ b/config.xml
@@ -7,9 +7,21 @@
Jellyfin for Samsung Smart TV (Tizen).
+
+
+
+
+
+
+ jellyfin_service
+ Jellyfin Smarthub Preview Handler Service
+
+
+
Jellyfin
+
diff --git a/gulpfile.babel.js b/gulpfile.babel.js
index 8ff8079..f8761b0 100644
--- a/gulpfile.babel.js
+++ b/gulpfile.babel.js
@@ -107,6 +107,13 @@ function modifyIndex() {
appMode.text = 'window.appMode=\'cordova\';';
injectTarget.insertBefore(appMode, apploader);
+
+ // inject smarthub.js
+ const smarthub = this.createElement('script');
+ smarthub.setAttribute('src', '../smarthub.js');
+ smarthub.setAttribute('defer', '');
+ injectTarget.insertBefore(smarthub, apploader);
+
// inject tizen.js
const tizen = this.createElement('script');
tizen.setAttribute('src', '../tizen.js');
diff --git a/service/service.js b/service/service.js
new file mode 100644
index 0000000..59bfb29
--- /dev/null
+++ b/service/service.js
@@ -0,0 +1,101 @@
+var packageId = tizen.application.getCurrentApplication().appInfo.packageId;
+var applicationId = packageId + '.Jellyfin'
+var remoteMessagePort = undefined;
+
+
+/**
+ * Sends a message to Application and write out the log.
+ * It is needed because logs are not visible from service
+ *
+ * @param {string} value - The value to send in the message and wite to console
+ */
+function logAndSend(value)
+{
+ console.log(value);
+ sendMessage(value);
+}
+
+/**
+ * Sends a message to the remote message port.
+ *
+ * @param {string} value - The value to send in the message.
+ * @param {string} [key="KEY"] - The key associated with the value. Defaults to "KEY".
+ */
+function sendMessage(value, key) {
+ key = key || "KEY";
+ if (remoteMessagePort === undefined) {
+ remoteMessagePort = tizen.messageport.requestRemoteMessagePort(applicationId, packageId);
+ }
+ if (remoteMessagePort ) {
+ try {
+ remoteMessagePort.sendMessage([{ key, value }]);
+ } catch (e) {
+ console.error("Error sending message:", e.message);
+ }
+ } else {
+ console.log("Message port is undefined");
+ }
+
+}
+
+function handleDataInRequest()
+{
+ try {
+ var reqAppControl = tizen.application.getCurrentApplication().getRequestedAppControl();
+
+ if (!!reqAppControl) {
+ var appControlData = reqAppControl.appControl.data;
+
+ // Iterate through all keys in appControl.data
+ for (var i = 0; i < appControlData.length; i++) {
+ var key = appControlData[i].key;
+ var value = appControlData[i].value;
+
+ if (key === 'Preview') {
+ var previewData = value;
+ var previewData2 = JSON.parse(previewData);
+ logAndSend("Preview Data received: " + previewData);
+
+ try {
+ webapis.preview.setPreviewData(
+ JSON.stringify(previewData2),
+ function () {
+ logAndSend("Preview Set!");
+ tizen.application.getCurrentApplication().exit();
+ },
+ function (e) {
+ logAndSend("PreviewData Setting failed: " + e.message);
+ }
+ );
+ } catch (e) {
+ logAndSend("PreviewData Setting exception: " + e.message);
+ }
+ } else {
+ logAndSend("Unhandled key: " + key + ", value: " + value);
+ }
+ }
+ }
+ }
+ catch (e) {
+ logAndSend('On error exception : ' + e.message);
+ }
+}
+
+module.exports.onStart = function () {
+ logAndSend('OnStart recieved');
+};
+
+module.exports.onRequest = function () {
+ logAndSend('onRequest recieved');
+ handleDataInRequest();
+}
+
+
+module.exports.onStop = function () {
+ logAndSend('Service stopping...');
+};
+
+
+module.exports.onExit = function () {
+ logAndSend("Service exiting...");
+}
\ No newline at end of file
diff --git a/smarthub.js b/smarthub.js
new file mode 100644
index 0000000..417cb4d
--- /dev/null
+++ b/smarthub.js
@@ -0,0 +1,367 @@
+
+ var packageId = tizen.application.getCurrentApplication().appInfo.packageId;
+ var serviceId = packageId + ".service";
+ var smartViewJsonData = undefined;
+ var remoteMessagePort = undefined;
+ var localMessagePort = undefined;
+ var messagePortListener = undefined;
+
+
+
+
+
+/** Get the URL of the card's image.
+ * @param {Object} item - Item for which to generate tileimageurk
+ * @returns {CardImageUrl} Object representing the URL of the card's image.
+ */
+function getTileImageUrl(item) {
+ item = item.ProgramInfo || item;
+
+ options ={
+ preferThumb: true,
+ inheritThumb: true,
+ }
+
+ preferThumb = true;
+ let height = 250;
+ let imgUrl = null;
+ let imgTag = null;
+ let imgType = null;
+ let itemId = null;
+
+ /* eslint-disable sonarjs/no-duplicated-branches */
+ if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
+ imgType = 'Thumb';
+ imgTag = item.ImageTags.Thumb;
+ } else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
+ imgType = 'Thumb';
+ imgTag = item.SeriesThumbImageTag;
+ itemId = item.SeriesId;
+ } else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') {
+ imgType = 'Thumb';
+ imgTag = item.ParentThumbImageTag;
+ itemId = item.ParentThumbItemId;
+ } else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
+ imgType = 'Backdrop';
+ imgTag = item.BackdropImageTags[0];
+ forceName = true;
+ } else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') {
+ imgType = 'Backdrop';
+ imgTag = item.ParentBackdropImageTags[0];
+ itemId = item.ParentBackdropItemId;
+ } else if (item.ImageTags && item.ImageTags.Primary && (item.Type !== 'Episode' || item.ChildCount !== 0)) {
+ imgType = 'Primary';
+ imgTag = item.ImageTags.Primary;
+
+ if (primaryImageAspectRatio && uiAspect) {
+ coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2;
+ }
+ } else if (item.SeriesPrimaryImageTag) {
+ imgType = 'Primary';
+ imgTag = item.SeriesPrimaryImageTag;
+ itemId = item.SeriesId;
+ } else if (item.PrimaryImageTag) {
+ imgType = 'Primary';
+ imgTag = item.PrimaryImageTag;
+ itemId = item.PrimaryImageItemId;
+
+ if (primaryImageAspectRatio && uiAspect) {
+ coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2;
+ }
+ } else if (item.ParentPrimaryImageTag) {
+ imgType = 'Primary';
+ imgTag = item.ParentPrimaryImageTag;
+ itemId = item.ParentPrimaryImageItemId;
+ } else if (item.AlbumId && item.AlbumPrimaryImageTag) {
+ imgType = 'Primary';
+ imgTag = item.AlbumPrimaryImageTag;
+ itemId = item.AlbumId;
+
+ if (primaryImageAspectRatio && uiAspect) {
+ coverImage = (Math.abs(primaryImageAspectRatio - uiAspect) / uiAspect) <= 0.2;
+ }
+ } else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) {
+ imgType = 'Thumb';
+ imgTag = item.ImageTags.Thumb;
+ } else if (item.BackdropImageTags && item.BackdropImageTags.length) {
+ imgType = 'Backdrop';
+ imgTag = item.BackdropImageTags[0];
+ } else if (item.ImageTags && item.ImageTags.Thumb) {
+ imgType = 'Thumb';
+ imgTag = item.ImageTags.Thumb;
+ } else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
+ imgType = 'Thumb';
+ imgTag = item.SeriesThumbImageTag;
+ itemId = item.SeriesId;
+ } else if (item.ParentThumbItemId && options.inheritThumb !== false) {
+ imgType = 'Thumb';
+ imgTag = item.ParentThumbImageTag;
+ itemId = item.ParentThumbItemId;
+ } else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
+ imgType = 'Backdrop';
+ imgTag = item.ParentBackdropImageTags[0];
+ itemId = item.ParentBackdropItemId;
+ }
+ /* eslint-enable sonarjs/no-duplicated-branches */
+
+ if (!itemId) {
+ itemId = item.Id;
+ }
+
+ if (imgTag && imgType) {
+ var params = {
+ type: imgType,
+ fillHeight: height,
+ quality: 96,
+ tag: imgTag,
+ format: "jpg"
+ };
+ var playedPercentage = item && item.UserData && item.UserData.PlayedPercentage;
+ if (playedPercentage !== null && playedPercentage !== undefined) {
+ params.percentPlayed = playedPercentage;
+ }
+ imgUrl = ApiClient.getScaledImageUrl(itemId, params);
+ }
+
+ return imgUrl;
+
+}
+
+
+
+
+/**
+ * Creates a JSON object representing one title for the smart view.
+ *
+ * @param {Object} title_data - The title data containing details about the media items.
+ * @param {string} title_data.ServerId - The server ID associated with the media.
+ * @param {string} title_data.Id - The unique ID of the "Episode"
+ * @param {number} title_data.ParentIndexNumber - The "Series" index number of the media.
+ * @param {number} title_data.IndexNumber - The index number of the media.
+ * @param {string} title_data.Name - The name of the Movie
+ * @param {string} title_data.SeriesName - The name of the Series
+ * @param {string} title_data.ParentBackdropItemId - The ID for the backdrop image.
+ * @param {Object} title_data.UserData - User-specific data, including played percentage.
+ * @param {number} title_data.UserData.PlayedPercentage - Percentage of the media played.
+ * @returns {Object|null} The formatted title JSON object or `null` if data is invalid.
+ */
+ function generateTitleJson(title_data) {
+
+ if (!title_data) {
+ console.warn("Missing title_data");
+ return null;
+ }
+
+
+ var action_data =
+ {
+ serverid: title_data.ServerId,
+ id: title_data.Id
+ };
+ var title = null;
+
+ var imgURL = getTileImageUrl(title_data);
+
+
+ if(title_data.Type =="Episode"){
+
+ action_data.type = 'episode';
+ action_data.seasonid = title_data.SeasonId;
+ action_data.seriesid = title_data.SeriesId;
+ series_episode = "";
+ if (title_data.ParentIndexNumber !== undefined && title_data.IndexNumber !== undefined)
+ series_episode = "S" + title_data.ParentIndexNumber + ":E" + title_data.IndexNumber + " - "
+
+ title = {
+ title: series_episode + title_data.Name,
+ subtitle: title_data.SeriesName,
+ image_ratio: "16by9",
+ image_url: imgURL,
+ action_data: JSON.stringify(action_data),
+ is_playable: true
+ };}
+ else if(title_data.Type =="Movie"){
+ action_data.type = 'movie';
+ title = {
+ title: title_data.Name,
+ image_ratio: "16by9",
+ image_url: imgURL,
+ action_data: JSON.stringify(action_data),
+ is_playable: true
+ };
+ }
+ return title;
+ }
+
+
+/**
+ * Creates a JSON object for the smart view containing multiple sections and their tiles.
+ *
+ * @param {Array