├── enhancedYouTube.css ├── enhancedTimer.css ├── enhancedPDF.css ├── enhancedLanguage.js ├── enhancedLanguage.css ├── enhancedUtility.js ├── enhancedOCR.js ├── enhancedTimer.js ├── README.md ├── enhancedYouTube.js └── enhancedPDF.js /enhancedYouTube.css: -------------------------------------------------------------------------------- 1 | .yt-activated{ 2 | border : 0px; 3 | border-style : inset; 4 | border-radius : 25px; 5 | } 6 | 7 | .timestamp-control{ 8 | background-color: rgba(108,109,36,0.1); 9 | color: rgb(251,106,13); 10 | margin-right: 8px; 11 | margin-top: 0px; 12 | margin-left: 0px; 13 | margin-bottom: 5px; 14 | 15 | border-radius: 50% !important; 16 | border-style: inset; 17 | border-color: #FF3200; 18 | font-size: 0.9em; 19 | } 20 | .timestamp-control:hover { 21 | background-color: rgba(108,109,36,0.25); 22 | border-style: outset; 23 | color: #FFFFFF; 24 | } 25 | /* Hide YouTube Breadcrumb */ 26 | .parent-path-wrapper > div > span > div > iframe, 27 | .parent-path-wrapper > div > span > .hoverparent { 28 | display: none; 29 | } -------------------------------------------------------------------------------- /enhancedTimer.css: -------------------------------------------------------------------------------- 1 | .timer-activated{ 2 | box-shadow: 1px 1px !important; 3 | border-radius: 25px 50px 50px 25px !important; 4 | color: rgb(41,44,54) !important; 5 | min-height: 16px!important; 6 | height:18px!important; 7 | margin-bottom: 3px!important; 8 | padding-bottom: 1px!important; 9 | font-weight: 800!important; 10 | } 11 | .timer-activated.running{ 12 | /*background-color : rgb(6,226,6);*/ 13 | background-image: linear-gradient(rgb(0,190,0), rgb(51,221,51), rgb(0,190,0)); 14 | color: white !important; 15 | } 16 | 17 | .timer-activated.running:hover{ 18 | /*background-color : rgb(6,226,6);*/ 19 | background-image: linear-gradient(rgb(51,221,51), rgb(0,190,0),rgb(51,221,51)); 20 | color: white !important; 21 | } 22 | 23 | .timer-activated.paused{ 24 | /*background-color : rgb(255,38,38);*/ 25 | background-image: linear-gradient(rgb(219,0,0), rgb(255,62,62), rgb(219,0,0)); 26 | color: white !important; 27 | } 28 | 29 | .timer-activated.paused:hover{ 30 | /*background-color : rgb(255,38,38);*/ 31 | background-image: linear-gradient(rgb(255,62,62), rgb(219,0,0), rgb(255,62,62)); 32 | color: white !important; 33 | } 34 | 35 | .timer-zoomed, .counter-zoomed{ 36 | display: none; 37 | } 38 | 39 | .counter-activated{ 40 | box-shadow: 1px 1px !important; 41 | border-radius: 25px 50px 50px 25px !important; 42 | min-height: 16px!important; 43 | height:18px!important; 44 | margin-bottom: 3px!important; 45 | padding-bottom: 1px!important; 46 | font-weight: 800!important; 47 | background-image: linear-gradient(rgb(0, 125, 209), rgb(51, 167, 221), rgb(0, 125, 209)) !important; 48 | color: white !important; 49 | } -------------------------------------------------------------------------------- /enhancedPDF.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | --col1: rgba(255, 243, 174, .8); 3 | --col2: rgba(255, 132, 132, .8); 4 | --col3: rgba(155, 253, 130, .8); 5 | --col4: rgba(130, 169, 255, .8); 6 | --col5: rgba(220, 131, 255, .7); 7 | --col6: rgba(172,172,172, .7); 8 | } 9 | 10 | [data-tag^="h:"] { 11 | display:none !important; 12 | } 13 | 14 | [data-tag^="h:"] + .rm-highlight, 15 | [data-tag^="h:"] + span > .rm-page-ref--link { 16 | color: rgb(0,0,0) !important; 17 | /*border-radius: 5px; 18 | padding-left: 5px; 19 | padding-right: 5px; 20 | font-weight: bold;*/ 21 | } 22 | 23 | [data-tag^="h:yellow"] + .rm-highlight, 24 | [data-tag^="h:yellow"] + span > .rm-page-ref--link { 25 | background-color: var(--col1) !important; 26 | } 27 | [data-tag^="h:yellow"] + .rm-italics, 28 | [data-tag^="h:yellow"] + .rm-bold 29 | {color: var(--col1);} 30 | 31 | [data-tag^="h:red"] + .rm-highlight, 32 | [data-tag^="h:red"] + span > .rm-page-ref--link { 33 | background-color: var(--col2) !important; 34 | } 35 | [data-tag^="h:red"] + .rm-italics, 36 | [data-tag^="h:red"] + .rm-bold 37 | {color: var(--col2); } 38 | 39 | 40 | [data-tag^="h:green"] + .rm-highlight, 41 | [data-tag^="h:green"] + span > .rm-page-ref--link { 42 | background-color: var(--col3) !important; 43 | } 44 | [data-tag^="h:green"] + .rm-italics, 45 | [data-tag^="h:green"] + .rm-bold 46 | {color: var(--col3); } 47 | 48 | [data-tag^="h:blue"] + .rm-highlight, 49 | [data-tag^="h:blue"] + span > .rm-page-ref--link { 50 | background-color: var(--col4) !important; 51 | } 52 | [data-tag^="h:blue"] + .rm-italics, 53 | [data-tag^="h:blue"] + .rm-bold 54 | {color: var(--col4); } 55 | 56 | [data-tag^="h:purple"] + .rm-highlight, 57 | [data-tag^="h:purple"] + span > .rm-page-ref--link { 58 | background-color: var(--col5) !important; 59 | } 60 | [data-tag^="h:purple"] + .rm-italics, 61 | [data-tag^="h:purple"] + .rm-bold 62 | {color: var(--col5); } 63 | 64 | [data-tag^="h:grey"] + .rm-highlight, 65 | [data-tag^="h:grey"] + span > .rm-page-ref--link { 66 | background-color: var(--col6) !important; 67 | } 68 | [data-tag^="h:grey"] + .rm-italics, 69 | [data-tag^="h:grey"] + .rm-bold 70 | {color: var(--col6); } 71 | 72 | /*All btns*/ 73 | .btn{padding: 0px !important; border: 3px !important;} 74 | 75 | /*All main highlight btns*/ 76 | .btn-pdf-activated{ 77 | border-radius: 14px !important; 78 | font-size: 12px !important; 79 | font-weight: bold; 80 | min-width: 18px !important; 81 | min-height: 18px !important; 82 | margin-top: 3px !important; 83 | } 84 | 85 | .btn-main-annotation{ 86 | background-color : rgb(221,220,220) !important; 87 | color: rgb(0,0,0); 88 | margin-top: 0px !important; 89 | } 90 | 91 | /*All reference to highlight buttons*/ 92 | .btn-rep-text{ 93 | } 94 | .btn-rep-alias{ 95 | } 96 | .btn-ref-annotation{ 97 | background-image: linear-gradient(rgb(249,249,49), rgb(246,246,170), rgb(210,210,9)); 98 | color: rgb(6,6,6); 99 | } 100 | 101 | /* Hide PDF Breadcrumb */ 102 | .parent-path-wrapper > div > span > div > div { 103 | display: none; 104 | } -------------------------------------------------------------------------------- /enhancedLanguage.js: -------------------------------------------------------------------------------- 1 | function isRTL(s) { 2 | let ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' + '\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF', 3 | rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC', 4 | rtlDirCheck = new RegExp('^[^' + ltrChars + ']*[' + rtlChars + ']'); 5 | 6 | return rtlDirCheck.test(s); 7 | }; 8 | 9 | function changeDir(txtEl, forceCheck = false) { 10 | if (!txtEl) return; 11 | let content = (txtEl.innerText !== '') ? txtEl.innerText : txtEl.textContent; 12 | if (content === undefined) return; 13 | if (content.length > 0) { 14 | let container = txtEl.closest('.roam-block-container') 15 | let header = (container === null) 16 | container = header ? txtEl.closest('h1') : container 17 | let starPage = (container === null) 18 | container = starPage ? txtEl.closest('a') : container 19 | if ((!container.dataset.direction || container.dataset.wasTxt) || forceCheck) { //forceCheck to response to dynamic change in textareas 20 | let newContent = content.replace(/[0-9\x20-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]*/gi, ''); 21 | let rtl = isRTL(newContent[0]) 22 | if(forceCheck) container.dataset.wasTxt = 'true'; 23 | else if(container.dataset.wasTxt) delete container.dataset.wasTxt 24 | if (rtl) container.dataset.direction = 'rtl'; 25 | else container.dataset.direction = 'ltr'; 26 | if (!header && !starPage) { 27 | let main = txtEl.closest('.rm-block-main') 28 | main.classList.remove('main-rtl', 'main-ltr') 29 | let multibar = container.querySelector('.rm-multibar') 30 | multibar.classList.remove('multibar-rtl', 'multibar-ltr') 31 | if (rtl) { 32 | main.classList.add('main-rtl'); 33 | multibar.classList.add('multibar-rtl'); 34 | } 35 | else { 36 | main.classList.add('main-ltr'); 37 | multibar.classList.add('multibar-ltr'); 38 | } 39 | 40 | } 41 | } 42 | } 43 | } 44 | 45 | const activateAutoDir = () => { 46 | if (typeof (document) == 'undefined') return; 47 | //Fix all txts in blocks 48 | Array.from(document.querySelectorAll('div.rm-block__input > span')).forEach(txtEl => changeDir(txtEl)); 49 | Array.from(document.querySelectorAll('h1.rm-title-display > span')).forEach(txtEl => changeDir(txtEl)); 50 | //Fix star pages in left side bar 51 | Array.from(document.querySelectorAll('div.starred-pages > a > div')).forEach(txtEl => changeDir(txtEl)); 52 | 53 | //Fix txts in the body - textarea 54 | changeDir(document.querySelector('textarea'), true); 55 | //Fix txts in the title - textarea 56 | changeDir(document.querySelector('h1.rm-title-editing-display > span'), true); 57 | //Fix dropdown menu - page ref 58 | // let dropDown = document.querySelector('div.main-rtl > div.rm-autocomplete__wrapper > div.bp3-elevation-3'); 59 | // if(dropDown){ 60 | // if(!dropDown.classList.contains('direction-fixed')){ 61 | // console.log("inner if") 62 | // dropDown.classList.add('direction-fixed') 63 | // let main = document.querySelector('textarea'); 64 | // let fullWidth = window.getComputedStyle(main).getPropertyValue('width'); 65 | // let left = window.getComputedStyle(dropDown).getPropertyValue('left'); 66 | // console.log("fullWidth", fullWidth) 67 | // dropDown.style.left = parseInt(fullWidth, 10) - parseInt(left, 10) + "px" 68 | // } 69 | // } 70 | } 71 | 72 | 73 | 74 | 75 | setInterval(activateAutoDir, 1000); 76 | -------------------------------------------------------------------------------- /enhancedLanguage.css: -------------------------------------------------------------------------------- 1 | .rm-block-separator { 2 | min-width: var(--rm-block-sep-min-width); 3 | } 4 | /*****RTL*****/ 5 | /**Direction**/ 6 | .main-rtl:not(.katex):not(.katex span){ 7 | direction: rtl; 8 | } 9 | 10 | .multibar-rtl{ 11 | left:unset; 12 | right:-10px; 13 | border-right:unset; 14 | border-left:1px solid #BFCCD6 15 | } 16 | 17 | .multibar-rtl:hover{ 18 | border-right:unset; 19 | border-left:3px solid #5C7080 20 | } 21 | 22 | .main-rtl .rm-caret.rm-caret-closed { 23 | transform: rotate(90deg) !important; 24 | } 25 | 26 | .roam-block-container[data-direction*='rtl'] 27 | .rm-emoji-block-view { 28 | margin-left: 0px; 29 | margin-right: 40px; 30 | direction: rtl; 31 | } 32 | .roam-block-container[data-direction*='rtl'] 33 | .rm-emoji-block-view .rm-emoji-button .rm-emoji-number { 34 | height: 100%; 35 | font-size: 10px; 36 | margin-left: 6px; 37 | margin-right: 0px; 38 | font-weight: 500; 39 | } 40 | 41 | .roam-block-container[data-direction*='rtl']> .rm-block-children { 42 | margin-right: var(--rtl-margin-right) !important; 43 | margin-left: var(--rtl-margin-left) !important; 44 | } 45 | 46 | .main-rtl > .controls > .rm-bullet { 47 | margin-top: var(--rtl-bullet-margin-top); 48 | } 49 | 50 | .main-rtl > .controls > .block-expand > .rm-caret{ 51 | margin-top: var(--rtl-control-margin-top); 52 | } 53 | 54 | /***************/ 55 | /*****Fonts*****/ 56 | .main-rtl span:not(.bp3-icon-standard):not(.katex):not(.katex span){ 57 | font-family: var(--rtl-font), var(--rtl-generic-font); 58 | font-size: var(--rtl-font-size); 59 | } 60 | 61 | .main-rtl textarea{ 62 | font-family: var(--rtl-font), var(--rtl-generic-font); 63 | background-color: var(--rtl-textarea-background-color); 64 | font-size: var(--rtl-textarea-font-size); 65 | line-height: var(--rtl-textarea-line-height); 66 | } 67 | 68 | a[data-direction*='rtl'] > div{ 69 | direction: rtl; 70 | font-family: var(--rtl-font), var(--rtl-generic-font); 71 | } 72 | 73 | h1.rm-title-display[data-direction*='rtl']{ 74 | direction: rtl; 75 | } 76 | 77 | h1.rm-title-display[data-direction*='rtl'] span{ 78 | font-family:var(--rtl-font), var(--rtl-generic-font); 79 | direction: rtl; 80 | } 81 | 82 | h1.rm-title-editing-display[data-direction*='rtl'] textarea{ 83 | font-family:var(--rtl-font), var(--rtl-generic-font); 84 | direction: rtl; 85 | } 86 | 87 | /******************************************************/ 88 | .CodeMirror-line span{ 89 | font-family:monospace !important; 90 | font-size: 1em; 91 | } 92 | .roam-block-container[data-direction*='rtl'] .rm-block-children 93 | .rm-bullet--numbered-double-digit { 94 | margin-left: 10px; 95 | margin-right: unset; 96 | } 97 | .roam-block-container[data-direction*='rtl'] .rm-block-children 98 | .rm-bullet--numbered-single-digit { 99 | margin-left: -4px; 100 | margin-right: unset; 101 | } 102 | /******************************************************/ 103 | /*****LTR*****/ 104 | /**Direction**/ 105 | .main-ltr:not(.katex):not(.katex span){ 106 | direction: ltr; 107 | } 108 | 109 | .multibar-ltr{ 110 | right:unset; 111 | left:-10px; 112 | border-left:unset; 113 | border-right:1px solid #BFCCD6 114 | } 115 | 116 | .multibar-rtl:hover{ 117 | border-left:unset; 118 | border-right:3px solid #5C7080 119 | } 120 | 121 | .main-ltr .rm-caret.rm-caret-closed { 122 | transform: rotate(-90deg) !important; 123 | } 124 | 125 | .roam-block-container[data-direction*='ltr'] 126 | .rm-emoji-block-view { 127 | margin-left: 40px; 128 | margin-right: 0px; 129 | direction: ltr; 130 | } 131 | .roam-block-container[data-direction*='ltr'] 132 | .rm-emoji-block-view .rm-emoji-button .rm-emoji-number { 133 | height: 100%; 134 | font-size: 10px; 135 | margin-left: 0px; 136 | margin-right: 6px; 137 | font-weight: 500; 138 | } 139 | 140 | .roam-block-container[data-direction*='ltr']> .rm-block-children { 141 | margin-left: var(--ltr-margin-left) !important; 142 | margin-right: var(--ltr-margin-right) !important; 143 | } 144 | 145 | .main-ltr > .controls > .rm-bullet { 146 | margin-top: var(--ltr-bullet-margin-top); 147 | } 148 | 149 | .main-ltr > .controls > .block-expand > .rm-caret{ 150 | margin-top: var(--ltr-control-margin-top); 151 | } 152 | 153 | /***************/ 154 | /*****Fonts*****/ 155 | .main-ltr span:not(.bp3-icon-standard):not(.katex):not(.katex span){ 156 | font-family: var(--ltr-font), var(--ltr-generic-font); 157 | font-size: var(--ltr-font-size); 158 | } 159 | 160 | .main-ltr textarea{ 161 | font-family: var(--ltr-font), var(--ltr-generic-font); 162 | background-color: var(--ltr-textarea-background-color) !important; 163 | font-size: var(--ltr-textarea-font-size); 164 | line-height: var(--ltr-textarea-line-height); 165 | } 166 | 167 | a[data-direction*='ltr'] > div{ 168 | direction: ltr; 169 | font-family: var(--ltr-font), var(--ltr-generic-font); 170 | } 171 | 172 | 173 | 174 | h1.rm-title-display[data-direction*='ltr']{ 175 | direction: ltr; 176 | } 177 | 178 | h1.rm-title-display[data-direction*='ltr'] span{ 179 | font-family: var(--ltr-font), var(--ltr-generic-font); 180 | direction: ltr; 181 | } 182 | 183 | h1.rm-title-editing-display[data-direction*='ltr'] textarea{ 184 | font-family: var(--ltr-font), var(--ltr-generic-font); 185 | direction: ltr; 186 | } 187 | 188 | /******************************************************/ -------------------------------------------------------------------------------- /enhancedUtility.js: -------------------------------------------------------------------------------- 1 | var ccc = window.ccc || {}; 2 | ccc.util = ((c3u) => { 3 | ///////////////Front-End/////////////// 4 | c3u.getUidOfContainingBlock = (el) => { 5 | return el.closest('.rm-block__input').id.slice(-9) 6 | } 7 | 8 | c3u.insertAfter = (newEl, anchor) => { 9 | anchor.parentElement.insertBefore(newEl, anchor.nextSibling) 10 | } 11 | 12 | c3u.getNthChildUid = (parentUid, order) => { 13 | const allChildren = c3u.allChildrenInfo(parentUid)[0][0].children; 14 | const childrenOrder = allChildren.map(function (child) { return child.order; }); 15 | const index = childrenOrder.findIndex(el => el === order); 16 | return index !== -1 ? allChildren[index].uid : null; 17 | } 18 | 19 | c3u.sleep = m => new Promise(r => setTimeout(r, m)) 20 | 21 | c3u.createPage = (pageTitle) => { 22 | let pageUid = c3u.createUid() 23 | const status = window.roamAlphaAPI.createPage( 24 | { 25 | "page": 26 | { "title": pageTitle, "uid": pageUid } 27 | }) 28 | return status ? pageUid : null 29 | } 30 | 31 | c3u.updateBlockString = (blockUid, newString) => { 32 | return window.roamAlphaAPI.updateBlock({ 33 | block: { uid: blockUid, string: newString } 34 | }); 35 | } 36 | 37 | c3u.hashCode = (str) => { 38 | let hash = 0, i, chr; 39 | for (i = 0; i < str.length; i++) { 40 | chr = str.charCodeAt(i); 41 | hash = ((hash << 5) - hash) + chr; 42 | hash |= 0; // Convert to 32bit integer 43 | } 44 | return hash; 45 | } 46 | 47 | c3u.createChildBlock = (parentUid, order, childString, childUid) => { 48 | return window.roamAlphaAPI.createBlock( 49 | { 50 | location: { "parent-uid": parentUid, order: order }, 51 | block: { string: childString.toString(), uid: childUid } 52 | }) 53 | } 54 | 55 | c3u.openBlockInSidebar = (windowType, blockUid) => { 56 | return window.roamAlphaAPI.ui.rightSidebar.addWindow({ window: { type: windowType, 'block-uid': blockUid } }) 57 | } 58 | 59 | c3u.deletePage = (pageUid) => { 60 | return window.roamAlphaAPI.deletePage({ page: { uid: pageUid } }); 61 | } 62 | 63 | 64 | c3u.createUid = () => { 65 | return roamAlphaAPI.util.generateUID(); 66 | } 67 | 68 | 69 | 70 | ///////////////Back-End/////////////// 71 | c3u.existBlockUid = (blockUid) => { 72 | const res = window.roamAlphaAPI.q( 73 | `[:find (pull ?block [:block/uid]) 74 | :where 75 | [?block :block/uid \"${blockUid}\"]]`) 76 | return res.length ? blockUid : null 77 | } 78 | 79 | c3u.deleteBlock = (blockUid) => { 80 | return window.roamAlphaAPI.deleteBlock({ "block": { "uid": blockUid } }); 81 | } 82 | 83 | c3u.parentBlockUid = (blockUid) => { 84 | const res = window.roamAlphaAPI.q( 85 | `[:find (pull ?parent [:block/uid]) 86 | :where 87 | [?parent :block/children ?block] 88 | [?block :block/uid \"${blockUid}\"]]`) 89 | return res.length ? res[0][0].uid : null 90 | } 91 | 92 | c3u.blockString = (blockUid) => { 93 | return window.roamAlphaAPI.q( 94 | `[:find (pull ?block [:block/string]) 95 | :where [?block :block/uid \"${blockUid}\"]]`)[0][0].string 96 | } 97 | 98 | c3u.allChildrenInfo = (blockUid) => { 99 | let results = window.roamAlphaAPI.q( 100 | `[:find (pull ?parent 101 | [* {:block/children [:block/string :block/uid :block/order]}]) 102 | :where 103 | [?parent :block/uid \"${blockUid}\"]]`) 104 | return (results.length == 0) ? undefined : results 105 | 106 | } 107 | 108 | c3u.queryAllTxtInChildren = (blockUid) => { 109 | return window.roamAlphaAPI.q(`[ 110 | :find (pull ?block [ 111 | :block/string 112 | {:block/children ...} 113 | ]) 114 | :where [?block :block/uid \"${blockUid}\"]]`) 115 | } 116 | 117 | c3u.getPageUid = (pageTitle) => { 118 | const res = window.roamAlphaAPI.q( 119 | `[:find (pull ?page [:block/uid]) 120 | :where [?page :node/title \"${pageTitle}\"]]`) 121 | return res.length ? res[0][0].uid : null 122 | } 123 | 124 | c3u.getOrCreatePageUid = (pageTitle, initString = null) => { 125 | let pageUid = c3u.getPageUid(pageTitle) 126 | if (!pageUid) { 127 | pageUid = c3u.createPage(pageTitle); 128 | if (initString) 129 | c3u.createChildBlock(pageUid, 0, initString, c3u.createUid()); 130 | } 131 | return pageUid; 132 | } 133 | 134 | c3u.isAncestor = (a, b) => { 135 | const results = window.roamAlphaAPI.q( 136 | `[:find (pull ?root [* {:block/children [:block/uid {:block/children ...}]}]) 137 | :where 138 | [?root :block/uid \"${a}\"]]`); 139 | if (!results.length) return false; 140 | let descendantUids = []; 141 | c3u.getUidFromNestedNodes(results[0][0], descendantUids) 142 | return descendantUids.includes(b); 143 | } 144 | 145 | c3u.getUidFromNestedNodes = (node, descendantUids) => { 146 | if (node.uid) descendantUids.push(node.uid) 147 | if (node.children) 148 | node.children.forEach(child => c3u.getUidFromNestedNodes(child, descendantUids)) 149 | } 150 | 151 | return c3u; 152 | })(ccc.util || {}); 153 | 154 | -------------------------------------------------------------------------------- /enhancedOCR.js: -------------------------------------------------------------------------------- 1 | const ocrParams = window.ocrParams; 2 | /* Begin Importing Other Packages */ 3 | if (!document.getElementById("Tesseract")) { 4 | let s = document.createElement("script"); 5 | s.type = "text/javascript"; 6 | s.src = "https://unpkg.com/tesseract.js@2.0.0/dist/tesseract.min.js"; 7 | s.id = "Tesseract" 8 | document.getElementsByTagName("head")[0].appendChild(s); 9 | } 10 | if (!document.getElementById("Mousetrap")) { 11 | let s = document.createElement("script"); 12 | s.type = "text/javascript"; 13 | s.src = "https://unpkg.com/mousetrap@1.6.5/mousetrap.js"; 14 | s.id = "Mousetrap" 15 | s.onload = () => { bindShortkeys() } 16 | document.getElementsByTagName("head")[0].appendChild(s); 17 | } 18 | /* End Importing Other Packages */ 19 | 20 | /* Begin Importing Utility Functions */ 21 | if (typeof ccc !== 'undefined' && typeof ccc.util !== 'undefined') { 22 | //Somebody has already loaded the utility 23 | startC3OcrExtension(); 24 | } else { 25 | let s = document.createElement("script"); 26 | s.type = "text/javascript"; 27 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedUtility.js" 28 | s.id = 'c3util4ocr' 29 | s.onload = () => { startC3OcrExtension() } 30 | try { document.getElementById('c3util').remove() } catch (e) { }; 31 | document.getElementsByTagName('head')[0].appendChild(s); 32 | } 33 | /* End Importing Utility Functions */ 34 | 35 | function startC3OcrExtension() { 36 | var ccc = window.ccc || {}; 37 | var c3u = ccc.util; 38 | let parsedStr = ''; 39 | 40 | function scanForNewImages(mutationsList = null) { 41 | let oldImg = document.querySelectorAll('.rm-inline-img.img-ready4ocr'); 42 | let curImg = document.getElementsByClassName('rm-inline-img'); 43 | if (oldImg.length === curImg.length) return; 44 | Array.from(curImg).forEach(im => { 45 | if (!im.classList.contains('img-ready4ocr')) { 46 | im.classList.add('img-ready4ocr'); 47 | im.addEventListener('click', async function (e) { 48 | let ocrBlockUid; 49 | try { 50 | if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) { 51 | e.preventDefault(); 52 | e.stopPropagation(); 53 | e.stopImmediatePropagation(); 54 | 55 | const blockUid = c3u.getUidOfContainingBlock(e.target); 56 | ocrBlockUid = c3u.createUid(); 57 | c3u.createChildBlock(blockUid, 0, "Granting wishes...", ocrBlockUid) 58 | 59 | parsedStr = await parseImage(e); 60 | let postfix = ocrParams.saveRef2Img ? " [*](" + e.target.src + ") " : ""; 61 | c3u.updateBlockString(ocrBlockUid, parsedStr + postfix) 62 | } 63 | } 64 | catch (err) { 65 | let msg = "OCR was unsuccessful." 66 | c3u.updateBlockString(ocrBlockUid, msg); 67 | } 68 | }); 69 | } 70 | }); 71 | }; 72 | 73 | 74 | async function parseImage(e) { 75 | const tempImg = new Image(); 76 | tempImg.crossOrigin = "Anonymous"; 77 | const canvas = document.createElement("canvas"); 78 | const ctx = canvas.getContext("2d"); 79 | 80 | tempImg.src = "https://ccc-cors-anywhere.herokuapp.com/" + e.target.src //+ "?not-from-cache-please" 81 | let str = tempImg.onload = async function () { 82 | let ocrStr; 83 | canvas.width = tempImg.width; 84 | canvas.height = tempImg.height; 85 | ctx.drawImage(tempImg, 0, 0); 86 | if (e.ctrlKey || e.metaKey) { //Math OCR 87 | ocrStr = await parseMath(e.target.src); 88 | } 89 | if (e.shiftKey) { 90 | ocrStr = await parseLan(tempImg, ocrParams.lang1); 91 | } 92 | if (e.altKey) { 93 | ocrStr = await parseLan(tempImg, ocrParams.lang2); 94 | } 95 | return ocrStr 96 | }(); 97 | return str; 98 | } 99 | 100 | //OCR the image in url using language lan 101 | async function parseLan(url, lan) { 102 | return Tesseract.recognize(url, lan) 103 | .then(({ data: { text } }) => { 104 | return (text.replace(/\n/g, " ")); 105 | }); 106 | } 107 | 108 | //OCR the given image using the Mathpix API 109 | async function parseMath(url) { 110 | //Send the request to Mathpix API 111 | let ocrReq = { 112 | "src": url, 113 | "formats": "text", 114 | } 115 | let latexStr = await postData('https://api.mathpix.com/v3/text', ocrReq) 116 | .then(response => { 117 | return (response.text) 118 | }); 119 | //Make the math Roam-readable 120 | latexStr = latexStr.replace(/(\\\( )|( \\\))/g, "$$$$"); 121 | latexStr = latexStr.replace(/(\n\\\[\n)|(\n\\\]\n?)/g, " $$$$ "); 122 | return (latexStr) 123 | } 124 | 125 | async function postData(url = '', data = {}) { 126 | // Default options are marked with * 127 | const response = await fetch(url, { 128 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 129 | headers: { 130 | "content-type": "application/json", 131 | "app_id": ocrParams.appID, 132 | "app_key": ocrParams.appKey 133 | }, 134 | body: JSON.stringify(data) // body data type must match "Content-Type" header 135 | }); 136 | return response.json(); // parses JSON response into native JavaScript objects 137 | } 138 | 139 | observerImg = new MutationObserver(scanForNewImages); 140 | observerImg.observe(document, { childList: true, subtree: true }) 141 | } 142 | 143 | function bindShortkeys() { 144 | Mousetrap.prototype.stopCallback = function () { return false } 145 | 146 | Mousetrap.bind(ocrParams.cleanKey, async function (e) { 147 | e.preventDefault(); 148 | const activeTxt = document.querySelector('textarea.rm-block-input'); 149 | let recognizedTxt = activeTxt.value; 150 | const blockUid = window.ccc.util.getUidOfContainingBlock(activeTxt); 151 | const parentUid = window.ccc.util.parentBlockUid(blockUid); 152 | window.ccc.util.deleteBlock(blockUid); 153 | window.ccc.util.updateBlockString(parentUid, recognizedTxt); 154 | return false; 155 | }, 'keydown'); 156 | } -------------------------------------------------------------------------------- /enhancedTimer.js: -------------------------------------------------------------------------------- 1 | /* Begin Importing Utility Functions */ 2 | if (typeof ccc !== 'undefined' && typeof ccc.util !== 'undefined') { 3 | //Somebody has already loaded the utility 4 | startC3TimerExtension(); startC3CounterExtension(); 5 | } else { 6 | let s = document.createElement("script"); 7 | s.type = "text/javascript"; 8 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedUtility.js" 9 | s.id = 'c3util4timer' 10 | s.onload = () => { startC3TimerExtension(); startC3CounterExtension(); } 11 | //try { document.getElementById('c3util').remove() } catch(e){}; 12 | document.getElementsByTagName('head')[0].appendChild(s); 13 | } 14 | /* End Importing Utility Functions */ 15 | 16 | 17 | 18 | function startC3CounterExtension() { 19 | var ccc = window.ccc || {}; 20 | var c3u = ccc.util; 21 | 22 | function activateCounter() { 23 | Array.from(document.getElementsByTagName('button')) 24 | .filter(btn => btn.textContent === "c3-counter") 25 | .forEach(counter => { 26 | if (counter.closest('.rm-zoom-item') !== null) { 27 | counter.innerHTML = '' 28 | counter.classList.add('counter-zoomed') 29 | } else { 30 | if (!counter.classList.contains('counter-activated')) { 31 | const isRef = counter.closest('.rm-block-ref'); 32 | const counterBlockUid = isRef ? isRef.dataset.uid : c3u.getUidOfContainingBlock(counter); 33 | const match = c3u.blockString(counterBlockUid).match(/.*{{\[\[c3-counter\]\]:\s*(\d*)}}.*/) 34 | counter.innerHTML = (match) ? match[1] : '0' 35 | if (!match) { 36 | const replacement = c3u.blockString(counterBlockUid).replace(']}}', ']:0}}'); 37 | c3u.updateBlockString(counterBlockUid, replacement) 38 | } 39 | if (!isRef) { 40 | counter.addEventListener("click", counterClicked) 41 | } 42 | } 43 | } 44 | counter.classList.add('counter-activated') 45 | }); 46 | }; 47 | 48 | function counterClicked(e) { 49 | const counter = e.target; 50 | const counterBlockUid = c3u.getUidOfContainingBlock(counter); 51 | let counterVal = parseInt(e.target.innerHTML); 52 | if (e.ctrlKey) { 53 | e.preventDefault(); 54 | e.stopPropagation(); 55 | e.stopImmediatePropagation(); 56 | counterVal-- 57 | } else { 58 | counterVal++ 59 | } 60 | counterVal = counterVal < 0 ? 0 : counterVal; 61 | e.target.innerHTML = counterVal; //to speed up the render update this first 62 | if (!counter.classList.contains('counter-observed')) { 63 | counterVanishObserver.observe(counter); 64 | counter.classList.add('counter-observed') 65 | } 66 | } 67 | 68 | function updateCounterString(entries) { 69 | entries.forEach((entry) => { 70 | if (entry.intersectionRatio < .5) { 71 | const blockUid = c3u.getUidOfContainingBlock(entry.target); 72 | const counterVal = entry.target.innerHTML; 73 | const replacement = c3u.blockString(blockUid).replace(/\]:*\s*\d*}}/, `]:${counterVal}}}`); 74 | c3u.updateBlockString(blockUid, replacement) 75 | } 76 | }); 77 | } 78 | 79 | let options = { 80 | root: document.querySelector('.roam-app'), 81 | rootMargin: '0px', 82 | threshold: 1.0 83 | } 84 | counterVanishObserver = new IntersectionObserver(updateCounterString, options); 85 | setInterval(activateCounter, 1000); 86 | } 87 | 88 | function startC3TimerExtension() { 89 | var ccc = window.ccc || {}; 90 | var c3u = ccc.util; 91 | segmentLogPage = 'roam/js/c3/timer/segmentlog' 92 | runningTimersLog = 'roam/js/c3/timer/running' 93 | runningPageUid = c3u.getOrCreatePageUid(runningTimersLog, 'Log of Running Timers'); 94 | 95 | savedTime = {} //sum of all previously completed segments. maps uid => time 96 | totalElapsed = {} //savedTime + elapsed time of the current running segment. maps uid => time 97 | tInterval = {} //intervals for each timer element (not uid). maps timer element id => interval 98 | uid2allElements = {} //maps uid => array of all corresponding html elements' id 99 | 100 | 101 | function activateTimers() { 102 | Array.from(document.getElementsByTagName('button')) 103 | .filter(btn => btn.textContent === "c3-timer") 104 | .forEach(timer => { 105 | if (timer.closest('.rm-zoom-item') !== null) { 106 | timer.innerHTML = '' 107 | timer.classList.add('timer-zoomed') 108 | } else { 109 | if (!timer.classList.contains('timer-activated')) { 110 | const isRef = timer.closest('.rm-block-ref'); 111 | const timerBlockUid = isRef ? isRef.dataset.uid : c3u.getUidOfContainingBlock(timer); 112 | savedTime[timerBlockUid] = calcSavedElapsedTime(timerBlockUid) //save it in a map 113 | timer.id = "c3timer-" + timer.closest('.rm-block__input').id 114 | const runningLog = readLatestStartTime(timerBlockUid) 115 | if (!uid2allElements[timerBlockUid]) uid2allElements[timerBlockUid] = []; 116 | if (uid2allElements[timerBlockUid].indexOf(timer.id) == -1) 117 | uid2allElements[timerBlockUid].push(timer.id) 118 | if (runningLog) { //isRunning 119 | timer.classList.add('running'); 120 | const startTime = parseInt(c3u.allChildrenInfo(runningLog[0].uid)[0][0].children[0].string); 121 | tInterval[timer.id] = setInterval(function () { showTime(true, timer, timerBlockUid, startTime) }, 1000); 122 | } else { 123 | timer.classList.add('paused') 124 | showTime(false, timer, timerBlockUid, 0) 125 | } 126 | if (!isRef) { 127 | timer.addEventListener("click", timerClicked) 128 | } 129 | } 130 | } 131 | timer.classList.add('timer-activated') 132 | }); 133 | }; 134 | 135 | 136 | 137 | 138 | 139 | function calcSavedElapsedTime(timerBlockUid) { 140 | const match = c3u.blockString(timerBlockUid).match(/.*{{\[\[c3-timer\]\]:\s*\(\((.........)\)\)}}.*/); 141 | if (!match) return 0; //timer is not started. 142 | const timerLogs = c3u.allChildrenInfo(match[1])[0][0].children; 143 | if (timerLogs === undefined) return 0; //timer started but record is deleted. 144 | const elapsedTimes = timerLogs.map(x => calcElapsedTime(x.string)); 145 | return elapsedTimes.reduce((a, b) => a + b) 146 | } 147 | 148 | function calcElapsedTime(log) { 149 | let start, end, duration, s, e, d; 150 | if (log.includes('>')) { 151 | [s, e] = log.split(">"); 152 | start = new Date(s) 153 | end = new Date(e) 154 | if (start == "Invalid Date" || end == "Invalid Date") return 0; 155 | return (end < start) ? 0 : end - start; 156 | } else if (log.includes('+')) { 157 | [s, d] = log.split("+"); 158 | start = new Date(s).getTime(); 159 | duration = parseDuration(d); 160 | if (start == "Invalid Date" || duration == "Invalid Time") return 0; 161 | return duration; 162 | } else { 163 | [e, d] = log.split("-"); 164 | end = new Date(e).getTime() 165 | duration = parseDuration(d); 166 | if (end == "Invalid Date" || duration == "Invalid Time") return 0; 167 | return duration; 168 | } 169 | } 170 | 171 | function parseDuration(d) { 172 | const match = d.match(/\s*(([\d]*)\s*h)*\s*(([\d]*)\s*m)*\s*(([\d]*)\s*s)*/) 173 | let [h, m, s] = [match[2], match[4], match[6]].map(x => (x == undefined) ? 0 : parseInt(x)) 174 | if (isNaN(h) || isNaN(m) || isNaN(s)) return "Invalid Time"; 175 | return h * (60 * 60 * 1000) + m * (60 * 1000) + s * 1000 176 | } 177 | 178 | 179 | function timerClicked(e) { 180 | const timer = e.target; 181 | const timerBlockUid = c3u.getUidOfContainingBlock(timer); 182 | if (e.ctrlKey) { 183 | e.preventDefault(); 184 | e.stopPropagation(); 185 | e.stopImmediatePropagation(); 186 | pauseAllTimerElements(timerBlockUid) 187 | } else if (e.shiftKey) { 188 | e.preventDefault(); 189 | e.stopPropagation(); 190 | e.stopImmediatePropagation(); 191 | openRecordedTimes(timerBlockUid) 192 | } else if (!timer.classList.contains('running')) { 193 | startTimer(timerBlockUid); 194 | } else { 195 | pauseTimer(timerBlockUid); 196 | } 197 | } 198 | 199 | // Don't care if the timer is running or not 200 | // Just opens all of the recorded times in the side bar 201 | function openRecordedTimes(timerBlockUid) { 202 | const match = c3u.blockString(timerBlockUid) 203 | .match(/.*{{\[\[c3-timer\]\]:\s*\(\((.........)\)\)}}.*/); 204 | if (!match) return; //timer is not started. 205 | c3u.openBlockInSidebar("block", match[1]); 206 | } 207 | 208 | function pauseAllTimerElements(timerBlockUid) { 209 | uid2allElements[timerBlockUid] 210 | .forEach(function (timerId, index, array) { 211 | let timer = document.getElementById(timerId); 212 | if (timer) { 213 | clearInterval(tInterval[timerId]); 214 | timer.classList.add('paused'); 215 | timer.classList.remove('running'); 216 | showTime(false, timer, timerBlockUid, 0) 217 | } else array.splice(index, 1); //the element is not on the page so remove it. 218 | }); 219 | const runningLog = readLatestStartTime(timerBlockUid); 220 | if (!runningLog) return null 221 | 222 | c3u.deleteBlock(runningLog[0].uid) 223 | return runningLog; 224 | } 225 | 226 | 227 | function readLatestStartTime(timerBlockUid) { 228 | const allRunningTimers = c3u.allChildrenInfo(runningPageUid)[0][0].children; 229 | if (allRunningTimers === undefined) return false; 230 | const runningLog = allRunningTimers 231 | .map(function (x) { return (x.string === timerBlockUid) ? x : null; }) 232 | .filter(t => t != null); 233 | return (runningLog.length != 0) ? runningLog : false; 234 | } 235 | 236 | function writeLatestStartTime(timerBlockUid, startTime) { 237 | const logUid = c3u.createUid(); 238 | c3u.createChildBlock(runningPageUid, 1, timerBlockUid, logUid) 239 | c3u.createChildBlock(logUid, 0, startTime, c3u.createUid()) 240 | } 241 | 242 | function writeSegmentLog(timerBlockUid, startTime, endTime) { 243 | //if !exist a log for the timer create one 244 | const match = c3u 245 | .blockString(timerBlockUid) 246 | .match(/.*{{\[\[c3-timer\]\]:\s*\(\((.........)\)\)}}.*/); 247 | let segmentUid; 248 | if (!match) { 249 | segmentUid = c3u.createUid(); 250 | const today = new Date(); 251 | c3u.createChildBlock( 252 | c3u.getOrCreatePageUid(segmentLogPage + '/' + today.getFullYear() + '/' + today.getMonth(), 'Log of Stopped Timers'), 253 | 1, timerBlockUid, segmentUid) 254 | const replacement = c3u.blockString(timerBlockUid).replace(']}}', ']:((' + segmentUid + '))}}'); 255 | c3u.updateBlockString(timerBlockUid, replacement) 256 | } else { 257 | segmentUid = match[1] 258 | } 259 | c3u.createChildBlock(segmentUid, 0, new Date(startTime).toString().slice(4, 24) + ' > ' + new Date(endTime).toString().slice(4, 24), c3u.createUid()) 260 | } 261 | 262 | 263 | function startTimer(timerBlockUid) { 264 | startTime = new Date().getTime(); 265 | writeLatestStartTime(timerBlockUid, startTime); 266 | uid2allElements[timerBlockUid].forEach(function (timerId, index, array) { 267 | let timer = document.getElementById(timerId); 268 | if (timer) { 269 | tInterval[timerId] = setInterval(function () { showTime(true, timer, timerBlockUid, startTime) }, 1000); 270 | timer.classList.add('running'); 271 | timer.classList.remove('paused'); 272 | } else array.splice(index, 1); //the element is not on the page so remove it. 273 | }) 274 | pauseSameBranchTimers(timerBlockUid) 275 | } 276 | 277 | function pauseSameBranchTimers(timerBlockUid) { 278 | const allRunningTimers = c3u.allChildrenInfo(runningPageUid)[0][0].children; 279 | if (allRunningTimers === undefined) return false; 280 | const allRunningTimersUid = allRunningTimers.map(function (x) { return x.string; }); 281 | 282 | allRunningTimersUid.forEach(runningUid => { 283 | if (c3u.isAncestor(runningUid, timerBlockUid) || c3u.isAncestor(timerBlockUid, runningUid)) { 284 | pauseTimer(runningUid) 285 | } 286 | }) 287 | } 288 | 289 | function pauseTimer(timerBlockUid) { 290 | const endTime = new Date().getTime(); 291 | savedTime[timerBlockUid] = totalElapsed[timerBlockUid]; 292 | const runningLog = pauseAllTimerElements(timerBlockUid) 293 | const startTime = parseInt(c3u.blockString(c3u.getNthChildUid(runningLog[0].uid, 0))); 294 | writeSegmentLog(timerBlockUid, startTime, endTime); 295 | } 296 | 297 | function showTime(run, timer, timerBlockUid, startTime) { 298 | updatedTime = startTime ? new Date().getTime() : 0; 299 | total = (updatedTime - startTime) + savedTime[timerBlockUid]; 300 | var hours = Math.floor(total / (1000 * 60 * 60)); 301 | var minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); 302 | var seconds = Math.floor((total % (1000 * 60)) / 1000); 303 | hours = (hours != 0) ? hours + 'h ' : ''; 304 | minutes = (minutes != 0) ? minutes + 'm ' : ''; 305 | seconds = seconds + 's' 306 | const char = run ? '❚❚' : '►'; 307 | timer.innerHTML = char + ' ' + hours + minutes + seconds; 308 | totalElapsed[timerBlockUid] = total; 309 | } 310 | 311 | setInterval(activateTimers, 1000); 312 | } 313 | 314 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | - [Introduction](#intro) 4 | - [JavaScript Installation](#js) 5 | - [CSS Installation](#css) 6 | - [Demo Videos](#demo) 7 | - [Reporting Issues and Feature Request](#bug) 8 | - [Enhancements](#enhance) 9 | - [Math and Multi Language OCR](#ocr) 10 | - [Timer and Counter](#timer) 11 | - [Mixed Text Direction](#dir) 12 | - [PDF Highlighter](#pdf) 13 | - [Enhanced YouTube Player ](#yt) 14 | 15 | 16 | 17 | 18 | # Introduction 19 | 20 | This repository contains a set of JavaScript plugin/extensions (and their related CSS) for Roam Research. I call them enhancements because they improve my daily interaction with Roam. I will gradually add more extensions that I develop for my needs here and update the old ones. 21 | 22 | Here is the general installation guideline for all of the JavaScript and CSS codes. 23 | 24 | 25 | 26 | ## JavaScript Installation 27 | 28 | To install, do the same thing you do for any roam/js script. 29 | 30 | 1. Create page in Roam (if not already present) called [[roam/js]] 31 | 32 | 1. Create a block in the [[roam/js]] page and enter {{[[roam/js]]}} 33 | 34 | 1. Create a new block under the {{[[roam/js]]}} block and enter: ``` 35 | 36 | 1. This ``` create a code block for which you can select a language. 37 | 38 | 1. Make sure the code language is set as **JavaScript** 39 | 40 | 1. Paste the JavaScript code into the code block. Usually it looks something like this: 41 | 42 | ``` javascript 43 | window.parameters = { 44 | //Parameters that you can customize 45 | }; 46 | var s = document.createElement("script"); 47 | s.type = "text/javascript"; 48 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedX.js"; 49 | document.getElementsByTagName("head")[0].appendChild(s); 50 | ``` 51 | 52 | 1. A red warning box shows up asking you to review the risks of using roam/js. 53 | 54 | 1. Once you have reviewed the warning and understand/accept the risk, click Yes. 55 | 56 | 1. Refresh Roam and the script should now be installed! 57 | 58 | 59 | 60 | ## CSS Installation 61 | 62 | To install the CSS put this line in a CSS code block on you [[roam/css]] page. Make sure the code language is set to **CSS** 63 | 64 | ```css 65 | @import url('https://c3founder.github.io/Roam-Enhancement/enhancedX.css'); 66 | ``` 67 | 68 | 69 | 70 | ## Demo Videos 71 | 72 | I usually make YouTube demo video(s) for each extension to explain functionalities and known issues. The main purpose of videos is to prevent confusion and ultimately reduce the number of questions I receive. So please watch them before sending in your questions! 73 | 74 | 75 | 76 | ## Reporting Issues and Feature Request 77 | 78 | You can report bugs and suggest new features through GitHub: 79 | 80 | https://github.com/c3founder/Roam-Enhancement/issues 81 | 82 | Each extension has its own label that you can use when reporting issues. 83 | 84 | I'll post community wetted solutions to issues here over time. 85 | 86 | 87 | 88 | # Enhancements 89 | 90 | 91 | 92 | ## Math and Multi Language OCR 93 | 94 | This extension OCRs images that you have in roam. It supports up to two languages plus math and handwritten text. Images do not need to be uploaded into your roam graph (i.e., no CORS issue). It works on extracted area highlights of the [PDF Highlighter](#pdf). 95 | 96 | #### JavaScript 97 | 98 | ```javascript 99 | window.ocrParams = { 100 | lang1: "eng", //Shift + Click 101 | lang2: "ara", //Alt + Click 102 | //Mathpix parameters 103 | appId: "YOUR_APP_ID", 104 | appKey: "YOUR_APP_KEY", 105 | //Cleanup Shortcut 106 | cleanKey: 'alt+a c', 107 | //Edit options 108 | saveRef2Img: false 109 | }; 110 | 111 | var s = document.createElement("script"); 112 | s.type = "text/javascript"; 113 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedOCR.js"; 114 | document.getElementsByTagName("head")[0].appendChild(s); 115 | ``` 116 | 117 | #### Functionalities 118 | - **Parameters** 119 | The extracted text will be the child block of the image. If you are only interested in the extracted text and not the original image, you can "cleanup" by pressing the `cleanKey` shortcut: It will replace the image block with the extracted text and remove the text block. If you want to save a reference to the original image (just in case, as an alias) you can set `saveRef2Img: true`. 120 | 121 | - **Mathpix Support** 122 | You need to set up a mathpix account and get an app id and key. Read more about mathpix great service [here](https://mathpix.com/#features). And find their API [here](https://docs.mathpix.com/#introduction). 123 | 124 | - **YouTube Demos** 125 | - New tutorial: 126 | [![ocrwithcors](https://img.youtube.com/vi/N8DOqIZQFLU/0.jpg)](https://www.youtube.com/watch?v=N8DOqIZQFLU) 127 | 128 | - Older tutorial: 129 | [![ocrgist](https://img.youtube.com/vi/BSVxxDsZVNQ/0.jpg)](https://youtu.be/BSVxxDsZVNQ) 130 | 131 | 132 | 133 | 134 | 135 | 136 | ## Time and Habit Tracking 137 | This is an ongoing effort to build a time+habit+goal tracker in roam. Timer and counter are done, stay tuned for statistics and habit tracker. 138 | 139 | #### JavaScript 140 | 141 | ```javascript 142 | var s = document.createElement("script"); 143 | s.type = "text/javascript"; 144 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedTimer.js"; 145 | document.getElementsByTagName("head")[0].appendChild(s); 146 | ``` 147 | 148 | #### CSS 149 | 150 | ```css 151 | @import url('https://c3founder.github.io/Roam-Enhancement/enhancedTimer.css'); 152 | ``` 153 | #### Functionalities 154 | 155 | ##### Timer 156 | {{[[c3-timer]]}} makes a stopwatch to track time spend on each block and its children. You can have multiple timers running but on each branch only one timer can be active. In other words, when you start a timer it will stop any other running timer on the same branch to prevent double counting. 157 | 158 | - **Shortcuts** 159 | - Click: Start/Stop 160 | - Shift Click: Open the timer's time entries in the right side bar 161 | - Control Click: On a running timer will delete the current time period 162 | 163 | - **Time Entry Format** You can manually edit the time and also put in duration. Here is the notation: 164 | - start > end 165 | - start + duration 166 | - end - duration 167 | An example for the duration format is: 12h 5m 3s. 168 | 169 | - **YouTube Demo** 170 | - Timer set up tutorial: 171 | [![timersetup](https://img.youtube.com/vi/GR_eZDEE7jo/0.jpg)](https://youtu.be/GR_eZDEE7jo) 172 | 173 | ##### Counter 174 | {{[[c3-counter]]}} makes is a counter that goes up/down by click/shift-click. You can also manually enter the count as {{[[c3-counter]]:count}}. 175 | 176 | 177 | 178 | ## Mixed Text Direction 179 | 180 | This extension detects right-to-left and left-to-right characters at the beginning of each block and changes the block direction. You can infinitely nest rtl and ltr blocks with no issue. You can also have different font for each of rtl and ltr languages. 181 | 182 | #### JavaScript 183 | 184 | ```javascript 185 | var s = document.createElement("script"); 186 | s.type = "text/javascript"; 187 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedLanguage.js"; 188 | document.getElementsByTagName("head")[0].appendChild(s); 189 | ``` 190 | 191 | #### CSS 192 | 193 | ```css 194 | @import url('https://c3founder.github.io/Roam-Enhancement/enhancedLanguage.css'); 195 | /* More fonts here: https://fonts.google.com/?subset=arabic 196 | For example for 'Markazi Text', import the following: */ 197 | @import url('https://fonts.googleapis.com/css?family=Markazi+Text'); 198 | @import url(//fonts.googleapis.com/earlyaccess/notonaskharabic.css); 199 | @import url(//fonts.googleapis.com/earlyaccess/notonaskharabicui.css); 200 | 201 | :root { 202 | --rm-block-sep-min-width: 0px; 203 | /*****RTL Variables*****/ 204 | --rtl-margin-right: 31px; 205 | --rtl-margin-left: 31px; 206 | --rtl-bullet-margin-top: 5px; 207 | --rtl-control-margin-top: 4px; 208 | --rtl-generic-font: sans-serif; 209 | --rtl-font: 'Noto Naskh Arabic'; /*'Markazi Text'*/ /*Make sure you select the generic font first*/ 210 | --rtl-font-size: 1em; 211 | --rtl-textarea-background-color: rgba(253,253,168,0.53); 212 | --rtl-textarea-font-size: 1em; 213 | --rtl-textarea-line-height: 1.75em; 214 | /*****LTR Variables*****/ 215 | --ltr-margin-right: 31px; 216 | --ltr-margin-left: 31px; 217 | --ltr-bullet-margin-top: unset; 218 | --ltr-control-margin-top: unset; 219 | --ltr-generic-font: unset; /*san-serif;*/ 220 | --ltr-font: unset; /*'Lato';*/ /*Make sure you select the generic font first*/ 221 | --ltr-font-size: unset; /*1em;*/ 222 | --ltr-textarea-background-color: rgba(253,253,168,0.53); /*unset;*/ 223 | --ltr-textarea-font-size: unset; /*1em;*/ 224 | --ltr-textarea-line-height: unset; /*1.5em*/ 225 | } 226 | 227 | ``` 228 | 229 | #### Functionalities 230 | New tutorial: 231 | - [![mixedrtl](https://img.youtube.com/vi/z3BoV-vkSRY/0.jpg)](https://www.youtube.com/watch?v=z3BoV-vkSRY) 232 | 233 | Older tutorial: 234 | - [![rtl](https://img.youtube.com/vi/fp6akQlmyEw/0.jpg)](https://www.youtube.com/watch?v=fp6akQlmyEw) 235 | 236 | 237 | 238 | 239 | ## PDF Highlighter 240 | 241 | Thanks to the following roamcult members who supported the development of this extension (no specific order): 242 | - [Abhay Prasanna](https://twitter.com/AbhayPrasanna) 243 | - [Owen Cyrulnik](https://twitter.com/cyrulnik) 244 | - [Stian Håklev](https://twitter.com/houshuang) 245 | - [Ryan Muller](https://twitter.com/cicatriz) 246 | - [Mridula Duggal](https://twitter.com/Mridgyy) 247 | - [Joel Chan](https://twitter.com/JoelChan86) 248 | - [Lester](https://twitter.com/lesroco) 249 | - [Ekim Nazım Kaya](https://twitter.com/ekimnazimkaya) 250 | - [Tomas Baranek](https://twitter.com/tombarys) 251 | - [Conor](https://twitter.com/Conaw) 252 | 253 | #### JavaScript 254 | 255 | ``` javascript 256 | window.pdfParams = { 257 | //Highlight 258 | ///Placement 259 | outputHighlighAt: 'cousin', //cousin, child 260 | highlightHeading: '**Highlights**', //for cousin mode only 261 | appendHighlight: true, //append: true, prepend: false 262 | ///Rest of Highlight Options 263 | breadCrumbAttribute: 'Title', //Title, Author, Citekey, etc. 264 | addColoredHighlight: true,//bring the color of highlights into your graph 265 | //Rerference to Highlight 266 | ///Block References Related 267 | copyBlockRef: true,//false: copy captured text 268 | sortBtnText: 'sort them all!',//{{sort them all!}} button will sorted highlight references. 269 | ///Block Reference Buttons 270 | aliasChar: '✳', //use '' to disable 271 | textChar: 'T', //use '' to disable 272 | //PDF Viewer 273 | pdfMinHeight: 900, 274 | //Citation 275 | ///Format 276 | ////use Citekey and page in any formating string 277 | ////page can be offset by `Page Offset` attribute. 278 | ////common usecase: 279 | /////Zotero imports with 'roam page title' = @Citekey and Citekey attribute 280 | ////examples: 281 | /////"[${Citekey}]([[@${Citekey}]])" 282 | /////"[(${Citekey}, ${page})]([[@${Citekey}]])" 283 | /////use '' to disable 284 | citationFormat: '', 285 | ///BlockQuote 286 | blockQPerfix: ''//use '' to disable. Alternatives are: > or [[>]]. 287 | }; 288 | var s = document.createElement("script"); 289 | s.type = "text/javascript"; 290 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedPDF.js"; 291 | document.getElementsByTagName("head")[0].appendChild(s); 292 | ``` 293 | 294 | #### CSS 295 | 296 | ```css 297 | @import url('https://c3founder.github.io/Roam-Enhancement/enhancedPDF.css'); 298 | ``` 299 | 300 | #### Functionalities 301 | Full tutorial here: 302 | - [![pdfhighlighter](https://img.youtube.com/vi/-yVqQqUEHKI/0.jpg)](https://www.youtube.com/watch?v=-yVqQqUEHKI&ab_channel=CCC) 303 | 304 | 305 | 306 | ## Enhanced YouTube Player 307 | 308 | #### JavaScript 309 | 310 | To install, do the same thing you do for any roam/js script. 311 | 312 | ```javascript 313 | window.ytParams = { 314 | //Player 315 | //Shortcuts 316 | grabTitleKey : 'alt+a t', 317 | grabTimeKey : 'alt+a n', 318 | ////Speed Controls 319 | normalSpeedKey : 'alt+a 0', 320 | speedUpKey: 'alt+a =', 321 | speedDownKey: 'alt+a -', 322 | ////Volume Controls 323 | muteKey: 'alt+a m', 324 | volUpKey: 'alt+a i', 325 | volDownKey: 'alt+a k', 326 | ////Playback Controls 327 | playPauseKey : 'alt+a p', 328 | backwardKey: 'alt+a j', 329 | forwardKey: 'alt+a l' 330 | }; 331 | 332 | var s = document.createElement("script"); 333 | s.type = "text/javascript"; 334 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedYouTube.js"; 335 | document.getElementsByTagName("head")[0].appendChild(s); 336 | ``` 337 | 338 | #### CSS 339 | 340 | ~~~css 341 | @import url('https://c3founder.github.io/Roam-Enhancement/enhancedYouTube.css'); 342 | ~~~ 343 | 344 | #### Functionalities 345 | 346 | ##### Responsive/Resizable Player 347 | You can set the original iframe size here in the code plus the border style. 348 | 349 | - **Parameters:** Border style of the video and its height and width when the right sidebar is closed. 350 | - borderStyle : border style 351 | - border : [border thickness](https://www.w3schools.com/jsref/prop_style_borderstyle.asp) 352 | - borderRadius : curvature of corners 353 | - vidHeight : height 354 | - vidWidth : width 355 | - **YouTube Demo** 356 | - [![responsive player](https://img.youtube.com/vi/vJ3gPX89fz0/0.jpg)](https://www.youtube.com/watch?v=vJ3gPX89fz0&ab_channel=ConnectedCognitionCrumbs) 357 | 358 | 359 | ##### YouTube Timestamp 360 | You can add timestamps to videos using a shortcut. 361 | 362 | - **Parameters:** 363 | - grabTitleKey: if in a DIRECT child block of the YT video, grabs the title and paste it to the beginning of the current block. 364 | - grabTimeKey: if in ANY child blocks of the YT video, it captures the player's current time and pastes it to the beginning of the block. 365 | - **YouTube Demo** 366 | - [![timestamp](https://img.youtube.com/vi/Kgo_Lkw-2CA/0.jpg)](https://www.youtube.com/watch?v=Kgo_Lkw-2CA&ab_channel=ConnectedCognitionCrumbs) 367 | 368 | 369 | ##### In-text Controllable Player 370 | You can control the YT player while you are typing. 371 | 372 | - If you have one player on the page, shortcuts will control the player, easy. 373 | - When multi YT players are present 374 | - If only one is playing: shortcuts will control the playing one. 375 | - If nothing is playing, shortcuts will control the last playing video you paused by shortcut (not mouse click). For example, you can mute/unmute or -10/+10 the last video you paused or play it with `alt+a p`. 376 | - If multiple videos are playing, everything is ambiguous, so you can only control the first one (according to the order of appearance on the page). You can pause them all in order by `alt+a p`, though. 377 | 378 | - **Parameters:** 379 | - playPauseKey: play/pause the most recent player or the first one 380 | - backwardKey: go backward 10 sec 381 | - forwardKey: go forward 10 sec 382 | - normalSpeedKey: set the playback rate to 1 383 | - speedUpKey: increase the rate by .25 384 | - speedDownKey: decrease the rate by .25 385 | - muteKey: mute the player 386 | - volUpKey: increase volume by 10/100 387 | - volDownKey: decrease volume by 10/100 388 | - **YouTube Demo** 389 | - [![timestamp](https://img.youtube.com/vi/ADJvhW31xj4/0.jpg)](https://www.youtube.com/watch?v=ADJvhW31xj4&ab_channel=ConnectedCognitionCrumbs) 390 | 391 | - **Known Issues with Shortcuts:** 392 | - **Common installation problem:** You need to have the code block as the child of {{[[roam/js]]}} so you need a tab befor the code block. 393 | - **Shortcuts in mac:** I'm not a mac user, I've compiled this list based on feedback I received, this is why the language is uncertain; I have not tested them myself. Special thanks to [Abhay Prasanna](https://twitter.com/AbhayPrasanna) and [Jerome Wong](https://github.com/DarkArcZ). 394 | - For mac users 'option' instead of 'alt' has worked. 395 | - For example, you can replace 'alt+a n' with 'option+a n' 396 | - I specific keyboard modes 'option+a' will generate 'å' in mac. You can read about it [here](https://en.wikipedia.org/wiki/Option_key#Alternative_keyboard_input). 397 | - It seems you can fix this by changing the keyboard Input Source to "Unicode Hex Input" from "ABC". 398 | 1. Go to Keyboard on your Mac System Preferences 399 | 1. Click on Input Sources on the top 400 | 1. Press the "+" button and add "Unicode Hex Input" 401 | 1. Go to where you pasted the code on roam 402 | 1. Change the shortcut key to something else besides alt (cmd, option, ctrl) 403 | 1. Restart Roam 404 | 405 | - **General notes:** 406 | - To press 'alt+a n' you need to hold alt and a together for a fraction of a second (like when you press alt+tab to switch windows) and then RELEASE them and tap 'n'. 407 | - Make sure that the perfix 'alt+a' ('option+a' in mac), is not already assigned and captured by other programs. 408 | - Those programs can be installed on your operating system or be extensions in your browsers. 409 | - If there is a conflict, you need to change the shortcut of either that program or YT extension. 410 | - You can have 'alt+a+n' which means you need to hold all three buttons. 411 | - Pros: It is harder to miss compare to 'alt+a n'. 412 | - Cons: You need to make sure that there is no conflict with both sub-sequence key combination, i.e., 'alt+a' and 'alt+n'. 413 | 414 | 415 | -------------------------------------------------------------------------------- /enhancedYouTube.js: -------------------------------------------------------------------------------- 1 | 2 | // ==UserScript== 3 | // @name Responsive YouTube Player & Timestamp Control & Player Controller for Roamresearch 4 | // @author Connected Cognition Crumbs 5 | // @require - 6 | // @version 0.5 7 | // @description Add timestamp controls to YouTube videos embedded in Roam and makes the player responsive. 8 | // Parameters: 9 | // Shortcuts: 10 | // grabTitleKey: if in a DIRECT child block of the YT video, 11 | // grabs the title and paste it to the beginning of the current block. 12 | // grabTimeKey: if in ANY child blocks of the YT video, 13 | // grabs the current time of the player and paste it to the beginning. 14 | // normalSpeedKey: set the playback rate to 1 15 | // speedUpKey: increase the rate by .25 16 | // speedDownKey: decrease the rate by .25 17 | // muteKey: mut the player 18 | // volUpKey: increase volume by 10/100 19 | // volDownKey: decrease volume by 10/100 20 | // playPauseKey: play/pause the most recent player or the first one 21 | // backwardKey: go backward 10 sec 22 | // forwardKey: go forward 10 sec 23 | // Player Size: Video height and width when the right sidebar is closed. 24 | // @match https://*.roamresearch.com 25 | 26 | /* mousetrap v1.6.5 craig.is/killing/mice */ 27 | (function (q, u, c) { 28 | function v(a, b, g) { a.addEventListener ? a.addEventListener(b, g, !1) : a.attachEvent("on" + b, g) } function z(a) { if ("keypress" == a.type) { var b = String.fromCharCode(a.which); a.shiftKey || (b = b.toLowerCase()); return b } return n[a.which] ? n[a.which] : r[a.which] ? r[a.which] : String.fromCharCode(a.which).toLowerCase() } function F(a) { var b = []; a.shiftKey && b.push("shift"); a.altKey && b.push("alt"); a.ctrlKey && b.push("ctrl"); a.metaKey && b.push("meta"); return b } function w(a) { 29 | return "shift" == a || "ctrl" == a || "alt" == a || 30 | "meta" == a 31 | } function A(a, b) { var g, d = []; var e = a; "+" === e ? e = ["+"] : (e = e.replace(/\+{2}/g, "+plus"), e = e.split("+")); for (g = 0; g < e.length; ++g) { var m = e[g]; B[m] && (m = B[m]); b && "keypress" != b && C[m] && (m = C[m], d.push("shift")); w(m) && d.push(m) } e = m; g = b; if (!g) { if (!p) { p = {}; for (var c in n) 95 < c && 112 > c || n.hasOwnProperty(c) && (p[n[c]] = c) } g = p[e] ? "keydown" : "keypress" } "keypress" == g && d.length && (g = "keydown"); return { key: m, modifiers: d, action: g } } function D(a, b) { return null === a || a === u ? !1 : a === b ? !0 : D(a.parentNode, b) } function d(a) { 32 | function b(a) { 33 | a = 34 | a || {}; var b = !1, l; for (l in p) a[l] ? b = !0 : p[l] = 0; b || (x = !1) 35 | } function g(a, b, t, f, g, d) { var l, E = [], h = t.type; if (!k._callbacks[a]) return []; "keyup" == h && w(a) && (b = [a]); for (l = 0; l < k._callbacks[a].length; ++l) { var c = k._callbacks[a][l]; if ((f || !c.seq || p[c.seq] == c.level) && h == c.action) { var e; (e = "keypress" == h && !t.metaKey && !t.ctrlKey) || (e = c.modifiers, e = b.sort().join(",") === e.sort().join(",")); e && (e = f && c.seq == f && c.level == d, (!f && c.combo == g || e) && k._callbacks[a].splice(l, 1), E.push(c)) } } return E } function c(a, b, c, f) { 36 | k.stopCallback(b, 37 | b.target || b.srcElement, c, f) || !1 !== a(b, c) || (b.preventDefault ? b.preventDefault() : b.returnValue = !1, b.stopPropagation ? b.stopPropagation() : b.cancelBubble = !0) 38 | } function e(a) { "number" !== typeof a.which && (a.which = a.keyCode); var b = z(a); b && ("keyup" == a.type && y === b ? y = !1 : k.handleKey(b, F(a), a)) } function m(a, g, t, f) { 39 | function h(c) { return function () { x = c; ++p[a]; clearTimeout(q); q = setTimeout(b, 1E3) } } function l(g) { c(t, g, a); "keyup" !== f && (y = z(g)); setTimeout(b, 10) } for (var d = p[a] = 0; d < g.length; ++d) { 40 | var e = d + 1 === g.length ? l : h(f || 41 | A(g[d + 1]).action); n(g[d], e, f, a, d) 42 | } 43 | } function n(a, b, c, f, d) { k._directMap[a + ":" + c] = b; a = a.replace(/\s+/g, " "); var e = a.split(" "); 1 < e.length ? m(a, e, b, c) : (c = A(a, c), k._callbacks[c.key] = k._callbacks[c.key] || [], g(c.key, c.modifiers, { type: c.action }, f, a, d), k._callbacks[c.key][f ? "unshift" : "push"]({ callback: b, modifiers: c.modifiers, action: c.action, seq: f, level: d, combo: a })) } var k = this; a = a || u; if (!(k instanceof d)) return new d(a); k.target = a; k._callbacks = {}; k._directMap = {}; var p = {}, q, y = !1, r = !1, x = !1; k._handleKey = function (a, 44 | d, e) { var f = g(a, d, e), h; d = {}; var k = 0, l = !1; for (h = 0; h < f.length; ++h)f[h].seq && (k = Math.max(k, f[h].level)); for (h = 0; h < f.length; ++h)f[h].seq ? f[h].level == k && (l = !0, d[f[h].seq] = 1, c(f[h].callback, e, f[h].combo, f[h].seq)) : l || c(f[h].callback, e, f[h].combo); f = "keypress" == e.type && r; e.type != x || w(a) || f || b(d); r = l && "keydown" == e.type }; k._bindMultiple = function (a, b, c) { for (var d = 0; d < a.length; ++d)n(a[d], b, c) }; v(a, "keypress", e); v(a, "keydown", e); v(a, "keyup", e) 45 | } if (q) { 46 | var n = { 47 | 8: "backspace", 9: "tab", 13: "enter", 16: "shift", 17: "ctrl", 48 | 18: "alt", 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up", 39: "right", 40: "down", 45: "ins", 46: "del", 91: "meta", 93: "meta", 224: "meta" 49 | }, r = { 106: "*", 107: "+", 109: "-", 110: ".", 111: "/", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" }, C = { "~": "`", "!": "1", "@": "2", "#": "3", $: "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", _: "-", "+": "=", ":": ";", '"': "'", "<": ",", ">": ".", "?": "/", "|": "\\" }, B = { 50 | option: "alt", command: "meta", "return": "enter", 51 | escape: "esc", plus: "+", mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "meta" : "ctrl" 52 | }, p; for (c = 1; 20 > c; ++c)n[111 + c] = "f" + c; for (c = 0; 9 >= c; ++c)n[c + 96] = c.toString(); d.prototype.bind = function (a, b, c) { a = a instanceof Array ? a : [a]; this._bindMultiple.call(this, a, b, c); return this }; d.prototype.unbind = function (a, b) { return this.bind.call(this, a, function () { }, b) }; d.prototype.trigger = function (a, b) { if (this._directMap[a + ":" + b]) this._directMap[a + ":" + b]({}, a); return this }; d.prototype.reset = function () { 53 | this._callbacks = {}; 54 | this._directMap = {}; return this 55 | }; d.prototype.stopCallback = function (a, b) { if (-1 < (" " + b.className + " ").indexOf(" mousetrap ") || D(b, this.target)) return !1; if ("composedPath" in a && "function" === typeof a.composedPath) { var c = a.composedPath()[0]; c !== a.target && (b = c) } return "INPUT" == b.tagName || "SELECT" == b.tagName || "TEXTAREA" == b.tagName || b.isContentEditable }; d.prototype.handleKey = function () { return this._handleKey.apply(this, arguments) }; d.addKeycodes = function (a) { for (var b in a) a.hasOwnProperty(b) && (n[b] = a[b]); p = null }; 56 | d.init = function () { var a = d(u), b; for (b in a) "_" !== b.charAt(0) && (d[b] = function (b) { return function () { return a[b].apply(a, arguments) } }(b)) }; d.init(); q.Mousetrap = d; "undefined" !== typeof module && module.exports && (module.exports = d); "function" === typeof define && define.amd && define(function () { return d }) 57 | } 58 | })("undefined" !== typeof window ? window : null, "undefined" !== typeof window ? document : null); 59 | 60 | Mousetrap.prototype.stopCallback = function () { return false } 61 | 62 | 63 | const ytParams = window.ytParams; 64 | 65 | const activateYtVideos = () => { 66 | if (typeof (YT) == 'undefined') return; 67 | Array.from(document.getElementsByTagName('IFRAME')) 68 | .filter(iframe => iframe.src.includes('youtube.com')) 69 | .forEach(ytEl => { 70 | if (ytEl.closest('.rm-zoom-item') !== null) { 71 | return; //ignore breadcrumbs and page log 72 | } 73 | const block = ytEl.closest('.roam-block-container'); 74 | let frameId; 75 | if (!ytEl.classList.contains('yt-activated')) { 76 | var ytId = extractVideoID(ytEl.src); 77 | frameId = "yt-" + ytEl.closest('.roam-block').id; 78 | var ytWrapper = document.createElement('div'); 79 | ytWrapper.id = frameId; 80 | ytEl.parentNode.insertBefore(ytWrapper, ytEl); 81 | ytEl.remove() 82 | let newYTEl = new window.YT.Player(frameId, {videoId: ytId}) 83 | let iframe = document.getElementById(frameId) 84 | iframe.classList.add('rm-iframe', 'rm-video-player', 'yt-activated') 85 | iframe.closest('div').classList.add('rm-iframe__container', 'rm-video-player__container', 'hoverparent') 86 | players.set(frameId, newYTEl); 87 | } else { 88 | frameId = ytEl.id 89 | } 90 | addTimestampControls(block, players.get(frameId)); 91 | }); 92 | }; 93 | 94 | const addTimestampControls = (block, player) => { 95 | if (block.children.length < 2) return null; 96 | const childBlocks = Array.from(block.children[1].querySelectorAll('.rm-block__input')); 97 | childBlocks.forEach(child => { 98 | const timestamp = getTimestamp(child); 99 | const buttonIfPresent = getControlButton(child); 100 | const timestampChanged = buttonIfPresent !== null && timestamp != buttonIfPresent.dataset.timestamp; 101 | if (buttonIfPresent !== null && (timestamp === null || timestampChanged)) { 102 | buttonIfPresent.remove(); 103 | } 104 | if (timestamp !== null && (buttonIfPresent === null || timestampChanged)) { 105 | addControlButton(child, timestamp, () => player.seekTo(timestamp, true)); 106 | } 107 | }); 108 | }; 109 | 110 | const getControlButton = (block) => block.parentElement.querySelector('.timestamp-control'); 111 | 112 | const addControlButton = (block, timestamp, fn) => { 113 | const button = document.createElement('button'); 114 | button.innerText = '►'; 115 | button.classList.add('timestamp-control'); 116 | button.dataset.timestamp = timestamp; 117 | button.style.borderRadius = '50%'; 118 | button.addEventListener('click', fn); 119 | block.parentElement.insertBefore(button, block); 120 | }; 121 | 122 | const getTimestamp = (block) => { 123 | var myspan = block.querySelector('span') 124 | if (myspan === null) return null; 125 | const blockText = myspan.textContent; 126 | const matches = blockText.match(/^((?:\d+:)?\d+:\d\d)\D/); // start w/ m:ss or h:mm:ss 127 | if (!matches || matches.length < 2) return null; 128 | const timeParts = matches[1].split(':').map(part => parseInt(part)); 129 | if (timeParts.length == 3) return timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]; 130 | else if (timeParts.length == 2) return timeParts[0] * 60 + timeParts[1]; 131 | else return null; 132 | }; 133 | 134 | const extractVideoID = (url) => { 135 | var regExp = /^(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/\/?|user\/.*\/u\/\d+\/)|youtu\.be\/)([_0-9a-z-]+)/i; 136 | var match = url.match(regExp); 137 | if (match && match[7].length == 11) { 138 | return match[7]; 139 | } else { 140 | return null; 141 | } 142 | }; 143 | 144 | var ytReady = setInterval(() => { 145 | if (typeof (YT) == 'undefined' || typeof (YT.Player) == 'undefined') { 146 | const tag = document.createElement('script'); 147 | tag.src = 'https://www.youtube.com/iframe_api'; 148 | const firstScriptTag = document.getElementsByTagName('script')[0]; 149 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 150 | clearInterval(ytReady); 151 | } 152 | }, 1000); 153 | 154 | //Fill out the current block with the given text 155 | function fillTheBlock(givenTxt) { 156 | var setValue = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; 157 | let newTextArea = document.querySelector("textarea.rm-block-input"); 158 | setValue.call(newTextArea, givenTxt); 159 | var e = new Event('input', { bubbles: true }); 160 | newTextArea.dispatchEvent(e); 161 | } 162 | 163 | //Getting the playing player 164 | function whatIsPlaying() { 165 | for (let player of players.values()) { 166 | if (player.getPlayerState() == 1) { 167 | return player; 168 | } 169 | } 170 | return null; 171 | } 172 | 173 | //Getting the first uncued player 174 | function whatIsPresent() { 175 | for (let [playerId, player] of players) { 176 | if (document.getElementById(playerId) === null) { 177 | continue; 178 | } 179 | return player; 180 | } 181 | return null; 182 | } 183 | 184 | //Getting the target player 185 | //1)playing or 2)most recent one or 3) the first one 186 | function targetPlayer() { 187 | var playing = whatIsPlaying(); 188 | if (playing !== null) 189 | return playing; 190 | if (paused !== null) 191 | return paused; 192 | //If nothing is playing return the fist player (if exists) that is not cued. 193 | if (players.size > 0) 194 | return whatIsPresent(); 195 | return null; 196 | } 197 | //Setting all shortcuts 198 | //Title 199 | Mousetrap.bind(ytParams.grabTitleKey, async function (e) { 200 | e.preventDefault() 201 | if (e.srcElement.localName == "textarea") { 202 | var container = e.srcElement.closest('.roam-block-container'); 203 | var parContainer = container.parentElement.closest('.roam-block-container'); 204 | var myIframe = parContainer.querySelector("iframe"); 205 | if (myIframe === null) return false; 206 | var oldTxt = document.querySelector("textarea.rm-block-input").value; 207 | var newValue = players.get(myIframe.id).getVideoData().title + " " + oldTxt; 208 | fillTheBlock(newValue); 209 | } 210 | return false; 211 | }, 'keydown'); 212 | //TimeStamp 213 | Mousetrap.bind(ytParams.grabTimeKey, async function (e) { 214 | e.preventDefault() 215 | var playing = targetPlayer(); 216 | if (playing !== null) { 217 | var timeStr = new Date(playing.getCurrentTime() * 1000).toISOString().substr(11, 8) 218 | var oldTxt = document.querySelector("textarea.rm-block-input").value; 219 | fillTheBlock(timeStr + " " + oldTxt); 220 | return false; 221 | } 222 | return false; 223 | }, 'keydown'); 224 | //Play-Pause 225 | Mousetrap.bind(ytParams.playPauseKey, async function (e) { 226 | e.preventDefault(); 227 | var playing = whatIsPlaying(); 228 | //If something is playing => pause it 229 | if (playing !== null) { 230 | playing.pauseVideo(); 231 | paused = playing; 232 | return false; 233 | } 234 | //If there is an active paused video => play it 235 | if (paused !== null) { 236 | paused.playVideo(); 237 | paused = null; 238 | return false; 239 | } 240 | //If nothing is playing or paused => play the first video 241 | if (players.size > 0) { 242 | playing = whatIsPresent(); 243 | if (playing !== null) { 244 | playing.playVideo(); 245 | return false; 246 | } 247 | } 248 | return false; 249 | }, 'keydown'); 250 | //Mute 251 | Mousetrap.bind(ytParams.muteKey, async function (e) { 252 | e.preventDefault(); 253 | var playing = targetPlayer(); 254 | if (playing !== null) { 255 | if (playing.isMuted()) { 256 | playing.unMute(); 257 | } else { 258 | playing.mute(); 259 | } 260 | return false; 261 | } 262 | return false; 263 | }, 'keydown'); 264 | //Volume Up 265 | Mousetrap.bind(ytParams.volUpKey, async function (e) { 266 | e.preventDefault(); 267 | var playing = targetPlayer(); 268 | if (playing !== null) { 269 | playing.setVolume(Math.min(playing.getVolume() + 10, 100)) 270 | return false; 271 | } 272 | return false; 273 | }, 'keydown'); 274 | //Volume Down 275 | Mousetrap.bind(ytParams.volDownKey, async function (e) { 276 | e.preventDefault(); 277 | var playing = targetPlayer(); 278 | if (playing !== null) { 279 | playing.setVolume(Math.max(playing.getVolume() - 10, 0)) 280 | return false; 281 | } 282 | return false; 283 | }, 'keydown'); 284 | //Speed Up 285 | Mousetrap.bind(ytParams.speedUpKey, async function (e) { 286 | e.preventDefault(); 287 | var playing = targetPlayer(); 288 | if (playing !== null) { 289 | playing.setPlaybackRate(Math.min(playing.getPlaybackRate() + 0.25, 2)) 290 | return false; 291 | } 292 | return false; 293 | }, 'keydown'); 294 | //Speed Down 295 | Mousetrap.bind(ytParams.speedDownKey, async function (e) { 296 | e.preventDefault(); 297 | var playing = targetPlayer(); 298 | if (playing !== null) { 299 | playing.setPlaybackRate(Math.max(playing.getPlaybackRate() - 0.25, 0)) 300 | return false; 301 | } 302 | return false; 303 | }, 'keydown'); 304 | //Normal Speed 305 | Mousetrap.bind(ytParams.normalSpeedKey, async function (e) { 306 | e.preventDefault(); 307 | var playing = targetPlayer(); 308 | if (playing !== null) { 309 | playing.setPlaybackRate(1, 0) 310 | return false; 311 | } 312 | return false; 313 | }, 'keydown'); 314 | //Move Forward 315 | Mousetrap.bind(ytParams.forwardKey, async function (e) { 316 | e.preventDefault(); 317 | var playing = targetPlayer(); 318 | if (playing !== null) { 319 | var duration = playing.getDuration(); 320 | playing.seekTo(Math.min(playing.getCurrentTime() + 10, duration), true) 321 | return false; 322 | } 323 | return false; 324 | }, 'keydown'); 325 | //Move Backward 326 | Mousetrap.bind(ytParams.backwardKey, async function (e) { 327 | e.preventDefault(); 328 | var playing = targetPlayer(); 329 | if (playing !== null) { 330 | var duration = playing.getDuration(); 331 | playing.seekTo(Math.max(playing.getCurrentTime() - 10, 0), true) 332 | return false; 333 | } 334 | return false; 335 | }, 'keydown'); 336 | 337 | var paused = null; 338 | const players = new Map(); 339 | 340 | setInterval(activateYtVideos, 1000); 341 | -------------------------------------------------------------------------------- /enhancedPDF.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Enhanced-PDF Extension for Roam Research 3 | // @author hungriesthippo & CCC 4 | // @require - 5 | // @version 1.0 6 | // @match https://*.roamresearch.com 7 | // @description Handle PDF Highlights. 8 | // MAIN OUTPUT MODES: 9 | // 1) cousin 10 | // Highlights are added as the cousin block. 11 | // The breadCrumbAttribute & citeKeyAttribute are searched for in the PDF grand parent subtree 12 | // - PDF grand parent block 13 | // - PDF parent block 14 | // - PDF block, e.g., {{pdf: https://firebasestorage.googleapis.com/v0/b/exampleurl}} 15 | // - Highlight 16 | // - Cousin of the PDF block 17 | // 2) child 18 | // Highlights are added as the child block: 19 | // The breadCrumbAttribute & citeKeyAttribute are searched for in the PDF parent subtree 20 | // - PDF parent block 21 | // - PDF block 22 | // - Child of the PDF block 23 | /*******************************************************/ 24 | /*******************Parameter BEGIN*********************/ 25 | const pdfParams = window.pdfParams; 26 | /*******************Parameter END***********************/ 27 | /*******************************************************/ 28 | /* Begin Importing Utility Functions */ 29 | if (typeof ccc !== 'undefined' && typeof ccc.util !== 'undefined') { 30 | //Somebody has already loaded the utility 31 | startC3PdfExtension(); 32 | } else { 33 | let s = document.createElement("script"); 34 | s.type = "text/javascript"; 35 | s.src = "https://c3founder.github.io/Roam-Enhancement/enhancedUtility.js" 36 | s.id = 'c3util4pdf' 37 | s.onload = () => { startC3PdfExtension() } 38 | try { document.getElementById('c3util4pdf').remove() } catch (e) { }; 39 | document.getElementsByTagName('head')[0].appendChild(s); 40 | } 41 | /* End Importing Utility Functions */ 42 | 43 | 44 | 45 | function startC3PdfExtension() { 46 | var ccc = window.ccc || {}; 47 | var c3u = ccc.util; 48 | /*******************************************************/ 49 | /**********************Main BEGIN***********************/ 50 | 51 | // const serverPerfix = 'http://localhost:3000/?url='; 52 | const serverPerfix = 'https://roampdf.web.app/?url='; 53 | const pdfChar = ' '; 54 | 55 | 56 | function initPdf() { 57 | Array.from(document.getElementsByTagName('iframe')).forEach(iframe => { 58 | if (!iframe.classList.contains('pdf-activated')) { 59 | try { 60 | if (new URL(iframe.src).pathname.endsWith('.pdf')) { 61 | const originalPdfUrl = iframe.src; //the permanent pdfId 62 | iframe.id = "pdf-" + iframe.closest('.roam-block').id; //window level pdfId 63 | const pdfBlockUid = c3u.getUidOfContainingBlock(iframe); //for click purpose 64 | allPdfIframes.push(iframe); //save for interaction 65 | renderPdf(iframe); //render pdf through the server 66 | sendHighlights(iframe, originalPdfUrl, pdfBlockUid); 67 | } 68 | } catch { } // some iframes have invalid src 69 | } 70 | if (iframe.src.startsWith(serverPerfix)) { 71 | adjustPdfIframe(iframe); 72 | } 73 | }) 74 | activateButtons(); 75 | } 76 | ///////////////Responsive PDF Iframe 77 | function adjustPdfIframe(iframe) { 78 | const reactParent = iframe.closest('.react-resizable') 79 | const reactHandle = reactParent.querySelector(".react-resizable-handle") 80 | const hoverParent = iframe.closest('.hoverparent') 81 | reactHandle.style.display = 'none'; 82 | reactParent.style.width = '100%'; 83 | reactParent.style.height = '100%'; 84 | hoverParent.style.width = '100%'; 85 | hoverParent.style.height = '100%'; 86 | } 87 | /************************Main END***********************/ 88 | /*******************************************************/ 89 | 90 | /*******************************************************/ 91 | /*************Look for Highlight Delete BEGIN***********/ 92 | let hlDeletionObserver = new MutationObserver(mutations => { 93 | mutations.forEach(mutation => { 94 | mutation.removedNodes.forEach(node => { 95 | if (typeof (node.classList) !== 'undefined') { 96 | if (node.classList.contains("roam-block-container")) { //if a block is deleted 97 | handleHighlightDelete(node) 98 | handlePdfDelete(node) 99 | } 100 | } 101 | }); 102 | }); 103 | }); 104 | 105 | function handleHighlightDelete(node) { 106 | Array.from(node.getElementsByTagName('button')) //if it had a button 107 | .filter(isHighlightBtn) 108 | .forEach(async function (btn) { 109 | const match = btn.id.match(/main-hlBtn-(.........)/) 110 | if (match) { 111 | if (c3u.existBlockUid(match[1])) {//If the data page was deleted ignore 112 | await c3u.sleep(3000) //Maybe someone is moving blocks or undo 113 | if (!c3u.existBlockUid(c3u.blockString(match[1]))) { 114 | //Delete the date row 115 | const hlDataRowUid = match[1]; 116 | const hlDataRow = c3u.queryAllTxtInChildren(hlDataRowUid); 117 | const toDeleteHighlight = getHighlight(hlDataRow[0][0]); 118 | const dataTableUid = c3u.parentBlockUid(hlDataRowUid); 119 | c3u.deleteBlock(hlDataRowUid) 120 | //Delete the hl on pdf (send message to render) 121 | const dataPageUid = c3u.parentBlockUid(dataTableUid); 122 | const pdfUrl = encodePdfUrl(c3u.blockString(c3u.getNthChildUid(dataPageUid, 0))); 123 | Array.from(document.getElementsByTagName('iframe')) 124 | .filter(x => x.src === pdfUrl) 125 | .forEach(iframe => { 126 | iframe.contentWindow.postMessage({ deleted: toDeleteHighlight }, '*'); 127 | }); 128 | } 129 | } 130 | } 131 | }); 132 | } 133 | 134 | function handlePdfDelete(node) { 135 | Array.from(node.getElementsByTagName('iframe')) 136 | .filter(x => { return x.src.indexOf(serverPerfix) !== -1; }) 137 | .forEach(async function (iframe) { 138 | await c3u.sleep(1000) //Maybe someone is moving blocks or undo 139 | const pdfUid = iframe.id.slice(-9) 140 | if (!c3u.existBlockUid(pdfUid)) { //pdf block deleted 141 | const pdfUrl = decodePdfUrl(iframe.src) 142 | const pdfDataPageTitle = getDataPageTitle(pdfUrl); 143 | const pdfDataPageUid = c3u.getPageUid(pdfDataPageTitle); 144 | if (pdfDataPageUid) { //If the data page exists 145 | const tableUid = c3u.getNthChildUid(pdfDataPageUid, 2); 146 | const res = c3u.allChildrenInfo(tableUid)[0][0]; 147 | c3u.deletePage(pdfDataPageUid) 148 | res.children.map(async function (child) { 149 | //You can check their existence but seems redundent. 150 | c3u.deleteBlock(child.string); 151 | }); 152 | } 153 | } 154 | }); 155 | } 156 | 157 | 158 | ///////////////Wait for roam to fully load then observe 159 | let roamArticle; 160 | let roamArticleReady = setInterval(() => { 161 | if (!document.querySelector('.roam-app')) return; 162 | roamArticle = document.querySelector('.roam-app') 163 | hlDeletionObserver.observe(roamArticle, { 164 | childList: true, 165 | subtree: true 166 | }); 167 | clearInterval(roamArticleReady); 168 | }, 1000); 169 | /*************Look for Highlight Delete END***********/ 170 | /*****************************************************/ 171 | 172 | /*******************************************************/ 173 | /**************Button Activation BEGIN******************/ 174 | /*****Fixing Highlight Btns Appearance and Functions****/ 175 | function activateButtons() { 176 | Array.from(document.getElementsByTagName('button')) 177 | .filter(isUnObservedHighlightBtn) 178 | .forEach(btn => { 179 | if (!btn.closest('.rm-zoom-item')) 180 | hlBtnAppearsObserver.observe(btn); 181 | btn.classList.add('btn-observed'); 182 | }) 183 | activateSortButtons(); 184 | } 185 | 186 | function activateSortButtons() { 187 | Array.from(document.getElementsByTagName('button')) 188 | .filter(isInactiveSortBtn) 189 | .forEach(btn => { 190 | const sortBtnBlockUid = c3u.getUidOfContainingBlock(btn); 191 | btn.classList.add('btn-sort-highlight', 'btn-pdf-activated'); 192 | const pdfUid = c3u.parentBlockUid(sortBtnBlockUid) 193 | const match = c3u.blockString(pdfUid).match(/\{{\[?\[?pdf\]?\]?:\s(.*)}}/); 194 | if (match[1]) { 195 | const pdfUrl = match[1]; 196 | let highlights = getAllHighlights(pdfUrl, pdfUid) 197 | highlights.sort(function (a, b) { 198 | if (a.position.pageNumber < b.position.pageNumber) 199 | return -1 200 | if (a.position.pageNumber > b.position.pageNumber) 201 | return +1 202 | if (a.position.boundingRect.x2 < b.position.boundingRect.x1) 203 | return -1 204 | if (b.position.boundingRect.x2 < a.position.boundingRect.x1) 205 | return +1 206 | if (a.position.boundingRect.y1 < b.position.boundingRect.y1) 207 | return -1 208 | return +1 209 | }); 210 | let cnt = 0 211 | btn.onclick = () => 212 | highlights.map(function (item) { c3u.createChildBlock(sortBtnBlockUid, cnt++, "((" + item.id + "))", c3u.createUid()); }) 213 | 214 | } 215 | }) 216 | } 217 | let options = { 218 | root: document.querySelector('.roam-app'), 219 | rootMargin: "0px 0px 500px 0px", 220 | threshold: 1.0 221 | } 222 | 223 | function activateSingleBtn(entries) { 224 | entries.forEach((entry) => { 225 | const btn = entry.target; 226 | if (isInactiveHighlightBtn(btn) && entry.intersectionRatio > .25) { 227 | const hlInfo = getHlInfoFromBtn(btn); 228 | const highlight = getSingleHighlight(hlInfo.uid) 229 | let pdfInfo = getPdfInfoFromHighlight(hlInfo.uid); 230 | if (pdfInfo) { 231 | const btnBlock = btn.closest(".rm-block__input"); 232 | const page = btn.innerText; 233 | addBreadcrumb(btnBlock, page, pdfInfo.uid); 234 | pdfInfo.url = encodePdfUrl(pdfInfo.url); 235 | handleBtn(btn, pdfInfo, hlInfo, highlight); 236 | } 237 | } 238 | }); 239 | } 240 | 241 | hlBtnAppearsObserver = new IntersectionObserver(activateSingleBtn, options); 242 | 243 | ///////////////////////////////////////////////////////// 244 | ///////////////Portal to the Data Page ////////////////// 245 | ///////////////////////////////////////////////////////// 246 | ///////////////From highlight => Data page => Retrieve PDF url and uid. 247 | function getPdfInfoFromHighlight(hlBlockUid) { 248 | let match = c3u.blockString(hlBlockUid).match(/\[..?]\(\(\((.........)\)\)\)/); 249 | if (!match[1]) return null; 250 | const pdfUid = match[1]; 251 | match = c3u.blockString(pdfUid).match(/\{{\[?\[?pdf\]?\]?:\s(.*)}}/); 252 | if (!match[1]) return null; 253 | const pdfUrl = match[1]; 254 | return { url: pdfUrl, uid: pdfUid }; 255 | } 256 | 257 | ///////////////From highlight => Row of the data table => Highlight coordinates 258 | function getSingleHighlight(hlBlockUid) { 259 | const hlDataRowUid = getHighlightDataBlockUid(hlBlockUid); 260 | const hlDataRow = c3u.queryAllTxtInChildren(hlDataRowUid); 261 | if (hlDataRow.length === 0) return null; 262 | return getHighlight(hlDataRow[0][0]); 263 | } 264 | 265 | ///////////////From button's text jump to the corresponding data table row 266 | function getHighlightDataBlockUid(hlBlockUid) { 267 | const match = c3u.blockString(hlBlockUid).match(/{{\d+:\s*(.........)}}/); 268 | if (!match) return null; 269 | return match[1] 270 | } 271 | 272 | ///////////////Get the Original Highlight 273 | ///////////////Where am I? Main Hilight or Reference? 274 | function getHlInfoFromBtn(btn) { 275 | let hlType, hlUid; 276 | const blockRefSpan = btn.closest('.rm-block-ref') 277 | if (!blockRefSpan) { 278 | hlType = 'main'; 279 | hlUid = c3u.getUidOfContainingBlock(btn); 280 | } else { 281 | hlType = 'ref'; 282 | hlUid = blockRefSpan.dataset.uid 283 | } 284 | return { type: hlType, uid: hlUid }; 285 | } 286 | 287 | function getHighlightsFromTable(uid) { 288 | const hls = c3u.queryAllTxtInChildren(uid)[0][0].children; 289 | return hls.map(function (x) { return getHighlight(x); }).filter(hl => hl != null); 290 | } 291 | 292 | function getHighlight(hl) { //the column order is: (hlUid, hlInfo(pos, color), hlTxt) 293 | //Extracting Text 294 | const hlText = hl.children[0].children[0].string; 295 | //Extracting Info = (position, color) 296 | const hlInfo = JSON.parse(hl.children[0].string); 297 | let position, color; 298 | if (typeof (hlInfo.position) === 'undefined') {//if older version highlight 299 | position = JSON.parse(hl.children[0].string); 300 | color = 0; 301 | } else { 302 | position = hlInfo.position; 303 | color = hlInfo.color; 304 | } 305 | //Extracting Id 306 | const id = hl.string 307 | return { id, content: { text: hlText }, position, color }; 308 | } 309 | 310 | //////////////////////////////////////////////////////////// 311 | ////////Activate all of the Highlight Buttons//////// 312 | //////////////////////////////////////////////////////////// 313 | 314 | ////////Open the PDF and send the HLs to server 315 | async function handleHighlightClick(e, pdfInfo, highlight) { 316 | e.preventDefault(); 317 | e.stopPropagation(); 318 | e.stopImmediatePropagation(); 319 | let iframe = getOpenIframeElementWithSrc(pdfInfo.url); 320 | if (!iframe) { //Iframe is closed 321 | c3u.openBlockInSidebar('block', pdfInfo.uid) 322 | await c3u.sleep(3000); 323 | iframe = getOpenIframeElementWithSrc(pdfInfo.url); 324 | } 325 | iframe.contentWindow.postMessage({ scrollTo: highlight }, '*'); 326 | } 327 | 328 | function getOpenIframeElementWithSrc(iframeSrc) { 329 | return Array.from(document.getElementsByTagName('iframe')) 330 | .find(iframe => iframe.src === iframeSrc); 331 | } 332 | 333 | function handleBtn(btn, pdfInfo, hlInfo, highlight) { 334 | const blockUid = c3u.getUidOfContainingBlock(btn); 335 | //Shared for main and reference jump btns 336 | const extraClass = 'btn-' + hlInfo.type + '-annotation' 337 | btn.classList.add(extraClass, 'btn', 'btn-default', 'btn-pdf-activated'); 338 | const btnId = getHighlightDataBlockUid(hlInfo.uid); 339 | btn.id = hlInfo.type + '-hlBtn-' + btnId; 340 | btn.addEventListener("click", (e) => { handleHighlightClick(e, pdfInfo, highlight) }); 341 | 342 | if (hlInfo.type === 'ref') { 343 | //Fix highlight btn 344 | btn.classList.add('popup'); 345 | const wrapperSpan = btn.closest('.bp3-popover-wrapper') 346 | const closestSpan = btn.closest('span') 347 | closestSpan.classList.add('displacedBtns') 348 | closestSpan.parentElement.closest('span').querySelector('.rm-block__ref-count-footnote')?.closest('.bp3-popover-wrapper').remove(); 349 | c3u.insertAfter(closestSpan, wrapperSpan) 350 | //Fix footnote btn if exists 351 | const footnote = wrapperSpan.closest('span').querySelector('.rm-block__ref-count-footnote') 352 | footnote?.classList.add('popup'); 353 | //Add replace with text and alias btns 354 | 355 | let btnRepText = null, btnRepAlias = null; 356 | if (pdfParams.textChar !== '') { 357 | btnRepText = createCtrlBtn(1, blockUid, hlInfo.uid); 358 | btnRepText.addEventListener("click", function (e) { 359 | replaceHl(blockUid, hlInfo.uid, 1, btn) 360 | }); 361 | c3u.insertAfter(btnRepText, btn) 362 | } 363 | if (pdfParams.aliasChar !== '') { 364 | btnRepAlias = createCtrlBtn(0, blockUid, hlInfo.uid); 365 | btnRepAlias.addEventListener("click", function (e) { 366 | replaceHl(blockUid, hlInfo.uid, 0, btn) 367 | }); 368 | c3u.insertAfter(btnRepAlias, btn) 369 | } 370 | } 371 | } 372 | 373 | function createCtrlBtn(asText, btnBlockUid, hlBlockUid) { 374 | const trail = asText ? 'text' : 'alias'; 375 | const btnText = asText ? pdfParams.textChar : pdfParams.aliasChar; 376 | const cssClass = 'btn-rep-' + trail; 377 | 378 | const newBtn = document.createElement('button'); 379 | newBtn.classList.add(cssClass, 'btn', 'btn-default', 'btn-pdf-activated', 'popup'); 380 | newBtn.innerText = btnText; 381 | newBtn.title = 'Replace with ' + trail; 382 | newBtn.id = btnBlockUid + hlBlockUid + asText 383 | return newBtn; 384 | } 385 | 386 | 387 | 388 | 389 | ////////////////////Breadcrumb Addition//////////////////// 390 | ////////////////////Breadcrumb Placement 391 | let pdf2attr = {} 392 | function addBreadcrumb(btnBlock, pageNumber, pdfUid) { 393 | if (!pdf2attr[pdfUid]) pdf2attr[pdfUid] = findPDFAttribute(pdfUid, pdfParams.breadCrumbAttribute) 394 | btnBlock.firstChild.setAttribute("title", pdf2attr[pdfUid] + "/Pg" + pageNumber); 395 | btnBlock.firstChild.classList.add("breadCrumb"); 396 | } 397 | ////////////////////Search the sub-tree of HL/PDF's 398 | ////////////////////shared parents for the meta info 399 | function findPDFAttribute(pdfUid, attribute) { 400 | let gParentRef; 401 | if (pdfParams.outputHighlighAt === 'cousin') { 402 | gParentRef = c3u.parentBlockUid(c3u.parentBlockUid(pdfUid)); 403 | if (!gParentRef) gParentRef = pdfUid; 404 | } 405 | else //child mode 406 | gParentRef = pdfUid; //parentBlockUid(pdfUid); 407 | 408 | let ancestorrule = `[ 409 | [ (ancestor ?b ?a) 410 | [?a :block/children ?b] ] 411 | [ (ancestor ?b ?a) 412 | [?parent :block/children ?b ] 413 | (ancestor ?parent ?a) ] ] ]`; 414 | 415 | const res = window.roamAlphaAPI.q( 416 | `[:find (pull ?block [:block/string]) 417 | :in $ % 418 | :where 419 | [?block :block/string ?attr] 420 | [(clojure.string/starts-with? ?attr \"${attribute}:\")] 421 | (ancestor ?block ?gblock) 422 | [?gblock :block/uid \"${gParentRef}\"]]`, ancestorrule) 423 | if (!res.length) return ' '; 424 | // match attribute: or attribute:: 425 | const attrMatch = new RegExp(`^${attribute}::?\\s*(.*)$`); 426 | return res[0][0].string.match(attrMatch)[1]; 427 | } 428 | 429 | ///////////////////Main Button Replacement////////////////// 430 | 431 | 432 | 433 | 434 | ///////////////////Main Button Onclick////////////////// 435 | ///////////////HL Reference Replacement: As Text or Alias 436 | function replaceHl(btnBlockUid, hlBlockUid, asText, btn) { 437 | //Prepare the substitute string 438 | const hl = c3u.blockString(hlBlockUid); 439 | const match = hl.match(/\{\{\d+:\s*.........\}\}\s*\[..?\]\(\(\(.........\)\)\)/) 440 | const hlText = hl.substring(0, match.index); 441 | const hlAlias = hlText + "[*](((" + hlBlockUid + ")))"; 442 | 443 | //Search for what to replace 444 | const blockTxt = c3u.blockString(btnBlockUid); 445 | const re = new RegExp("\\(\\(" + hlBlockUid + "\\)\\)", "g"); 446 | let newBlockTxt; 447 | 448 | if (asText) 449 | newBlockTxt = blockTxt.replace(re, hlText) 450 | else 451 | newBlockTxt = blockTxt.replace(re, hlAlias) 452 | 453 | c3u.updateBlockString(btnBlockUid, newBlockTxt) 454 | 455 | const toRemoveSpans = btn.closest('.rm-block__input').querySelectorAll('.displacedBtns') 456 | toRemoveSpans.forEach(item => item.remove()) 457 | } 458 | 459 | /***************Button Activation END*******************/ 460 | /*******************************************************/ 461 | 462 | /*******************************************************/ 463 | /************Handle New HL Received BEGIN***************/ 464 | window.addEventListener('message', handleRecievedMessage, false); 465 | 466 | ///////////Recieve Highlight Data, Output Highlight Text, Store HL Data 467 | function handleRecievedMessage(event) { 468 | switch (event.data.actionType) { 469 | case 'added': 470 | handleNewHighlight(event) 471 | break; 472 | case 'updated': 473 | handleUpdatedHighlight(event) 474 | break; 475 | case 'deleted': 476 | handleDeletedHighlight(event) 477 | break; 478 | case 'openHlBlock': 479 | handleOpenHighlight(event) 480 | break; 481 | case 'copyRef': 482 | handleCopyHighlightRef(event) 483 | break; 484 | } 485 | } 486 | 487 | function handleCopyHighlightRef(event) { 488 | const toOpenHlTextUid = event.data.highlight.id; 489 | const toOpenHlDataRowUid = getHighlightDataBlockUid(toOpenHlTextUid); 490 | const hlBlockUid = c3u.blockString(toOpenHlDataRowUid); 491 | navigator.clipboard.writeText("((" + hlBlockUid + "))"); 492 | } 493 | 494 | function handleOpenHighlight(event) { 495 | const toOpenHlTextUid = event.data.highlight.id; 496 | const toOpenHlDataRowUid = getHighlightDataBlockUid(toOpenHlTextUid); 497 | const hlBlockUid = c3u.blockString(toOpenHlDataRowUid); 498 | c3u.openBlockInSidebar('block', hlBlockUid) 499 | c3u.sleep(30) //prevent multiple block opening. 500 | } 501 | 502 | function handleUpdatedHighlight(event) { 503 | const newColorNum = parseInt(event.data.highlight.color); 504 | if (pdfParams.addColoredHighlight) { 505 | const hlId = event.data.highlight.id; 506 | updateHighlightText(newColorNum, hlId); 507 | updateHighlightData(newColorNum, hlId); 508 | } 509 | } 510 | 511 | function updateHighlightText(newColorNum, hlId) { 512 | console.log(hlId) 513 | console.log(newColorNum) 514 | let newColorString = ''; 515 | switch (newColorNum) { 516 | case 0: newColorString = ""; break; 517 | case 1: newColorString = "yellow"; break; 518 | case 2: newColorString = "red"; break; 519 | case 3: newColorString = "green"; break; 520 | case 4: newColorString = "blue"; break; 521 | case 5: newColorString = "purple"; break; 522 | case 6: newColorString = "grey"; break; 523 | } 524 | // if (newColorString !== '') { 525 | let hlText = c3u.blockString(hlId); 526 | //Separate Perfix and Main Text 527 | const hasPerfix1 = hlText.match(/>(.*)/); 528 | const hasPerfix2 = hlText.match(/\[\[>\]\](.*)/); 529 | let perfix, restTxt; 530 | if (hasPerfix1) { 531 | perfix = '>'; 532 | restTxt = hasPerfix1[1]; 533 | } else if (hasPerfix2) { 534 | perfix = '[[>]]'; 535 | restTxt = hasPerfix2[1]; 536 | } else { 537 | perfix = ''; 538 | restTxt = hlText; 539 | } 540 | const content = restTxt.match(/(.*)({{\d+:\s*.........}}\s*\[..?\]\(\(\(.........\)\)\))/); 541 | const isImage = restTxt.match(/.*(\!\[.*\]\(.*\))\s*{{\d+:\s*.........}}\s*\[..?\]\(\(\(.........\)\)\)/) 542 | const isHlTxt = restTxt.match(/.*(\^{2}(.*)\^{2}).*/) 543 | let mainContent; 544 | if (isImage) 545 | mainContent = ` ${isImage[1]}`; 546 | else if (isHlTxt) { 547 | mainHl = isHlTxt[1]; 548 | mainTxt = isHlTxt[2]; 549 | } 550 | else { 551 | mainHl = `^^${content[1]}^^`; 552 | mainTxt = content[1]; 553 | } 554 | const trail = content[2]; 555 | let colorPerfix = `#h:${newColorString}`; 556 | mainContent = mainHl; 557 | if (newColorString == '') { //Reset color 558 | colorPerfix = ''; 559 | mainContent = mainTxt; 560 | } 561 | c3u.updateBlockString(hlId, `${perfix} ${colorPerfix}${mainContent} ${trail}`); 562 | // } 563 | } 564 | 565 | function updateHighlightData(newColorNum, toUpdateHlTextUid) { 566 | const toUpdateHlDataRowUid = getHighlightDataBlockUid(toUpdateHlTextUid) 567 | const toUpdateHlInfoUid = c3u.getNthChildUid(toUpdateHlDataRowUid, 0); 568 | const toUpdateHlInfoString = c3u.blockString(toUpdateHlInfoUid); 569 | let toUpdateHlInfo = JSON.parse(toUpdateHlInfoString) 570 | let hlPosition; 571 | if (typeof (toUpdateHlInfo.position) === 'undefined') //if older version highlight 572 | hlPosition = toUpdateHlInfo; 573 | else 574 | hlPosition = toUpdateHlInfo.position; 575 | c3u.updateBlockString(toUpdateHlInfoUid, JSON.stringify({ position: hlPosition, color: newColorNum })); 576 | } 577 | 578 | function handleNewHighlight(event) { 579 | if (event.data.highlight.position.rects.length == 0) { 580 | event.data.highlight.position.rects[0] = event.data.highlight.position.boundingRect; 581 | } 582 | const page = event.data.highlight.position.pageNumber; 583 | const hlInfo = JSON.stringify({ 584 | position: event.data.highlight.position, color: event.data.highlight.color 585 | }); 586 | const iframe = document.getElementById(activePdfIframeId); 587 | const pdfBlockUid = c3u.getUidOfContainingBlock(iframe); 588 | let hlContent; 589 | const pdfAlias = `[${pdfChar}](((${pdfBlockUid})))`; 590 | const hlDataUid = c3u.createUid(); 591 | const hlTextUid = event.data.highlight.id; 592 | const hlBtn = `{{${page}: ${hlDataUid}}}`; 593 | 594 | if (event.data.highlight.imageUrl) { 595 | hlContent = `![](${event.data.highlight.imageUrl})`; 596 | } else { 597 | hlContent = `${event.data.highlight.content.text}`; 598 | } 599 | writeHighlightText(pdfBlockUid, hlTextUid, hlBtn, hlContent, pdfAlias, page); 600 | saveHighlightData(pdfBlockUid, decodePdfUrl(iframe.src), hlDataUid, hlTextUid, hlInfo, hlContent); 601 | } 602 | 603 | function handleDeletedHighlight(event) { 604 | const toDeleteHlTextUid = event.data.highlight.id; 605 | const toDeleteHlDataRowUid = getHighlightDataBlockUid(toDeleteHlTextUid) 606 | c3u.deleteBlock(toDeleteHlTextUid) 607 | c3u.deleteBlock(toDeleteHlDataRowUid) 608 | } 609 | 610 | ///////////For the Cousin Output Mode: Find the Uncle of the PDF Block. 611 | function getUncleBlock(pdfBlockUid) { 612 | const pdfParentBlockUid = c3u.parentBlockUid(pdfBlockUid); 613 | const gParentBlockUid = c3u.parentBlockUid(pdfParentBlockUid); 614 | let dictUid2Ord = {}; 615 | let dictOrd2Uid = {}; 616 | if (!gParentBlockUid) return null; 617 | const mainBlocksUid = c3u.allChildrenInfo(gParentBlockUid); 618 | mainBlocksUid[0][0].children.map(child => { 619 | dictUid2Ord[child.uid] = child.order; 620 | dictOrd2Uid[child.order] = child.uid; 621 | }); 622 | //Single assumption: PDF & Highlights are assumed to be siblings. 623 | let hlParentBlockUid = dictOrd2Uid[dictUid2Ord[pdfParentBlockUid] + 1]; 624 | if (!hlParentBlockUid) { 625 | hlParentBlockUid = c3u.createUid() 626 | c3u.createChildBlock(gParentBlockUid, dictUid2Ord[pdfParentBlockUid] + 1, 627 | pdfParams.highlightHeading, hlParentBlockUid); 628 | } 629 | return hlParentBlockUid; 630 | } 631 | 632 | ////////////Write the Highlight Text Using the Given Format 633 | let pdf2citeKey = {} 634 | let pdf2pgOffset = {} 635 | async function writeHighlightText(pdfBlockUid, hlTextUid, hlBtn, hlContent, pdfAlias, page) { 636 | let hlParentBlockUid; 637 | //Find where to write 638 | if (pdfParams.outputHighlighAt === 'cousin') { 639 | hlParentBlockUid = getUncleBlock(pdfBlockUid); 640 | await c3u.sleep(100); 641 | if (!hlParentBlockUid) hlParentBlockUid = pdfBlockUid; //there is no gparent, write hl as a child 642 | } else { //outputHighlighAt ==='child' 643 | hlParentBlockUid = pdfBlockUid 644 | } 645 | //Make the citation 646 | const perfix = (pdfParams.blockQPerfix === '') ? '' : pdfParams.blockQPerfix + ' '; 647 | let Citekey = ''; 648 | if (pdfParams.citationFormat !== '') { 649 | if (!pdf2citeKey[pdfBlockUid]) { 650 | pdf2citeKey[pdfBlockUid] = findPDFAttribute(pdfBlockUid, "Citekey") 651 | } 652 | if (!pdf2pgOffset[pdfBlockUid]) { 653 | const tempOffset = parseInt(findPDFAttribute(pdfBlockUid, "Page Offset")); 654 | pdf2pgOffset[pdfBlockUid] = isNaN(tempOffset) ? 0 : tempOffset; 655 | } 656 | Citekey = pdf2citeKey[pdfBlockUid]; 657 | page = page - pdf2pgOffset[pdfBlockUid]; 658 | } 659 | const citation = eval('`' + pdfParams.citationFormat + '`').replace(/\s+/g, ''); 660 | const hlText = `${perfix}${hlContent}${citation} ${hlBtn} ${pdfAlias}`; 661 | let ord = (pdfParams.appendHighlight) ? 'last' : 0; 662 | //Finally: writing 663 | c3u.createChildBlock(hlParentBlockUid, ord, hlText, hlTextUid); 664 | //What to put in clipboard 665 | if (pdfParams.copyBlockRef) 666 | navigator.clipboard.writeText("((" + hlTextUid + "))"); 667 | else 668 | navigator.clipboard.writeText(hlContent); 669 | } 670 | 671 | ///////////Save Annotations in the PDF Data Page in a Table 672 | function saveHighlightData(pdfUid, pdfUrl, hlDataUid, hlTextUid, hlInfo, hlContent) { 673 | const dataTableUid = getDataTableUid(pdfUrl, pdfUid); 674 | c3u.createChildBlock(dataTableUid, 0, hlTextUid, hlDataUid); 675 | const posUid = c3u.createUid(); 676 | c3u.createChildBlock(hlDataUid, 0, hlInfo, posUid); 677 | c3u.createChildBlock(posUid, 0, hlContent, c3u.createUid()); 678 | } 679 | 680 | /************Handle New HL Received END****************/ 681 | /*******************************************************/ 682 | 683 | /*******************************************************/ 684 | /**********Render PDF and Highlights BEGIN**************/ 685 | /////////////////////Find pdf iframe being highlighted 686 | let allPdfIframes = []; //History of opened pdf on page 687 | let activePdfIframeId = null; //Last active pdf iframe.id 688 | 689 | window.addEventListener('blur', function () { 690 | setTimeout(function () { 691 | activePdfIframe = allPdfIframes.find(x => x === document.activeElement); 692 | activePdfIframeId = activePdfIframe?.id; 693 | }, 500) 694 | }); 695 | 696 | /////////////////////Show the PDF through the Server 697 | function renderPdf(iframe) { 698 | iframe.classList.add('pdf-activated'); 699 | iframe.src = encodePdfUrl(iframe.src); 700 | iframe.style.minWidth = `${pdfParams.pdfMinWidth}px`; 701 | iframe.style.minHeight = `${pdfParams.pdfMinHeight}px`; 702 | } 703 | 704 | /////////////////////Send Old Saved Highlights to Server to Render 705 | function sendHighlights(iframe, originalPdfUrl, pdfBlockUid) { 706 | const highlights = getAllHighlights(originalPdfUrl, pdfBlockUid); 707 | window.setTimeout( // give it 5 seconds to load 708 | () => iframe.contentWindow.postMessage({ highlights }, '*'), 2000); 709 | } 710 | 711 | /////////////////////From PDF URL => Data Page => Retrieve Data 712 | function getAllHighlights(pdfUrl, pdfUid) { 713 | const dataTableUid = getDataTableUid(pdfUrl, pdfUid); 714 | return getHighlightsFromTable(dataTableUid); 715 | } 716 | 717 | function getDataTableUid(pdfUrl, pdfUid) { 718 | const pdfDataPageTitle = getDataPageTitle(pdfUrl); 719 | let pdfDataPageUid = c3u.getPageUid(pdfDataPageTitle); 720 | if (!pdfDataPageUid) //If this is the first time uploading the pdf 721 | pdfDataPageUid = createDataPage(pdfDataPageTitle, pdfUrl, pdfUid); 722 | return c3u.getNthChildUid(pdfDataPageUid, 2); 723 | } 724 | 725 | function getDataPageTitle(pdfUrl) { 726 | return 'roam/js/pdf/data/' + c3u.hashCode(pdfUrl); 727 | } 728 | /////////////////////Initialize a Data Page. Format is: 729 | /////////////////////pdfPageTitle 730 | /////////////////////////pdfUrl 731 | /////////////////////////pdfUid 732 | /////////////////////////{{table}} 733 | function createDataPage(pdfPageTitle, pdfUrl, pdfUid) { 734 | const pdfDataPageUid = c3u.createPage(pdfPageTitle); 735 | c3u.createChildBlock(pdfDataPageUid, 0, pdfUrl, c3u.createUid()); 736 | c3u.createChildBlock(pdfDataPageUid, 1, pdfUid, c3u.createUid()); 737 | c3u.createChildBlock(pdfDataPageUid, 2, "{{table}}", c3u.createUid()); 738 | return pdfDataPageUid; 739 | } 740 | /***********Render PDF and Highlights END***************/ 741 | /*******************************************************/ 742 | 743 | /*******************************************************/ 744 | /*********Helper API Functions BEGIN************/ 745 | 746 | ///////////////////////////////////////////////////////// 747 | //////////////Gather Buttons Information //////////////// 748 | ///////////////////////////////////////////////////////// 749 | ///////////////Are these highlight buttons? 750 | function isRoamBtn(btn) { 751 | return btn.classList.contains('block-ref-count-button') 752 | || btn.classList.contains('bp3-minimal') 753 | } 754 | 755 | function isInactive(btn) { 756 | return !btn.classList.contains('btn-pdf-activated'); 757 | } 758 | 759 | function isUnObserved(btn) { 760 | return !btn.classList.contains('btn-observed'); 761 | } 762 | 763 | function isHighlightBtn(btn) { 764 | return !isRoamBtn(btn) 765 | && btn.innerText.match(/^\d+$/) 766 | } 767 | 768 | function isSortBtn(btn) { 769 | return !isRoamBtn(btn) 770 | && btn.innerText.match(new RegExp(pdfParams.sortBtnText)) 771 | } 772 | 773 | function isInactiveSortBtn(btn) { 774 | return isSortBtn(btn) && isInactive(btn) 775 | } 776 | 777 | function isInactiveHighlightBtn(btn) { 778 | return isHighlightBtn(btn) && isInactive(btn) 779 | } 780 | 781 | function isUnObservedHighlightBtn(btn) { 782 | return isHighlightBtn(btn) && isUnObserved(btn) 783 | } 784 | 785 | 786 | function encodePdfUrl(url) { 787 | return serverPerfix + encodeURI(url); 788 | } 789 | 790 | function decodePdfUrl(url) { 791 | return decodeURI(url).substring(serverPerfix.length); 792 | } 793 | 794 | /*********Helper Functions END************/ 795 | window.setInterval(initPdf, 1000); 796 | 797 | } --------------------------------------------------------------------------------