Steamcn 7L Optimisation

try to take over the world!

  1. function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
  2.  
  3. // ==UserScript==
  4. // @name Steamcn 7L Optimisation
  5. // @namespace http://tampermonkey.net/
  6. // @version 2.1.0
  7. // @description try to take over the world!
  8. // @icon https://steamcn.com/favicon-hq.ico
  9. // @author Bisumaruko
  10. // @include https://steamcn.com/*
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @run-at document_start
  14. // ==/UserScript==
  15.  
  16. /* global swal, Fuse, validate_7l */
  17.  
  18. const URL7LTop = 'https://steamcn.com/plugin.php?id=steamcn_gift:7l';
  19. const URL7LCreate = 'https://steamcn.com/forum.php?mod=post&action=newthread&fid=298&specialextra=steamcn_gift';
  20. const URL7LList = 'https://steamcn.com/plugin.php?id=steamcn_gift:chosen&type=';
  21. const fetchTimer = 3 * 24 * 60 * 60 * 1000; // fetch game list every 3 days
  22.  
  23. const has = Object.prototype.hasOwnProperty;
  24. const inject = (arg, onload = null) => {
  25. if (Array.isArray(arg)) arg.forEach(inject);else if (typeof arg === 'string') inject({ url: arg, onload });else if (arg instanceof Object) {
  26. let tag = null;
  27.  
  28. if (has.call(arg, 'url')) {
  29. if (arg.url.endsWith('.js')) {
  30. tag = document.createElement('script');
  31. tag.src = arg.url;
  32. if (typeof arg.onload === 'function') tag.onload = arg.onload;
  33. } else if (arg.url.endsWith('.css')) {
  34. tag = document.createElement('link');
  35. tag.href = arg.url;
  36. tag.rel = 'stylesheet';
  37. tag.type = 'text/css';
  38. }
  39. } else if (has.call(arg, 'css')) {
  40. tag = document.createElement('style');
  41. tag.type = 'text/css';
  42. tag.appendChild(document.createTextNode(arg.css));
  43. }
  44.  
  45. if (tag !== null) document.head.appendChild(tag);
  46. }
  47. };
  48.  
  49. const timezones = {
  50. 'GMT-12': '',
  51. 'GMT-11': 'Pacific/Midway',
  52. 'GMT-10': 'Pacific/Honolulu',
  53. 'GMT-9': 'America/Anchorage',
  54. 'GMT-8': 'America/Los_Angeles',
  55. 'GMT-7': 'America/Phoenix',
  56. 'GMT-6': 'America/Chicago',
  57. 'GMT-5': 'America/New_York',
  58. 'GMT-4': 'America/Halifax',
  59. 'GMT-3.5': 'America/St_Johns',
  60. 'GMT-3': 'America/Sao_Paulo',
  61. 'GMT-2': 'Atlantic/South_Georgia',
  62. 'GMT-1': 'Atlantic/Cape_Verde',
  63. GMT: 'Europe/London',
  64. 'GMT+1': 'Europe/Berlin',
  65. 'GMT+2': 'Europe/Kiev',
  66. 'GMT+3': 'Europe/Moscow',
  67. 'GMT+3.5': 'Asia/Tehran',
  68. 'GMT+4': 'Asia/Dubai',
  69. 'GMT+4.5': 'Asia/Kabul',
  70. 'GMT+5': 'Asia/Karachi',
  71. 'GMT+5.5': 'Asia/Colombo',
  72. 'GMT+5.75': 'Asia/Kathmandu',
  73. 'GMT+6': 'Asia/Urumqi',
  74. 'GMT+6.5': 'Asia/Yangon',
  75. 'GMT+7': 'Asia/Bangkok',
  76. 'GMT+8': 'Asia/Taipei',
  77. 'GMT+9': 'Asia/Tokyo',
  78. 'GMT+9.5': 'Australia/Adelaide',
  79. 'GMT+10': 'Australia/Sydney',
  80. 'GMT+11': 'Asia/Magadan',
  81. 'GMT+12': 'Pacific/Auckland'
  82. };
  83. const games = {
  84. data: JSON.parse(GM_getValue('steamCN7L_games') || '{}'),
  85. set(value) {
  86. if (value instanceof Object) this.data = value;
  87.  
  88. GM_setValue('steamCN7L_games', JSON.stringify(this.data));
  89. },
  90. get(key) {
  91. return has.call(this.data, key) ? this.data[key] : null;
  92. },
  93. fetch() {
  94. var _this = this;
  95.  
  96. return _asyncToGenerator(function* () {
  97. const res = yield fetch(`${URL7LList}steam`, {
  98. method: 'GET',
  99. headers: {
  100. accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
  101. 'upgrade-insecure-requests': 1
  102. },
  103. credentials: 'same-origin'
  104. });
  105.  
  106. if (res.ok) {
  107. const list = [];
  108. const options = yield res.text();
  109. const regExpGame = /value=.+?(\d+)+.+?>(.+?)(?=<\/option>)/g;
  110. let match;
  111.  
  112. // parse game list
  113. while (match = regExpGame.exec(options)) {
  114. if (match.length === 3) {
  115. list.push({
  116. game: match[2].replace(/^【.+?】/, '').replace(/\(([^)]*)\)[^(]*$/, '').trim(),
  117. text: match[2].trim(),
  118. value: match[1]
  119. });
  120. }
  121. }
  122.  
  123. _this.set({
  124. list,
  125. lastFetched: Date.now()
  126. });
  127.  
  128. if (swal.isVisible()) {
  129. swal({
  130. text: 'Finished loading',
  131. type: 'success'
  132. });
  133. }
  134. }
  135. })();
  136. }
  137. };
  138. const handler = () => {
  139. const $ = jQuery;
  140. let fuse = {};
  141. let searchTimer = null;
  142. // inject css
  143. inject({
  144. css: `
  145. [class*="7LOptimise"] #ga_id { display: none !important; }
  146. [class*="7LOptimise"] [class="7LSearchBox"] { display: block !important; }
  147. [class="7LSearchBox"] {
  148. width: 500px;
  149. height: 27px;
  150. display: none;
  151. padding-left: 5px;
  152. border: 1px solid;
  153. border-color: #E4E4E4 #E0E0E0 #E0E0E0 #E4E4E4;
  154. box-sizing: border-box;
  155. }
  156. [class="7LSearchResult"] {
  157. width: 500px;
  158. height: 100px;
  159. display: none;
  160. position: absolute;
  161. padding: 5px;
  162. overflow: auto;
  163. overflow-x: hidden;
  164. background-color: white;
  165. box-sizing: border-box;
  166. }
  167. [class="7LSearchResult"] > span {
  168. width: 100%;
  169. display: inline-block;
  170. overflow: hidden;
  171. text-overflow: ellipsis;
  172. white-space: nowrap;
  173. border: 1px solid white;
  174. cursor: pointer;
  175. }
  176. [class="7LSearchResult"] > span:hover { border-color: #57bae8; }
  177. `
  178. });
  179.  
  180. // initialise
  181. const $body = $('body');
  182. const $gaID = $('#ga_id');
  183. const $gaType = $('#ga_type');
  184. const searchBox = $('<input/>', {
  185. type: 'text',
  186. class: '7LSearchBox',
  187. placeholder: '选择要赠出的礼物(点击输入名称过滤)'
  188. });
  189. const searchResult = $('<div/>', {
  190. class: '7LSearchResult',
  191. text: 'Searching...'
  192. });
  193. const timezone = $('.subforunm_foot_intro_right > div:last-child').text().split(',')[0].trim();
  194.  
  195. // override original change event on gaType
  196. $gaType.off('change').change(_asyncToGenerator(function* () {
  197. const type = $gaType.val();
  198.  
  199. $gaID.empty();
  200.  
  201. if (type === 'steam') $body.addClass('7LOptimise');else {
  202. const res = yield fetch(URL7LList + type);
  203.  
  204. if (res.ok) {
  205. const data = yield res.text();
  206.  
  207. $gaID.append(data);
  208. $body.removeClass('7LOptimise');
  209. } else swal('Oops', 'Loading failed', 'error');
  210. }
  211. }));
  212.  
  213. // insert search box replacement
  214. $gaID.after(searchBox, searchResult);
  215. $body.addClass('7LOptimise');
  216.  
  217. // initialise fuse
  218. fuse = new Fuse(games.get('list'), {
  219. shouldSort: true,
  220. includeScore: true,
  221. threshold: 0.1,
  222. location: 0,
  223. distance: 100,
  224. maxPatternLength: 32,
  225. keys: ['game']
  226. });
  227.  
  228. // bind event to search box
  229. searchBox.keyup(e => {
  230. const $ele = $(e.delegateTarget);
  231. const input = $ele.val().slice(0, 100);
  232.  
  233. if (input.length > 0) {
  234. searchResult.css('display', 'block');
  235. searchBox.focus(() => {
  236. searchResult.css('display', 'block');
  237. });
  238.  
  239. // only perform search if 0.3 second has passed
  240. clearTimeout(searchTimer);
  241. searchTimer = setTimeout(() => {
  242. const results = fuse.search(input);
  243. let i = 0;
  244.  
  245. searchResult.empty();
  246.  
  247. if (results.length > 0) {
  248. while (i < 30) {
  249. const result = results[i];
  250.  
  251. if (!result) break;
  252.  
  253. searchResult.append($(`<span>${result.item.text}</span>`).click(() => {
  254. searchBox.val(result.item.text);
  255. $gaID.empty().append(`<option selected value=${result.item.value}></option>`);
  256. $('#subject').val(`[${$('#ga_type > option:selected').text()}] ${result.item.game}`);
  257. }));
  258.  
  259. // if (result.score === 0) break; // perfect match
  260. i += 1;
  261. }
  262. }
  263. }, 300);
  264. }
  265. });
  266.  
  267. // hide searchResult when click
  268. $body.click(e => {
  269. if (!$(e.target).hasClass('7LSearchBox')) searchResult.css('display', 'none');
  270. });
  271.  
  272. // change copy & giveRate field type
  273. $('#ga_copy, #ga_ratio').attr({
  274. type: 'number',
  275. min: 1
  276. }).css({
  277. width: '54px',
  278. 'background-color': 'white'
  279. });
  280.  
  281. // override default date time picker
  282. $('#ga_start, #ga_end').removeAttr('onclick').datetimepicker({
  283. dateFormat: 'yy-mm-dd'
  284. });
  285.  
  286. // override quick start & end event
  287. $('.quicksetstart, .quicksetend').off().click(e => {
  288. const $ele = $(e.delegateTarget);
  289. const type = $ele.attr('class').split('set').pop();
  290. const hour = $ele.attr('data-hour') || 0;
  291. let base = Date.now();
  292.  
  293. if ($ele.hasClass('quicksetend')) {
  294. const startTime = new Date($('#ga_start').val()).getTime();
  295.  
  296. if (startTime) base = startTime; // change base to start time if quicksetend
  297. }
  298.  
  299. $(`#ga_${type}`).val(new Date(base + hour * 60 * 60 * 1000).toLocaleDateString('zh', {
  300. hour12: false,
  301. year: 'numeric',
  302. month: '2-digit',
  303. day: '2-digit',
  304. hour: '2-digit',
  305. minute: '2-digit'
  306. }).replace(/\//g, '-'));
  307. });
  308.  
  309. // offset timezone before submit
  310. const form = $('#postform');
  311.  
  312. form.removeAttr('onsubmit').submit(e => {
  313. e.preventDefault();
  314.  
  315. $('#ga_start, #ga_end').each((index, element) => {
  316. const $ele = $(element);
  317.  
  318. $ele.val(new Date($ele.val()).toLocaleDateString('zh', {
  319. timeZone: timezones[timezone] || 'Asia/Taipei',
  320. hour12: false,
  321. year: 'numeric',
  322. month: '2-digit',
  323. day: '2-digit',
  324. hour: '2-digit',
  325. minute: '2-digit'
  326. }).replace(/\//g, '-'));
  327. });
  328.  
  329. validate_7l(form[0]);
  330. });
  331. };
  332.  
  333. // update games list
  334. if (!games.get('lastFetched') || games.get('lastFetched') < Date.now() - fetchTimer) games.fetch();
  335.  
  336. // inject swal
  337. inject(['https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.26.11/sweetalert2.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.26.11/sweetalert2.min.css']);
  338.  
  339. document.addEventListener('click', (() => {
  340. var _ref2 = _asyncToGenerator(function* (e) {
  341. if (e.target.id === 'd_gw_nav_newgw') {
  342. swal('Loading');
  343. swal.showLoading();
  344.  
  345. const res = yield fetch(URL7LCreate, {
  346. method: 'GET',
  347. headers: {
  348. accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
  349. 'upgrade-insecure-requests': 1
  350. },
  351. credentials: 'same-origin'
  352. });
  353.  
  354. if (res.ok) {
  355. let html = yield res.text();
  356.  
  357. // remove chosen.js & chosen.css
  358. html = html.replace(/<script.+?chosen.+?\.js.+?<\/script>/, '');
  359. html = html.replace(/<link.+?chosen\.min\.css.+?>/, '');
  360.  
  361. // trim the inline script initialising chosen
  362. html = html.split('jQuery(\'#postform\').attr');
  363. html[1] = html[1].slice(html[1].indexOf('var serverTime'), html[1].indexOf('function formatedTimeAfter')) + html[1].slice(html[1].indexOf('</script>'));
  364. html = html.join('');
  365.  
  366. // inject script & css
  367. html = html.split('</head>');
  368. html[0] += `
  369. <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
  370. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.js"></script>
  371. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-sliderAccess.js"></script>
  372. <script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.0.5/fuse.min.js"></script>
  373. <link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet" type="text/css">
  374. <link href="https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.css" rel="stylesheet" type="text/css">
  375. `;
  376. html = html.join('</head>');
  377.  
  378. document.open('text/html');
  379. document.write(html);
  380. document.close();
  381.  
  382. window.history.pushState({}, '', URL7LCreate);
  383. window.addEventListener('popstate', function (f) {
  384. if (f.state === null) location.href = URL7LTop;
  385. });
  386.  
  387. (function loaded() {
  388. if (document.querySelector('#ga_id')) handler();else setTimeout(loaded, 10);
  389. })();
  390. } else swal('Oops', 'Loading failed', 'error');
  391. }
  392. });
  393.  
  394. return function (_x) {
  395. return _ref2.apply(this, arguments);
  396. };
  397. })());
  398. document.addEventListener('DOMContentLoaded', () => {
  399. // remvoe create 7l anchor tag href
  400. const create7lLink = document.querySelector('#d_gw_nav_newgw');
  401.  
  402. if (create7lLink) create7lLink.parentNode.removeAttribute('href');
  403.  
  404. // insert manual update gme list button
  405. const anchor = document.querySelector('#d_gw_detail.d_gw_detail_hoverable');
  406.  
  407. if (anchor && (location.pathname.startsWith('/steamcn_gift-7l.html') || location.href.includes('id=steamcn_gift'))) {
  408. inject({
  409. css: `
  410. div[class*="7LFetch"] {
  411. margin-bottom: 20px;
  412. }
  413. `
  414. });
  415.  
  416. const block = document.createElement('div');
  417. let lastFetched = 'No Data';
  418.  
  419. if (games.get('lastFetched')) {
  420. const timeElapsed = Date.now() - games.get('lastFetched');
  421. const hoursElapsed = Math.round(timeElapsed / 1000 / 60 / 60);
  422. const daysElapsed = Math.round(hoursElapsed / 24);
  423.  
  424. lastFetched = daysElapsed > 0 ? `${daysElapsed}天${hoursElapsed - daysElapsed * 24}小時前` : `${hoursElapsed}小時前`;
  425. }
  426.  
  427. block.className = '7LFetch';
  428. block.innerHTML = `
  429. <button>手動更新遊戲列表</button>
  430. <span>上次更新:${lastFetched}</span>
  431. `;
  432.  
  433. block.querySelector('button').addEventListener('click', () => {
  434. swal('Loading');
  435. swal.showLoading();
  436. games.fetch();
  437. });
  438.  
  439. anchor.before(block);
  440. }
  441. });