Rain Classroom Helper

优化雨课堂使用体验

安装此脚本?
作者推荐脚本

您可能也喜欢XuetangX Keyboard Shortcut

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