index.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <!--
  5. Develop by tobychui
  6. All Right Reserved
  7. -->
  8. <meta charset="UTF-8" />
  9. <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
  10. <meta name="description" content="" />
  11. <meta name="keywords" content="" />
  12. <meta name="team" content="" />
  13. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  14. <link rel="icon" href="./img/pattern.png" />
  15. <!-- Framework-->
  16. <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
  17. <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  18. <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
  19. <script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
  20. <script src="https://unpkg.com/[email protected]/dist/typed.umd.js"></script>
  21. <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
  22. <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>
  23. <script src="main.js"></script>
  24. <!-- Font Replacement -->
  25. <link rel="preconnect" href="https://fonts.googleapis.com">
  26. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  27. <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
  28. <!-- HTML Meta Tags -->
  29. <title>4-key Macropad | imuslab</title>
  30. <meta name="description" content="Simple and easy to use compile config Macropad designed by imuslab">
  31. <style>
  32. body{
  33. background-color: black;
  34. }
  35. #keypad{
  36. text-align: center;
  37. width: 100%;
  38. height: 360px;
  39. }
  40. #titleText{
  41. text-align: center;
  42. color: white;
  43. }
  44. /* SVG internal css */
  45. .macrobutton{
  46. cursor: pointer;
  47. }
  48. .macroswitch{
  49. cursor: pointer;
  50. }
  51. .actiongroup{
  52. color: white;
  53. margin-top: 0.4em;
  54. }
  55. .actiongroup .button{
  56. color: rgb(29, 29, 29);
  57. background-color: rgb(238, 238, 238);
  58. border: 1px solid rgb(71, 71, 71);
  59. }
  60. .white.button{
  61. color: rgb(236, 236, 236);
  62. background-color: rgb(20, 20, 20);
  63. border: 1px solid rgb(71, 71, 71);
  64. margin-top: 0.4em;
  65. }
  66. #keyseq{
  67. height: 300px;
  68. overflow-y:scroll;
  69. border: 1px solid white;
  70. padding: 0.4em;
  71. padding-left: 1.2em;
  72. padding-right: 1.2em;
  73. }
  74. #keyseq.A{
  75. border: 1px solid #2EA7E0;
  76. }
  77. #keyseq.B{
  78. border: 1px solid red;
  79. }
  80. </style>
  81. </head>
  82. <body>
  83. <br>
  84. <div class="ui text container">
  85. <div id="keypad">
  86. </div>
  87. <p id="titleText" style="height: 60px;"></p>
  88. <div id="keyseq" class="A">
  89. </div>
  90. <div class="ui divider"></div>
  91. <p style="color: white; text-align: center;">Advance Key Options</p>
  92. <div style="width: 100%;" align="center">
  93. <button class="circular ui basic green button" onclick="addConsumerKey();">
  94. <i class="add icon"></i> Add Consumer Key (Select Below)
  95. </button>
  96. <button class="circular ui basic teal button" onclick="addDelayTime();">
  97. <i class="hourglass half icon"></i> Add Delay
  98. </button>
  99. <button class="circular ui basic red button" onclick="deleteLastAction();">
  100. <i class="remove icon"></i> Delete Last Keystroke(s)
  101. </button>
  102. <div style="margin-top: 0.4em;">
  103. <select class="ui mini search black fluid dropdown" id="complexKeys">
  104. <option value="">HID Consumer Keys</option>
  105. </select>
  106. </div>
  107. </div>
  108. <div id="specialKeys" style="width: 100%; margin-top: 1em;" align="center">
  109. </div>
  110. <div class="ui divider"></div>
  111. <p style="color: white; text-align: center;">Save, Load and Export Options</p>
  112. <button class="circular ui black button" onclick="saveConfigAsFile();">
  113. <i class="save icon"></i> Save Configs
  114. </button>
  115. <button class="circular ui black button" onclick="loadConfigFromFile();">
  116. <i class="folder open icon"></i> Load Config
  117. </button>
  118. <button class="circular ui teal right floated button" onclick="generateArduinoCode();">
  119. <i class="download icon"></i> Download .ino Package
  120. </button>
  121. <div class="ui divider"></div>
  122. <small style="color: rgb(207, 207, 207);">CopyRight tobychui feat. project imuslab</small>
  123. <br><br><br>
  124. </div>
  125. <script>
  126. //Load in the layouts and startup animations
  127. let typed;
  128. let currentMode = 0; //0=A. 1=B
  129. let codekeyMap = {}; //Lookup key string by keyCode
  130. let charmap = {}; //Unicode mapping, currently only support zh-tw
  131. //Runtime Environments
  132. let editingKey = 0;
  133. let keySeqA = {
  134. "1":"",
  135. "2":"",
  136. "3":"",
  137. "4":"",
  138. };
  139. let keySeqB = {
  140. "1":"",
  141. "2":"",
  142. "3":"",
  143. "4":"",
  144. };
  145. $("#keypad").hide();
  146. //Load keymap
  147. $.get("keymap.json", function(data){
  148. charmap = data;
  149. });
  150. //Preload export dependencies
  151. let exportDependencies = ["USBconstant.c", "USBconstant.h", "USBhandler.c", "USBhandler.h", "USBHIDMediaKeyboard.c", "USBHIDMediaKeyboard.h"];
  152. let dependenciesPayload = {};
  153. exportDependencies.forEach(function(filename){
  154. $.get("template/src/userUsbHidMediaKeyboard/" + filename, function(data){
  155. dependenciesPayload[filename] = data;
  156. });
  157. });
  158. //Load the keyboard layout
  159. $("#keypad").load("img/layout.svg", function(){
  160. //Print Welcome message
  161. $("#keypad").transition('fly down');
  162. typed = new Typed('#titleText', {
  163. strings: ['<h1 style="margin: 0;">The 4-keys Macropad</h1><p>Pick a key and start typing :)</p>'],
  164. typeSpeed: 50,
  165. });
  166. setTimeout(function(){
  167. $("#titleText").append("");
  168. }, 3000);
  169. //Bind events to the buttons
  170. $(".macrobutton").mouseenter(function(){
  171. if (!$(this).hasClass("active")){
  172. let targetFillObject = $(this).find("path")[0];
  173. $(targetFillObject).attr("fill", "#262626");
  174. }
  175. });
  176. $(".macrobutton").mouseleave(function(){
  177. if (!$(this).hasClass("active")){
  178. let targetFillObject = $(this).find("path")[0];
  179. $(targetFillObject).attr("fill", "black");
  180. }
  181. });
  182. $(".macrobutton").on("click", function(){
  183. handleKeySelect($(this).attr("keyid"));
  184. });
  185. //Bind events to the switch
  186. $(".macroswitch").mouseenter(function(){
  187. $(this).attr("fill", "#262626");
  188. });
  189. $(".macroswitch").mouseleave(function(){
  190. $(this).attr("fill", "black");
  191. });
  192. $(".macroswitch").on("click", function(){
  193. toggleMode();
  194. });
  195. //Make sure the svg is fluid with the container
  196. $("#keypad").find("svg").attr("width", "100%");
  197. });
  198. //Render special keys
  199. let speckey = "~!@#$%^&*()_+{}|:\"?<>";
  200. for (const c of speckey) {
  201. $("#specialKeys").append(`<div class="ui mini white button" onclick="insertSpecialChar(this);" data-value="${c}">${c}</div>`);
  202. }
  203. for (const [key, value] of Object.entries(keycodeMap)) {
  204. $("#specialKeys").append(`<div class="ui mini white button" onclick="clickSpecialFunctionKey(this);" data-value="${key}">${key.substr(4).split("_").join(" ")}</div>`);
  205. codekeyMap[value] = key;
  206. }
  207. for (const [key, value] of Object.entries(mediaKeycode)) {
  208. $("#complexKeys").append(`<option value="${key}">${key.split("_").join(" ")}</option>`);
  209. codekeyMap[value] = key;
  210. }
  211. $(".dropdown").dropdown();
  212. function toggleMode(){
  213. //Save the current key config
  214. saveCurrentKeyConfig();
  215. if (currentMode == 0){
  216. //Mode B
  217. currentMode = 1;
  218. $(".macroswitch_latch").attr("x", "140.96");
  219. $(".modeALed").attr("fill", "black");
  220. $(".modeBLed").attr("fill", "red");
  221. $("#keyseq").attr("class", "B");
  222. }else{
  223. //Mode A
  224. currentMode = 0;
  225. $(".macroswitch_latch").attr("x", "115.387");
  226. $(".modeALed").attr("fill", "#2EA7E0");
  227. $(".modeBLed").attr("fill", "black");
  228. $("#keyseq").attr("class", "A");
  229. }
  230. loadCurrentKeyConfig();
  231. }
  232. function getKeyObjectById(keyid){
  233. let targetKeyDOMObject;
  234. $(".macrobutton").each(function(){
  235. if ($(this).attr("keyid") == keyid){
  236. targetKeyDOMObject = $(this);
  237. }
  238. });
  239. return targetKeyDOMObject;
  240. }
  241. function handleKeySelect(keyid){
  242. //Clear all previous selected key
  243. $(".macrobutton.active").each(function(){
  244. let targetFillObject = $(this).find("path")[0];
  245. $(targetFillObject).attr("fill", "black");
  246. $(this).removeClass('active');
  247. });
  248. //Highlight the key
  249. let targetkeyObject = getKeyObjectById(keyid);
  250. $(targetkeyObject).addClass('active');
  251. let targetFillObject = $(targetkeyObject).find("path")[0];
  252. $(targetFillObject).attr("fill", "#f7f7f7");
  253. //Save previous key configs
  254. saveCurrentKeyConfig();
  255. //Set editing key id
  256. editingKey = keyid;
  257. //Clear and load corrisponding key sequence
  258. loadCurrentKeyConfig();
  259. }
  260. //Load the key config using currentMode and editingKey
  261. function loadCurrentKeyConfig(){
  262. let usingKeySeq = keySeqA;
  263. if (currentMode == 1){
  264. usingKeySeq = keySeqB;
  265. }
  266. $("#keyseq").html(usingKeySeq[editingKey]);
  267. }
  268. //Save the key config using currentMode and editingkey
  269. function saveCurrentKeyConfig(){
  270. let usingKeySeq = keySeqA;
  271. if (currentMode == 1){
  272. usingKeySeq = keySeqB;
  273. }
  274. usingKeySeq[editingKey] = $("#keyseq").html();
  275. }
  276. /*
  277. Key Listener
  278. */
  279. let ctrlHolding = false;
  280. let shiftHolding = false;
  281. let altHolding = false;
  282. let notSupportKeycode = [91, 144, 19, 145, 44];
  283. function keyNotSupported(keycode){
  284. return notSupportKeycode.includes(keycode);
  285. }
  286. $(document).on("keydown", function(e){
  287. let focusedObject = $(':focus');
  288. if ($(focusedObject).is("input")){
  289. //User is searching stuffs
  290. return;
  291. }else if (editingKey == 0){
  292. //No key selected
  293. $(".macrobutton").transition('shake');
  294. return
  295. }
  296. e.preventDefault();
  297. let keyCode = e.keyCode;
  298. if (keyCode == 16){
  299. //Shift
  300. shiftHolding = true;
  301. }else if (keyCode == 17){
  302. //Ctrl
  303. ctrlHolding = true;
  304. }else if (keyCode == 18){
  305. //Alt
  306. altHolding = true;
  307. }
  308. });
  309. $(document).on("keyup", function(e){
  310. let focusedObject = $(':focus');
  311. if ($(focusedObject).is("input")){
  312. //User is searching stuffs
  313. return;
  314. }else if (editingKey == 0){
  315. //No key selected
  316. //$(".macrobutton").transition('shake');
  317. return
  318. }else if (keyNotSupported(e.keyCode)){
  319. //This key is not supported
  320. console.log("This key is not supported: " + e.keyCode + "(" + e.key + ")");
  321. return
  322. }
  323. e.preventDefault();
  324. let keyCode = e.keyCode;
  325. if (keyCode == 16){
  326. //Shift
  327. shiftHolding = false;
  328. }else if (keyCode == 17){
  329. //Ctrl
  330. ctrlHolding = false;
  331. }else if (keyCode == 18){
  332. //Alt
  333. altHolding = false;
  334. }else{
  335. //Other keys
  336. let keyString = e.key;
  337. insertChar(keyString, codekeyMap[keyCode]);
  338. }
  339. });
  340. function insertSpecialChar(btn){
  341. if (editingKey == 0){
  342. //No key selected
  343. $(".macrobutton").transition('shake');
  344. return
  345. }
  346. let charValue = $(btn).attr("data-value");
  347. insertChar(charValue.split("_").join(" "), charValue);
  348. }
  349. function clickSpecialFunctionKey(btn){
  350. if (editingKey == 0){
  351. //No key selected
  352. $(".macrobutton").transition('shake');
  353. return
  354. }
  355. let charValue = $(btn).attr("data-value");
  356. let displayText = charValue.split("_").join(" ")
  357. displayText = displayText.replace("KEY ", "");
  358. insertChar(displayText, charValue);
  359. }
  360. //Add comsumer key from dropdown
  361. function addConsumerKey(){
  362. let dropdownValue = $("#complexKeys").dropdown("get value");
  363. if (dropdownValue == ""){
  364. alert("Select a special media key from the list below before add");
  365. return
  366. }
  367. insertChar(dropdownValue.split("_").join(" "), dropdownValue, true);
  368. }
  369. function insertChar(charString, value, isMediaKey = false){
  370. let indexNumber = $(".actiongroup").length + 1;
  371. let payload = encodeURIComponent(JSON.stringify([charString, value, isMediaKey, shiftHolding, ctrlHolding, altHolding]));
  372. let html = `<div class="actiongroup" data="${payload}">`;
  373. if (shiftHolding){
  374. html += `<div class="ui small button">SHIFT</div> + `;
  375. }
  376. if (ctrlHolding){
  377. html += `<div class="ui small button">CTRL</div> + `;
  378. }
  379. if (altHolding){
  380. html += `<div class="ui small button">ALT</div> + `;
  381. }
  382. html += `<div class="ui small button">${charString}</div>`;
  383. html += `</div>`;
  384. $("#keyseq").append(html);
  385. console.log(shiftHolding, ctrlHolding, altHolding, charString, value);
  386. //Scroll to bottom of the div
  387. $("#keyseq")[0].scrollTop = $("#keyseq")[0].scrollHeight;
  388. }
  389. function deleteLastAction(){
  390. $(".actiongroup").last().remove();
  391. $("#keyseq")[0].scrollTop = $("#keyseq")[0].scrollHeight;
  392. }
  393. function addDelayTime(){
  394. if (editingKey == 0){
  395. //No key selected
  396. $(".macrobutton").transition('shake');
  397. return
  398. }
  399. let delayTime = prompt("Enter delay time in ms (1000ms = 1s)", "100");
  400. if (delayTime == null || delayTime == "") {
  401. //Cancel
  402. return;
  403. }else{
  404. delayTime = parseInt(delayTime);
  405. if (isNaN(delayTime)){
  406. alert("Invalid delay value!");
  407. return
  408. }
  409. $("#keyseq").append(`<div class="actiongroup delay" value="${delayTime}"><i class="hourglass half icon"></i> Delay ${delayTime}ms</div>`);
  410. }
  411. }
  412. /*
  413. Generate Arduino Code
  414. */
  415. function generateArduinoCode(){
  416. //Save the config before generating
  417. saveCurrentKeyConfig();
  418. //Get template from template/template.ino
  419. $.get("template/template.ino", function(data){
  420. let templateCode = data;
  421. //Replace the template code segment
  422. templateCode = templateCode.replace("{{codeA1}}", generateCodeFromSequence(keySeqA, 1));
  423. templateCode = templateCode.replace("{{codeA2}}", generateCodeFromSequence(keySeqA, 2));
  424. templateCode = templateCode.replace("{{codeA3}}", generateCodeFromSequence(keySeqA, 3));
  425. templateCode = templateCode.replace("{{codeA4}}", generateCodeFromSequence(keySeqA, 4));
  426. templateCode = templateCode.replace("{{codeB1}}", generateCodeFromSequence(keySeqB, 1));
  427. templateCode = templateCode.replace("{{codeB2}}", generateCodeFromSequence(keySeqB, 2));
  428. templateCode = templateCode.replace("{{codeB3}}", generateCodeFromSequence(keySeqB, 3));
  429. templateCode = templateCode.replace("{{codeB4}}", generateCodeFromSequence(keySeqB, 4));
  430. let arduinoCode = templateCode;
  431. //Get other depednencies and pack it into a zip file
  432. var zip = new JSZip();
  433. //Generate a arduino compatible folder structure
  434. var rootfolder = zip.folder("macrokey");
  435. rootfolder.file("macrokey.ino", arduinoCode);
  436. //Inject the dependencies into the userUsbHidMediaKeyboard folder
  437. var srcfolder = rootfolder.folder("src");
  438. var usbHidFolder = srcfolder.folder("userUsbHidMediaKeyboard");
  439. for (const [filename, content] of Object.entries(dependenciesPayload)) {
  440. usbHidFolder.file(filename, content);
  441. }
  442. zip.generateAsync({type:"blob"}).then(function(content) {
  443. // see FileSaver.js
  444. saveAs(content, "macrokey.zip");
  445. });
  446. })
  447. }
  448. /*
  449. Config Save and Load
  450. */
  451. //Seq: keyseqA or B, id = {1/2/3/4} (aka key id, see PCB silkscreen)
  452. function generateCodeFromSequence(Seq, id){
  453. //Save the config before generating
  454. saveCurrentKeyConfig();
  455. function getCodeFromSeq(html){
  456. let code = '';
  457. //console.log(html);
  458. //console.log($(html));
  459. $(html).each(function(){
  460. if ($(this).hasClass("delay")){
  461. let delayTime = $(this).attr("value");
  462. code += `delay(${delayTime});\n`;
  463. return;
  464. }
  465. let keyCombination = $(this).attr("data");
  466. if (keyCombination == undefined){
  467. return;
  468. }
  469. keyCombination = JSON.parse(decodeURIComponent(keyCombination));
  470. let shiftHolding = keyCombination[3];
  471. let ctrlHolding = keyCombination[4];
  472. let altHolding = keyCombination[5];
  473. if (shiftHolding){
  474. code += `Keyboard_press(KEY_LEFT_SHIFT);\ndelay(30);\n`;
  475. }
  476. if (ctrlHolding){
  477. code += `Keyboard_press(KEY_LEFT_CTRL);\ndelay(30);\n`;
  478. }
  479. if (altHolding){
  480. code += `Keyboard_press(KEY_LEFT_ALT);\ndelay(30);\n`;
  481. }
  482. if (keyCombination[2] == true){
  483. //Ths is a media key. Use value from [1]
  484. code += `Consumer_write(${keyCombination[1]});\ndelay(30);\n`;
  485. }else{
  486. //This is a normal key. Use value from [0]
  487. code += `Keyboard_write(${remapValueToHIDKeyboardConst(keyCombination[0])});\ndelay(30);\n`;
  488. }
  489. if (shiftHolding){
  490. code += `Keyboard_release(KEY_LEFT_SHIFT);\ndelay(30);\n`;
  491. }
  492. if (ctrlHolding){
  493. code += `Keyboard_release(KEY_LEFT_CTRL);\ndelay(30);\n`;
  494. }
  495. if (altHolding){
  496. code += `Keyboard_release(KEY_LEFT_ALT);\ndelay(30);\n`;
  497. }
  498. });
  499. return code;
  500. }
  501. let codeSnippet = getCodeFromSeq(Seq[id]);
  502. return codeSnippet;
  503. }
  504. function saveConfigAsFile(){
  505. let config = JSON.stringify([keySeqA, keySeqB]);
  506. download(Date.now() + "_macro-conf.json", config);
  507. }
  508. function loadConfigFromFile(){
  509. let input = document.createElement('input');
  510. input.type = 'file';
  511. input.multiple = true;
  512. input.onchange = e => {
  513. let files = e.target.files;
  514. for (var i = 0; i < files.length; i++){
  515. let read = new FileReader();
  516. read.readAsBinaryString(files[i]);
  517. read.onloadend = function(){
  518. //Content of the file selected
  519. try{
  520. handleKeySelect(1);
  521. let macroSequences = JSON.parse(read.result);
  522. keySeqA = macroSequences[0];
  523. keySeqB = macroSequences[1];
  524. $("#selectedKey").html('<i class="keyboard icon"></i> Currently Editing Key: '+ 1);
  525. $($(".macrokey")[0]).addClass("active");
  526. loadCurrentKeyConfig();
  527. }catch(ex){
  528. alert("Unable to load config file. See console for more info.")
  529. console.log(ex);
  530. }
  531. }
  532. }
  533. }
  534. input.click();
  535. }
  536. function handleFileSelect (e) {
  537. var files = e.target.files;
  538. if (files.length < 1) {
  539. alert('select a file...');
  540. return;
  541. }
  542. var file = files[0];
  543. var reader = new FileReader();
  544. reader.onload = onFileLoaded;
  545. reader.readAsDataURL(file);
  546. }
  547. //Download functions
  548. function download(filename, text) {
  549. var element = document.createElement('a');
  550. element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent(text));
  551. element.setAttribute('download', filename);
  552. element.style.display = 'none';
  553. document.body.appendChild(element);
  554. element.click();
  555. document.body.removeChild(element);
  556. }
  557. </script>
  558. </body>
  559. </html>