Rain Classroom Helper

优化雨课堂使用体验

目前为 2020-02-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Rain Classroom Helper
  3. // @namespace https://raineggplant.com/
  4. // @version 0.2.3
  5. // @description 优化雨课堂使用体验
  6. // @author RainEggplant
  7. // @match *://www.yuketang.cn/web*
  8. // @match *://pro.yuketang.cn/web*
  9. // @grant GM_addStyle
  10. // @require https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js
  11. // @homepageURL https://github.com/RainEggplant/rain-classroom-helper
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. const DEBUG = false;
  17.  
  18. // 调整左边栏样式
  19. GM_addStyle(`
  20. .panel {
  21. padding-top: 34px !important;
  22. }
  23. .nav-list {
  24. font-size: 18px !important;
  25. }
  26. .nav-item {
  27. height: 50px !important;
  28. line-height: 32px !important;
  29. }
  30. .kecheng, .kejian, .shiti, .geren, .addlink {
  31. width: 32px !important;
  32. }
  33. .left .contact-us {
  34. bottom: 10px !important;
  35. display: block !important;
  36. }
  37. `);
  38.  
  39. // 调整右边栏样式
  40. GM_addStyle(`
  41. .right {
  42. width: 320px !important;
  43. }
  44. .control-panel {
  45. padding-top: 32px !important;
  46. }
  47. .title {
  48. font-size: 22px !important;
  49. }
  50. .page-nav-control {
  51. width: 320px !important;
  52. }
  53. .page-control {
  54. padding-top: 15px !important;
  55. }
  56. .control-desc {
  57. display: none !important;
  58. }
  59. .page-nav {
  60. padding: unset !important;
  61. font-size: 18px !important;
  62. }
  63. .kecheng, .kejian, .shiti, .geren, .addlink {
  64. width: 32px !important;
  65. }
  66. .print-preview-box {
  67. margin: 0px 0 0 !important;
  68. }
  69. .contact-us {
  70. display: none;
  71. }
  72. .right .control-panel .page-control .paging-number-list .page-no:nth-child(5n) {
  73. margin: 0 14px 13px 0 !important;
  74. }
  75. `);
  76.  
  77. // 调整中间 iframe 为自适应宽度
  78. GM_addStyle(`
  79. .wrapper-inner {
  80. width: 92% !important;
  81. }
  82. .center {
  83. width: auto !important;
  84. margin-left: 180px !important;
  85. margin-right: 320px !important;
  86. float: none !important;
  87. }
  88. .rain-iframe {
  89. width: calc(95% - 40px) !important;
  90. }
  91. .student__timeline-wrapper {
  92. top: 2.33rem !important;
  93. }
  94. `);
  95.  
  96. // 调整布局
  97. waitForKeyElements('div.index-view.none.J_index', function () {
  98. // note: you must move div.right instead of div.center, or the sidebar
  99. // will lost its funtion
  100. $('div.right.fr').insertBefore($('div.center.fl'));
  101. });
  102.  
  103. // 缩小 “体验新版” 尺寸
  104. waitForKeyElements('a.newWebEntry', function () {
  105. $('a.newWebEntry')
  106. .find('img')
  107. .attr('style', 'width: 150px; margin-top: 20px;');
  108. });
  109.  
  110. // 添加右边栏视频框
  111. GM_addStyle(`
  112. #video-iframe {
  113. width: 320px;
  114. height: 304px;
  115. }
  116. `);
  117.  
  118. waitForKeyElements('div.control-panel.Absolute-Center', function () {
  119. const videoIFrame = '<iframe id="video-iframe" src="about:blank" style="display: none;"/>';
  120. $('div.page-control.J_pageNo').after(videoIFrame);
  121. });
  122.  
  123. // 添加 GitHub 项目图标
  124. waitForKeyElements('ul.nav-list', function () {
  125. const liAbout = `
  126. <li class="nav-item clearfix J_nav">
  127. <a href="https://github.com/RainEggplant/rain-classroom-helper" target="_blank">
  128. <img alt="GitHub stars" style="width:auto;" src="https://img.shields.io/github/stars/RainEggplant/rain-classroom-helper?style=social">
  129. </a>
  130. </li>
  131. `;
  132. $('ul.nav-list').append(liAbout);
  133. });
  134.  
  135. waitForKeyElements('div.left.fl', function () {
  136. const divLeftHidden = `
  137. <div id="left-hidden" class="fl" style="display: none;">
  138. <p id="show-left" style="top: 50%; position: absolute; color: #fff; z-index: 1;">▶</p>
  139. </div>
  140. `;
  141. $('div.left.fl').before(divLeftHidden);
  142. $('#show-left').on('click', showLeftPanel);
  143. });
  144.  
  145. // 添加控制样式的滑块
  146. waitForKeyElements('div.panel.Absolute-Center', function () {
  147. const panelWidthControls = `
  148. <div style="margin-top: 20px; color: #fff; font-size: 14px;">
  149. <p>
  150. <label for="panel-width">右边栏宽度</label>
  151. <input type="range" id="right-panel-width" value="320" min="200" max="576" style="width: 160px;">
  152. </p>
  153. <p>
  154. <label for="panel-width">全屏占比</label>
  155. <input type="range" id="content-width" value="92" min="50" max="98" style="width: 160px;">
  156. </p>
  157. <p>
  158. <button id="hide-left" type="button" style="color: #000; font-size: 12px;">◀ 隐藏左边栏</button>
  159. </p>
  160. </div>
  161. `;
  162. $('div.panel.Absolute-Center').append(panelWidthControls);
  163. $('#right-panel-width').on('change', setRightPanelWidth);
  164. $('#content-width').on('change', setContentWidth);
  165. $('#hide-left').on('click', hideLeftPanel);
  166. });
  167.  
  168. function setRightPanelWidth() {
  169. const width = 'width: ' + $(this).val() + 'px !important;';
  170. const height = 'height: ' + (0.75 * $(this).val() + 60) + 'px !important;';
  171. const marginLeft = 'margin-left: ' + $('div.center.fl').css('margin-left') + ';';
  172. const marginRight = 'margin-right: ' + $(this).val() + 'px !important;';
  173. const iframeDisplay = 'display: ' + $('#video-iframe').css('display') + ';';
  174. $('div.right.fr').attr('style', width);
  175. $('div.page-nav-control').attr('style', width);
  176. $('div.center.fl').attr('style', marginLeft + ' ' + marginRight);
  177. $('#video-iframe').attr('style', width + ' ' + height + ' ' + iframeDisplay);
  178. }
  179.  
  180. function setContentWidth() {
  181. const width = 'width: ' + $(this).val() + '% !important;';
  182. $('div.wrapper-inner.clearfix.J_inner').attr('style', width);
  183. }
  184.  
  185. function hideLeftPanel() {
  186. const marginLeft = 'margin-left: 0px !important;';
  187. const marginRight = 'margin-right: '
  188. + $('div.center.fl').css('margin-right') + ' !important;';
  189. $('div.left.fl').css('display', 'none');
  190. $('div.center.fl').attr('style', marginLeft + ' ' + marginRight);
  191. $('#left-hidden').css('display', 'block');
  192. }
  193.  
  194. function showLeftPanel() {
  195. const marginLeft = 'margin-left: 180px !important;';
  196. const marginRight = 'margin-right: '
  197. + $('div.center.fl').css('margin-right') + ' !important;';
  198. $('div.left.fl').css('display', 'block');
  199. $('div.center.fl').attr('style', marginLeft + ' ' + marginRight);
  200. $('#left-hidden').css('display', 'none');
  201. }
  202.  
  203. waitForKeyElements('body', function () {
  204. // 中间 iframe 加载出内容后绑定处理视频的函数
  205. addVideoHandler();
  206. }, true, '#rainiframe');
  207.  
  208. function addVideoHandler() {
  209. // 首次进入或通过左边栏改变页面时
  210. let iframeUrl = '';
  211. let isVideoLoaded = false;
  212. let iframeBody = $('#rainiframe').contents().find('body')[0];
  213. const iframeObserver = new MutationObserver(function () {
  214. DEBUG && console.log('iframe mutated');
  215. const newIFrameUrl = $('#rainiframe').contents()[0].location.href;
  216. DEBUG && console.log(newIFrameUrl);
  217.  
  218. const videoSection = iframeBody.querySelector('section.live__wrap');
  219. if (videoSection) {
  220. // 存在视频
  221. // 去除中央 rainIFrame 中的视频
  222. $(videoSection).contents().find('video').removeAttr('src');
  223. $(videoSection).empty();
  224.  
  225. if (iframeUrl && newIFrameUrl.includes(iframeUrl)) {
  226. DEBUG && console.log('entering a sub page');
  227. return;
  228. }
  229.  
  230. // 在右边栏显示视频
  231. // note: 不要使用 $("#video-iframe").attr("src", iframeUrl);
  232. // 因为这样会留下访问记录,从而使后退、前进功能异常
  233. iframeUrl = newIFrameUrl;
  234. const videoIFrame = $('#video-iframe')[0];
  235. videoIFrame.contentWindow.location.replace(iframeUrl);
  236. $('#video-iframe').css({ display: 'block' });
  237.  
  238. // 去除视频框中无关元素
  239. waitForKeyElements('.live__view', function () {
  240. const liveView = $('#video-iframe').contents().find('.live__view');
  241. // liveView.children('section.live__wrap').css('padding-top', '0px');
  242. liveView.children(':not(.live__wrap)').remove();
  243. const observer = new MutationObserver(function (mutations) {
  244. mutations.forEach(function (mutation) {
  245. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  246. liveView.children(':not(.live__wrap)').remove();
  247. }
  248. });
  249. });
  250. observer.observe(liveView[0], { childList: true });
  251. }, true, '#video-iframe');
  252.  
  253. isVideoLoaded = true;
  254. } else {
  255. if (isVideoLoaded && !(iframeUrl && newIFrameUrl.includes(iframeUrl))) {
  256. // 退出视频课程时停止播放并隐藏右边栏视频
  257. $('#video-iframe').css({ display: 'none' });
  258. iframeUrl = newIFrameUrl;
  259. // DO NOT USE: $("#video-iframe").attr("src", "/v/index");
  260. const videoIFrame = $('#video-iframe')[0];
  261. videoIFrame.contentWindow.location.replace('about:blank');
  262.  
  263. isVideoLoaded = false;
  264. }
  265. }
  266. });
  267.  
  268. const config = { childList: true, subtree: true };
  269. iframeObserver.observe(iframeBody, config);
  270. DEBUG && console.log('observation started');
  271.  
  272. // 在 iframe 被重新加载(点击左侧导航栏、进入直播)时,重新添加 handler
  273. $('#rainiframe')[0].contentWindow.addEventListener('unload', () => {
  274. DEBUG && console.log('#rainiframe has (re)loaded');
  275. // 右边栏视频停止播放并隐藏
  276. $('#video-iframe').css({ display: 'none' });
  277. const videoIFrame = $('#video-iframe')[0];
  278. videoIFrame.contentWindow.location.replace('about:blank');
  279. $('#rainiframe').contents().empty();
  280. waitForKeyElements('body', function () {
  281. addVideoHandler();
  282. }, true, '#rainiframe');
  283. });
  284. }
  285.  
  286.  
  287. // ==== DO NOT MODIFY
  288. // ==== third-party utility functions:
  289. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  290. that detects and handles AJAXed content.
  291. IMPORTANT: This function requires your script to have loaded jQuery.
  292. */
  293. function waitForKeyElements(
  294. selectorTxt, /* Required: The jQuery selector string that
  295. specifies the desired element(s).
  296. */
  297. actionFunction, /* Required: The code to run when elements are
  298. found. It is passed a jNode to the matched
  299. element.
  300. */
  301. bWaitOnce, /* Optional: If false, will continue to scan for
  302. new elements even after the first match is
  303. found.
  304. */
  305. iframeSelector /* Optional: If set, identifies the iframe to
  306. search.
  307. */
  308. ) {
  309. var targetNodes, btargetsFound;
  310.  
  311. if (typeof iframeSelector == 'undefined')
  312. targetNodes = $(selectorTxt);
  313. else
  314. targetNodes = $(iframeSelector).contents()
  315. .find(selectorTxt);
  316.  
  317. if (targetNodes && targetNodes.length > 0) {
  318. btargetsFound = true;
  319. /*--- Found target node(s). Go through each and act if they
  320. are new.
  321. */
  322. targetNodes.each(function () {
  323. var jThis = $(this);
  324. var alreadyFound = jThis.data('alreadyFound') || false;
  325.  
  326. if (!alreadyFound) {
  327. //--- Call the payload function.
  328. var cancelFound = actionFunction(jThis);
  329. if (cancelFound)
  330. btargetsFound = false;
  331. else
  332. jThis.data('alreadyFound', true);
  333. }
  334. });
  335. }
  336. else {
  337. btargetsFound = false;
  338. }
  339.  
  340. //--- Get the timer-control variable for this selector.
  341. var controlObj = waitForKeyElements.controlObj || {};
  342. var controlKey = selectorTxt.replace(/[^\w]/g, '_');
  343. var timeControl = controlObj[controlKey];
  344.  
  345. //--- Now set or clear the timer as appropriate.
  346. if (btargetsFound && bWaitOnce && timeControl) {
  347. //--- The only condition where we need to clear the timer.
  348. clearInterval(timeControl);
  349. delete controlObj[controlKey];
  350. }
  351. else {
  352. //--- Set a timer, if needed.
  353. if (!timeControl) {
  354. timeControl = setInterval(function () {
  355. waitForKeyElements(
  356. selectorTxt, actionFunction, bWaitOnce, iframeSelector
  357. );
  358. }, 300);
  359. controlObj[controlKey] = timeControl;
  360. }
  361. }
  362. waitForKeyElements.controlObj = controlObj;
  363. }
  364.  
  365. })();