nativeshell.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. const viewdata = JSON.parse(window.atob("@@data@@"));
  2. const features = [
  3. "filedownload",
  4. "displaylanguage",
  5. "htmlaudioautoplay",
  6. "htmlvideoautoplay",
  7. "externallinks",
  8. "clientsettings",
  9. "multiserver",
  10. "exitmenu",
  11. "remotecontrol",
  12. "fullscreenchange",
  13. "filedownload",
  14. "remotevideo",
  15. "displaymode",
  16. "screensaver",
  17. "fileinput"
  18. ];
  19. const plugins = [
  20. 'mpvVideoPlayer',
  21. 'mpvAudioPlayer',
  22. 'jmpInputPlugin',
  23. 'jmpUpdatePlugin'
  24. ];
  25. function loadScript(src) {
  26. return new Promise((resolve, reject) => {
  27. const s = document.createElement('script');
  28. s.src = src;
  29. s.onload = resolve;
  30. s.onerror = reject;
  31. document.head.appendChild(s);
  32. });
  33. }
  34. // Add plugin loaders
  35. for (const plugin of plugins) {
  36. window[plugin] = async () => {
  37. await loadScript(`${viewdata.scriptPath}${plugin}.js`);
  38. return window["_" + plugin];
  39. };
  40. }
  41. window.NativeShell = {
  42. openUrl(url, target) {
  43. window.api.system.openExternalUrl(url);
  44. },
  45. downloadFile(downloadInfo) {
  46. window.api.system.openExternalUrl(downloadInfo.url);
  47. },
  48. openClientSettings() {
  49. showSettingsModal();
  50. },
  51. getPlugins() {
  52. return plugins;
  53. }
  54. };
  55. function getDeviceProfile() {
  56. return {
  57. 'Name': 'Jellyfin Media Player',
  58. 'MaxStaticBitrate': 1000000000,
  59. 'MusicStreamingTranscodingBitrate': 1280000,
  60. 'TimelineOffsetSeconds': 5,
  61. 'TranscodingProfiles': [
  62. {'Type': 'Audio'},
  63. {
  64. 'Container': 'ts',
  65. 'Type': 'Video',
  66. 'Protocol': 'hls',
  67. 'AudioCodec': 'aac,mp3,ac3,opus,flac,vorbis',
  68. 'VideoCodec': 'h264,h265,hevc,mpeg4,mpeg2video',
  69. 'MaxAudioChannels': '6'
  70. },
  71. {'Container': 'jpeg', 'Type': 'Photo'}
  72. ],
  73. 'DirectPlayProfiles': [{'Type': 'Video'}, {'Type': 'Audio'}, {'Type': 'Photo'}],
  74. 'ResponseProfiles': [],
  75. 'ContainerProfiles': [],
  76. 'CodecProfiles': [],
  77. 'SubtitleProfiles': [
  78. {'Format': 'srt', 'Method': 'External'},
  79. {'Format': 'srt', 'Method': 'Embed'},
  80. {'Format': 'ass', 'Method': 'External'},
  81. {'Format': 'ass', 'Method': 'Embed'},
  82. {'Format': 'sub', 'Method': 'Embed'},
  83. {'Format': 'sub', 'Method': 'External'},
  84. {'Format': 'ssa', 'Method': 'Embed'},
  85. {'Format': 'ssa', 'Method': 'External'},
  86. {'Format': 'smi', 'Method': 'Embed'},
  87. {'Format': 'smi', 'Method': 'External'},
  88. {'Format': 'pgssub', 'Method': 'Embed'},
  89. {'Format': 'dvdsub', 'Method': 'Embed'},
  90. {'Format': 'dvbsub', 'Method': 'Embed'},
  91. {'Format': 'pgs', 'Method': 'Embed'}
  92. ]
  93. };
  94. }
  95. async function createApi() {
  96. await loadScript('qrc:///qtwebchannel/qwebchannel.js');
  97. const channel = await new Promise((resolve) => {
  98. /*global QWebChannel */
  99. new QWebChannel(window.qt.webChannelTransport, resolve);
  100. });
  101. return channel.objects;
  102. }
  103. window.NativeShell.AppHost = {
  104. init() {
  105. window.apiPromise = createApi();
  106. (async () => {
  107. window.api = await window.apiPromise;
  108. })();
  109. },
  110. getDefaultLayout() {
  111. return viewdata.mode;
  112. },
  113. supports(command) {
  114. return features.includes(command.toLowerCase());
  115. },
  116. getDeviceProfile,
  117. getSyncProfile: getDeviceProfile,
  118. appName() {
  119. return "Jellyfin Media Player";
  120. },
  121. appVersion() {
  122. return navigator.userAgent.split(" ")[1];
  123. },
  124. deviceName() {
  125. return viewdata.deviceName;
  126. },
  127. exit() {
  128. window.api.system.exit();
  129. }
  130. };
  131. async function showSettingsModal() {
  132. let settings = await new Promise(resolve => {
  133. window.api.settings.settingDescriptions(resolve);
  134. });
  135. const modalContainer = document.createElement("div");
  136. modalContainer.className = "dialogContainer";
  137. modalContainer.style.backgroundColor = "rgba(0,0,0,0.5)";
  138. modalContainer.addEventListener("click", e => {
  139. if (e.target == modalContainer) {
  140. modalContainer.remove();
  141. }
  142. });
  143. document.body.appendChild(modalContainer);
  144. const modalContainer2 = document.createElement("div");
  145. modalContainer2.className = "focuscontainer dialog dialog-fixedSize dialog-small formDialog opened";
  146. modalContainer.appendChild(modalContainer2);
  147. const modalHeader = document.createElement("div");
  148. modalHeader.className = "formDialogHeader";
  149. modalContainer2.appendChild(modalHeader);
  150. const title = document.createElement("h3");
  151. title.className = "formDialogHeaderTitle";
  152. title.textContent = "Jellyfin Media Player Settings";
  153. modalHeader.appendChild(title);
  154. const modalContents = document.createElement("div");
  155. modalContents.className = "formDialogContent smoothScrollY";
  156. modalContents.style.paddingTop = "2em";
  157. modalContents.style.marginBottom = "6.2em";
  158. modalContainer2.appendChild(modalContents);
  159. for (let section of settings) {
  160. const group = document.createElement("fieldset");
  161. group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
  162. group.style.border = 0;
  163. group.style.outline = 0;
  164. modalContents.appendChild(group);
  165. const createSection = async (clear) => {
  166. if (clear) {
  167. group.innerHTML = "";
  168. }
  169. const values = await new Promise(resolve => {
  170. window.api.settings.allValues(section.key, resolve);
  171. });
  172. const legend = document.createElement("legend");
  173. const legendHeader = document.createElement("h2");
  174. legendHeader.textContent = section.key;
  175. legendHeader.style.textTransform = "capitalize";
  176. legend.appendChild(legendHeader);
  177. group.appendChild(legend);
  178. for (const setting of section.settings) {
  179. const label = document.createElement("label");
  180. label.className = "inputContainer";
  181. label.style.marginBottom = "1.8em";
  182. label.style.display = "block";
  183. label.style.textTransform = "capitalize";
  184. if (setting.options) {
  185. const safeValues = {};
  186. const control = document.createElement("select");
  187. control.className = "emby-select-withcolor emby-select";
  188. for (const option of setting.options) {
  189. safeValues[String(option.value)] = option.value;
  190. const opt = document.createElement("option");
  191. opt.value = option.value;
  192. opt.selected = option.value == values[setting.key];
  193. let optionName = option.title;
  194. const swTest = `${section.key}.${setting.key}.`;
  195. const swTest2 = `${section.key}.`;
  196. if (optionName.startsWith(swTest)) {
  197. optionName = optionName.substring(swTest.length);
  198. } else if (optionName.startsWith(swTest2)) {
  199. optionName = optionName.substring(swTest2.length);
  200. }
  201. opt.appendChild(document.createTextNode(optionName));
  202. control.appendChild(opt);
  203. }
  204. control.addEventListener("change", async (e) => {
  205. await new Promise(resolve => {
  206. window.api.settings.setValue(section.key, setting.key, safeValues[e.target.value], resolve);
  207. });
  208. if (setting.key == "devicetype") {
  209. section = (await new Promise(resolve => {
  210. window.api.settings.settingDescriptions(resolve);
  211. })).filter(x => x.key == section.key)[0];
  212. createSection(true);
  213. }
  214. });
  215. const labelText = document.createElement('label');
  216. labelText.className = "inputLabel";
  217. labelText.textContent = setting.key + ": ";
  218. label.appendChild(labelText);
  219. label.appendChild(control);
  220. } else {
  221. const control = document.createElement("input");
  222. control.type = "checkbox";
  223. control.checked = values[setting.key];
  224. control.addEventListener("change", e => {
  225. window.api.settings.setValue(section.key, setting.key, e.target.checked);
  226. });
  227. label.appendChild(control);
  228. label.appendChild(document.createTextNode(" " + setting.key));
  229. }
  230. group.appendChild(label);
  231. }
  232. };
  233. createSection();
  234. }
  235. const closeContainer = document.createElement("div");
  236. closeContainer.className = "formDialogFooter";
  237. modalContents.appendChild(closeContainer);
  238. const close = document.createElement("button");
  239. close.className = "raised button-cancel block btnCancel formDialogFooterItem emby-button";
  240. close.textContent = "Close"
  241. close.addEventListener("click", () => {
  242. modalContainer.remove();
  243. });
  244. closeContainer.appendChild(close);
  245. }