nativeshell.js 8.6 KB

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