nativeshell.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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. 'jellyscrubPlugin',
  25. 'skipIntroPlugin'
  26. ];
  27. function loadScript(src) {
  28. return new Promise((resolve, reject) => {
  29. const s = document.createElement('script');
  30. s.src = src;
  31. s.onload = resolve;
  32. s.onerror = reject;
  33. document.head.appendChild(s);
  34. });
  35. }
  36. // Add plugin loaders
  37. for (const plugin of plugins) {
  38. window[plugin] = async () => {
  39. await loadScript(`${viewdata.scriptPath}${plugin}.js`);
  40. return window["_" + plugin];
  41. };
  42. }
  43. window.NativeShell = {
  44. openUrl(url, target) {
  45. window.api.system.openExternalUrl(url);
  46. },
  47. downloadFile(downloadInfo) {
  48. window.api.system.openExternalUrl(downloadInfo.url);
  49. },
  50. openClientSettings() {
  51. showSettingsModal();
  52. },
  53. getPlugins() {
  54. return plugins;
  55. }
  56. };
  57. function getDeviceProfile() {
  58. const CodecProfiles = [
  59. {
  60. 'Type': 'Video',
  61. 'Conditions': [
  62. {
  63. 'Condition': 'NotEquals',
  64. 'Property': 'VideoRangeType',
  65. 'Value': 'DOVI'
  66. }
  67. ]
  68. }
  69. ];
  70. if (viewdata.force_transcode_hdr) {
  71. CodecProfiles.push({
  72. 'Type': 'Video',
  73. 'Conditions': [
  74. {
  75. 'Condition': 'Equals',
  76. 'Property': 'VideoRangeType',
  77. 'Value': 'SDR'
  78. }
  79. ]
  80. });
  81. }
  82. return {
  83. 'Name': 'Jellyfin Media Player',
  84. 'MaxStaticBitrate': 1000000000,
  85. 'MusicStreamingTranscodingBitrate': 1280000,
  86. 'TimelineOffsetSeconds': 5,
  87. 'TranscodingProfiles': [
  88. {'Type': 'Audio'},
  89. {
  90. 'Container': 'ts',
  91. 'Type': 'Video',
  92. 'Protocol': 'hls',
  93. 'AudioCodec': 'aac,mp3,ac3,opus,flac,vorbis',
  94. 'VideoCodec': viewdata.allow_transcode_to_hevc
  95. ? 'h264,h265,hevc,mpeg4,mpeg2video'
  96. : 'h264,mpeg4,mpeg2video',
  97. 'MaxAudioChannels': '6'
  98. },
  99. {'Container': 'jpeg', 'Type': 'Photo'}
  100. ],
  101. 'DirectPlayProfiles': [{'Type': 'Video'}, {'Type': 'Audio'}, {'Type': 'Photo'}],
  102. 'ResponseProfiles': [],
  103. 'ContainerProfiles': [],
  104. CodecProfiles,
  105. 'SubtitleProfiles': [
  106. {'Format': 'srt', 'Method': 'External'},
  107. {'Format': 'srt', 'Method': 'Embed'},
  108. {'Format': 'ass', 'Method': 'External'},
  109. {'Format': 'ass', 'Method': 'Embed'},
  110. {'Format': 'sub', 'Method': 'Embed'},
  111. {'Format': 'sub', 'Method': 'External'},
  112. {'Format': 'ssa', 'Method': 'Embed'},
  113. {'Format': 'ssa', 'Method': 'External'},
  114. {'Format': 'smi', 'Method': 'Embed'},
  115. {'Format': 'smi', 'Method': 'External'},
  116. {'Format': 'pgssub', 'Method': 'Embed'},
  117. {'Format': 'dvdsub', 'Method': 'Embed'},
  118. {'Format': 'dvbsub', 'Method': 'Embed'},
  119. {'Format': 'pgs', 'Method': 'Embed'}
  120. ]
  121. };
  122. }
  123. async function createApi() {
  124. try {
  125. await loadScript('qrc:///qtwebchannel/qwebchannel.js');
  126. } catch (e) {
  127. // try clearing out any cached CSPs
  128. let foundCache = false;
  129. for (const cache of await caches.keys()) {
  130. const dataDeleted = await caches.delete(cache);
  131. if (dataDeleted) {
  132. foundCache = true;
  133. }
  134. }
  135. if (foundCache) {
  136. window.location.reload();
  137. }
  138. throw e;
  139. }
  140. const channel = await new Promise((resolve) => {
  141. /*global QWebChannel */
  142. new QWebChannel(window.qt.webChannelTransport, resolve);
  143. });
  144. return channel.objects;
  145. }
  146. window.NativeShell.AppHost = {
  147. init() {
  148. window.apiPromise = createApi();
  149. (async () => {
  150. window.api = await window.apiPromise;
  151. })();
  152. },
  153. getDefaultLayout() {
  154. return viewdata.mode;
  155. },
  156. supports(command) {
  157. return features.includes(command.toLowerCase());
  158. },
  159. getDeviceProfile,
  160. getSyncProfile: getDeviceProfile,
  161. appName() {
  162. return "Jellyfin Media Player";
  163. },
  164. appVersion() {
  165. return navigator.userAgent.split(" ")[1];
  166. },
  167. deviceName() {
  168. return viewdata.deviceName;
  169. },
  170. exit() {
  171. window.api.system.exit();
  172. }
  173. };
  174. async function showSettingsModal() {
  175. let settings = await new Promise(resolve => {
  176. window.api.settings.settingDescriptions(resolve);
  177. });
  178. const modalContainer = document.createElement("div");
  179. modalContainer.className = "dialogContainer";
  180. modalContainer.style.backgroundColor = "rgba(0,0,0,0.5)";
  181. modalContainer.addEventListener("click", e => {
  182. if (e.target == modalContainer) {
  183. modalContainer.remove();
  184. }
  185. });
  186. document.body.appendChild(modalContainer);
  187. const modalContainer2 = document.createElement("div");
  188. modalContainer2.className = "focuscontainer dialog dialog-fixedSize dialog-small formDialog opened";
  189. modalContainer.appendChild(modalContainer2);
  190. const modalHeader = document.createElement("div");
  191. modalHeader.className = "formDialogHeader";
  192. modalContainer2.appendChild(modalHeader);
  193. const title = document.createElement("h3");
  194. title.className = "formDialogHeaderTitle";
  195. title.textContent = "Jellyfin Media Player Settings";
  196. modalHeader.appendChild(title);
  197. const modalContents = document.createElement("div");
  198. modalContents.className = "formDialogContent smoothScrollY";
  199. modalContents.style.paddingTop = "2em";
  200. modalContents.style.marginBottom = "6.2em";
  201. modalContainer2.appendChild(modalContents);
  202. for (let section of settings) {
  203. const group = document.createElement("fieldset");
  204. group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
  205. group.style.border = 0;
  206. group.style.outline = 0;
  207. modalContents.appendChild(group);
  208. const createSection = async (clear) => {
  209. if (clear) {
  210. group.innerHTML = "";
  211. }
  212. const values = await new Promise(resolve => {
  213. window.api.settings.allValues(section.key, resolve);
  214. });
  215. const legend = document.createElement("legend");
  216. const legendHeader = document.createElement("h2");
  217. legendHeader.textContent = section.key;
  218. legendHeader.style.textTransform = "capitalize";
  219. legend.appendChild(legendHeader);
  220. if (section.key == "plugins") {
  221. const legendSubHeader = document.createElement("h4");
  222. legendSubHeader.textContent = "Plugins are UNOFFICIAL and require a restart to take effect.";
  223. legend.appendChild(legendSubHeader);
  224. }
  225. group.appendChild(legend);
  226. for (const setting of section.settings) {
  227. const label = document.createElement("label");
  228. label.className = "inputContainer";
  229. label.style.marginBottom = "1.8em";
  230. label.style.display = "block";
  231. label.style.textTransform = "capitalize";
  232. if (setting.options) {
  233. const safeValues = {};
  234. const control = document.createElement("select");
  235. control.className = "emby-select-withcolor emby-select";
  236. for (const option of setting.options) {
  237. safeValues[String(option.value)] = option.value;
  238. const opt = document.createElement("option");
  239. opt.value = option.value;
  240. opt.selected = option.value == values[setting.key];
  241. let optionName = option.title;
  242. const swTest = `${section.key}.${setting.key}.`;
  243. const swTest2 = `${section.key}.`;
  244. if (optionName.startsWith(swTest)) {
  245. optionName = optionName.substring(swTest.length);
  246. } else if (optionName.startsWith(swTest2)) {
  247. optionName = optionName.substring(swTest2.length);
  248. }
  249. opt.appendChild(document.createTextNode(optionName));
  250. control.appendChild(opt);
  251. }
  252. control.addEventListener("change", async (e) => {
  253. await new Promise(resolve => {
  254. window.api.settings.setValue(section.key, setting.key, safeValues[e.target.value], resolve);
  255. });
  256. if (setting.key == "devicetype") {
  257. section = (await new Promise(resolve => {
  258. window.api.settings.settingDescriptions(resolve);
  259. })).filter(x => x.key == section.key)[0];
  260. createSection(true);
  261. }
  262. });
  263. const labelText = document.createElement('label');
  264. labelText.className = "inputLabel";
  265. labelText.textContent = setting.key + ": ";
  266. label.appendChild(labelText);
  267. label.appendChild(control);
  268. } else {
  269. const control = document.createElement("input");
  270. control.type = "checkbox";
  271. control.checked = values[setting.key];
  272. control.addEventListener("change", e => {
  273. window.api.settings.setValue(section.key, setting.key, e.target.checked);
  274. });
  275. label.appendChild(control);
  276. label.appendChild(document.createTextNode(" " + setting.key));
  277. }
  278. group.appendChild(label);
  279. }
  280. };
  281. createSection();
  282. }
  283. const savedServer = await new Promise(resolve => {
  284. window.api.settings.value('main', 'userWebClient', resolve);
  285. });
  286. if (savedServer) {
  287. const group = document.createElement("fieldset");
  288. group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
  289. group.style.border = 0;
  290. group.style.outline = 0;
  291. modalContents.appendChild(group);
  292. const legend = document.createElement("legend");
  293. const legendHeader = document.createElement("h2");
  294. legendHeader.textContent = "Saved Server";
  295. legend.appendChild(legendHeader);
  296. const legendSubHeader = document.createElement("h4");
  297. legendSubHeader.textContent = (
  298. "The server you first connected to is your saved server. " +
  299. "It provides the web client for Jellyfin Media Player in the absence of a bundled one. " +
  300. "You can use this option to change it to another one. This does NOT log you off."
  301. );
  302. legend.appendChild(legendSubHeader);
  303. group.appendChild(legend);
  304. const resetSavedServer = document.createElement("button");
  305. resetSavedServer.className = "raised button-cancel block btnCancel emby-button";
  306. resetSavedServer.textContent = "Reset Saved Server"
  307. resetSavedServer.style.marginLeft = "auto";
  308. resetSavedServer.style.marginRight = "auto";
  309. resetSavedServer.style.maxWidth = "50%";
  310. resetSavedServer.addEventListener("click", async () => {
  311. await new Promise(resolve => {
  312. window.api.settings.setValue('main', 'userWebClient', '', resolve);
  313. });
  314. window.location.href = viewdata.scriptPath + "/find-webclient.html";
  315. });
  316. group.appendChild(resetSavedServer);
  317. }
  318. const closeContainer = document.createElement("div");
  319. closeContainer.className = "formDialogFooter";
  320. modalContents.appendChild(closeContainer);
  321. const close = document.createElement("button");
  322. close.className = "raised button-cancel block btnCancel formDialogFooterItem emby-button";
  323. close.textContent = "Close"
  324. close.addEventListener("click", () => {
  325. modalContainer.remove();
  326. });
  327. closeContainer.appendChild(close);
  328. }