├── .gitignore ├── LICENSE ├── README.md ├── chrome ├── package.json ├── src │ ├── App.js │ ├── background.js │ ├── components │ │ ├── ClickHandler.js │ │ ├── CollectionsDropdown.js │ │ └── NoteField.js │ ├── content.js │ ├── css │ │ ├── bootstrap.min.css │ │ ├── content.css │ │ └── popup.css │ ├── images │ │ ├── archived │ │ │ ├── icon128-1.png │ │ │ ├── icon128-old.png │ │ │ ├── icon128.png │ │ │ ├── icon16-old.png │ │ │ ├── icon16.png │ │ │ ├── icon24-old.png │ │ │ ├── icon24.png │ │ │ ├── icon32-old.png │ │ │ ├── icon32.png │ │ │ └── logo-network │ │ │ │ ├── icon128.png │ │ │ │ ├── icon16.png │ │ │ │ ├── icon24.png │ │ │ │ └── icon32.png │ │ ├── icon-screenshot.png │ │ ├── icon-slack.png │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon24.png │ │ ├── icon32.png │ │ └── spinner.gif │ ├── index.js │ ├── main.js │ ├── manifest.json │ ├── popup.html │ ├── popup.js │ └── vendor │ │ ├── jquery-3.4.1.min.js │ │ ├── tesseract.min.js │ │ └── tiny-tfidf.js └── webpack.config.js └── firefox ├── package.json ├── src ├── background.js ├── content.js ├── css │ ├── bootstrap.min.css │ ├── content.css │ └── popup.css ├── images │ ├── icon-screenshot.png │ ├── icon-slack.png │ ├── icon128.png │ ├── icon16.png │ ├── icon24.png │ ├── icon32.png │ └── spinner.gif ├── index.js ├── main.js ├── manifest.json ├── popup.html ├── popup.js └── vendor │ └── jquery-3.4.1.min.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | chrome/package-lock.json 2 | firefox/package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rumin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rumin Web Clipper 2 | Browser extension for [Rumin](https://getrumin.com) which allows you to save learnings from online resources. ([Chrome Web Store link](https://chrome.google.com/webstore/detail/rumin/eboiffdknchlbeboepkadciilgmaojbj), [Firefox Add-on link](https://addons.mozilla.org/en-US/firefox/addon/rumin/)) 3 | 4 | It comes with site-specific logic for automatically extracting key information, such as video playing time on YouTube, course info and reference on edX, metadata on Kindle notes etc. This can be easily extended to add extraction logic to any web page. 5 | 6 | This is useful for learning, research, and saving interesting ideas for creative work. 7 | 8 | You can also search / look up existing content (defaulting to the Rumin backend) using the "Lookup" tab. 9 | 10 | ### Demo video 11 | [![YouTube thumbnail for video demo](https://storage.googleapis.com/rumin-gcs-bucket/newsletter/youtube-thumbnail.PNG)](https://www.youtube.com/watch?v=auZGwCc1B_o) 12 | 13 | 14 | ### Saving with one click 15 | You can use the extension to bookmark a page, or select a particular passage by simply highlighting it. 16 | 17 | By default, the extension can be opened using the `Ctrl / Cmd + K` keyboard shortcut. 18 | 19 | ### Screenshot 20 | Take a screenshot in one click, using the "Screenshot" button. It captures the content in the current browser window. 21 | 22 | 23 | ### Page-specific Parsing Logic 24 | The extension comes with page-specific logic extracts the key metadata, and turns page content into structured data. This way, you don't have to copy and paste back and forth. 25 | 26 | Currently supported sites include: 27 | - YouTube video 28 | - Skillshare 29 | - edX 30 | - Linkedin Learning 31 | - Netflix 32 | - Messages on Slack 33 | - Comments on Reddit 34 | and more 35 | 36 | ### Lookup 37 | The "Lookup" tab fetches existing captured notes/snippets from the current url by default. You can also use it to search your knowledge base. 38 | 39 | ## Extensible 40 | It is easy to add support for automatic extraction on more sites. All you need is logic to check the url (or content) of the current page, and add logic to update `customFields` accordingly. 41 | 42 | For example, it can be extended to save the top Hacker News comments on a thread, or save metadata of an answer on Quora. 43 | 44 | ## Custom Backend 45 | To use this extension for a different project, simply swap out the getrumin.com API calls with your custom endpoints. 46 | 47 | ## Upcoming work 48 | - Replace jQuery code with React components. 49 | - Support custom logic for extraction on each page. e.g. saved extraction scripts 50 | - Allow editing of automatically extracted fields 51 | - Support multi-capture on the same page 52 | - Allow for adjustment of the dimensions of the screenshot 53 | - Add OCR to captured images 54 | - Support PDF format 55 | - Faster fetching of existing collections 56 | - Navigate nested collections (similar to nested folders) 57 | -------------------------------------------------------------------------------- /chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rumin_Chrome_extension", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --mode development --output ./src/main.js --watch", 8 | "build": "webpack --mode production --output ./src/main.js --watch" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.8.4", 15 | "@babel/plugin-proposal-class-properties": "^7.8.3", 16 | "@babel/plugin-transform-async-to-generator": "^7.8.3", 17 | "@babel/plugin-transform-regenerator": "^7.8.3", 18 | "@babel/preset-env": "^7.8.4", 19 | "@babel/preset-react": "^7.8.3", 20 | "babel-loader": "^8.0.6", 21 | "react": "^16.13.1", 22 | "react-datepicker": "^2.13.0", 23 | "react-dom": "^16.13.1", 24 | "tesseract.js": "^2.1.1", 25 | "tiny-tfidf": "^0.9.1", 26 | "webpack": "^4.41.5", 27 | "webpack-cli": "^3.3.11" 28 | }, 29 | "dependencies": { 30 | "slate": "^0.57.2", 31 | "slate-react": "^0.57.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /chrome/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from 'react-dom' 3 | import { CollectionsDropdown } from './components/CollectionsDropdown' 4 | 5 | const App = (props) => { 6 | return( 7 |
8 | 9 |
10 | ) 11 | // } 12 | } 13 | 14 | // export default App 15 | 16 | const container = document.getElementById("add_to_collection") 17 | render(, container) 18 | -------------------------------------------------------------------------------- /chrome/src/background.js: -------------------------------------------------------------------------------- 1 | // Called when the user clicks on the browser action. but doesn't work if there's a popup 2 | // chrome.browserAction.onClicked.addListener(function(tab) { 3 | // console.log("browserAction onClicked"); 4 | // }); 5 | 6 | function sendMsgToContentScript(msg) { 7 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 8 | chrome.tabs.sendMessage(tabs[0].id, msg, function(response) {}) 9 | }); 10 | } 11 | 12 | // chrome.runtime.onConnect.addListener(() => { 13 | // }) 14 | 15 | // chrome.permissions.contains({ 16 | // permissions: ['storage'] 17 | // }, function(result) { 18 | // if (result) { 19 | // // notify content script 20 | // // sendMsgToContentScript({hasStoragePermission: true}) 21 | // } 22 | // }) 23 | 24 | 25 | chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){ 26 | if (message.popupOpen) { 27 | // console.log('popup is open'); 28 | 29 | sendMsgToContentScript({"message": "clicked_browser_action"}) 30 | } 31 | 32 | if (message.takeScreenshot) { 33 | // console.log('TODO - take a screenshot') 34 | 35 | chrome.tabs.captureVisibleTab(null, { 36 | format : "png" 37 | }, function(data) { 38 | // screenshot.data = data; 39 | // console.log('screenshot data', data) 40 | 41 | chrome.runtime.sendMessage({screenshotCaptured: true, screenshotData: data}); 42 | }); 43 | } 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /chrome/src/components/ClickHandler.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | 3 | export class ClickHandler extends PureComponent { 4 | constructor(props, context) { 5 | super(props, context) 6 | 7 | this.handleClickOutside = this.handleClickOutside.bind(this) 8 | } 9 | 10 | componentDidMount() { 11 | document.addEventListener("mousedown", this.handleClickOutside) 12 | } 13 | 14 | componentWillUnmount() { 15 | document.removeEventListener("mousedown", this.handleClickOutside) 16 | } 17 | 18 | handleClickOutside(e) { 19 | if ( 20 | this.wrapperRef && 21 | !this.wrapperRef.contains(e.target) 22 | ) { 23 | this.props.close() 24 | } 25 | } 26 | 27 | render() { 28 | return( 29 | (this.wrapperRef = node)} 32 | style={{width: '100%', height: '100%', display: `${this.props.displayBlock ? 'block' : 'inline'}`}} 33 | >{this.props.children} 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /chrome/src/components/CollectionsDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { ClickHandler } from './ClickHandler' 3 | 4 | export const CollectionsDropdown = (props) => { 5 | const [query, setQuery] = useState('') 6 | const [selectedSpaces, setSelectedSpaces] = useState([]) 7 | const [prevSelectedSpaces, setPrevSelectedSpaces] = useState([]) 8 | const [prevSpaceSuggestions, setPrevSpaceSuggestions] = useState([]) 9 | const [hasStoragePermission, setHasStoragePermission] = useState(false) 10 | const [queryTimer, setQueryTimer] = useState(null) 11 | 12 | const [isFetchingSuggestions, setIsFetchingSuggestions] = useState(false) 13 | const [hasFetchedSuggestions, setHasFetchedSuggestions] = useState(false) 14 | const [showSuggestionsDropdown, setShowSuggestionsDropdown] = useState(false) 15 | 16 | const [spaceSuggestions, setSpaceSuggestions] = useState([]) // results 17 | 18 | useEffect(() => { 19 | chrome.permissions.contains({ 20 | permissions: ['storage'] 21 | }, result => { 22 | if (result) { 23 | setHasStoragePermission(true) 24 | 25 | chrome.storage.local.get(['selectedSpaces', 'prevSelectedSpaces', 'prevSpaceSuggestions'], result => { 26 | if (result.selectedSpaces && result.selectedSpaces.length > 0 && selectedSpaces.length === 0) { 27 | setSelectedSpaces(result.selectedSpaces) 28 | window.selectedSpaces = result.selectedSpaces 29 | } 30 | 31 | if (result.prevSelectedSpaces && result.prevSelectedSpaces.length > 0) { 32 | setPrevSelectedSpaces(result.prevSelectedSpaces) 33 | } 34 | // if (result.prevSpaceSuggestions && result.prevSpaceSuggestions.length > 0) { 35 | // setPrevSpaceSuggestions(result.prevSpaceSuggestions) 36 | // } 37 | }) 38 | } 39 | }) 40 | }, []) 41 | 42 | const uniqueSuggestions = (suggestions) => { 43 | let results = [] 44 | let seen = {} 45 | suggestions.forEach(s => { 46 | if (seen[s.id] === true) { 47 | return 48 | } 49 | 50 | results.push(s) 51 | seen[s.id] = true 52 | }) 53 | 54 | return results 55 | } 56 | 57 | const updateSelectedSpaces = (selected) => { 58 | setSelectedSpaces(selected) 59 | window.selectedSpaces = selected 60 | 61 | if (hasStoragePermission) { 62 | chrome.storage.local.set({ selectedSpaces: selected }) 63 | 64 | // cache the previously selected spaces 65 | chrome.storage.local.get(['prevSelectedSpaces'], result => { 66 | const cachedResults = result.prevSelectedSpaces || [] 67 | const suggestionsToCache = uniqueSuggestions([...cachedResults.slice(0, 100), ...selected]) 68 | chrome.storage.local.set({ prevSelectedSpaces: suggestionsToCache }) 69 | }) 70 | } 71 | } 72 | 73 | const handleSuggestionClick = (space) => { 74 | const updatedSelection = [...selectedSpaces, space] 75 | updateSelectedSpaces(updatedSelection) 76 | setQuery('') 77 | setShowSuggestionsDropdown(false) 78 | } 79 | 80 | const handleRemoveToken = (space) => { 81 | const updatedSelection = selectedSpaces.filter(s => s.id !== space.id) 82 | updateSelectedSpaces(updatedSelection) 83 | } 84 | 85 | const fetchAutosuggest = () => { 86 | if (query.length < 2) return 87 | 88 | setHasFetchedSuggestions(false) 89 | setIsFetchingSuggestions(true) 90 | 91 | fetch(`https://getrumin.com/api/v1/search/?q=${query}&content_type=Space&lite=true`, { 92 | method: 'GET', 93 | headers: { 94 | 'Content-type': 'application/json' 95 | } 96 | }) 97 | .then(res => { 98 | if (!res.ok) throw new Error(res.status) 99 | else return res.json() 100 | }) 101 | .then(data => { 102 | setShowSuggestionsDropdown(true) 103 | setSpaceSuggestions(data.results) 104 | 105 | setIsFetchingSuggestions(false) 106 | setHasFetchedSuggestions(true) 107 | 108 | // cache the response 109 | // if (hasStoragePermission) { 110 | // chrome.storage.local.get(['prevSpaceSuggestions'], result => { 111 | // const cachedResults = result.prevSpaceSuggestions || [] 112 | 113 | // const suggestionsToCache = uniqueSuggestions([...cachedResults.slice(0, 100), ...data.results]) 114 | // chrome.storage.local.set({ prevSpaceSuggestions: suggestionsToCache }) 115 | // }) 116 | // } 117 | }) 118 | .catch(error => { 119 | console.log('error: ' + error) 120 | }) 121 | } 122 | 123 | const handleInputFocus = () => { 124 | 125 | // if there is no query, use previously selected spaces 126 | if (query.length === 0 && prevSelectedSpaces.length > 0) { 127 | setShowSuggestionsDropdown(true) 128 | 129 | const suggestions = prevSelectedSpaces.slice(0, 20).filter(s => { 130 | return !selectedSpaces.map(ss => ss.id).includes(s.id) 131 | }) 132 | // console.log('in handleInputFocus', suggestions, spaceSuggestions.map(ss => ss.id), prevSelectedSpaces) 133 | 134 | setSpaceSuggestions(suggestions) 135 | } 136 | } 137 | 138 | const handleQueryChange = (e) => { 139 | setQuery(e.target.value) 140 | } 141 | 142 | const handleQueryKeyDown = (e) => { 143 | if (e.key === 'Backspace' && query.length === 0) { 144 | handleRemoveToken(selectedSpaces[selectedSpaces.length-1]) 145 | } 146 | 147 | if (e.key === 'Escape') { 148 | props.closeDropdown() 149 | } 150 | } 151 | 152 | const handleQueryKeyUp = () => { 153 | clearTimeout(queryTimer) 154 | const timer = setTimeout(() => { fetchAutosuggest() }, 750) 155 | setQueryTimer(timer) 156 | } 157 | 158 | const handleNewSpaceClick = () => { 159 | const body = { 160 | title: query 161 | } 162 | 163 | fetch(`https://getrumin.com/api/v1/spaces/`, { 164 | method: 'POST', 165 | headers: { 166 | 'Content-type': 'application/json' 167 | }, 168 | body: JSON.stringify(body) 169 | }) 170 | .then(res => { 171 | if (!res.ok) throw new Error(res.status) 172 | else return res.json() 173 | }) 174 | .then(space => { 175 | // success 176 | const updatedSelection = [...selectedSpaces, space] 177 | updateSelectedSpaces(updatedSelection) 178 | setQuery('') 179 | }) 180 | .catch(error => { 181 | console.log('error: ' + error) 182 | }) 183 | } 184 | 185 | const buildResults = () => { 186 | // console.log(query.length, hasFetchedSuggestions, spaceSuggestions.length, spaceSuggestions) 187 | 188 | if (query.length > 0 && hasFetchedSuggestions && spaceSuggestions.length === 0) { 189 | return( 190 |
193 | No matching results 194 |
195 | ) 196 | } 197 | 198 | // if (query.length < 2) return '' 199 | return spaceSuggestions.map(space => { 200 | return( 201 | 206 | ); 207 | }); 208 | } 209 | 210 | const buildSelectedTokens = () => { 211 | return selectedSpaces.map(space => { 212 | return( 213 |
214 | 215 | { space.title } 216 | 217 |
handleRemoveToken(space)} 220 | > 221 | 222 |
223 |
224 | ) 225 | }) 226 | } 227 | 228 | const buildFetchingMoreMsg = () => { 229 | if (!isFetchingSuggestions) return '' 230 | 231 | return( 232 |
235 | fetching more results... 236 |
237 | ) 238 | } 239 | 240 | const buildASResultsSection = () => { 241 | if (!showSuggestionsDropdown) return '' 242 | // if (!showSuggestionsDropdown || spaceSuggestions.length < 1) return '' 243 | 244 | return( 245 | setShowSuggestionsDropdown(false)} 247 | > 248 |
249 |
250 | { buildCreateCollection() } 251 | { buildResults() } 252 | { buildFetchingMoreMsg() } 253 |
254 |
255 |
256 | ) 257 | } 258 | 259 | const buildCreateCollection = () => { 260 | if (query.length < 1) return '' 261 | 262 | return( 263 |
268 | + New page "{ query }" 269 |
270 | ) 271 | } 272 | 273 | return( 274 |
275 |
Add to collections
276 | 277 |
278 | { buildSelectedTokens() } 279 | 280 |
281 | 290 |
291 |
292 | 293 | { buildASResultsSection() } 294 |
295 | ); 296 | } 297 | 298 | const SuggestionResult = (props) => { 299 | const handleClick = () => { 300 | props.handleSuggestionClick(props.space) 301 | } 302 | 303 | return( 304 |
308 | { props.space.title } 309 |
310 | ) 311 | } 312 | 313 | const SuggestedLinkToken = (props) => { 314 | return( 315 |
316 | { props.space.title } 317 |
320 | 321 |
322 |
323 | ) 324 | } 325 | -------------------------------------------------------------------------------- /chrome/src/components/NoteField.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | export const NoteField = (props) => { 4 | return( 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /chrome/src/content.js: -------------------------------------------------------------------------------- 1 | const MOUSE_VISITED_CLASSNAME = 'crx_mouse_visited'; 2 | let prevDOM = null 3 | let selectingDOM = false 4 | let selectedElements = [] 5 | let hasStoragePermission = false 6 | 7 | // function isElementVisible(el) { 8 | // const o = new IntersectionObserver(([entry]) => { 9 | // return entry.intersectionRatio === 1; 10 | // }); 11 | // o.observe(el) 12 | // } 13 | 14 | const getTopTermsInDoc = () => { 15 | const corpus = new TinyTFIDF.Corpus(['doc1'], [$('body').text()]) 16 | return corpus.getTopTermsForDocument('doc1').slice(0, 15) 17 | } 18 | 19 | const initSelectedElements = () => { 20 | // get fetch existing selectedElements 21 | chrome.storage.local.get(['selectedElements'], function(result) { 22 | console.log('result.selectedElements', result.selectedElements) 23 | 24 | selectedElements = result.selectedElements || [] 25 | }) 26 | } 27 | 28 | const clearSelection = () => { 29 | unstyleSelectedElements() 30 | 31 | selectedElements = [] 32 | $(document).unbind('mousemove') 33 | } 34 | 35 | const unstyleSelectedElements = () => { 36 | if (selectedElements.length > 0) { 37 | selectedElements.forEach(el => { 38 | // console.log('el in selectedElements', el) 39 | el.classList.remove(MOUSE_VISITED_CLASSNAME); 40 | }) 41 | } 42 | } 43 | 44 | const styleSelectedElements = () => { 45 | if (selectedElements.length > 0) { 46 | selectedElements.forEach(el => { 47 | // console.log('el in selectedElements', el) 48 | el.classList.add(MOUSE_VISITED_CLASSNAME); 49 | }) 50 | } 51 | } 52 | 53 | // Extract selected fields for Slack 54 | const slackFields = () => { 55 | const messages = selectedElements.map(el => { 56 | let selector = $(el) 57 | if (!el.classList.contains('c-virtual_list__item') && !el.classList.contains('c-message_kit__gutter')) { 58 | selector = $(el).closest('.c-virtual_list__item') 59 | } 60 | 61 | if (selector.length === 0) return null 62 | 63 | const sender = selector.find('.c-message__sender').text() 64 | const link = selector.find('a.c-link.c-timestamp')[0]['href'] 65 | const text = selector.find('.p-rich_text_block').text() 66 | 67 | return({ 68 | sender_name: sender, 69 | message_link: link, 70 | message_text: text 71 | }) 72 | }) 73 | 74 | return { messages: messages.filter((m) => m) } 75 | } 76 | 77 | const redditSelectedFields = () => { 78 | const comments = selectedElements.map(el => { 79 | let selector = $(el) 80 | 81 | let comment = {} 82 | 83 | const commentBody = $(el).find('div[data-test-id="comment"]') 84 | if (commentBody.length === 0) return null 85 | comment.body = commentBody.text() 86 | 87 | const timestamp = $(el).find('[id^=CommentTopMeta--Created--]') 88 | if (timestamp.length > 0) { 89 | comment.url = timestamp[0]['href'] 90 | } 91 | 92 | return comment 93 | }) 94 | 95 | return { comments: comments.filter(c => c) } 96 | } 97 | 98 | // parse '(hh):mm:ss' string into seconds 99 | const timeStringToSeconds = (str) => { 100 | return str.split(':').reverse().reduce((prev, curr, i) => prev + curr*Math.pow(60, i), 0) 101 | } 102 | 103 | const youtubeFields = () => { 104 | const currentTime = $('.ytp-time-current')[0].innerText 105 | const channelName = $('.ytd-channel-name yt-formatted-string')[0].innerText 106 | const channelUrl = $('.ytd-channel-name yt-formatted-string a')[0]['href'] 107 | const publishedDate = $('#date yt-formatted-string.ytd-video-primary-info-renderer')[0].innerText.replace('Premiered ', '') // FIXME - this can break for other languages 108 | 109 | return ({ 110 | current_time: currentTime, 111 | channel_name: channelName, 112 | channel_url: channelUrl, 113 | published_date: publishedDate 114 | }) 115 | } 116 | 117 | const linkedinLearningFields = () => { 118 | const classTitle = $('.classroom-nav__details h1').text() 119 | const currentTime = $('.vjs-current-time').text() 120 | const teacherName = $('.authors-entity__name-text').text().trim().split("\n")[0] 121 | const teacherUrl = $('a.course-author-entity__lockup').attr('href') 122 | const sessionTitle = $('.classroom-toc-item--selected').text() 123 | const sessionTranscript = $('.transcripts-component__sections').text().trim() 124 | 125 | return({ 126 | class_title: classTitle.trim(), 127 | current_time: currentTime, 128 | teacher_name: teacherName.trim(), 129 | teacher_url: teacherUrl, 130 | session_title: sessionTitle.trim(), 131 | session_transcript: sessionTranscript 132 | }) 133 | } 134 | 135 | const skillShareFields = () => { 136 | const classTitle = $('.class-details-header-name').text() 137 | const currentTime = $('.vjs-current-time-display').text() 138 | const teacherName = $('.class-details-header-teacher-link').text() 139 | const teacherUrl = $('.class-details-header-teacher-link').attr('href') 140 | const sessionTitle = $('.session-item.active .session-item-title').text() 141 | 142 | return({ 143 | class_title: classTitle.trim(), 144 | current_time: currentTime, 145 | teacher_name: teacherName.trim(), 146 | teacher_url: teacherUrl, 147 | session_title: sessionTitle.trim() 148 | }) 149 | } 150 | 151 | const netflixFields = () => { 152 | const videoTitle = $('.video-title h4').text() 153 | const episodeTitle = $('.video-title span').text() 154 | const currentTime = $('.scrubber-head').attr('aria-valuetext') 155 | 156 | return({ 157 | video_title: videoTitle, 158 | episode_title: episodeTitle, 159 | current_time: currentTime 160 | }) 161 | } 162 | 163 | const edxLectureFields = () => { 164 | const provider = $('.course-header .provider').text() 165 | const courseCode = $('.course-header .course-number').text() 166 | const courseName = $('.course-header .course-name').text() 167 | 168 | const videoUrl = $('.video-sources').length > 0 ? $('.video-sources').get(0)['href'] : null 169 | const slidesUrl = $('a[href$=pdf]').length > 0 ? $('a[href$=pdf]').get(0)['href'] : null 170 | 171 | const vidTime = $('.vidtime').length > 0 ? $('.vidtime').text().split('/')[0].trim() : null 172 | 173 | return({ 174 | course_provider: provider, 175 | course_code: courseCode, 176 | course_name: courseName, 177 | video_url: videoUrl, 178 | slides_url: slidesUrl, 179 | current_time: vidTime 180 | }) 181 | } 182 | 183 | const isLinkedinLearningPage = () => { 184 | return location.href.startsWith('https://www.linkedin.com/learning/') && $('.classroom-layout__content').length > 0 185 | } 186 | 187 | const isSkillshareVideoPage = () => { 188 | return location.href.startsWith('https://www.skillshare.com/classes/') 189 | } 190 | 191 | const isNetflixVideoPage = () => { 192 | return location.href.startsWith('https://www.netflix.com/watch/') 193 | } 194 | 195 | const isYoutubeVideoPage = () => { 196 | return location.href.startsWith('https://www.youtube.com/watch?v=') 197 | } 198 | 199 | const isKindleCloudReaderPage = () => { 200 | return location.href.startsWith('https://read.amazon.com') && !location.href.includes('notebook') 201 | } 202 | 203 | const isKindleNotebookPage = () => { 204 | return location.href.startsWith('https://read.amazon.com/notebook') 205 | } 206 | 207 | const isSlackPage = () => { 208 | return location.href.startsWith('https://app.slack.com/client/') 209 | } 210 | 211 | const isRedditPage = () => { 212 | return location.href.includes('reddit.com/r/') && location.href.includes('comments') 213 | } 214 | 215 | const isEdxLecturePage = () => { 216 | return location.href.startsWith('https://courses.edx.org/courses/') && location.href.includes('courseware') 217 | } 218 | 219 | const parseSelectedElements = () => { 220 | if (isSlackPage()) { 221 | return slackFields() 222 | } 223 | 224 | if (isRedditPage()) { 225 | return redditSelectedFields() 226 | } 227 | } 228 | 229 | chrome.runtime.onMessage.addListener( 230 | function(request, sender, sendResponse) { 231 | // console.log(request.message); 232 | 233 | // if (request.disconnect === true) { 234 | // if (hasStoragePermission) { 235 | // chrome.storage.local.clear() 236 | // chrome.storage.local.remove('selectedElements') 237 | // } 238 | // } 239 | 240 | if (request.hasStoragePermission === true) { 241 | hasStoragePermission = true 242 | sendResponse({ack: true}) 243 | } 244 | 245 | if (request.clearSelection === true) { 246 | clearSelection() 247 | sendResponse({ack: true}) 248 | } 249 | 250 | if (request.message === "clicked_browser_action") { 251 | const sel = window.getSelection() 252 | const selectionText = sel.toString() 253 | 254 | // console.log('selectionText', selectionText) 255 | 256 | let titleOverride = null 257 | let urlOverride = null 258 | let customFields = {} 259 | let closestId = '' 260 | 261 | if (sel && sel.rangeCount > 0) { 262 | const selectionEl = sel.getRangeAt(0).startContainer.parentNode 263 | 264 | if (selectionEl.id) { 265 | closestId = selectionEl.id 266 | } 267 | else { 268 | const prevSibling = $(selectionEl).prev('[id]') 269 | const prevParent = $(selectionEl).closest('[id]') 270 | 271 | if (prevSibling.length > 0) { 272 | closestId = prevSibling[0].id 273 | } 274 | else if (prevParent.length > 0) { 275 | closestId = prevParent[0].id 276 | } 277 | } 278 | 279 | if (closestId) { 280 | urlOverride = `${location.href}#${closestId}` 281 | } 282 | } 283 | 284 | // Index the DOM 285 | getTopTermsInDoc() 286 | 287 | 288 | // Youtube video 289 | if (isYoutubeVideoPage()) { 290 | const fields = youtubeFields() 291 | Object.assign(customFields, fields) 292 | 293 | // TODO - replace an existing t parameter 294 | if (location.search.includes('t=')) { 295 | urlOverride = `${location.origin}${location.pathname}${location.search.replace(/t=[0-9]+s/, 't=' + timeStringToSeconds(fields.current_time) + 's')}` 296 | } 297 | else { 298 | urlOverride = `${location.href}&t=${timeStringToSeconds(fields.current_time)}` 299 | } 300 | } 301 | 302 | // Netflix Video 303 | if (isNetflixVideoPage()) { 304 | const fields = netflixFields() 305 | Object.assign(customFields, fields) 306 | } 307 | 308 | // Skillshare video 309 | if (isSkillshareVideoPage()) { 310 | const fields = skillShareFields() 311 | Object.assign(customFields, fields) 312 | } 313 | 314 | // Linkedin Learning 315 | if (isLinkedinLearningPage()) { 316 | const fields = linkedinLearningFields() 317 | Object.assign(customFields, fields) 318 | } 319 | 320 | 321 | // Kindle Cloud reader 322 | if (isKindleCloudReaderPage()) { 323 | 324 | } 325 | 326 | // Page title 327 | if ($('h1').length > 0) { 328 | customFields.page_title = $('h1')[0].textContent.trim() 329 | } 330 | 331 | // edX 332 | if (isEdxLecturePage()) { 333 | Object.assign(customFields, edxLectureFields()) 334 | } 335 | 336 | // if (isMedium) 337 | // Kindle Notes and Highlights: https://read.amazon.com/notebook 338 | // Go to the first book 339 | // $('.kp-notebook-library-each-book a.a-link-normal')[0].click() 340 | // document in the first kindle iframe 341 | // $('#KindleReaderIFrame').get(0).contentDocument 342 | if (isKindleNotebookPage()) { 343 | 344 | console.log('is kindle notebook page!') 345 | 346 | titleOverride = $('h3').text() 347 | customFields.page_title = $('h3').text() 348 | customFields.book_title = $('h3').text() 349 | customFields.book_author = $('p.kp-notebook-metadata')[1].innerText 350 | 351 | let currRow 352 | 353 | if (sel && sel.rangeCount > 0) { 354 | const selectionEl = sel.getRangeAt(0).startContainer.parentNode 355 | 356 | if (selectionEl.classList.contains('a-row')) { 357 | currRow = selectionEl 358 | // closestId = selectionEl.id 359 | } 360 | else { 361 | const prevSibling = $(selectionEl).prev('.a-row') 362 | const prevParent = $(selectionEl).closest('.a-row') 363 | 364 | if (prevSibling.length > 0) { 365 | // closestId = prevSibling[0].id 366 | currRow = prevSibling 367 | } 368 | else if (prevParent.length > 0) { 369 | // closestId = prevParent[0].id 370 | currRow = prevParent 371 | } 372 | } 373 | 374 | console.log('currRow', currRow) 375 | const prevRow = $(selectionEl).closest('.kp-notebook-row-separator') 376 | console.log('prevRow', prevRow) 377 | 378 | 379 | customFields.book_location = prevRow.find('#annotationHighlightHeader')[0].innerText 380 | 381 | } 382 | } 383 | 384 | 385 | if (selectedElements.length > 0) { 386 | 387 | const selectedFields = parseSelectedElements() 388 | 389 | customFields = { 390 | ...customFields, 391 | ...selectedFields 392 | } 393 | } 394 | 395 | const pageContext = { 396 | pageContext: { 397 | urlOverride: urlOverride, 398 | titleOverride: titleOverride, 399 | selection: selectionText, 400 | // closestId: closestId, 401 | // page_dom: document.documentElement.outerHTML, 402 | customFields: customFields 403 | } 404 | }; 405 | 406 | // console.log('sending pageContext', pageContext, window.getSelection().toString()) 407 | 408 | chrome.runtime.sendMessage(pageContext, function(response) { 409 | }); 410 | } 411 | 412 | if (request.addSelection) { 413 | selectingDOM = true 414 | 415 | console.log('in addSelection in content script') 416 | sendResponse({ack: true}) 417 | 418 | $(document).mousemove(function(e) { 419 | var target = e.target; 420 | 421 | // console.log('target', target) 422 | const whiteListedNodes = ['DIV', 'IMG', 'A', 'P', 'SPAN', 'H1', 'H2', 'H3', 'H4', 'H5'] 423 | 424 | // if (whiteListedClasses && target.class) 425 | // TODO - perhaps we should restrict what elements can be added? 426 | // do it by source 427 | 428 | if (whiteListedNodes.includes(target.nodeName)) { 429 | // For NPE checking, we check safely. We need to remove the class name 430 | // Since we will be styling the new one after. 431 | if (prevDOM != null && !selectedElements.includes(prevDOM)) { 432 | prevDOM.classList.remove(MOUSE_VISITED_CLASSNAME); 433 | } 434 | // Add a visited class name to the element. So we can style it. 435 | target.classList.add(MOUSE_VISITED_CLASSNAME); 436 | // The current element is now the previous. So we can remove the class 437 | // during the next iteration. 438 | prevDOM = target; 439 | } 440 | }) 441 | } 442 | 443 | if (request.cancelSelection) { 444 | selectingDOM = false 445 | $(document).unbind('mousemove') 446 | } 447 | } 448 | ); 449 | 450 | 451 | $(function() { 452 | $(document).click(function(e) { 453 | if (selectingDOM) { 454 | if (hasStoragePermission) { 455 | let element = e.target 456 | 457 | if (selectedElements && selectedElements.includes(element.outerHTML)) return 458 | 459 | selectedElements.push(element) 460 | } 461 | } 462 | }) 463 | }) 464 | -------------------------------------------------------------------------------- /chrome/src/css/content.css: -------------------------------------------------------------------------------- 1 | .crx_mouse_visited { 2 | background-color: #cbe6ff !important; 3 | outline: 1px solid #5166bb !important; 4 | } 5 | -------------------------------------------------------------------------------- /chrome/src/css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 250px; 3 | position: relative; 4 | } 5 | 6 | button { 7 | height: 30px; 8 | width: 30px; 9 | outline: none; 10 | } 11 | 12 | .flex { 13 | display: flex; 14 | } 15 | 16 | 17 | ::-webkit-scrollbar { 18 | width: 0px; 19 | } 20 | 21 | ::-webkit-scrollbar-corner { 22 | background: transparent; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb { 26 | background-color: rgba(0,0,0,.2); 27 | background-clip: padding-box; 28 | border: solid transparent; 29 | border-width: 1px 1px 1px 6px; 30 | min-height: 28px; 31 | padding: 100px 0 0; 32 | } 33 | 34 | .hidden { 35 | display: none !important; 36 | } 37 | 38 | .section-heading { 39 | cursor: pointer; 40 | } 41 | 42 | .gray-text { 43 | color: #777; 44 | } 45 | 46 | .section-heading .text { 47 | font-weight: 600; 48 | font-size: 0.7em; 49 | text-transform: uppercase; 50 | margin-bottom: 8px; 51 | } 52 | 53 | .btn.btn-primary { 54 | background-color: #0f8bff; 55 | } 56 | 57 | .inline-block { 58 | display: inline-block; 59 | } 60 | 61 | i.small-icon { 62 | font-size: 0.8em; 63 | } 64 | 65 | .field-label { 66 | font-size: 0.85em; 67 | color: #777; 68 | margin-bottom: 4px; 69 | } 70 | 71 | #screenshot_img { 72 | width: 100%; 73 | } 74 | 75 | .capture-form-container { 76 | padding-bottom: 2em; 77 | overflow-y: scroll; 78 | overflow: visible; 79 | } 80 | 81 | /*.capture-form-container::-webkit-scrollbar { 82 | width: 12px; 83 | }*/ 84 | 85 | .save-btn-container { 86 | position: fixed; 87 | width: 100%; 88 | left: 0; 89 | right: 0; 90 | bottom: 0; 91 | padding: 1em; 92 | background-color: white; 93 | } 94 | 95 | .permission-bar { 96 | font-size: 0.9em; 97 | text-align: center; 98 | background-color: #CBE6FF; 99 | padding: 0.5em 0; 100 | } 101 | 102 | .inline-block { 103 | display: inline-block; 104 | } 105 | 106 | .btn-icon { 107 | width: 14px; 108 | margin-top: -4px; 109 | } 110 | 111 | .tabs { 112 | white-space: nowrap; 113 | border-bottom: 1px solid #eee; 114 | } 115 | 116 | .tabs .tab { 117 | display: inline-block; 118 | width: 50%; 119 | text-align: center; 120 | box-sizing: border-box; 121 | padding: 0.5em 0; 122 | color: #777; 123 | cursor: pointer; 124 | } 125 | 126 | .tabs .tab:hover { 127 | background-color: #f0f0f0; 128 | } 129 | 130 | .tab.active { 131 | border-bottom: 2px solid #0f8bff; 132 | } 133 | 134 | .section-container, 135 | .capture-container { 136 | width: 350px; 137 | } 138 | 139 | .capture-container { 140 | padding: 1em; 141 | } 142 | 143 | .captured-selection { 144 | padding: 1em; 145 | font-size: 0.8em; 146 | background-color: #f9f9f9; 147 | margin-bottom: 1em; 148 | 149 | max-height: 200px; 150 | overflow-y: scroll; 151 | } 152 | 153 | .captured-selection::-webkit-scrollbar { 154 | width: 12px; 155 | } 156 | 157 | .capture-container textarea { 158 | resize: none; 159 | min-height: 5em; 160 | overflow: hidden; 161 | } 162 | 163 | textarea.note-field { 164 | min-height: 7em; 165 | } 166 | 167 | .search-box-container { 168 | padding: 1em; 169 | border-bottom: 1px solid #eee; 170 | } 171 | 172 | .search-box { 173 | width: 100%; 174 | border: none; 175 | border-bottom: 1px solid #eee; 176 | /*margin: 1em;*/ 177 | } 178 | 179 | .search-box:focus { 180 | outline: none; 181 | } 182 | 183 | .search-results { 184 | min-height: 100px; 185 | background-color: #f0f0f0; 186 | padding: 1em; 187 | max-height: 550px; 188 | overflow-y: scroll; 189 | } 190 | 191 | .search-results .list-item { 192 | background-color: white; 193 | padding: 1em; 194 | border-radius: 12px; 195 | margin-bottom: 1em; 196 | -webkit-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 197 | -moz-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 198 | box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 199 | overflow-wrap: break-word; 200 | } 201 | 202 | .list-item .title { 203 | margin-bottom: 0.5em; 204 | } 205 | 206 | .list-item .title a { 207 | font-size: 1.15em; 208 | } 209 | 210 | .list-item .text-body { 211 | color: #666; 212 | font-size: 0.9em; 213 | } 214 | 215 | .search-results .heading { 216 | font-weight: 600; 217 | font-size: 0.7em; 218 | text-transform: uppercase; 219 | margin-bottom: 8px; 220 | } 221 | 222 | .list-item .timestamp { 223 | font-size: 0.8em; 224 | margin-bottom: 0.5em; 225 | color: #888; 226 | } 227 | 228 | #custom_fields { 229 | /*overflow-wrap: break-word;*/ 230 | color: #666; 231 | max-height: 200px; 232 | overflow-y: scroll; 233 | } 234 | 235 | #custom_fields::-webkit-scrollbar { 236 | width: 14px; 237 | } 238 | 239 | #custom_fields .prop-value { 240 | overflow-wrap: break-word; 241 | white-space: pre-wrap; 242 | font-size: 0.9em 243 | } 244 | 245 | #custom_fields .prop-name { 246 | font-size: 0.9em; 247 | font-weight: 550; 248 | } 249 | 250 | #custom_fields .divider { 251 | height: 1px; 252 | background-color: #ddd; 253 | margin: 1em; 254 | } 255 | 256 | .collections-dropdown-container { 257 | position: relative; 258 | } 259 | 260 | .collections-dropdown { 261 | /*padding: 0.75em;*/ 262 | position: absolute; 263 | width: 100%; 264 | border: 1px solid #ccc; 265 | border-radius: 8px; 266 | background-color: white; 267 | z-index: 2; 268 | 269 | border-top-left-radius: 0; 270 | border-top-right-radius: 0; 271 | 272 | -webkit-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 273 | -moz-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 274 | box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 275 | } 276 | 277 | .as-msg { 278 | padding: 0.5em 0.75em; 279 | font-size: 0.9em; 280 | } 281 | 282 | .as-result { 283 | padding: 0.5em 0.75em; 284 | cursor: pointer; 285 | color: #666; 286 | font-size: 0.9em; 287 | } 288 | 289 | .as-result:hover { 290 | background-color: #eff7ff; 291 | outline: none; 292 | } 293 | 294 | .collections-selected { 295 | display: flex; 296 | flex-shrink: 0; 297 | max-height: 240px; 298 | 299 | flex-wrap: wrap; 300 | align-items: flex-start; 301 | 302 | border: 1px solid #ced4da; 303 | border-radius: 0.25rem; 304 | padding: .375rem .75rem; 305 | } 306 | 307 | .collections-selected .token { 308 | display: flex; 309 | flex-shrink: 1; 310 | height: 18px; 311 | align-items: center; 312 | background-color: #eff7ff; 313 | padding: 2px 6px; 314 | margin-right: 6px; 315 | margin-bottom: 6px; 316 | } 317 | 318 | .token .remove-btn { 319 | cursor: pointer; 320 | display: flex; 321 | align-items: center; 322 | justify-content: center; 323 | flex-grow: 0; 324 | flex-shrink: 0; 325 | margin-left: 2px; 326 | margin-right: 2px; 327 | width: 18px; 328 | height: 18px; 329 | } 330 | 331 | .results { 332 | max-height: 200px; 333 | overflow-y: scroll; 334 | } 335 | 336 | .results::-webkit-scrollbar { 337 | width: 10px; 338 | } 339 | 340 | .collections-dropdown .collections-search-container { 341 | display: flex; 342 | align-items: center; 343 | min-width: 100px; 344 | flex: 1; 345 | } 346 | 347 | .collections-search { 348 | font-family: sans-serif; 349 | font-size: 1em; 350 | width: 100%; 351 | border: none; 352 | padding-bottom: 0.5em; 353 | } 354 | 355 | .collections-search:focus { 356 | outline: none; 357 | } 358 | -------------------------------------------------------------------------------- /chrome/src/images/archived/icon128-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon128-1.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon128-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon128-old.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon128.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon16-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon16-old.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon16.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon24-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon24-old.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon24.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon32-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon32-old.png -------------------------------------------------------------------------------- /chrome/src/images/archived/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/icon32.png -------------------------------------------------------------------------------- /chrome/src/images/archived/logo-network/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/logo-network/icon128.png -------------------------------------------------------------------------------- /chrome/src/images/archived/logo-network/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/logo-network/icon16.png -------------------------------------------------------------------------------- /chrome/src/images/archived/logo-network/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/logo-network/icon24.png -------------------------------------------------------------------------------- /chrome/src/images/archived/logo-network/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/archived/logo-network/icon32.png -------------------------------------------------------------------------------- /chrome/src/images/icon-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon-screenshot.png -------------------------------------------------------------------------------- /chrome/src/images/icon-slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon-slack.png -------------------------------------------------------------------------------- /chrome/src/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon128.png -------------------------------------------------------------------------------- /chrome/src/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon16.png -------------------------------------------------------------------------------- /chrome/src/images/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon24.png -------------------------------------------------------------------------------- /chrome/src/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/icon32.png -------------------------------------------------------------------------------- /chrome/src/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/chrome/src/images/spinner.gif -------------------------------------------------------------------------------- /chrome/src/index.js: -------------------------------------------------------------------------------- 1 | // import App from "../components/App"; 2 | import App from './App' -------------------------------------------------------------------------------- /chrome/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rumin", 3 | "version": "0.0.0.15", 4 | "description": "The fastest way to collect information from diverse sources on the web - Rumin helps you capture any content instantly.", 5 | "permissions": ["activeTab"], 6 | "optional_permissions": ["storage"], 7 | "background": { 8 | "scripts": ["background.js"] 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "*://*/*" 14 | ], 15 | "js": ["vendor/jquery-3.4.1.min.js", "vendor/tiny-tfidf.js", "content.js"], 16 | "css": ["css/content.css"] 17 | } 18 | ], 19 | "browser_action": { 20 | "default_popup": "popup.html", 21 | "default_icon": { 22 | "16": "images/icon16.png", 23 | "24": "images/icon24.png", 24 | "32": "images/icon32.png" 25 | } 26 | }, 27 | "commands": { 28 | "_execute_browser_action": { 29 | "suggested_key": { 30 | "default": "Ctrl+Shift+K", 31 | "windows": "Ctrl+Shift+K", 32 | "mac": "Command+Shift+K" 33 | } 34 | } 35 | }, 36 | "icons": { 37 | "16": "images/icon16.png", 38 | "24": "images/icon24.png", 39 | "32": "images/icon32.png", 40 | "128": "images/icon128.png" 41 | }, 42 | "manifest_version": 2 43 | } 44 | -------------------------------------------------------------------------------- /chrome/src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | Using local storage helps us serve you better 12 |
13 | Why? 14 |
15 | 16 |
17 |
18 | Save 19 |
20 |
21 | Lookup 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
Title
30 | 32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
Note
43 | 44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 | 53 | 82 | 83 | 84 | 88 | 91 | 92 | 93 | 97 | 100 | 101 | 102 | 103 | 104 |
105 | 106 | Add a screenshot 107 |
108 | 109 |
110 | 111 |
112 |
Save
113 |
114 | 115 | 116 | 127 |
128 | 129 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /chrome/src/popup.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let favIconUrl; 3 | let title; 4 | let pageUrl; 5 | let selection; 6 | let note; 7 | let page_dom; 8 | let hasStoragePermission = false; 9 | let searchQuery = ''; 10 | let customFields = {}; 11 | 12 | function delay(fn, ms) { 13 | let timer = 0 14 | return function(...args) { 15 | clearTimeout(timer) 16 | timer = setTimeout(fn.bind(this, ...args), ms || 0) 17 | } 18 | } 19 | 20 | function escapeHtml(unsafe) { 21 | return unsafe 22 | .replace(/&/g, "&") 23 | .replace(//g, ">") 25 | .replace(/"/g, """) 26 | .replace(/'/g, "'"); 27 | } 28 | 29 | const serializeHTML = (node) => { 30 | if (typeof node.text === 'string') { 31 | return escapeHtml(node.text) 32 | } 33 | 34 | const children = node.children.map(n => serializeHTML(n)).join('') 35 | 36 | let url 37 | if (node.url) { 38 | if (node.url.startsWith('/spaces/') || node.url.startsWith('/activities/')) { 39 | url = `https://getrumin.com${node.url}` 40 | } 41 | else { 42 | url = node.url 43 | } 44 | } 45 | 46 | switch (node.type) { 47 | case 'quote': 48 | return `

${children}

` 49 | case 'paragraph': 50 | return `

${children}

` 51 | case 'link': 52 | return `${children}` 53 | case 'list-item': 54 | return `
  • ${children}
  • ` 55 | case 'heading-one': 56 | return `

    ${children}

    ` 57 | case 'heading-two': 58 | return `

    ${children}

    ` 59 | case 'heading-three': 60 | return `

    ${children}

    ` 61 | default: 62 | return children 63 | } 64 | } 65 | 66 | function activityData() { 67 | title = $('#captured_title_field').val(); 68 | note = $('#captured_note_field').val(); 69 | 70 | let params = { 71 | title: title, 72 | url: pageUrl, 73 | selection: selection, 74 | favicon_url: favIconUrl, 75 | note: note || '', 76 | // page_dom: page_dom, 77 | screenshot: $('#screenshot_img').attr('src'), 78 | custom_fields: customFields 79 | } 80 | 81 | if (window.selectedSpaces) { 82 | params.in_collections = window.selectedSpaces 83 | } 84 | 85 | return params 86 | } 87 | 88 | function buildActivity(activity) { 89 | let youtubeString = '' 90 | if (activity.url && activity.url.startsWith('https://www.youtube.com') && activity.custom_fields && activity.custom_fields.current_time) { 91 | youtubeString = `
    Video at ${activity.custom_fields.current_time}
    ` 92 | } 93 | 94 | return(` 95 |
    96 | 99 |
    100 | created ${ new Date(activity.created_at).toISOString().slice(0,10) }    last updated ${ new Date(activity.updated_at).toISOString().slice(0,10) } 101 |
    102 | ${ youtubeString } 103 |
    104 | 105 |
    106 | ${ activity.selection } 107 |
    108 |
    109 |
    110 |
    111 |
    ${ serializeHTML({ children: activity.json_body }) }
    112 |
    113 |
    114 | `) 115 | } 116 | 117 | function buildSpace(space) { 118 | return(` 119 |
    120 |
    121 | ${ space.title } 122 |
    123 |
    124 | created ${ new Date(space.created_at).toISOString().slice(0,10) }    last updated ${ new Date(space.created_at).toISOString().slice(0,10) } 125 |
    126 |
    127 |
    ${ space.text_body.length > 500 ? space.text_body.slice(0, 500) + '...' : space.text_body }
    128 |
    129 |
    130 | `) 131 | } 132 | 133 | function buildCustomField(name, value) { 134 | if (typeof value === 'object') { 135 | value = JSON.stringify(value, null, 2) 136 | } 137 | 138 | return(` 139 |
    140 |
    ${ name }:
    141 |
    ${ value }
    142 |
    143 | `) 144 | } 145 | 146 | function fetchMatchingPages() { 147 | $.ajax({ 148 | url: `https://getrumin.com/api/v1/search/?url=${encodeURIComponent(encodeURIComponent(pageUrl))}`, 149 | method: 'GET', 150 | contentType: 'application/json', 151 | success: function(data) { 152 | if (data.length === 0) { 153 | $('#search_results').html('
    No matching results on this page
    ') 154 | return 155 | } 156 | 157 | const resultElements = data.map(obj => { 158 | // console.log('obj', obj.content_type, obj.title, obj) 159 | 160 | if (obj.content_type === 'Activity') { 161 | return buildActivity(obj) 162 | } 163 | else { 164 | return buildSpace(obj) 165 | } 166 | }).join('') 167 | 168 | 169 | $('#search_results').html(`
    Related to this page
    ${resultElements}`) 170 | }, 171 | error: function(error) { 172 | console.log('API error', error); 173 | } 174 | }); 175 | } 176 | 177 | function sendMsgToContentScript(msg) { 178 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 179 | chrome.tabs.sendMessage(tabs[0].id, msg, function(response) {}) 180 | }); 181 | } 182 | 183 | chrome.runtime.sendMessage({popupOpen: true}); 184 | 185 | chrome.runtime.onMessage.addListener(function(req, sender, sendResponse) { 186 | 187 | if (hasStoragePermission) { 188 | chrome.storage.local.get(['noteValue', 'titleValue'], function(result) { 189 | if (result.titleValue) { 190 | title = result.titleValue 191 | $('#captured_title_field').val(result.titleValue) 192 | } 193 | 194 | if (result.noteValue) { 195 | $('#captured_note_field').val(result.noteValue) 196 | } 197 | }) 198 | } 199 | 200 | if (req.pageContext) { 201 | 202 | favIconUrl = sender.tab.favIconUrl; 203 | 204 | // no saved title from local storage 205 | if (!title || (title && title.trim() === '')) { 206 | title = req.pageContext.titleOverride ? req.pageContext.titleOverride : sender.tab.title; 207 | title = title.slice(0, 300) 208 | } 209 | 210 | pageUrl = req.pageContext.urlOverride ? req.pageContext.urlOverride : sender.tab.url; 211 | selection = req.pageContext.selection; 212 | // page_dom = req.pageContext.page_dom; 213 | customFields = req.pageContext.customFields; 214 | 215 | // get it for the Lookup tab 216 | fetchMatchingPages() 217 | 218 | if (title.trim() !== '') { 219 | $('#captured_title_field').val(title.trim()); 220 | } 221 | else { 222 | $('#captured_title_field').val(pageUrl); 223 | } 224 | 225 | if (selection.trim()) { 226 | $('#captured_selection').text(selection.slice(0, 300)); 227 | } 228 | else { 229 | $('.captured-selection').hide() 230 | } 231 | 232 | if (Object.keys(customFields).length > 0) { 233 | $('#custom_fields_section').removeClass('hidden') 234 | 235 | const divider = '
    ' 236 | 237 | const customFieldElements = Object.keys(customFields).map(fieldName => { 238 | 239 | return buildCustomField(fieldName, customFields[fieldName]) 240 | }).join(divider) 241 | 242 | $('#custom_fields').html(` 243 |
    244 | ${ customFieldElements } 245 |
    246 | `) 247 | } 248 | 249 | // Site-specific selection and actions 250 | if (pageUrl.startsWith('https://app.slack.com/client/')) { 251 | $('#add_slack_selection').removeClass('hidden') 252 | } 253 | 254 | if (pageUrl.includes('reddit.com/r/') && pageUrl.includes('comments')) { 255 | // console.log('showing reddit btn') 256 | $('#add_reddit_selection').removeClass('hidden') 257 | } 258 | } 259 | 260 | if (req.screenshotCaptured) { 261 | $('#screenshot_img').removeClass('hidden') 262 | $('#screenshot_img').attr('src', req.screenshotData) 263 | 264 | // Tesseract.recognize( 265 | // req.screenshotData, 266 | // 'eng', 267 | // { logger: m => console.log(m) } 268 | // ).then(({ data: { text } }) => { 269 | // console.log('recognized text from screenshot', text); 270 | // }) 271 | 272 | // TODO - convert OCR here 273 | } 274 | }); 275 | 276 | $(function() { 277 | var noteSelectionStart; 278 | var noteSelectionEnd; 279 | 280 | chrome.permissions.contains({ 281 | permissions: ['storage'] 282 | }, function(result) { 283 | if (result) { 284 | $('#permission_bar').hide() 285 | hasStoragePermission = true 286 | 287 | // notify content script 288 | sendMsgToContentScript({hasStoragePermission: true}) 289 | } 290 | }) 291 | 292 | $('#save_btn').click(function() { 293 | // disable button 294 | $('#save_btn').html('

    Saving...Do not close this

    ') 295 | $('#save_btn').removeClass() 296 | 297 | 298 | if (hasStoragePermission) { 299 | // chrome.storage.local.clear() 300 | chrome.storage.local.remove(['titleValue', 'noteValue']) 301 | sendMsgToContentScript({ cancelSelection: true }) 302 | sendMsgToContentScript({clearSelection: true}) 303 | } 304 | 305 | $.ajax({ 306 | url: 'https://getrumin.com/api/v1/activities/', //'http://127.0.0.1:8000/api/v1/activities/',//'https://getrumin.com/api/v1/activities/', 307 | method: 'POST', 308 | contentType: 'application/json', 309 | data: JSON.stringify(activityData()), 310 | success: function(data) { 311 | $('.capture-container').html(` 312 |

    The content is successfully saved.

    313 |
    314 | Go to page 315 |
    316 | `) 317 | }, 318 | error: function(error) { 319 | console.log('API error', error); 320 | } 321 | }); 322 | }); 323 | 324 | $('#custom_fields_heading').click(function() { 325 | $('.custom-fields-content').toggleClass('hidden') 326 | $('#custom_fields_display_icon').toggleClass('hidden') 327 | }) 328 | 329 | $('#screenshot_btn').click(function() { 330 | chrome.runtime.sendMessage({takeScreenshot: true}); 331 | }) 332 | 333 | // TODO - finish implementing, so that the user doesn't remove the selection 334 | // $('#captured_note_field').keydown(function(e) { 335 | // var cursorStart = e.target.selectionStart 336 | // var cursorEnd = e.target.selectionEnd 337 | 338 | // if (cursorEnd > cursorStart && e.key === '{') { 339 | // noteSelectionStart = cursorStart 340 | // noteSelectionEnd = cursorEnd 341 | // } 342 | // }) 343 | 344 | 345 | $('#captured_title_field').change(function(e) { 346 | if (hasStoragePermission) { 347 | chrome.storage.local.set({titleValue: e.target.value}, function() { console.log('setting noteValue in storage') }) 348 | } 349 | }) 350 | 351 | $('#captured_note_field').change(function(e) { 352 | if (hasStoragePermission) { 353 | chrome.storage.local.set({noteValue: e.target.value}, function() {}) 354 | } 355 | }) 356 | 357 | 358 | $('#captured_note_field').keyup(function(e) { 359 | // console.log('selection start', e.target.selectionStart, 'selection end', e.target.selectionEnd) 360 | 361 | if (e.key === '{') { 362 | // this is wrong 363 | // console.log(noteSelectionStart, noteSelectionEnd) 364 | 365 | var cursorStart = e.target.selectionStart 366 | var cursorEnd = e.target.selectionEnd 367 | var value = e.target.value 368 | 369 | 370 | e.target.value = value.slice(0, cursorStart) + '}' + value.slice(cursorStart,) 371 | e.target.selectionEnd = cursorStart 372 | } 373 | }) 374 | 375 | // fetch search results 376 | $('#search_box').keyup(delay(function(e) { 377 | searchQuery = e.target.value 378 | $('#search_results').html('
    fetching...
    ') 379 | 380 | $.ajax({ 381 | url: `https://getrumin.com/api/v1/search?q=${searchQuery}&is_as=true/`, 382 | method: 'GET', 383 | contentType: 'application/json', 384 | success: function(data) { 385 | const resultsElements = data.results.map(obj => { 386 | if (obj.content_type === 'Activity') { 387 | return buildActivity(obj) 388 | } 389 | else { 390 | return buildSpace(obj) 391 | } 392 | }).join('') 393 | 394 | $('#search_results').html(`
    Results for ${searchQuery}
    ${resultsElements}`) 395 | }, 396 | error: function(error) { 397 | console.log('API error', error); 398 | } 399 | }) 400 | }, 500)) 401 | 402 | // switching tabs 403 | $('#lookup_tab_btn').click(function(e) { 404 | $('#save_tab_btn').removeClass('active') 405 | $('#lookup_tab_btn').addClass('active') 406 | 407 | $('#save_tab').addClass('hidden') 408 | $('#lookup_tab').removeClass('hidden') 409 | }) 410 | 411 | $('#save_tab_btn').click(function(e) { 412 | $('#lookup_tab_btn').removeClass('active') 413 | $('#save_tab_btn').addClass('active') 414 | 415 | $('#lookup_tab').addClass('hidden') 416 | $('#save_tab').removeClass('hidden') 417 | }) 418 | 419 | 420 | // custom selection for Slack 421 | $('#add_slack_selection').click(function() { 422 | sendMsgToContentScript({addSelection: true}) 423 | 424 | $('#add_slack_selection').addClass('hidden') 425 | $('#cancel_slack_selection').removeClass('hidden') 426 | }) 427 | 428 | $('#cancel_slack_selection').click(function() { 429 | sendMsgToContentScript({ cancelSelection: true }) 430 | 431 | $('#cancel_slack_selection').addClass('hidden') 432 | $('#add_slack_selection').removeClass('hidden') 433 | }) 434 | 435 | 436 | // custom selection for Reddit 437 | $('#add_reddit_selection').click(function() { 438 | sendMsgToContentScript({addSelection: true}) 439 | 440 | $('#add_reddit_selection').addClass('hidden') 441 | $('#cancel_reddit_selection').removeClass('hidden') 442 | }) 443 | 444 | $('#cancel_reddit_selection').click(function() { 445 | sendMsgToContentScript({ cancelSelection: true }) 446 | 447 | $('#cancel_reddit_selection').addClass('hidden') 448 | $('#add_reddit_selection').removeClass('hidden') 449 | }) 450 | 451 | 452 | 453 | // $(document).ready(function () { 454 | // $("#add_to_field").tokenInput("http://127.0.0.1:8000/search", { 455 | // propertyToSearch: 'title' 456 | // }); 457 | // }); 458 | 459 | // requesting additional permission for storage 460 | $('#req_storage_permission').click(function(e) { 461 | chrome.permissions.request({ 462 | permissions: ['storage'] 463 | }, function(granted) { 464 | if (granted) { 465 | // console.log('storage permission granted') 466 | $('#permission_bar').html('Please re-open this extension') 467 | } 468 | else { 469 | console.log('storage permission not granted') 470 | } 471 | }) 472 | }) 473 | }) 474 | })(); 475 | -------------------------------------------------------------------------------- /chrome/src/vendor/tesseract.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Tesseract=t():e.Tesseract=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=7)}([function(e,t){e.exports=function(e,t){return"".concat(e,"-").concat(t,"-").concat(Math.random().toString(16).slice(3,8))}},function(e,t){var r=this,n=!1;t.logging=n,t.setLogging=function(e){n=e},t.log=function(){for(var e=arguments.length,t=new Array(e),o=0;o1)for(var r=1;r=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var s=r(11),f=r(14),l=r(4),p=r(1).log,d=r(0),h=r(15).defaultOEM,y=r(16),g=y.defaultOptions,b=y.spawnWorker,v=y.terminateWorker,m=y.onMessage,w=y.loadImage,O=y.send,j=0;e.exports=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=d("Worker",j),r=s(a({},g,{},e)),n=r.logger,i=r.errorHandler,c=u(r,["logger","errorHandler"]),y={},x={},k=b(c);j+=1;var P=function(e,t){y[e]=t},S=function(e,t){x[e]=t},E=function(e){var r=e.id,n=e.action,o=e.payload;return new Promise((function(e,i){p("[".concat(t,"]: Start ").concat(r,", action=").concat(n)),P(n,e),S(n,i),O(k,{workerId:t,jobId:r,action:n,payload:o})}))},L=function(e){return E(l({id:e,action:"load",payload:{options:c}}))},T=function(e,t,r){return E(l({id:r,action:"FS",payload:{method:"writeFile",args:[e,t]}}))},R=function(e,t){return E(l({id:t,action:"FS",payload:{method:"readFile",args:[e,{encoding:"utf8"}]}}))},_=function(e,t){return E(l({id:t,action:"FS",payload:{method:"unlink",args:[e]}}))},A=function(e,t,r){return E(l({id:r,action:"FS",payload:{method:e,args:t}}))},D=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"eng",t=arguments.length>1?arguments[1]:void 0;return E(l({id:t,action:"loadLanguage",payload:{langs:e,options:c}}))},N=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"eng",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:h,r=arguments.length>2?arguments[2]:void 0;return E(l({id:r,action:"initialize",payload:{langs:e,oem:t}}))},F=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;return E(l({id:t,action:"setParameters",payload:{params:e}}))},I=function(){var e=o(regeneratorRuntime.mark((function e(t){var r,n,o=arguments;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=o.length>1&&void 0!==o[1]?o[1]:{},n=o.length>2?o[2]:void 0,e.t0=E,e.t1=l,e.t2=n,e.next=7,w(t);case 7:return e.t3=e.sent,e.t4=r,e.t5={image:e.t3,options:e.t4},e.t6={id:e.t2,action:"recognize",payload:e.t5},e.t7=(0,e.t1)(e.t6),e.abrupt("return",(0,e.t0)(e.t7));case 13:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),M=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"Tesseract OCR Result",t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],r=arguments.length>2?arguments[2]:void 0;return E(l({id:r,action:"getPDF",payload:{title:e,textonly:t}}))},C=function(){var e=o(regeneratorRuntime.mark((function e(t,r){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.t0=E,e.t1=l,e.t2=r,e.next=5,w(t);case 5:return e.t3=e.sent,e.t4={image:e.t3},e.t5={id:e.t2,action:"detect",payload:e.t4},e.t6=(0,e.t1)(e.t5),e.abrupt("return",(0,e.t0)(e.t6));case 10:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}(),z=function(){var e=o(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return null!==k&&(v(k),k=null),e.abrupt("return",Promise.resolve());case 2:case"end":return e.stop()}}),e)})));return function(){return e.apply(this,arguments)}}();return m(k,(function(e){var t=e.workerId,r=e.jobId,o=e.status,c=e.action,u=e.data;if("resolve"===o){p("[".concat(t,"]: Complete ").concat(r));var s=u;"recognize"===c?s=f(u):"getPDF"===c&&(s=Array.from(a({},u,{length:Object.keys(u).length}))),y[c]({jobId:r,data:s})}else if("reject"===o){if(x[c](u),!i)throw Error(u);i(u)}else"progress"===o&&n(u)})),{id:t,worker:k,setResolve:P,setReject:S,load:L,writeText:T,readText:R,removeFile:_,FS:A,loadLanguage:D,initialize:N,setParameters:F,recognize:I,getPDF:M,detect:C,terminate:z}}},function(e,t){e.exports={TESSERACT_ONLY:0,LSTM_ONLY:1,TESSERACT_LSTM_COMBINED:2,DEFAULT:3}},function(e,t,r){function n(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}r(8);var i=r(10),a=r(5),c=r(25),u=r(6),s=r(26),f=r(1).setLogging;e.exports=function(e){for(var t=1;t=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var u=o.call(a,"catchLoc"),s=o.call(a,"finallyLoc");if(u&&s){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),E(r),y}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;E(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:T(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=r),y}},e}("object"===t(e)?e.exports:{});try{regeneratorRuntime=r}catch(e){Function("r","regeneratorRuntime = r")(r)}}).call(this,r(9)(e))},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},function(e,t,r){var n=this;function o(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t1?n-1:0),i=1;i=0)}}).call(this,r(2))},function(e,t){function r(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function n(e){for(var t=1;t!(e.length<2||e.match(/^\d/))).map(e=>e.toLowerCase()),this._termFrequencies=null}_calculateTermFrequencies(){this._termFrequencies=new Map,this._words.forEach(e=>{this._termFrequencies.has(e)?this._termFrequencies.set(e,this._termFrequencies.get(e)+1):this._termFrequencies.set(e,1)})}getTermFrequency(e){return this._termFrequencies||this._calculateTermFrequencies(),this._termFrequencies.has(e)?this._termFrequencies.get(e):null}getText(){return this._text}getLength(){return this._words.length}getUniqueTerms(){return this._termFrequencies||this._calculateTermFrequencies(),Array.from(this._termFrequencies.keys())}}const s=["me","my","myself","we","our","ours","ourselves","you","your","yours","yourself","yourselves","he","him","his","himself","she","her","hers","herself","it","its","itself","they","them","their","theirs","themselves","what","which","who","whom","this","that","these","those","am","is","are","was","were","be","been","being","have","has","had","having","do","does","did","doing","an","the","and","but","if","or","because","as","until","while","of","at","by","for","with","about","against","between","into","through","during","before","after","above","below","to","from","up","down","in","out","on","off","over","under","again","further","then","once","here","there","when","where","why","how","all","any","both","each","few","more","most","other","some","such","no","nor","not","only","own","same","so","than","too","very","can","will","just","don","could","should","would","now","ll","re","ve","aren","couldn","didn","doesn","hadn","hasn","haven","isn","mustn","needn","shouldn","wasn","weren","won","wouldn"];class o{constructor(e=!0,t=[]){const r=e?t.concat(s):t;this._stopwords=new Map(r.map(e=>[e,!0]))}includes(e){return this._stopwords.has(e)}getStopwordList(){return Array.from(this._stopwords.keys())}}class i{constructor(e,t,r=!0,s=[],i=2,c=.75){this._stopwords=new o(r,s),this._K1=i,this._b=c,this._documents=new Map;for(let r=0;r!this._stopwords.includes(e)).forEach(e=>{if(this._collectionFrequencies.has(e)){const t=this._collectionFrequencies.get(e);this._collectionFrequencies.set(e,t+1)}else this._collectionFrequencies.set(e,1)})}getTerms(){return this._collectionFrequencies||this._calculateCollectionFrequencies(),Array.from(this._collectionFrequencies.keys())}getCollectionFrequency(e){return this._collectionFrequencies||this._calculateCollectionFrequencies(),this._collectionFrequencies.has(e)?this._collectionFrequencies.get(e):null}getDocument(e){return this._documents.get(e)}getDocumentIdentifiers(){return Array.from(this._documents.keys())}getCommonTerms(e,t,r=10){const n=this.getDocumentVector(e),s=this.getDocumentVector(t);return Array.from(n.entries()).map(([e,t])=>[e,t*s.get(e)]).filter(e=>e[1]>0).sort((e,t)=>t[1]-e[1]).slice(0,r)}_calculateCollectionFrequencyWeights(){this._collectionFrequencies||this._calculateCollectionFrequencies(),this._collectionFrequencyWeights=new Map;const e=this._documents.size;for(const[t,r]of this._collectionFrequencies.entries())this._collectionFrequencyWeights.set(t,Math.log(e+1)-Math.log(r))}getCollectionFrequencyWeight(e){return this._collectionFrequencyWeights||this._calculateCollectionFrequencyWeights(),this._collectionFrequencyWeights.has(e)?this._collectionFrequencyWeights.get(e):null}_calculateDocumentVectors(){this._collectionFrequencyWeights||this._calculateCollectionFrequencyWeights(),this._documentVectors=new Map;const e=this._K1,t=this._b,r=Array.from(this._documents.values()).map(e=>e.getLength()).reduce((e,t)=>e+t,0)/this._documents.size;for(const[n,s]of this._documents){const o=new Map,i=s.getLength()/r;for(const[r,n]of this._collectionFrequencyWeights.entries()){const c=s.getTermFrequency(r),u=c?n*c*(e+1)/(e*(1-t+t*i)+c):0;o.set(r,u)}this._documentVectors.set(n,o)}}getDocumentVector(e){return this._documentVectors||this._calculateDocumentVectors(),this._documentVectors.get(e)}getTopTermsForDocument(e,t=30){const r=this.getDocumentVector(e);return r?Array.from(r.entries()).filter(e=>e[1]>0).sort((e,t)=>t[1]-e[1]).slice(0,t):[]}getResultsForQuery(e){if(!e||"string"!=typeof e||0===e.length)return[];const t=new n(e).getUniqueTerms();return this.getDocumentIdentifiers().map(e=>{const r=this.getDocumentVector(e);let n=0;return t.forEach(e=>{const t=r.get(e);t&&(n+=t)}),[e,n]}).filter(e=>e[1]>0).sort((e,t)=>t[1]-e[1])}getStopwords(){return this._stopwords}}class c{constructor(e){this._corpus=e,this._distanceMatrix=null}static cosineSimilarity(e,t){const r=Array.from(e.values()),n=Array.from(t.values());let s=0,o=0,i=0;const c=Math.min(r.length,n.length);for(let e=0;ethis._corpus.getDocumentVector(e)),r=new Array(t.length).fill(null).map(()=>new Array(t.length));for(let e=0;e { 13 | // }) 14 | 15 | // chrome.permissions.contains({ 16 | // permissions: ['storage'] 17 | // }, function(result) { 18 | // if (result) { 19 | // // notify content script 20 | // // sendMsgToContentScript({hasStoragePermission: true}) 21 | // } 22 | // }) 23 | 24 | 25 | chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){ 26 | if (message.popupOpen) { 27 | // console.log('popup is open'); 28 | 29 | sendMsgToContentScript({"message": "clicked_browser_action"}) 30 | } 31 | 32 | if (message.takeScreenshot) { 33 | // console.log('TODO - take a screenshot') 34 | 35 | chrome.tabs.captureVisibleTab(null, { 36 | format : "png" 37 | }, function(data) { 38 | // screenshot.data = data; 39 | // console.log('screenshot data', data) 40 | 41 | chrome.runtime.sendMessage({screenshotCaptured: true, screenshotData: data}); 42 | }); 43 | } 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /firefox/src/content.js: -------------------------------------------------------------------------------- 1 | const MOUSE_VISITED_CLASSNAME = 'crx_mouse_visited'; 2 | let prevDOM = null 3 | let selectingDOM = false 4 | let selectedElements = [] 5 | let hasStoragePermission = false 6 | 7 | // function isElementVisible(el) { 8 | // const o = new IntersectionObserver(([entry]) => { 9 | // return entry.intersectionRatio === 1; 10 | // }); 11 | // o.observe(el) 12 | // } 13 | 14 | const getTopTermsInDoc = () => { 15 | // const corpus = new TinyTFIDF.Corpus(['doc1'], [$('body').text()]) 16 | // return corpus.getTopTermsForDocument('doc1').slice(0, 15) 17 | } 18 | 19 | const initSelectedElements = () => { 20 | // get fetch existing selectedElements 21 | chrome.storage.local.get(['selectedElements'], function(result) { 22 | console.log('result.selectedElements', result.selectedElements) 23 | 24 | selectedElements = result.selectedElements || [] 25 | }) 26 | } 27 | 28 | const clearSelection = () => { 29 | unstyleSelectedElements() 30 | 31 | selectedElements = [] 32 | $(document).unbind('mousemove') 33 | } 34 | 35 | const unstyleSelectedElements = () => { 36 | if (selectedElements.length > 0) { 37 | selectedElements.forEach(el => { 38 | // console.log('el in selectedElements', el) 39 | el.classList.remove(MOUSE_VISITED_CLASSNAME); 40 | }) 41 | } 42 | } 43 | 44 | const styleSelectedElements = () => { 45 | if (selectedElements.length > 0) { 46 | selectedElements.forEach(el => { 47 | // console.log('el in selectedElements', el) 48 | el.classList.add(MOUSE_VISITED_CLASSNAME); 49 | }) 50 | } 51 | } 52 | 53 | // Extract selected fields for Slack 54 | const slackFields = () => { 55 | const messages = selectedElements.map(el => { 56 | let selector = $(el) 57 | if (!el.classList.contains('c-virtual_list__item') && !el.classList.contains('c-message_kit__gutter')) { 58 | selector = $(el).closest('.c-virtual_list__item') 59 | } 60 | 61 | if (selector.length === 0) return null 62 | 63 | const sender = selector.find('.c-message__sender').text() 64 | const link = selector.find('a.c-link.c-timestamp')[0]['href'] 65 | const text = selector.find('.p-rich_text_block').text() 66 | 67 | return({ 68 | sender_name: sender, 69 | message_link: link, 70 | message_text: text 71 | }) 72 | }) 73 | 74 | return { messages: messages.filter((m) => m) } 75 | } 76 | 77 | const redditSelectedFields = () => { 78 | const comments = selectedElements.map(el => { 79 | let selector = $(el) 80 | 81 | let comment = {} 82 | 83 | const commentBody = $(el).find('div[data-test-id="comment"]') 84 | if (commentBody.length === 0) return null 85 | comment.body = commentBody.text() 86 | 87 | const timestamp = $(el).find('[id^=CommentTopMeta--Created--]') 88 | if (timestamp.length > 0) { 89 | comment.url = timestamp[0]['href'] 90 | } 91 | 92 | return comment 93 | }) 94 | 95 | return { comments: comments.filter(c => c) } 96 | } 97 | 98 | // parse '(hh):mm:ss' string into seconds 99 | const timeStringToSeconds = (str) => { 100 | return str.split(':').reverse().reduce((prev, curr, i) => prev + curr*Math.pow(60, i), 0) 101 | } 102 | 103 | const youtubeFields = () => { 104 | const currentTime = $('.ytp-time-current')[0].innerText 105 | const channelName = $('.ytd-channel-name yt-formatted-string')[0].innerText 106 | const channelUrl = $('.ytd-channel-name yt-formatted-string a')[0]['href'] 107 | const publishedDate = $('#date yt-formatted-string.ytd-video-primary-info-renderer')[0].innerText.replace('Premiered ', '') // FIXME - this can break for other languages 108 | 109 | return ({ 110 | current_time: currentTime, 111 | channel_name: channelName, 112 | channel_url: channelUrl, 113 | published_date: publishedDate 114 | }) 115 | } 116 | 117 | const linkedinLearningFields = () => { 118 | const classTitle = $('.classroom-nav__details h1').text() 119 | const currentTime = $('.vjs-current-time').text() 120 | const teacherName = $('.authors-entity__name-text').text() 121 | const teacherUrl = $('a.course-author-entity__lockup').attr('href') 122 | const sessionTitle = $('.classroom-toc-item--selected').text() 123 | const sessionTranscript = $('.transcripts-component__sections').text().trim() 124 | 125 | return({ 126 | class_title: classTitle.trim(), 127 | current_time: currentTime, 128 | teacher_name: teacherName.trim(), 129 | teacher_url: teacherUrl, 130 | session_title: sessionTitle.trim(), 131 | session_transcript: sessionTranscript 132 | }) 133 | } 134 | 135 | const skillShareFields = () => { 136 | const classTitle = $('.class-details-header-name').text() 137 | const currentTime = $('.vjs-current-time-display').text() 138 | const teacherName = $('.class-details-header-teacher-link').text() 139 | const teacherUrl = $('.class-details-header-teacher-link').attr('href') 140 | const sessionTitle = $('.session-item.active .session-item-title').text() 141 | 142 | return({ 143 | class_title: classTitle.trim(), 144 | current_time: currentTime, 145 | teacher_name: teacherName.trim(), 146 | teacher_url: teacherUrl, 147 | session_title: sessionTitle.trim() 148 | }) 149 | } 150 | 151 | const netflixFields = () => { 152 | const videoTitle = $('.video-title h4').text() 153 | const episodeTitle = $('.video-title span').text() 154 | const currentTime = $('.scrubber-head').attr('aria-valuetext') 155 | 156 | return({ 157 | video_title: videoTitle, 158 | episode_title: episodeTitle, 159 | current_time: currentTime 160 | }) 161 | } 162 | 163 | const edxLectureFields = () => { 164 | const provider = $('.course-header .provider').text() 165 | const courseCode = $('.course-header .course-number').text() 166 | const courseName = $('.course-header .course-name').text() 167 | 168 | const videoUrl = $('.video-sources').length > 0 ? $('.video-sources').get(0)['href'] : null 169 | const slidesUrl = $('a[href$=pdf]').length > 0 ? $('a[href$=pdf]').get(0)['href'] : null 170 | 171 | const vidTime = $('.vidtime').length > 0 ? $('.vidtime').text().split('/')[0].trim() : null 172 | 173 | return({ 174 | course_provider: provider, 175 | course_code: courseCode, 176 | course_name: courseName, 177 | video_url: videoUrl, 178 | slides_url: slidesUrl, 179 | current_time: vidTime 180 | }) 181 | } 182 | 183 | const isLinkedinLearningPage = () => { 184 | return location.href.startsWith('https://www.linkedin.com/learning/') && $('.classroom-layout__content').length > 0 185 | } 186 | 187 | const isSkillshareVideoPage = () => { 188 | return location.href.startsWith('https://www.skillshare.com/classes/') 189 | } 190 | 191 | const isNetflixVideoPage = () => { 192 | return location.href.startsWith('https://www.netflix.com/watch/') 193 | } 194 | 195 | const isYoutubeVideoPage = () => { 196 | return location.href.startsWith('https://www.youtube.com/watch?v=') 197 | } 198 | 199 | const isKindleCloudReaderPage = () => { 200 | return location.href.startsWith('https://read.amazon.com') && !location.href.includes('notebook') 201 | } 202 | 203 | const isSlackPage = () => { 204 | return location.href.startsWith('https://app.slack.com/client/') 205 | } 206 | 207 | const isRedditPage = () => { 208 | return location.href.includes('reddit.com/r/') && location.href.includes('comments') 209 | } 210 | 211 | const isEdxLecturePage = () => { 212 | return location.href.startsWith('https://courses.edx.org/courses/') && location.href.includes('courseware') 213 | } 214 | 215 | const parseSelectedElements = () => { 216 | if (isSlackPage()) { 217 | return slackFields() 218 | } 219 | 220 | if (isRedditPage()) { 221 | return redditSelectedFields() 222 | } 223 | } 224 | 225 | chrome.runtime.onMessage.addListener( 226 | function(request, sender, sendResponse) { 227 | // console.log(request.message); 228 | 229 | // if (request.disconnect === true) { 230 | // if (hasStoragePermission) { 231 | // chrome.storage.local.clear() 232 | // chrome.storage.local.remove('selectedElements') 233 | // } 234 | // } 235 | 236 | if (request.hasStoragePermission === true) { 237 | hasStoragePermission = true 238 | sendResponse({ack: true}) 239 | } 240 | 241 | if (request.clearSelection === true) { 242 | clearSelection() 243 | sendResponse({ack: true}) 244 | } 245 | 246 | if (request.message === "clicked_browser_action") { 247 | const sel = window.getSelection() 248 | const selectionText = sel.toString() 249 | 250 | // console.log('selectionText', selectionText) 251 | 252 | let titleOverride = null 253 | let urlOverride = null 254 | let customFields = {} 255 | let closestId = '' 256 | 257 | if (sel && sel.rangeCount > 0) { 258 | const selectionEl = sel.getRangeAt(0).startContainer.parentNode 259 | 260 | if (selectionEl.id) { 261 | closestId = selectionEl.id 262 | } 263 | else { 264 | const prevSibling = $(selectionEl).prev('[id]') 265 | const prevParent = $(selectionEl).closest('[id]') 266 | 267 | if (prevSibling.length > 0) { 268 | closestId = prevSibling[0].id 269 | } 270 | else if (prevParent.length > 0) { 271 | closestId = prevParent[0].id 272 | } 273 | } 274 | 275 | if (closestId) { 276 | urlOverride = `${location.href}#${closestId}` 277 | } 278 | } 279 | 280 | // Index the DOM 281 | getTopTermsInDoc() 282 | 283 | 284 | // Youtube video 285 | if (isYoutubeVideoPage()) { 286 | const fields = youtubeFields() 287 | Object.assign(customFields, fields) 288 | 289 | // TODO - replace an existing t parameter 290 | if (location.search.includes('t=')) { 291 | urlOverride = `${location.origin}${location.pathname}${location.search.replace(/t=[0-9]+s/, 't=' + timeStringToSeconds(fields.current_time) + 's')}` 292 | } 293 | else { 294 | urlOverride = `${location.href}&t=${timeStringToSeconds(fields.current_time)}` 295 | } 296 | } 297 | 298 | // Netflix Video 299 | if (isNetflixVideoPage()) { 300 | const fields = netflixFields() 301 | Object.assign(customFields, fields) 302 | } 303 | 304 | // Skillshare video 305 | if (isSkillshareVideoPage()) { 306 | const fields = skillShareFields() 307 | Object.assign(customFields, fields) 308 | } 309 | 310 | // Linkedin Learning 311 | if (isLinkedinLearningPage()) { 312 | const fields = linkedinLearningFields() 313 | Object.assign(customFields, fields) 314 | } 315 | 316 | 317 | // Kindle Cloud reader 318 | if (isKindleCloudReaderPage()) { 319 | // TODO 320 | } 321 | 322 | // Page title 323 | if ($('h1').length > 0) { 324 | customFields.page_title = $('h1')[0].textContent.trim() 325 | } 326 | 327 | // edX 328 | if (isEdxLecturePage()) { 329 | Object.assign(customFields, edxLectureFields()) 330 | } 331 | 332 | // if (isMedium) 333 | // Kindle Notes and Highlights: https://read.amazon.com/notebook 334 | // Go to the first book 335 | // $('.kp-notebook-library-each-book a.a-link-normal')[0].click() 336 | // document in the first kindle iframe 337 | // $('#KindleReaderIFrame').get(0).contentDocument 338 | 339 | 340 | if (selectedElements.length > 0) { 341 | 342 | const selectedFields = parseSelectedElements() 343 | 344 | customFields = { 345 | ...customFields, 346 | ...selectedFields 347 | } 348 | } 349 | 350 | const pageContext = { 351 | pageContext: { 352 | urlOverride: urlOverride, 353 | titleOverride: titleOverride, 354 | selection: selectionText, 355 | // closestId: closestId, 356 | // page_dom: document.documentElement.outerHTML, 357 | customFields: customFields 358 | } 359 | }; 360 | 361 | // console.log('sending pageContext', pageContext, window.getSelection().toString()) 362 | 363 | chrome.runtime.sendMessage(pageContext, function(response) { 364 | }); 365 | } 366 | 367 | if (request.addSelection) { 368 | selectingDOM = true 369 | 370 | console.log('in addSelection in content script') 371 | sendResponse({ack: true}) 372 | 373 | $(document).mousemove(function(e) { 374 | var target = e.target; 375 | 376 | // console.log('target', target) 377 | const whiteListedNodes = ['DIV', 'IMG', 'A', 'P', 'SPAN', 'H1', 'H2', 'H3', 'H4', 'H5'] 378 | 379 | // if (whiteListedClasses && target.class) 380 | // TODO - perhaps we should restrict what elements can be added? 381 | // do it by source 382 | 383 | if (whiteListedNodes.includes(target.nodeName)) { 384 | // For NPE checking, we check safely. We need to remove the class name 385 | // Since we will be styling the new one after. 386 | if (prevDOM != null && !selectedElements.includes(prevDOM)) { 387 | prevDOM.classList.remove(MOUSE_VISITED_CLASSNAME); 388 | } 389 | // Add a visited class name to the element. So we can style it. 390 | target.classList.add(MOUSE_VISITED_CLASSNAME); 391 | // The current element is now the previous. So we can remove the class 392 | // during the next iteration. 393 | prevDOM = target; 394 | } 395 | }) 396 | } 397 | 398 | if (request.cancelSelection) { 399 | selectingDOM = false 400 | $(document).unbind('mousemove') 401 | } 402 | } 403 | ); 404 | 405 | 406 | $(function() { 407 | $(document).click(function(e) { 408 | if (selectingDOM) { 409 | if (hasStoragePermission) { 410 | let element = e.target 411 | 412 | if (selectedElements && selectedElements.includes(element.outerHTML)) return 413 | 414 | selectedElements.push(element) 415 | } 416 | } 417 | }) 418 | }) 419 | -------------------------------------------------------------------------------- /firefox/src/css/content.css: -------------------------------------------------------------------------------- 1 | .crx_mouse_visited { 2 | background-color: #cbe6ff !important; 3 | outline: 1px solid #5166bb !important; 4 | } 5 | -------------------------------------------------------------------------------- /firefox/src/css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 250px; 3 | position: relative; 4 | } 5 | 6 | button { 7 | height: 30px; 8 | width: 30px; 9 | outline: none; 10 | } 11 | 12 | .flex { 13 | display: flex; 14 | } 15 | 16 | 17 | ::-webkit-scrollbar { 18 | width: 0px; 19 | } 20 | 21 | ::-webkit-scrollbar-corner { 22 | background: transparent; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb { 26 | background-color: rgba(0,0,0,.2); 27 | background-clip: padding-box; 28 | border: solid transparent; 29 | border-width: 1px 1px 1px 6px; 30 | min-height: 28px; 31 | padding: 100px 0 0; 32 | } 33 | 34 | .hidden { 35 | display: none !important; 36 | } 37 | 38 | .section-heading { 39 | cursor: pointer; 40 | } 41 | 42 | .gray-text { 43 | color: #777; 44 | } 45 | 46 | .section-heading .text { 47 | font-weight: 600; 48 | font-size: 0.7em; 49 | text-transform: uppercase; 50 | margin-bottom: 8px; 51 | } 52 | 53 | .btn.btn-primary { 54 | background-color: #0f8bff; 55 | } 56 | 57 | .inline-block { 58 | display: inline-block; 59 | } 60 | 61 | i.small-icon { 62 | font-size: 0.8em; 63 | } 64 | 65 | .field-label { 66 | font-size: 0.85em; 67 | color: #777; 68 | margin-bottom: 4px; 69 | } 70 | 71 | #screenshot_img { 72 | width: 100%; 73 | } 74 | 75 | .capture-form-container { 76 | padding-bottom: 2em; 77 | overflow-y: scroll; 78 | overflow: visible; 79 | } 80 | 81 | /*.capture-form-container::-webkit-scrollbar { 82 | width: 12px; 83 | }*/ 84 | 85 | .save-btn-container { 86 | position: fixed; 87 | width: 100%; 88 | left: 0; 89 | right: 0; 90 | bottom: 0; 91 | padding: 1em; 92 | background-color: white; 93 | } 94 | 95 | .permission-bar { 96 | font-size: 0.9em; 97 | text-align: center; 98 | background-color: #CBE6FF; 99 | padding: 0.5em 0; 100 | } 101 | 102 | .inline-block { 103 | display: inline-block; 104 | } 105 | 106 | .btn-icon { 107 | width: 14px; 108 | margin-top: -4px; 109 | } 110 | 111 | .tabs { 112 | white-space: nowrap; 113 | border-bottom: 1px solid #eee; 114 | } 115 | 116 | .tabs .tab { 117 | display: inline-block; 118 | width: 50%; 119 | text-align: center; 120 | box-sizing: border-box; 121 | padding: 0.5em 0; 122 | color: #777; 123 | cursor: pointer; 124 | } 125 | 126 | .tabs .tab:hover { 127 | background-color: #f0f0f0; 128 | } 129 | 130 | .tab.active { 131 | border-bottom: 2px solid #0f8bff; 132 | } 133 | 134 | .section-container, 135 | .capture-container { 136 | width: 350px; 137 | } 138 | 139 | .capture-container { 140 | padding: 1em; 141 | } 142 | 143 | .captured-selection { 144 | padding: 1em; 145 | font-size: 0.8em; 146 | background-color: #f9f9f9; 147 | margin-bottom: 1em; 148 | 149 | max-height: 200px; 150 | overflow-y: scroll; 151 | } 152 | 153 | .captured-selection::-webkit-scrollbar { 154 | width: 12px; 155 | } 156 | 157 | .capture-container textarea { 158 | resize: none; 159 | min-height: 5em; 160 | overflow: hidden; 161 | } 162 | 163 | textarea.note-field { 164 | min-height: 7em; 165 | } 166 | 167 | .search-box-container { 168 | padding: 1em; 169 | border-bottom: 1px solid #eee; 170 | } 171 | 172 | .search-box { 173 | width: 100%; 174 | border: none; 175 | border-bottom: 1px solid #eee; 176 | /*margin: 1em;*/ 177 | } 178 | 179 | .search-box:focus { 180 | outline: none; 181 | } 182 | 183 | .search-results { 184 | min-height: 100px; 185 | background-color: #f0f0f0; 186 | padding: 1em; 187 | max-height: 550px; 188 | overflow-y: scroll; 189 | } 190 | 191 | .search-results .list-item { 192 | background-color: white; 193 | padding: 1em; 194 | border-radius: 12px; 195 | margin-bottom: 1em; 196 | -webkit-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 197 | -moz-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 198 | box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 199 | overflow-wrap: break-word; 200 | } 201 | 202 | .list-item .title { 203 | margin-bottom: 0.5em; 204 | } 205 | 206 | .list-item .title a { 207 | font-size: 1.15em; 208 | } 209 | 210 | .list-item .text-body { 211 | color: #666; 212 | font-size: 0.9em; 213 | } 214 | 215 | .search-results .heading { 216 | font-weight: 600; 217 | font-size: 0.7em; 218 | text-transform: uppercase; 219 | margin-bottom: 8px; 220 | } 221 | 222 | .list-item .timestamp { 223 | font-size: 0.8em; 224 | margin-bottom: 0.5em; 225 | color: #888; 226 | } 227 | 228 | #custom_fields { 229 | /*overflow-wrap: break-word;*/ 230 | color: #666; 231 | max-height: 200px; 232 | overflow-y: scroll; 233 | } 234 | 235 | #custom_fields::-webkit-scrollbar { 236 | width: 14px; 237 | } 238 | 239 | #custom_fields .prop-value { 240 | overflow-wrap: break-word; 241 | white-space: pre-wrap; 242 | font-size: 0.9em 243 | } 244 | 245 | #custom_fields .prop-name { 246 | font-size: 0.9em; 247 | font-weight: 550; 248 | } 249 | 250 | #custom_fields .divider { 251 | height: 1px; 252 | background-color: #ddd; 253 | margin: 1em; 254 | } 255 | 256 | .collections-dropdown-container { 257 | position: relative; 258 | } 259 | 260 | .collections-dropdown { 261 | /*padding: 0.75em;*/ 262 | position: absolute; 263 | width: 100%; 264 | border: 1px solid #ccc; 265 | border-radius: 8px; 266 | background-color: white; 267 | z-index: 2; 268 | 269 | border-top-left-radius: 0; 270 | border-top-right-radius: 0; 271 | 272 | -webkit-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 273 | -moz-box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 274 | box-shadow: 0px 5px 4px 0px rgba(171,171,171,1); 275 | } 276 | 277 | .as-msg { 278 | padding: 0.5em 0.75em; 279 | font-size: 0.9em; 280 | } 281 | 282 | .as-result { 283 | padding: 0.5em 0.75em; 284 | cursor: pointer; 285 | color: #666; 286 | font-size: 0.9em; 287 | } 288 | 289 | .as-result:hover { 290 | background-color: #eff7ff; 291 | outline: none; 292 | } 293 | 294 | .collections-selected { 295 | display: flex; 296 | flex-shrink: 0; 297 | max-height: 240px; 298 | 299 | flex-wrap: wrap; 300 | align-items: flex-start; 301 | 302 | border: 1px solid #ced4da; 303 | border-radius: 0.25rem; 304 | padding: .375rem .75rem; 305 | } 306 | 307 | .collections-selected .token { 308 | display: flex; 309 | flex-shrink: 1; 310 | height: 18px; 311 | align-items: center; 312 | background-color: #eff7ff; 313 | padding: 2px 6px; 314 | margin-right: 6px; 315 | margin-bottom: 6px; 316 | } 317 | 318 | .token .remove-btn { 319 | cursor: pointer; 320 | display: flex; 321 | align-items: center; 322 | justify-content: center; 323 | flex-grow: 0; 324 | flex-shrink: 0; 325 | margin-left: 2px; 326 | margin-right: 2px; 327 | width: 18px; 328 | height: 18px; 329 | } 330 | 331 | .results { 332 | max-height: 200px; 333 | overflow-y: scroll; 334 | } 335 | 336 | .results::-webkit-scrollbar { 337 | width: 10px; 338 | } 339 | 340 | .collections-dropdown .collections-search-container { 341 | display: flex; 342 | align-items: center; 343 | min-width: 100px; 344 | flex: 1; 345 | } 346 | 347 | .collections-search { 348 | font-family: sans-serif; 349 | font-size: 1em; 350 | width: 100%; 351 | border: none; 352 | padding-bottom: 0.5em; 353 | } 354 | 355 | .collections-search:focus { 356 | outline: none; 357 | } 358 | -------------------------------------------------------------------------------- /firefox/src/images/icon-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon-screenshot.png -------------------------------------------------------------------------------- /firefox/src/images/icon-slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon-slack.png -------------------------------------------------------------------------------- /firefox/src/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon128.png -------------------------------------------------------------------------------- /firefox/src/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon16.png -------------------------------------------------------------------------------- /firefox/src/images/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon24.png -------------------------------------------------------------------------------- /firefox/src/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/icon32.png -------------------------------------------------------------------------------- /firefox/src/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayBacc/rumin-web-clipper/ab1258784ebf16468394c67367392c32ba944569/firefox/src/images/spinner.gif -------------------------------------------------------------------------------- /firefox/src/index.js: -------------------------------------------------------------------------------- 1 | // import App from "../components/App"; 2 | import App from './App' -------------------------------------------------------------------------------- /firefox/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rumin", 3 | "version": "0.0.0.15", 4 | "description": "The fastest way to collect information from diverse sources on the web - Rumin helps you capture any content instantly.", 5 | "permissions": ["", "activeTab", "tabs", "storage"], 6 | "background": { 7 | "scripts": ["background.js"] 8 | }, 9 | "content_scripts": [ 10 | { 11 | "matches": [ 12 | "*://*/*" 13 | ], 14 | "js": ["vendor/jquery-3.4.1.min.js", "content.js"], 15 | "css": ["css/content.css"] 16 | } 17 | ], 18 | "browser_action": { 19 | "default_popup": "popup.html", 20 | "default_icon": { 21 | "16": "images/icon16.png", 22 | "24": "images/icon24.png", 23 | "32": "images/icon32.png" 24 | } 25 | }, 26 | "icons": { 27 | "16": "images/icon16.png", 28 | "24": "images/icon24.png", 29 | "32": "images/icon32.png", 30 | "128": "images/icon128.png" 31 | }, 32 | "manifest_version": 2 33 | } 34 | -------------------------------------------------------------------------------- /firefox/src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | Using local storage helps us serve you better 12 |
    13 | Why? 14 |
    15 | 16 |
    17 |
    18 | Save 19 |
    20 |
    21 | Lookup 22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    Title
    30 | 32 |
    33 | 34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    Note
    43 | 44 |
    45 | 46 |
    47 |
    48 | 49 |
    50 |
    51 | 52 | 53 | 82 | 83 | 84 | 88 | 91 | 92 | 93 | 97 | 100 | 101 | 102 | 103 | 104 |
    105 | 106 | Add a screenshot 107 |
    108 | 109 |
    110 | 111 |
    112 |
    Save
    113 |
    114 | 115 | 116 | 127 |
    128 | 129 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /firefox/src/popup.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let favIconUrl; 3 | let title; 4 | let pageUrl; 5 | let selection; 6 | let note; 7 | let page_dom; 8 | let hasStoragePermission = false; 9 | let searchQuery = ''; 10 | let customFields = {}; 11 | 12 | function delay(fn, ms) { 13 | let timer = 0 14 | return function(...args) { 15 | clearTimeout(timer) 16 | timer = setTimeout(fn.bind(this, ...args), ms || 0) 17 | } 18 | } 19 | 20 | function escapeHtml(unsafe) { 21 | return unsafe 22 | .replace(/&/g, "&") 23 | .replace(//g, ">") 25 | .replace(/"/g, """) 26 | .replace(/'/g, "'"); 27 | } 28 | 29 | const serializeHTML = (node) => { 30 | if (typeof node.text === 'string') { 31 | return escapeHtml(node.text) 32 | } 33 | 34 | const children = node.children.map(n => serializeHTML(n)).join('') 35 | 36 | let url 37 | if (node.url) { 38 | if (node.url.startsWith('/spaces/') || node.url.startsWith('/activities/')) { 39 | url = `https://getrumin.com${node.url}` 40 | } 41 | else { 42 | url = node.url 43 | } 44 | } 45 | 46 | switch (node.type) { 47 | case 'quote': 48 | return `

    ${children}

    ` 49 | case 'paragraph': 50 | return `

    ${children}

    ` 51 | case 'link': 52 | return `${children}` 53 | case 'list-item': 54 | return `
  • ${children}
  • ` 55 | case 'heading-one': 56 | return `

    ${children}

    ` 57 | case 'heading-two': 58 | return `

    ${children}

    ` 59 | case 'heading-three': 60 | return `

    ${children}

    ` 61 | default: 62 | return children 63 | } 64 | } 65 | 66 | function activityData() { 67 | title = $('#captured_title_field').val(); 68 | note = $('#captured_note_field').val(); 69 | 70 | let params = { 71 | title: title, 72 | url: pageUrl, 73 | selection: selection, 74 | favicon_url: favIconUrl, 75 | note: note || '', 76 | // page_dom: page_dom, 77 | screenshot: $('#screenshot_img').attr('src'), 78 | custom_fields: customFields 79 | } 80 | 81 | if (window.selectedSpaces) { 82 | params.in_collections = window.selectedSpaces 83 | } 84 | 85 | return params 86 | } 87 | 88 | function buildActivity(activity) { 89 | let youtubeString = '' 90 | if (activity.url && activity.url.startsWith('https://www.youtube.com') && activity.custom_fields && activity.custom_fields.current_time) { 91 | youtubeString = `
    Video at ${activity.custom_fields.current_time}
    ` 92 | } 93 | 94 | return(` 95 |
    96 | 99 |
    100 | created ${ new Date(activity.created_at).toISOString().slice(0,10) }    last updated ${ new Date(activity.updated_at).toISOString().slice(0,10) } 101 |
    102 | ${ youtubeString } 103 |
    104 | 105 |
    106 | ${ activity.selection } 107 |
    108 |
    109 |
    110 |
    111 |
    ${ serializeHTML({ children: activity.json_body }) }
    112 |
    113 |
    114 | `) 115 | } 116 | 117 | function buildSpace(space) { 118 | return(` 119 |
    120 |
    121 | ${ space.title } 122 |
    123 |
    124 | created ${ new Date(space.created_at).toISOString().slice(0,10) }    last updated ${ new Date(space.created_at).toISOString().slice(0,10) } 125 |
    126 |
    127 |
    ${ space.text_body.length > 500 ? space.text_body.slice(0, 500) + '...' : space.text_body }
    128 |
    129 |
    130 | `) 131 | } 132 | 133 | function buildCustomField(name, value) { 134 | if (typeof value === 'object') { 135 | value = JSON.stringify(value, null, 2) 136 | } 137 | 138 | return(` 139 |
    140 |
    ${ name }:
    141 |
    ${ value }
    142 |
    143 | `) 144 | } 145 | 146 | function fetchMatchingPages() { 147 | $.ajax({ 148 | url: `https://getrumin.com/api/v1/search/?url=${encodeURIComponent(encodeURIComponent(pageUrl))}`, 149 | method: 'GET', 150 | contentType: 'application/json', 151 | success: function(data) { 152 | if (data.length === 0) { 153 | $('#search_results').html('
    No matching results on this page
    ') 154 | return 155 | } 156 | 157 | const resultElements = data.map(obj => { 158 | // console.log('obj', obj.content_type, obj.title, obj) 159 | 160 | if (obj.content_type === 'Activity') { 161 | return buildActivity(obj) 162 | } 163 | else { 164 | return buildSpace(obj) 165 | } 166 | }).join('') 167 | 168 | 169 | $('#search_results').html(`
    Related to this page
    ${resultElements}`) 170 | }, 171 | error: function(error) { 172 | console.log('API error', error); 173 | } 174 | }); 175 | } 176 | 177 | function sendMsgToContentScript(msg) { 178 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 179 | chrome.tabs.sendMessage(tabs[0].id, msg, function(response) {}) 180 | }); 181 | } 182 | 183 | chrome.runtime.sendMessage({popupOpen: true}); 184 | 185 | chrome.runtime.onMessage.addListener(function(req, sender, sendResponse) { 186 | 187 | if (hasStoragePermission) { 188 | chrome.storage.local.get(['noteValue', 'titleValue'], function(result) { 189 | if (result.titleValue) { 190 | title = result.titleValue 191 | $('#captured_title_field').val(result.titleValue) 192 | } 193 | 194 | if (result.noteValue) { 195 | $('#captured_note_field').val(result.noteValue) 196 | } 197 | }) 198 | } 199 | 200 | if (req.pageContext) { 201 | 202 | favIconUrl = sender.tab.favIconUrl; 203 | 204 | // no saved title from local storage 205 | if (!title || (title && title.trim() === '')) { 206 | title = req.pageContext.titleOverride ? req.pageContext.titleOverride : sender.tab.title; 207 | title = title.slice(0, 300) 208 | } 209 | 210 | pageUrl = req.pageContext.urlOverride ? req.pageContext.urlOverride : sender.tab.url; 211 | selection = req.pageContext.selection; 212 | // page_dom = req.pageContext.page_dom; 213 | customFields = req.pageContext.customFields; 214 | 215 | // get it for the Lookup tab 216 | fetchMatchingPages() 217 | 218 | if (title.trim() !== '') { 219 | $('#captured_title_field').val(title.trim()); 220 | } 221 | else { 222 | $('#captured_title_field').val(pageUrl); 223 | } 224 | 225 | if (selection.trim()) { 226 | $('#captured_selection').text(selection.slice(0, 300)); 227 | } 228 | else { 229 | $('.captured-selection').hide() 230 | } 231 | 232 | if (Object.keys(customFields).length > 0) { 233 | $('#custom_fields_section').removeClass('hidden') 234 | 235 | const divider = '
    ' 236 | 237 | const customFieldElements = Object.keys(customFields).map(fieldName => { 238 | 239 | return buildCustomField(fieldName, customFields[fieldName]) 240 | }).join(divider) 241 | 242 | $('#custom_fields').html(` 243 |
    244 | ${ customFieldElements } 245 |
    246 | `) 247 | } 248 | 249 | // Site-specific selection and actions 250 | if (pageUrl.startsWith('https://app.slack.com/client/')) { 251 | $('#add_slack_selection').removeClass('hidden') 252 | } 253 | 254 | if (pageUrl.includes('reddit.com/r/') && pageUrl.includes('comments')) { 255 | // console.log('showing reddit btn') 256 | $('#add_reddit_selection').removeClass('hidden') 257 | } 258 | } 259 | 260 | if (req.screenshotCaptured) { 261 | $('#screenshot_img').removeClass('hidden') 262 | $('#screenshot_img').attr('src', req.screenshotData) 263 | 264 | // Tesseract.recognize( 265 | // req.screenshotData, 266 | // 'eng', 267 | // { logger: m => console.log(m) } 268 | // ).then(({ data: { text } }) => { 269 | // console.log('recognized text from screenshot', text); 270 | // }) 271 | 272 | // TODO - convert OCR here 273 | } 274 | }); 275 | 276 | $(function() { 277 | var noteSelectionStart; 278 | var noteSelectionEnd; 279 | 280 | chrome.permissions.contains({ 281 | permissions: ['storage'] 282 | }, function(result) { 283 | if (result) { 284 | $('#permission_bar').hide() 285 | hasStoragePermission = true 286 | 287 | // notify content script 288 | sendMsgToContentScript({hasStoragePermission: true}) 289 | } 290 | }) 291 | 292 | $('#save_btn').click(function() { 293 | // disable button 294 | $('#save_btn').html('

    Saving...Do not close this

    ') 295 | $('#save_btn').removeClass() 296 | 297 | 298 | if (hasStoragePermission) { 299 | // chrome.storage.local.clear() 300 | chrome.storage.local.remove(['titleValue', 'noteValue']) 301 | sendMsgToContentScript({ cancelSelection: true }) 302 | sendMsgToContentScript({clearSelection: true}) 303 | } 304 | 305 | $.ajax({ 306 | url: 'https://getrumin.com/api/v1/activities/', //'http://127.0.0.1:8000/api/v1/activities/',//'https://getrumin.com/api/v1/activities/', 307 | method: 'POST', 308 | contentType: 'application/json', 309 | data: JSON.stringify(activityData()), 310 | success: function(data) { 311 | $('.capture-container').html(` 312 |

    The content is successfully saved.

    313 |
    314 | Go to page 315 |
    316 | `) 317 | }, 318 | error: function(error) { 319 | console.log('API error', error); 320 | } 321 | }); 322 | }); 323 | 324 | $('#custom_fields_heading').click(function() { 325 | $('.custom-fields-content').toggleClass('hidden') 326 | $('#custom_fields_display_icon').toggleClass('hidden') 327 | }) 328 | 329 | $('#screenshot_btn').click(function() { 330 | chrome.runtime.sendMessage({takeScreenshot: true}); 331 | }) 332 | 333 | // TODO - finish implementing, so that the user doesn't remove the selection 334 | // $('#captured_note_field').keydown(function(e) { 335 | // var cursorStart = e.target.selectionStart 336 | // var cursorEnd = e.target.selectionEnd 337 | 338 | // if (cursorEnd > cursorStart && e.key === '{') { 339 | // noteSelectionStart = cursorStart 340 | // noteSelectionEnd = cursorEnd 341 | // } 342 | // }) 343 | 344 | 345 | $('#captured_title_field').change(function(e) { 346 | if (hasStoragePermission) { 347 | chrome.storage.local.set({titleValue: e.target.value}, function() { console.log('setting noteValue in storage') }) 348 | } 349 | }) 350 | 351 | $('#captured_note_field').change(function(e) { 352 | if (hasStoragePermission) { 353 | chrome.storage.local.set({noteValue: e.target.value}, function() {}) 354 | } 355 | }) 356 | 357 | 358 | $('#captured_note_field').keyup(function(e) { 359 | // console.log('selection start', e.target.selectionStart, 'selection end', e.target.selectionEnd) 360 | 361 | if (e.key === '{') { 362 | // this is wrong 363 | // console.log(noteSelectionStart, noteSelectionEnd) 364 | 365 | var cursorStart = e.target.selectionStart 366 | var cursorEnd = e.target.selectionEnd 367 | var value = e.target.value 368 | 369 | 370 | e.target.value = value.slice(0, cursorStart) + '}' + value.slice(cursorStart,) 371 | e.target.selectionEnd = cursorStart 372 | } 373 | }) 374 | 375 | // fetch search results 376 | $('#search_box').keyup(delay(function(e) { 377 | searchQuery = e.target.value 378 | $('#search_results').html('
    fetching...
    ') 379 | 380 | $.ajax({ 381 | url: `https://getrumin.com/api/v1/search?q=${searchQuery}&is_as=true/`, 382 | method: 'GET', 383 | contentType: 'application/json', 384 | success: function(data) { 385 | const resultsElements = data.results.map(obj => { 386 | if (obj.content_type === 'Activity') { 387 | return buildActivity(obj) 388 | } 389 | else { 390 | return buildSpace(obj) 391 | } 392 | }).join('') 393 | 394 | $('#search_results').html(`
    Results for ${searchQuery}
    ${resultsElements}`) 395 | }, 396 | error: function(error) { 397 | console.log('API error', error); 398 | } 399 | }) 400 | }, 500)) 401 | 402 | // switching tabs 403 | $('#lookup_tab_btn').click(function(e) { 404 | $('#save_tab_btn').removeClass('active') 405 | $('#lookup_tab_btn').addClass('active') 406 | 407 | $('#save_tab').addClass('hidden') 408 | $('#lookup_tab').removeClass('hidden') 409 | }) 410 | 411 | $('#save_tab_btn').click(function(e) { 412 | $('#lookup_tab_btn').removeClass('active') 413 | $('#save_tab_btn').addClass('active') 414 | 415 | $('#lookup_tab').addClass('hidden') 416 | $('#save_tab').removeClass('hidden') 417 | }) 418 | 419 | 420 | // custom selection for Slack 421 | $('#add_slack_selection').click(function() { 422 | sendMsgToContentScript({addSelection: true}) 423 | 424 | $('#add_slack_selection').addClass('hidden') 425 | $('#cancel_slack_selection').removeClass('hidden') 426 | }) 427 | 428 | $('#cancel_slack_selection').click(function() { 429 | sendMsgToContentScript({ cancelSelection: true }) 430 | 431 | $('#cancel_slack_selection').addClass('hidden') 432 | $('#add_slack_selection').removeClass('hidden') 433 | }) 434 | 435 | 436 | // custom selection for Reddit 437 | $('#add_reddit_selection').click(function() { 438 | sendMsgToContentScript({addSelection: true}) 439 | 440 | $('#add_reddit_selection').addClass('hidden') 441 | $('#cancel_reddit_selection').removeClass('hidden') 442 | }) 443 | 444 | $('#cancel_reddit_selection').click(function() { 445 | sendMsgToContentScript({ cancelSelection: true }) 446 | 447 | $('#cancel_reddit_selection').addClass('hidden') 448 | $('#add_reddit_selection').removeClass('hidden') 449 | }) 450 | 451 | 452 | 453 | // $(document).ready(function () { 454 | // $("#add_to_field").tokenInput("http://127.0.0.1:8000/search", { 455 | // propertyToSearch: 'title' 456 | // }); 457 | // }); 458 | 459 | // requesting additional permission for storage 460 | $('#req_storage_permission').click(function(e) { 461 | chrome.permissions.request({ 462 | permissions: ['storage'] 463 | }, function(granted) { 464 | if (granted) { 465 | // console.log('storage permission granted') 466 | $('#permission_bar').html('Please re-open this extension') 467 | } 468 | else { 469 | console.log('storage permission not granted') 470 | } 471 | }) 472 | }) 473 | }) 474 | })(); 475 | -------------------------------------------------------------------------------- /firefox/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js$/, 6 | exclude: /node_modules/, 7 | // use: { 8 | // loader: "babel-loader" 9 | // } 10 | loader: 'babel-loader', 11 | options: { 12 | presets: ['@babel/preset-env', 13 | '@babel/react',{ 14 | 'plugins': ['@babel/plugin-proposal-class-properties']}] 15 | } 16 | }, 17 | { 18 | test: /\.css$/, 19 | use: [ 20 | // 'style-loader', 21 | 'css-loader' 22 | ] 23 | } 24 | ], 25 | }, 26 | watch: true, 27 | watchOptions: { 28 | poll: true, 29 | ignored: /node_modules/ 30 | } 31 | }; 32 | --------------------------------------------------------------------------------