main.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. class Home {
  2. static start() {
  3. this.cache = {
  4. items: undefined,
  5. item: new Map(),
  6. };
  7. this.itemQuery = { ImageTypes: "Backdrop", EnableImageTypes: "Logo,Backdrop", IncludeItemTypes: "Movie,Series", SortBy: "ProductionYear, PremiereDate, SortName", Recursive: true, ImageTypeLimit: 1, Limit: 10, Fields: "ProductionYear", SortOrder: "Descending", EnableUserData: false, EnableTotalRecordCount: false };
  8. this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
  9. this.logoOptions = { type: "Logo", maxWidth: 3000 };
  10. this.initStart = false;
  11. setInterval(() => {
  12. if (window.location.href.indexOf("!/home") != -1) {
  13. if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
  14. this.initStart = false;
  15. this.initLoading();
  16. }
  17. if ($(".hide .misty-banner").length != 0) {
  18. $(".hide .misty-banner").remove();
  19. }
  20. if (!this.initStart && $(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
  21. this.initStart = true;
  22. this.init();
  23. }
  24. }
  25. }, 233);
  26. }
  27. static async init() {
  28. // Beta
  29. $(".view:not(.hide)").attr("data-type", "home");
  30. // Loading
  31. const serverName = await this.injectCall("serverName", "");
  32. $(".misty-loading h1").text(serverName).addClass("active");
  33. // Banner
  34. await this.initBanner();
  35. this.initEvent();
  36. }
  37. /* 插入Loading */
  38. static initLoading() {
  39. const load = `
  40. <div class="misty-loading">
  41. <h1></h1>
  42. <div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
  43. </div>
  44. `;
  45. $("body").append(load);
  46. }
  47. static injectCode(code) {
  48. let hash = md5(code + Math.random().toString());
  49. return new Promise((resolve, reject) => {
  50. if ("BroadcastChannel" in window) {
  51. const channel = new BroadcastChannel(hash);
  52. channel.addEventListener("message", (event) => resolve(event.data));
  53. } else if ("postMessage" in window) {
  54. window.addEventListener("message", (event) => {
  55. if (event.data.channel === hash) {
  56. resolve(event.data.message);
  57. }
  58. });
  59. }
  60. const script = `
  61. <script class="I${hash}">
  62. setTimeout(async ()=> {
  63. async function R${hash}(){${code}};
  64. if ("BroadcastChannel" in window) {
  65. const channel = new BroadcastChannel("${hash}");
  66. channel.postMessage(await R${hash}());
  67. } else if ('postMessage' in window) {
  68. window.parent.postMessage({channel:"${hash}",message:await R${hash}()}, "*");
  69. }
  70. document.querySelector("script.I${hash}").remove()
  71. }, 16)
  72. </script>
  73. `;
  74. $(document.head || document.documentElement).append(script);
  75. });
  76. }
  77. static injectCall(func, arg) {
  78. const script = `
  79. // const client = (await window.require(["ApiClient"]))[0];
  80. const client = await new Promise((resolve, reject) => {
  81. setInterval(() => {
  82. if (window.ApiClient != undefined) resolve(window.ApiClient);
  83. }, 16);
  84. });
  85. return await client.${func}(${arg})
  86. `;
  87. return this.injectCode(script);
  88. }
  89. static getItems(query) {
  90. if (this.cache.items == undefined) {
  91. this.cache.items = this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
  92. }
  93. return this.cache.items;
  94. }
  95. static async getItem(itemId) {
  96. // 双缓存 优先使用 WebStorage
  97. if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
  98. const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
  99. if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
  100. else this.cache.item.set(itemId, data);
  101. }
  102. return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
  103. }
  104. static getImageUrl(itemId, options) {
  105. return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
  106. }
  107. /* 插入Banner */
  108. static async initBanner() {
  109. const banner = `
  110. <div class="misty-banner">
  111. <div class="misty-banner-body">
  112. </div>
  113. <div class="misty-banner-library">
  114. <div class="misty-banner-logos"></div>
  115. </div>
  116. </div>
  117. `;
  118. $(".view:not(.hide) .homeSectionsContainer").prepend(banner);
  119. // $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");
  120. // 插入数据
  121. const data = await this.getItems(this.itemQuery);
  122. console.log(data);
  123. data.Items.forEach(async (item) => {
  124. const detail = await this.getItem(item.Id),
  125. itemHtml = `
  126. <div class="misty-banner-item" id="${detail.Id}">
  127. <img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
  128. <div class="misty-banner-info padded-left padded-right">
  129. <h1>${detail.Name}</h1>
  130. <div><p>${detail.Overview}</p></div>
  131. <div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
  132. </div>
  133. </div>
  134. `,
  135. logoHtml = `
  136. <img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
  137. `;
  138. if (detail.ImageTags && detail.ImageTags.Logo) {
  139. $(".misty-banner-logos").append(logoHtml);
  140. }
  141. $(".misty-banner-body").append(itemHtml);
  142. console.log(item.Id, detail);
  143. });
  144. // 只判断第一张海报加载完毕, 优化加载速度
  145. await new Promise((resolve, reject) => {
  146. let waitLoading = setInterval(() => {
  147. if (document.querySelector(".misty-banner-cover")?.complete) {
  148. clearInterval(waitLoading);
  149. resolve();
  150. }
  151. }, 16);
  152. });
  153. // 判断section0加载完毕
  154. await new Promise((resolve, reject) => {
  155. let waitsection0 = setInterval(() => {
  156. if ($(".view:not(.hide) .section0 .emby-scrollbuttons").length > 0 && $(".view:not(.hide) .section0.hide").length == 0) {
  157. clearInterval(waitsection0);
  158. resolve();
  159. }
  160. }, 16);
  161. });
  162. $(".view:not(.hide) .section0 .emby-scrollbuttons").remove();
  163. const items = $(".view:not(.hide) .section0 .emby-scroller .itemsContainer")[0].items;
  164. if (CommonUtils.checkType() === 'pc') {
  165. $(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");
  166. }
  167. $(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
  168. await CommonUtils.sleep(150);
  169. $(".view:not(.hide) .section0 .emby-scroller .itemsContainer")[0].items = items;
  170. // 置入场动画
  171. let delay = 80; // 动媒体库画间隔
  172. let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
  173. $(`.misty-banner-logo[id=${id}]`).addClass("active");
  174. await CommonUtils.sleep(200); // 间隔动画
  175. $(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
  176. $(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
  177. await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
  178. $(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动
  179. // 滚屏逻辑
  180. var index = 0;
  181. clearInterval(this.bannerInterval);
  182. this.bannerInterval = setInterval(() => {
  183. // 背景切换
  184. if (window.location.href.endsWith("home") && !document.hidden) {
  185. index += index + 1 == $(".misty-banner-item").length ? -index : 1;
  186. $(".misty-banner-body").css("left", -(index * 100).toString() + "%");
  187. // 信息切换
  188. $(".misty-banner-item.active").removeClass("active");
  189. let id = $(".misty-banner-item").eq(index).addClass("active").attr("id");
  190. // LOGO切换
  191. $(".misty-banner-logo.active").removeClass("active");
  192. $(`.misty-banner-logo[id=${id}]`).addClass("active");
  193. }
  194. }, 8000);
  195. }
  196. /* 初始事件 */
  197. static initEvent() {
  198. // 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
  199. const script = `
  200. // 挂载appRouter
  201. if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
  202. /* // 修复library事件参数
  203. const serverId = ApiClient._serverInfo.Id,
  204. librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
  205. librarys.forEach(library => {
  206. library.setAttribute("data-serverid", serverId);
  207. library.setAttribute("data-type", "CollectionFolder");
  208. }); */
  209. `;
  210. this.injectCode(script);
  211. }
  212. }
  213. // 运行
  214. if ("BroadcastChannel" in window || "postMessage" in window) {
  215. if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
  216. Home.start();
  217. }
  218. }