Rain Classroom Helper

优化雨课堂使用体验

目前為 2020-02-19 提交的版本,檢視 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址