将Trello改造为玩游戏做任务

可以在Trello上设定任务难度的级别,并且在完成任务后,发放对应级别的奖励值。

  1. // ==UserScript==
  2. // @name Use Trello AS Playing Game
  3. // @name:zh-CN 将Trello改造为玩游戏做任务
  4. //
  5. // @description You can set the level of the difficulty for every job, and when you complete it, you can get the proper reward.
  6. // @description:zh-CN 可以在Trello上设定任务难度的级别,并且在完成任务后,发放对应级别的奖励值。
  7. //
  8. // @namespace http://tampermonkey.net/
  9. // @version 0.2
  10. // @match https://trello.com/b/*
  11. // @match https://trello.com/c/*
  12. // @author oraant
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. // 更新:
  17. // 修复了Trello更新后,无法正常使用的问题。
  18. // 增加了自动触发功能,无需在看板页面等待,现在可以在卡片页面等待了。
  19.  
  20. window.onload = function(){ // 必须这么搞,否则选择器获取不到东西。decument.ready也不行!
  21.  
  22. // ------------------------------------------------------------------------------------------------------------------
  23. // 前置模块,存放通用的变量
  24. // ------------------------------------------------------------------------------------------------------------------
  25.  
  26. var CustomFields = document.getElementsByClassName("custom-field-detail-item");
  27. var RandomButtons = document.getElementsByClassName("random-button");
  28. var WindowWrapper = document.getElementsByClassName("window-wrapper")[0];
  29. var CheckLists = document.getElementsByClassName("editable non-empty checklist-title");
  30. var CheckItems = document.getElementsByClassName("checklist-item");
  31. var CardTitle = document.getElementsByClassName('window-title');
  32. var LandTitle = document.getElementsByClassName('js-board-editing-target');
  33.  
  34. // ------------------------------------------------------------------------------------------------------------------
  35. // common模块,存放通用的方法函数
  36. // ------------------------------------------------------------------------------------------------------------------
  37.  
  38. // ----------------- 从自定义字段中存取配置 -----------------
  39.  
  40. function GetDataDom(field){ // 获取指定的自定义字段的dom
  41. var dom; var i;
  42. switch (field){
  43. case 'Total': i = 0; break;
  44. case 'Copy': i = 1; break;
  45. case 'Object': i = 2; break;
  46. case 'Domain': i = 3; break;
  47. case 'Monster': i = 4; break;
  48. case 'Fstwin': i = 5; break;
  49. }
  50. dom = CustomFields[i].childNodes[1]
  51. return dom
  52. }
  53. function SetDataDom(field, value){ // 设置指定的自定义字段的内容
  54. var dataDom = GetDataDom(field);
  55. if (dataDom.value != value){
  56. dataDom.value = value;
  57. dataDom.focus({preventScroll: true});
  58. dataDom.blur();
  59. }
  60. }
  61. function GetDirectlyData(field){ // 获取指定的自定义字段的内容,若为空则解析为空字符串
  62. var dataDom = GetDataDom(field)
  63. var value = dataDom.value?dataDom.value:''
  64. return value
  65. }
  66. function SetDirectlyData(field, data){ // 覆写指定的自定义字段的内容
  67. var dataDom = GetDataDom(field)
  68. var value = JSON.stringify(data);
  69. SetDataDom(field, value);
  70. }
  71. function GetInternalData(field){ // 获取指定的自定义字段的内容,若为空则解析为空json
  72. var dataDom = GetDataDom(field)
  73. var value = dataDom.value?dataDom.value:'{}'
  74. return JSON.parse(value);
  75. }
  76. function SetInternalData(field, data){ // 覆写指定的自定义字段的内容
  77. var dataDom = GetDataDom(field)
  78. var value = JSON.stringify(data);
  79. SetDataDom(field, value);
  80. }
  81.  
  82. // ----------------- 获取某一DOM的内容中,是否存在符合格式的标记,格式为:ID#LV,比如3#S -----------------
  83. function GetSigns(dom){
  84. // 判断标题格式是否正确
  85. var titles = dom.innerText.split(' ');
  86. if (titles.length < 2){return []}
  87.  
  88. // 若标题格式正确,则判断标记格式是否正确
  89. var signs = titles[0].split('#');
  90. if (signs.length != 2){return []}
  91.  
  92. // 若标记格式正确,则返回标志列表
  93. return signs;
  94. }
  95.  
  96. // 获取徽章、最小值、最大值
  97. function GetConfigure(cls, level){
  98. // bedge, min, max configuration
  99. var LandConfiguration = [["🧱", 1, 10], ["💰", 10, 100], ["💿", 100, 1000], ["📀", 1000, 10000], ["💎", 10000, 100000], ["💣", 0, 0]];
  100. var AreaConfiguration = [["🧱", 1, 10], ["💰", 10, 100], ["💿", 100, 1000], ["📀", 1000, 10000], ["💎", 10000, 100000], ["💣", 0, 0]];
  101. var CopyConfiguration = [["🧱", 1, 10], ["💰", 10, 100], ["💿", 100, 1000], ["📀", 1000, 10000], ["💎", 10000, 100000], ["💣", 0, 0]];
  102. var DomainConfiguration = [["⭐", 1, 10], ["🌟", 10, 100], ["🌙", 100, 1000], ["🌝", 1000, 10000], ["🌞", 10000, 100000], ["🌠", 0, 0]];
  103. var MonsterConfiguration = [["🧱", 1, 10], ["💰", 10, 100], ["💿", 100, 1000], ["📀", 1000, 10000], ["💎", 10000, 100000], ["💣", 0, 0]];
  104.  
  105. var ObjectConfiguration = [["🧱", 1, 10], ["💰", 10, 100], ["💿", 100, 1000], ["📀", 1000, 10000], ["💎", 10000, 100000], ["💣", 0, 0]];
  106. var FstwinConfiguration = [["🌀", 3, 30], ["🌌", 30, 300], ["💧", 300, 3000], ["🔥", 3000, 30000], ["⚡", 30000, 300000], ["💀", 0, 0]];
  107.  
  108. var config; var suffix;
  109. switch(cls){
  110. case 'Land': config = LandConfiguration; break;
  111. case 'Area': config = AreaConfiguration; break;
  112. case 'Copy': config = CopyConfiguration; break;
  113. case 'Domain': config = DomainConfiguration; break;
  114. case 'Monster': config = MonsterConfiguration; break;
  115.  
  116. case 'Object': config = ObjectConfiguration; break;
  117. case 'Fstwin': config = FstwinConfiguration; break;
  118. }
  119. switch(level){
  120. case 'S': suffix = 4; break;
  121. case 'A': suffix = 3; break;
  122. case 'B': suffix = 2; break;
  123. case 'C': suffix = 1; break;
  124. case 'D': suffix = 0; break;
  125. default: suffix = 5;
  126. }
  127.  
  128. return config[suffix];
  129. }
  130.  
  131. // 获取一个随机数
  132. function GetRandomNum(min, max){
  133. return parseInt(Math.random()*(max-min+1)+min,10);
  134. }
  135.  
  136. // 判断按钮的内容是否正确,若正确时还一直插入,会引起栈溢出
  137. function SetInnerHTML(dom, inner){
  138. // console.log("正确内容为:" + inner + " 现在内容为:" + dom.innerHTML);
  139. if (dom.innerHTML != inner){
  140. dom.innerHTML = inner;
  141. }
  142. }
  143.  
  144. // 向某一dom中插入新的dom,index为插在第几个后面,0代表最前面。数太大则插在最后面
  145. function InsertCustomDOM(fdom, ndom, index){ // 旧的文本处理方式
  146. if (index == 0){
  147. fdom.innerHTML = ndom + fdom.innerHTML;
  148. }else if (index>fdom.children.length){
  149. fdom.innerHTML += ndom
  150. }else{
  151. fdom.children[index-1].innerHTML += ndom;
  152. }
  153. }
  154. // 将字符串转为dom
  155. function parseDom(arg) {
  156. var objE = document.createElement("div");
  157. objE.innerHTML = arg;
  158. return objE.children[0];
  159. };
  160. // 像Dom中的最后添加一个新的Dom,并且为新Dom指定一个点击监听器
  161. function CustomTopButton(head, button, listener){
  162. if(head.children.length == 1){
  163. var ButtonDom = parseDom(button);
  164. ButtonDom.addEventListener("click", listener);
  165. head.appendChild(ButtonDom);
  166. };
  167. }
  168.  
  169. // ------------------------------------------------------------------------------------------------------------------
  170. // 自定义计算总分
  171. // ------------------------------------------------------------------------------------------------------------------
  172.  
  173. var TotalButton = '<a class="random-button card-label button" style="display:inline;margin-left:5px;">🏆🏆🏆</a>'
  174.  
  175. function TotalButtonListener(event){
  176. // 获取所有的内部数据
  177. var copy_data = GetDirectlyData('Copy');
  178. var object_data = GetDirectlyData('Object');
  179. var domain_data = GetInternalData('Domain');
  180. var monster_data = GetInternalData('Monster');
  181. var fstwin_data = GetInternalData('Fstwin');
  182.  
  183. // 把所有存储的数据加起来
  184. var number = 0;
  185. if (copy_data != ''){number += parseInt(copy_data)};
  186. if (object_data != ''){number += parseInt(object_data)};
  187. if (domain_data != {}){
  188. for (var key1 in domain_data){
  189. number += domain_data[key1]
  190. }
  191. }
  192. if (monster_data != {}){
  193. for (var key2 in monster_data){
  194. number += monster_data[key2]
  195. }
  196. }
  197. if (fstwin_data != {}){
  198. for (var key3 in fstwin_data){
  199. number += fstwin_data[key3][1]
  200. }
  201. }
  202.  
  203. // 把数据在内部数据中显示出来
  204. SetDirectlyData('Total', number);
  205. }
  206.  
  207. function CustomTotalButton(){
  208. CustomTopButton(CustomFields[0].children[0], TotalButton, TotalButtonListener);
  209. }
  210.  
  211. // ------------------------------------------------------------------------------------------------------------------
  212. // 自定义副本积分
  213. // ------------------------------------------------------------------------------------------------------------------
  214.  
  215. var CopyButton = '<a class="random-button card-label button" style="display:inline;margin-left:5px;">🎲🎲🎲</a>'
  216.  
  217. function CopyButtonListener(event){
  218. // 获取标志
  219. var signs = GetSigns(CardTitle[0].children[0]);
  220. if (!signs.length){return}
  221. var id = signs[0]; var level = signs[1];
  222.  
  223. // 获取数据和配置
  224. var copy_data = GetDirectlyData('Copy');
  225. var copy_config = GetConfigure('Copy', level) // 获取配置信息 // todo: 获取级别、ID之类的
  226. var copy_bedge = copy_config[0]; var copy_min = copy_config[1]; var copy_max = copy_config[2];
  227. var copy_number = GetRandomNum(copy_min, copy_max);
  228. SetDirectlyData('Copy', copy_number);
  229. }
  230.  
  231. function CustomCopyButton(){
  232. CustomTopButton(CustomFields[1].children[0], CopyButton, CopyButtonListener);
  233. }
  234.  
  235. // ------------------------------------------------------------------------------------------------------------------
  236. // 自定义副本奖励
  237. // ------------------------------------------------------------------------------------------------------------------
  238.  
  239. var ObjectButton = '<a class="random-button card-label button" style="display:inline;margin-left:5px;">🎁🎁🎁</a>'
  240.  
  241. function ObjectButtonListener(event){
  242. // 获取标志
  243. var signs = GetSigns(CardTitle[0].children[0]);
  244. if (!signs.length){return}
  245. var id = signs[0]; var level = signs[1];
  246.  
  247. // 获取数据和配置
  248. var object_data = GetDirectlyData('Object'); // todo:换成object
  249. var object_config = GetConfigure('Object', level) // 获取配置信息
  250. var object_bedge = object_config[0]; var object_min = object_config[1]; var object_max = object_config[2];
  251. var object_number = GetRandomNum(object_min, object_max);
  252. SetDirectlyData('Object', object_number);
  253. }
  254.  
  255. function CustomObjectButton(){
  256. CustomTopButton(CustomFields[2].children[0], ObjectButton, ObjectButtonListener);
  257. }
  258.  
  259. // ------------------------------------------------------------------------------------------------------------------
  260. // 自定义清单
  261. // ------------------------------------------------------------------------------------------------------------------
  262.  
  263. var CheckListButton = '<a class="random-button button subtle hide-on-edit" style="margin:0 0 0 6px;color:#fff;background-color:#f17143;font-weight:bold;">???</a>'
  264.  
  265. function CustomCheckListsListener(event){
  266. var target = event.currentTarget;
  267. var title = target.parentNode.previousSibling; // 获取标题、ID、级别
  268. var signs = GetSigns(title);
  269. if (!signs.length){return}
  270. var id = signs[0]; var level = signs[1];
  271.  
  272. var domain_data = GetInternalData('Domain'); // 获取内部数据
  273. var domain_config = GetConfigure('Domain', level) // 获取配置信息
  274.  
  275. if (typeof(domain_data[id]) != "undefined"){return} // 若已有内部数据则禁止再次生成
  276.  
  277. var domain_bedge = domain_config[0]; var domain_min = domain_config[1]; var domain_max = domain_config[2];
  278. var domain_number = GetRandomNum(domain_min, domain_max);
  279. domain_data[id] = domain_number; // 更新内部数据
  280.  
  281. SetInternalData('Domain', domain_data)
  282. SetInnerHTML(target, domain_number);
  283. }
  284.  
  285. function CustomCheckLists(){
  286. if(CheckLists.length == 0){return};
  287. for (var i=0; i<CheckLists.length; i++){
  288. // 获取关键DOM、获取检查项的名称DOM、自定义DOM
  289. var title = CheckLists[i].children[0];
  290. var option = CheckLists[i].children[1];
  291.  
  292. // 获取标记中的信息
  293. var signs = GetSigns(title);
  294. if (!signs.length){continue}
  295. var id = signs[0]; var level = signs[1];
  296.  
  297. // 添加自定义按钮
  298. if(option.children.length == 3){
  299. var CheckListButtonDom = parseDom(CheckListButton);
  300. CheckListButtonDom.addEventListener("click", CustomCheckListsListener);
  301. option.appendChild(CheckListButtonDom)
  302. }
  303.  
  304. // 实时调整其按钮显示的内容
  305. else if(option.children.length == 4){
  306. var buttons = option.children; // 获取添加的自定义按钮
  307. var domain_data = GetInternalData('Domain'); // 获取内部数据
  308. var bedge = GetConfigure('Domain', level)[0] // 获取配置信息中的图标
  309.  
  310. // 计算应正确显示的内容
  311. var inner = ""; // 按钮要显示的内容
  312. if (typeof(domain_data[id]) == "undefined"){ // 打开卡片时,若数据中没有相关的数据,则显示礼包按钮
  313. inner = bedge;
  314. }else{ // 若已经有相关数据了,则显示相关数据
  315. inner = domain_data[id];
  316. }
  317.  
  318. // 判断按钮的内容是否正确,若正确时还一直插入,会引起栈溢出
  319. SetInnerHTML(buttons[0], '显');
  320. SetInnerHTML(buttons[1], '隐');
  321. SetInnerHTML(buttons[2], '删');
  322. SetInnerHTML(buttons[3], inner);
  323. }
  324. }
  325. }
  326.  
  327. // ------------------------------------------------------------------------------------------------------------------
  328. // 自定义检查项
  329. // ------------------------------------------------------------------------------------------------------------------
  330.  
  331. var CheckItemTag = '<span class="oraant-custom card-label" style="float:left; max-height:20px;padding:0px 2px;margin:8px 5px;background-color:#e3e7e9;overflow:initial;color:#17394d;">?</span>'
  332. var CheckItemCoin = '<span class="oraant-custom card-label card-label-green" style="float:right;max-height:20px;padding:0px 5px;margin:8px 2px;text-overflow:initial; overflow:initial;font-weight:bold;">?</span>'
  333. var CheckItemFstwin = '<span class="oraant-custom card-label card-label-sky" style="float:right;max-height:20px;padding:0px 5px;margin:8px 2px;text-overflow:initial; overflow:initial;">?</span>'
  334.  
  335. // 自定义检查项后的信息
  336. function CustomCheckItems(){
  337. if(CheckItems.length == 0){return};
  338.  
  339. for (var i=0; i<CheckItems.length; i++){
  340. // 获取详情DOM、获取检查项的名称DOM、自定义DOM
  341. var detail = CheckItems[i].children[1].children[0];
  342. var cbtag, cbtext, cbcoin, cbfstw;
  343. var signs, id, level;
  344.  
  345. // 计算应正确显示的内容
  346. // 添加自定义按钮
  347. if(detail.children.length == 2){
  348. // 单独验证格式是否合适,因为插入的原因,两次cbtext的位置是不一样的
  349. cbtext = detail.children[0];
  350. signs = GetSigns(cbtext);
  351. if (!signs.length){continue}
  352.  
  353. InsertCustomDOM(detail, CheckItemTag, 0)
  354. InsertCustomDOM(detail, CheckItemCoin, 10)
  355. InsertCustomDOM(detail, CheckItemFstwin, 20)
  356. }
  357.  
  358. // 实时调整其按钮显示的内容(之前那些DOM里Onclick的功能,也要做到这里面来。因为这个不是个按钮,不需要去按。)
  359. else if(detail.children.length == 5){
  360. // 获取各组件的dom
  361. cbtag = detail.children[0];
  362. cbtext = detail.children[1];
  363. cbcoin = detail.children[3];
  364. cbfstw = detail.children[4];
  365.  
  366. // 单独验证格式是否合适,因为插入的原因,两次cbtext的位置是不一样的
  367. signs = GetSigns(cbtext);
  368. if (!signs.length){continue}
  369. id = signs[0]; level = signs[1];
  370.  
  371. // console.log("->内容全面,判断是否要进行修正");
  372. var state = CheckItems[i].getAttribute("class"); // 获取类属性
  373. var monster_data = GetInternalData('Monster'); // 获取内部数据
  374. var fstwin_data = GetInternalData('Fstwin'); // 获取内部数据
  375. var cbcoin_config = GetConfigure('Monster', level) // 获取配置信息
  376. var cbfstw_config = GetConfigure('Fstwin', level) // 获取配置信息
  377. var cbcoin_bedge = cbcoin_config[0]; var cbcoin_min = cbcoin_config[1]; var cbcoin_max = cbcoin_config[2];
  378. var cbfstw_bedge = cbfstw_config[0]; var cbfstw_min = cbfstw_config[1]; var cbfstw_max = cbfstw_config[2];
  379.  
  380. var cbcoin_inner = ""; // 金币标签要显示的内容
  381. var cbfstw_inner = ""; // 首胜标签要显示的内容
  382. SetInnerHTML(cbtag, cbfstw_bedge); // 不管是否勾选,都在前面显示图标
  383.  
  384. if (state == "checklist-item"){ // 若未勾选,则根据难度,显示图标
  385. cbcoin_inner = cbcoin_bedge;
  386. cbfstw_inner = '';
  387. }else if(state.search("checklist-item-state-complete") != -1){ // 若已勾选 // todo:这里应该能去掉,和下面的一起if
  388. // 计算首胜标签应正确显示的内容
  389. // ----------------------------------------
  390.  
  391. // 判断今天的有没有记录过(必须得在coin前边,需要根据coin的状态,判断是否是已经有数据的)
  392. // 若已勾选但没有今日数据,且这个检查项也没有存过数据(否则昨天加了11号的后,今天还会加11号的首胜),则在数据栏中添加数据
  393. var options = {year: 'numeric', month: 'numeric', day: 'numeric' };
  394. var date = new Date().toLocaleDateString('ch-zh', options);
  395. if (typeof(fstwin_data[date]) == "undefined" && typeof(monster_data[id]) == "undefined"){
  396. var cbfstw_number = GetRandomNum(cbfstw_min, cbfstw_max); // 每日首胜奖励+3倍
  397. fstwin_data[date] = [id, cbfstw_number];
  398. SetInternalData('Fstwin', fstwin_data)
  399. }
  400.  
  401. // 根据以前的数据,将首胜信息展示出来
  402. for (var k in fstwin_data){
  403. if(fstwin_data[k][0] == id){ // 若已经有今日数据了,且是这个ID,则显示相关数据
  404. cbfstw_inner = fstwin_data[k][1];
  405. }
  406. }
  407.  
  408. // 计算金币标签应正确显示的内容
  409. // ----------------------------------------
  410. if (typeof(monster_data[id]) == "undefined"){ // 已勾选但未曾保存数据,则插入数据
  411. var cbcoin_number = GetRandomNum(cbcoin_min, cbcoin_max);
  412. monster_data[id] = cbcoin_number;
  413. SetInternalData('Monster', monster_data)
  414. cbcoin_inner = cbcoin_number
  415. }else{ // 若已经有相关数据了,则显示相关数据
  416. cbcoin_inner = monster_data[id];
  417. }
  418.  
  419. }else{console.log('很奇怪,检查项的类属性和预期的不同:'+state)}
  420.  
  421. SetInnerHTML(cbfstw, cbfstw_inner);
  422. SetInnerHTML(cbcoin, cbcoin_inner);
  423. if(cbfstw_inner == ""){
  424. cbfstw.style.display = "none";
  425. }else{
  426. cbfstw.style.display = "initial";
  427. }
  428. }
  429. }
  430. }
  431.  
  432. // ------------------------------------------------------------------------------------------------------------------
  433. // 程序入口
  434. // ------------------------------------------------------------------------------------------------------------------
  435.  
  436. var callback = function (records){
  437. // 检查看板的标题是否符合格式
  438. if(LandTitle.length == 0){return};
  439. var title_signs = GetSigns(LandTitle[0]);
  440. if (!title_signs.length){return}
  441.  
  442. // 校验是否有自定义域
  443. if(CustomFields.length == 0){return};
  444.  
  445. // 获取判断卡片标题是否符合要求
  446. var card_signs = GetSigns(CardTitle[0].children[0]);
  447. if (!card_signs.length){return}
  448.  
  449. CustomTotalButton()
  450. CustomCopyButton()
  451. CustomObjectButton()
  452.  
  453. CustomCheckLists()
  454. CustomCheckItems()
  455.  
  456. console.log('看看能不能输出日志')
  457. };
  458. var mo = new MutationObserver(callback);
  459. mo.observe(WindowWrapper, {'childList': true, 'subtree': true}); // 设置一个监听器,页面由变化就触发。
  460. callback(); // 如果直接打开一个页面的话,默认监听器不会被触发。这时手动触发一次就很有必要了。
  461. };
  462.