main.js 7.9 KB

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