CKUIToolkit

A simple settings modal framework.

目前為 2022-03-20 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/441653/1030335/CKUIToolkit.js

  1. // ==UserScript==
  2. // @name CKUIToolkit
  3. // @namespace ckylin-script-lib-combined-ui-components
  4. // @version 1.0
  5. // @match http://*
  6. // @match https://*
  7. // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1028655
  8. // @resource popjs https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.js
  9. // @resource popcss https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.css
  10. // @resource fpjs https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.js
  11. // @resource fpcss https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.modal.css
  12. // @author CKylinMC
  13. // @license GPL-3.0-only
  14. // @grant GM_getResourceText
  15. // @grant unsafeWindow
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. if (typeof (unsafeWindow) == 'undefined') {
  20. unsafeWindow = window;
  21. }
  22. if (typeof (GM_getResourceText) != 'function') {
  23. GM_getResourceText = () => null;
  24. }
  25. //======[Apply all resources]
  26. const resourceList = [
  27. { name: 'popjs', type: 'js', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.js'},
  28. { name: 'popcss', type: 'css', source:'https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.css' },
  29. { name: 'fpjs', type: 'js', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.js' },
  30. { name: 'fpcss', type: 'css', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.modal.css' },
  31. { name: 'popcsspatch', type: 'rawcss', content: "div.popNotifyUnitFrame{z-index:110000!important;}.CKTOOLS-modal-content{color: #616161!important;max-height: 80vh;overflow: auto;}" },
  32. { name: 'settingscss', type: 'rawcss', content: `
  33. .ckui-base .ckui-text{
  34. font-size: 14px;
  35. line-height: 1.428571429;
  36. }
  37. .ckui-base label{
  38. display: block;
  39. color: rgb(16, 140, 255);
  40. padding-top: 12px;
  41. }
  42. .ckui-base .ckui-texttoggle{
  43. display: block;
  44. color: rgb(16, 140, 255);
  45. padding-top: 12px;
  46. }
  47. .ckui-base .ckui-texttoggle-container::before{
  48. display: inline;
  49. content: "🔹";
  50. }
  51. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value{
  52. padding: 0px 6px;
  53. font-weight: bold;
  54. -webkit-user-select: none;
  55. -moz-user-select: none;
  56. -ms-user-select: none;
  57. user-select: none;
  58. cursor: pointer;
  59. }
  60. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::before{
  61. content: "["
  62. }
  63. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::after{
  64. content: "]"
  65. }
  66. .ckui-base .ckui-texttoggle-container::after{
  67. display: inline;
  68. content: "(点击切换)";
  69. color: gray;
  70. font-size: 12px;
  71. font-style: italic;
  72. padding-left: 12px;
  73. }
  74. .ckui-base .ckui-description{
  75. display: block;
  76. color: rgb(92, 92, 92);
  77. font-size: small;
  78. font-style: italic;
  79. }
  80. .ckui-base .ckui-toggle{
  81. padding-top: 12px;
  82. }
  83. .ckui-base label.ckui-inline-label{
  84. display: inline;
  85. line-height: 18px;
  86. }
  87. .ckui-base .ckui-input input{
  88. display: block;
  89. width: calc(100% - 28px);
  90. height: 34px;
  91. padding: 1px 3px;
  92. margin: 3px 6px;
  93. font-size: 14px;
  94. line-height: 1.428571429;
  95. color: rgb(51, 51, 51);
  96. background-color: rgb(255, 255, 255);
  97. border: 1px solid rgb(204, 204, 204);
  98. border-radius: 4px;
  99. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  100. }
  101. .ckui-base .ckui-inputnumber input{
  102. display: block;
  103. width: calc(100% - 36px);
  104. height: 34px;
  105. padding: 1px 12px;
  106. margin: 3px 6px;
  107. font-size: 14px;
  108. line-height: 1.428571429;
  109. color: rgb(51, 51, 51);
  110. background-color: rgb(255, 255, 255);
  111. border: 1px solid rgb(204, 204, 204);
  112. border-radius: 4px;
  113. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  114. }
  115. .ckui-base .ckui-inputarea textarea{
  116. display: block;
  117. width: calc(100% - 28px);
  118. height: 100px;
  119. padding: 6px 6px;
  120. margin: 3px 6px;
  121. font-size: 14px;
  122. line-height: 1.428571429;
  123. color: rgb(51, 51, 51);
  124. background-color: rgb(255, 255, 255);
  125. border: 1px solid rgb(204, 204, 204);
  126. border-radius: 4px;
  127. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  128. }
  129. .ckui-base .ckui-select select{
  130. display: block;
  131. width: calc(100% - 28px);
  132. height: 34px;
  133. padding: 6px 6px;
  134. margin: 3px 6px;
  135. font-size: 14px;
  136. line-height: 1.428571429;
  137. color: rgb(51, 51, 51);
  138. background-color: rgb(255, 255, 255);
  139. border: 1px solid rgb(204, 204, 204);
  140. border-radius: 4px;
  141. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  142. }
  143. .ckui-base .ckui-select select option{
  144. font-size: 16px;
  145. line-height: 1.428571429;
  146. color: rgb(51, 51, 51);
  147. }
  148. .ckui-base .ckui-header::before{
  149. content: "💠";
  150. }
  151. .ckui-base .ckui-header{
  152. width: calc(100% - 8px);
  153. display: block;
  154. color: rgb(16, 140, 255);
  155. padding: 12px 3px;
  156. border-bottom: 2px solid rgb(16, 140, 255);
  157. margin: 12px 0px 12px 0px;
  158. }
  159. .ckui-base .ckui-btns{
  160. display: flex;
  161. flex-wrap: wrap;
  162. flex-direction: row;
  163. }
  164. .ckui-base .ckui-btn{
  165. display: block;
  166. width: calc(50% - 8px);
  167. height: 40px;
  168. padding: 6px 12px;
  169. margin: 6px;
  170. font-size: 14px;
  171. line-height: 1.428571429;
  172. color: rgb(255, 255, 255);
  173. background-color: rgb(16, 140, 255);
  174. border: 1px solid rgb(16, 140, 255);
  175. border-radius: 4px;
  176. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  177. }
  178. .ckui-base .ckui-btn:hover{
  179. background-color: rgb(0, 122, 255);
  180. }
  181. .ckui-base .ckui-btns .ckui-btn{
  182. flex: 1;
  183. }` }
  184. ]
  185. async function applyResource() {
  186. resloop: for (let res of resourceList) {
  187. if (true||!document.querySelector("#" + res.name)) {
  188. let el;
  189. switch (res.type) {
  190. case 'js':
  191. case 'rawjs':
  192. el = document.createElement("script");
  193. break;
  194. case 'css':
  195. case 'rawcss':
  196. el = document.createElement("style");
  197. break;
  198. default:
  199. console.error('[CKUI]','Err:unknown type', res);
  200. continue resloop;
  201. }
  202. el.id = res.name;
  203. let result = res.type.startsWith('raw') ? res.content : GM_getResourceText(res.name);
  204. if (result == null || result == 'null' || typeof result === 'undefined') {
  205. console.info('[CKUI]', 'Alternative method is using:',res.type,res.name);
  206. if (!res.source) {
  207. console.info('[CKUI]', 'Failed to apply:',res.type,res.name);
  208. continue resloop;
  209. }
  210. try {
  211. let response = await fetch(res.source);
  212. if (!response.ok) {
  213. console.info('[CKUI]', 'Failed to apply:',res.type,res.name,response.statusText);
  214. continue resloop;
  215. }
  216. result = await response.text();
  217. } catch (e) {
  218. console.info('[CKUI]', 'Failed to apply:',res.type,res.name,e);
  219. continue resloop;
  220. }
  221. }
  222. el.appendChild(document.createTextNode(result));
  223. document.head.appendChild(el);
  224. console.info('[CKUI]', 'Applied:',res.type,res.name);
  225. }
  226. }
  227. console.info('[CKUI]', 'Resources all applied');
  228. }
  229. applyResource();
  230. let { domHelper, deepClone } = CKTools;
  231. if (!deepClone) {
  232. deepClone = (obj)=>{
  233. let newObject = {};
  234. if (Array.isArray(obj)) {
  235. newObject = [];
  236. for (let i = 0; i < obj.length; i++) {
  237. newObject.push(deepClone(obj[i]));
  238. }
  239. return newObject;
  240. }
  241. Object.keys(obj).map(key => {
  242. if (typeof obj[key] === 'object') {
  243. newObject[key] = deepClone(obj[key]);
  244. } else {
  245. newObject[key] = obj[key];
  246. }
  247. });
  248. return newObject;
  249. };
  250. }
  251. const CKUIToolkit = {};
  252. class CompUtils{
  253. static getId(name) {
  254. return 'ckui-settings-' + name;
  255. }
  256. static getClass(type) {
  257. return 'ckui-' + type;
  258. }
  259. static runChecker(checker = null,...args) {
  260. if (checker && typeof (checker) == 'function') {
  261. try {
  262. const result = checker(...args);
  263. return !!result;
  264. } catch (e) {
  265. return false;
  266. }
  267. } else return true;
  268. }
  269. static cfgValidator(cfg, keystr='') {
  270. let keys = keystr.split(',').map(el => el.trim()).filter(el => el.length > 0);
  271. keys.concat(['name', 'type']);
  272. if (!cfg) return false;
  273. for (let key of keys) {
  274. if (cfg[key]===undefined) return false;
  275. }
  276. return true;
  277. }
  278. }
  279. class Components{
  280. static text(cfg) {
  281. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  282. return domHelper('div', {
  283. id: CompUtils.getId(cfg.name),
  284. classlist: CompUtils.getClass(cfg.type),
  285. html: cfg.label
  286. });
  287. }
  288. static header(cfg) {
  289. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  290. return domHelper('div', {
  291. id: CompUtils.getId(cfg.name),
  292. classlist: CompUtils.getClass(cfg.type),
  293. html: cfg.label
  294. });
  295. }
  296. static toggle(cfg) {
  297. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  298. const customId = 'toggle'+CKTools.GUID.getShort();
  299. return domHelper('div', {
  300. id: CompUtils.getId(cfg.name),
  301. classlist: CompUtils.getClass(cfg.type),
  302. childs: [
  303. domHelper('input', {
  304. id: customId,
  305. attr: {
  306. type: 'checkbox',
  307. value: cfg.value??false
  308. },
  309. on: {
  310. change: (e) => {
  311. const value = !!(e.target?.value);
  312. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  313. // else TODO: error tip
  314. }
  315. }
  316. }),
  317. domHelper('label', {
  318. attr: {
  319. for: customId
  320. },
  321. classlist:'ckui-inline-label',
  322. html: cfg.label
  323. }),
  324. domHelper('span', {
  325. classList: 'ckui-description',
  326. html: cfg.description??''
  327. }),
  328. ]
  329. });
  330. }
  331. static input(cfg) {
  332. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  333. return domHelper('div', {
  334. id: CompUtils.getId(cfg.name),
  335. classlist: CompUtils.getClass(cfg.type),
  336. childs: [
  337. domHelper('label', {
  338. html: cfg.label
  339. }),
  340. domHelper('input', {
  341. attr:cfg.value,
  342. on: {
  343. keyup: CKTools.debounce((e) => {
  344. const value = e.target?.value;
  345. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  346. // else TODO: error tip
  347. })
  348. }
  349. }),
  350. domHelper('span', {
  351. classList: 'ckui-description',
  352. html: cfg.description??''
  353. }),
  354. ]
  355. });
  356. }
  357. static inputarea(cfg) {
  358. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  359. return domHelper('div', {
  360. id: CompUtils.getId(cfg.name),
  361. classlist: CompUtils.getClass(cfg.type),
  362. childs: [
  363. domHelper('label', {
  364. html: cfg.label
  365. }),
  366. domHelper('textarea', {
  367. attr: {
  368. value: cfg.value
  369. },
  370. on: {
  371. keyup: CKTools.debounce((e) => {
  372. const value = e.target?.value ?? e.target.innerHTMl;
  373. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  374. // else TODO: error tip
  375. })
  376. }
  377. }),
  378. domHelper('span', {
  379. classList: 'ckui-description',
  380. html: cfg.description??''
  381. }),
  382. ]
  383. });
  384. }
  385. static inputnumber(cfg) {
  386. if (!CompUtils.cfgValidator(cfg, 'label,min,max,step')) return;
  387. return domHelper('div', {
  388. id: CompUtils.getId(cfg.name),
  389. classlist: CompUtils.getClass(cfg.type),
  390. childs: [
  391. domHelper('label', {
  392. html: cfg.label
  393. }),
  394. domHelper('input', {
  395. attr: {
  396. type: 'number',
  397. value: cfg.value,
  398. min: cfg.min,
  399. max: cfg.max,
  400. step: cfg.step
  401. },
  402. on: {
  403. keyup: CKTools.debounce((e) => {
  404. const value = e.target?.value;
  405. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  406. // else TODO: error tip
  407. })
  408. }
  409. }),
  410. domHelper('span', {
  411. classList: 'ckui-description',
  412. html: cfg.description??''
  413. }),
  414. ]
  415. });
  416. }
  417. static select(cfg) {
  418. if (!CompUtils.cfgValidator(cfg, 'label,options')) return;
  419. return domHelper('div', {
  420. id: CompUtils.getId(cfg.name),
  421. classlist: CompUtils.getClass(cfg.type),
  422. childs: [
  423. domHelper('label', {
  424. html: cfg.label
  425. }),
  426. domHelper('select', {
  427. attr: {
  428. value: cfg.value??'',
  429. },
  430. init: select => {
  431. for (let option of cfg.options) {
  432. select.appendChild(domHelper('option', {
  433. attr: {
  434. value: option.value,
  435. selected: cfg.value == option.value
  436. },
  437. html: option.opt
  438. }));
  439. }
  440. },
  441. on: {
  442. change: CKTools.debounce((e) => {
  443. const value = !!(e.target?.value);
  444. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  445. // else TODO: error tip
  446. })
  447. }
  448. }),
  449. domHelper('span', {
  450. classList: 'ckui-description',
  451. html: cfg.description??''
  452. }),
  453. ]
  454. });
  455. }
  456. static texttoggle(cfg) {
  457. if (!CompUtils.cfgValidator(cfg, 'before,on,off,after')) return;
  458. return domHelper('div', {
  459. id: CompUtils.getId(cfg.name),
  460. classlist: CompUtils.getClass(cfg.type),
  461. childs: [
  462. domHelper('div', {
  463. classList:'ckui-texttoggle-container',
  464. childs: [
  465. domHelper('span', { text: cfg.before }),
  466. domHelper('span', {classList:'ckui-texttoggle-value',text:'...'}),
  467. domHelper('span', {text:cfg.after}),
  468. ],
  469. init: div => {
  470. const getText = () => cfg.value ? cfg.on : cfg.off;
  471. const setValue = (value) => {
  472. cfg.value = !!value;
  473. div.querySelector('.ckui-texttoggle-value').innerText = getText();
  474. }
  475. const toggleValue = () => setValue(!cfg.value);
  476. div.addEventListener('click', toggleValue);
  477. setValue(cfg.value);
  478. }
  479. }),
  480. domHelper('span', {
  481. classList: 'ckui-description',
  482. html: cfg.description??''
  483. }),
  484. ]
  485. });
  486. }
  487. static raw(cfg) {
  488. if (!CompUtils.cfgValidator(cfg, 'contents')) return;
  489. return domHelper('div', {
  490. id: CompUtils.getId(cfg.name),
  491. classlist: CompUtils.getClass(cfg.type),
  492. childs:domHelper(cfg.contents)
  493. })
  494. }
  495. static window(cfg) {
  496. if (!CompUtils.cfgValidator(cfg, 'label,config')) return;
  497. return domHelper('div', {
  498. id: CompUtils.getId(cfg.name),
  499. classlist: CompUtils.getClass(cfg.type),
  500. childs: [
  501. domHelper('button', {
  502. classList: 'ckui-btn',
  503. html: cfg.label,
  504. on: {
  505. click: async (e) => {
  506. cfg.config = await SettingsBuilder.open(cfg.config);
  507. }
  508. }
  509. })
  510. ]
  511. })
  512. }
  513. static btns(cfg) {
  514. if (!CompUtils.cfgValidator(cfg, 'btns')) return;
  515. return domHelper('div', {
  516. id: CompUtils.getId(cfg.name),
  517. classlist: CompUtils.getClass(cfg.type),
  518. init: el => {
  519. for(let btn of cfg.btns) {
  520. el.appendChild(domHelper('button', {
  521. classList: 'ckui-btn',
  522. html: btn.label,
  523. on: {
  524. click: async (e) => {
  525. await btn.onclick();
  526. }
  527. }
  528. }));
  529. }
  530. }
  531. })
  532. }
  533. }
  534. class SettingsBuilder{
  535. static async open(cfg) {
  536. const s = new SettingsBuilder(cfg);
  537. const result = await s.showWindow();
  538. return result;
  539. }
  540. constructor(config) {
  541. this.config = Object.assign({
  542. title: '设置',
  543. settings:[]
  544. },config);
  545. }
  546. async showWindow(config = this.config) {
  547. const copiedConfig = deepClone(config);
  548. console.log('Cloned:',config,copiedConfig)
  549. return new Promise(r => {
  550. FloatPopup.confirm(copiedConfig.title, domHelper('div', {
  551. classlist:'ckui-base',
  552. init: el => {
  553. for (const comp of copiedConfig.settings) {
  554. const r = this.makeComponent(comp);
  555. r&&el.appendChild(r);
  556. }
  557. }
  558. }), "保存", "取消").then(result => result?r(copiedConfig):r(config));
  559. })
  560. }
  561. makeComponent(cfg) {
  562. if (Components.hasOwnProperty(cfg.type)) {
  563. return Components[cfg.type](cfg);
  564. }
  565. }
  566. }
  567. CKUIToolkit.showSettings = SettingsBuilder.open;
  568.  
  569. unsafeWindow.CKUIToolkit = CKUIToolkit;
  570. })();