├── 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 | [](https://www.youtube.com/watch?v=N8DOqIZQFLU)
127 |
128 | - Older tutorial:
129 | [](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 | [](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 | - [](https://www.youtube.com/watch?v=z3BoV-vkSRY)
232 |
233 | Older tutorial:
234 | - [](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 | - [](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 | - [](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 | - [](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 | - [](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 = ``;
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 | }
--------------------------------------------------------------------------------