├── public ├── webpack.config.js ├── favicon.ico ├── index.html └── electron.js ├── .gitattributes ├── src ├── index.css ├── constants.js ├── index.js ├── App.test.js ├── components │ ├── AnnotationsPanel.css │ ├── SearchWidget.css │ ├── DataSourceDialog.css │ ├── SearchResults.css │ ├── FolderNode.css │ ├── OpenTray.js │ ├── MiniAnnotations.css │ ├── ContentViewer.css │ ├── OpenTray.css │ ├── SearchWidget.js │ ├── MemNode.js │ ├── AnnotationsPanel.js │ ├── App.css │ ├── MemNode.css │ ├── SearchResults.js │ ├── FolderNode.js │ ├── MiniAnnotations.js │ ├── DataSourceDialog.js │ ├── ContentViewer.js │ └── App.js ├── DataSourceConnector.js ├── DSConnectorRegistry.js ├── DataSourceWorkPump.js ├── Util.js ├── LocalDirectoryDSC.js ├── DataStore.js └── ChromeBookmarksDSC.js ├── media └── screens │ ├── screen1.png │ ├── screen2.png │ └── screen3.png ├── package.json ├── notes.txt ├── README.md └── .gitignore /public/webpack.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westoncb/mymex/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /media/screens/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westoncb/mymex/HEAD/media/screens/screen1.png -------------------------------------------------------------------------------- /media/screens/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westoncb/mymex/HEAD/media/screens/screen2.png -------------------------------------------------------------------------------- /media/screens/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westoncb/mymex/HEAD/media/screens/screen3.png -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | class Constants { 2 | static DS_TYPE_CHROME = 'ds-type-chrome' 3 | static DS_TYPE_DIRECTORY = 'ds-type-directory' 4 | } 5 | 6 | export default Constants -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | import './index.css' 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/AnnotationsPanel.css: -------------------------------------------------------------------------------- 1 | .annotations-panel { 2 | position: absolute; 3 | display: flex; 4 | justify-content: space-between; 5 | flex-direction: column; 6 | background-color: #192732fa; 7 | padding: 1rem; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | height: 10rem; 12 | z-index: 10; 13 | box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.25); 14 | } -------------------------------------------------------------------------------- /src/components/SearchWidget.css: -------------------------------------------------------------------------------- 1 | .search-widget-container { 2 | background-color: #495762; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: space-between; 6 | align-items: flex-start; 7 | padding: 3rem; 8 | padding-left: 6rem; 9 | } 10 | 11 | .input-results-group { 12 | display: flex; 13 | flex-direction: column; 14 | flex-grow: 1; 15 | margin-right: 5rem; 16 | } 17 | 18 | .search-input { 19 | height: 2rem; 20 | margin: 0; 21 | font-size: 1.2rem; 22 | padding: 1.5rem; 23 | box-sizing: border-box; 24 | } -------------------------------------------------------------------------------- /src/components/DataSourceDialog.css: -------------------------------------------------------------------------------- 1 | .data-source-control-group { 2 | margin-top: 1rem; 3 | } 4 | 5 | .data-source-list { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: flex-start; 10 | overflow-y: auto; 11 | max-height: 20rem; 12 | } 13 | 14 | .data-source-item { 15 | padding: 1rem; 16 | background-color: #394b59; 17 | width: 80%; 18 | height: 5rem; 19 | margin-bottom: 0.5rem; 20 | margin-top: 0.5rem; 21 | flex-shrink: 0; 22 | box-shadow: 0 0 12px 4px rgba(0, 0, 0, 0.15); 23 | } -------------------------------------------------------------------------------- /src/components/SearchResults.css: -------------------------------------------------------------------------------- 1 | .results-panel { 2 | background-color: #394752; 3 | } 4 | 5 | .result-path { 6 | color: #ffffff66; 7 | font-size: 0.9rem; 8 | text-align: left; 9 | padding-left: 1rem; 10 | margin-bottom: 1.5rem; 11 | } 12 | 13 | .result-section { 14 | border-bottom: 4px solid #293742; 15 | padding-bottom: 1.5rem; 16 | padding-top: 0.5rem; 17 | } 18 | 19 | .folder-children { 20 | 21 | } 22 | 23 | .leaf-children { 24 | display: flex; 25 | flex-direction: row; 26 | flex-wrap: wrap; 27 | align-items: stretch; 28 | align-content: stretch; 29 | padding: 0.5rem; 30 | } -------------------------------------------------------------------------------- /src/components/FolderNode.css: -------------------------------------------------------------------------------- 1 | .folder-node { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | font-size: 1.2rem; 6 | background-color: #474756; 7 | margin: 0.5rem; 8 | margin-left: 1rem; 9 | margin-right: 1rem; 10 | box-shadow: 0 0 10px 4px rgba(0, 0, 0, 0.2); 11 | transition: all 0.2s; 12 | min-height: 3rem; 13 | cursor: pointer; 14 | } 15 | 16 | .folder-node:hover { 17 | background-color: #676776; 18 | transition: all 0.15s; 19 | } 20 | 21 | .left-section { 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | padding: 0.5rem; 26 | padding-left: 1rem; 27 | } 28 | 29 | .node-text { 30 | margin-left: 1rem; 31 | } -------------------------------------------------------------------------------- /src/components/OpenTray.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import './OpenTray.css'; 3 | 4 | export default class OpenTray extends PureComponent { 5 | 6 | constructor(props) { 7 | super(props) 8 | } 9 | 10 | handleItemClick = (e) => { 11 | 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | { 18 | this.props.openItems.map(item => ( 19 |
20 |
21 |
{item.name}
22 |
23 | )) 24 | } 25 |
26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /src/components/MiniAnnotations.css: -------------------------------------------------------------------------------- 1 | .mini-annotations { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 0.75rem; 7 | height: 3rem; 8 | width: 100%; 9 | box-shadow: 0 0 10px 4px rgba(0, 0, 0, 0.12); 10 | background-color: rgba(255, 255, 255, 0.2); 11 | border-radius: 5px; 12 | border: 1px solid #333; 13 | margin-top: 0.25rem; 14 | will-change: transform, height, background-color; 15 | z-index: 10; 16 | } 17 | 18 | .mini-tag { 19 | margin-right: 0.25rem; 20 | margin-bottom: 0.25rem; 21 | } 22 | 23 | .tags-preview-list { 24 | display: flex; 25 | flex-direction: row; 26 | flex-wrap: wrap; 27 | flex-shrink: 0; 28 | } 29 | 30 | .notes-preview { 31 | flex-grow: 0; 32 | overflow: hidden; 33 | max-height: 6.25rem; 34 | margin-bottom: 0.5rem; 35 | } 36 | 37 | hr { 38 | border-color: rgba(255, 255, 255, 0.25); 39 | } -------------------------------------------------------------------------------- /src/components/ContentViewer.css: -------------------------------------------------------------------------------- 1 | .content-viewer { 2 | display: flex; 3 | position: relative; 4 | flex-grow: 1; 5 | align-self: stretch; 6 | bottom: 0; 7 | } 8 | 9 | .content-iframe { 10 | position: relative; 11 | flex-grow: 1; 12 | align-self: stretch; 13 | border: 0; 14 | bottom: 0; 15 | } 16 | 17 | .content-toolbar { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | padding: 0.5rem; 22 | position: absolute; 23 | background-color: #293742; 24 | box-shadow: 0 4px 10px 4px rgba(0, 0, 0, 0.25); 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | z-index: 10; 29 | } 30 | 31 | .local-status-message { 32 | display: flex; 33 | flex-direction: row; 34 | justify-content: center; 35 | align-items: center; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | } 40 | 41 | .toolbar-button { 42 | 43 | } 44 | 45 | .version-icon { 46 | margin-left: 1rem; 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mymex", 3 | "version": "0.1.0", 4 | "private": false, 5 | "main": "public/electron.js", 6 | "dependencies": { 7 | "@blueprintjs/core": "^3.23.1", 8 | "@blueprintjs/icons": "^3.13.0", 9 | "cryptiles": "^4.1.2", 10 | "electron-is-dev": "^1.1.0", 11 | "eslint": "^4.18.2", 12 | "hoek": "^4.2.1", 13 | "js-yaml": "^3.13.1", 14 | "md5": "^2.2.1", 15 | "nedb-promises": "^4.0.1", 16 | "open": "^6.0.0", 17 | "react": "^16.10.2", 18 | "react-dom": "^16.10.2", 19 | "react-scripts": "0.9.x", 20 | "react-spring": "^8.0.27", 21 | "walkdir": "^0.4.1", 22 | "webpack-dev-server": "^3.1.11" 23 | }, 24 | "devDependencies": { 25 | "concurrently": "^5.1.0", 26 | "cross-env": "^7.0.0", 27 | "electron": "9.0.5", 28 | "electron-builder": "^22.3.2", 29 | "wait-on": "^4.0.0" 30 | }, 31 | "scripts": { 32 | "start": "concurrently \"cross-env BROWSER=none react-scripts start\" \"wait-on http://localhost:3000 && electron .\"", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test --env=jsdom", 35 | "eject": "react-scripts eject" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/OpenTray.css: -------------------------------------------------------------------------------- 1 | .main-col { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | align-items: center; 6 | min-width: 17rem; 7 | background-color: #ededed; 8 | flex-grow: 0; 9 | padding-top: 2rem; 10 | overflow-y: scroll; 11 | } 12 | 13 | .item { 14 | position: relative; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: flex-start; 18 | align-items: center; 19 | background-color: white; 20 | padding: 0.5rem; 21 | box-shadow: 0 0 12px 6px rgba(0, 0, 0, 0.1); 22 | transition: all 0.2s; 23 | height: 14rem; 24 | width: 14rem; 25 | margin: 0.5rem; 26 | margin: 0.5rem; 27 | /* white-space: nowrap; */ 28 | /* overflow: hidden; */ 29 | text-overflow: ellipsis; 30 | cursor: pointer; 31 | margin-bottom: 2rem; 32 | } 33 | 34 | .item:hover { 35 | background-color: #eeffee; 36 | transition: all 0.2s; 37 | } 38 | 39 | .item-name { 40 | font-size: 0.9rem; 41 | color: #666; 42 | flex-shrink: 1; 43 | flex-grow: 0; 44 | } 45 | 46 | .item-thumbnail { 47 | min-width: 12rem; 48 | min-height: 8rem; 49 | background-color: #666; 50 | margin-bottom: 0.5rem; 51 | flex-shrink: 0; 52 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Mymex 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/SearchWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import DataSourceDialog from './DataSourceDialog'; 3 | import './SearchWidget.css' 4 | import { Icon, Button, InputGroup } from "@blueprintjs/core"; 5 | import { IconNames } from "@blueprintjs/icons"; 6 | 7 | export default function SearchWidget(props) { 8 | 9 | const [isDialogOpen, setIsDialogOpen] = useState(false) 10 | 11 | const handleTextChange = e => { 12 | const string = e.target.value || "" 13 | props.updateSearchString(string) 14 | } 15 | 16 | return ( 17 |
18 |
19 | 26 |
27 | 28 | 31 | setIsDialogOpen(false)}> 32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /src/DataSourceConnector.js: -------------------------------------------------------------------------------- 1 | 2 | class DataSourceConnector { 3 | async configureDataSource() { 4 | throw "All subclasses of DataSourceConnector must implement configureDataSource" 5 | } 6 | 7 | pullLatest() { 8 | throw "All subclasses of DataSourceConnector must implement pullLatest" 9 | } 10 | 11 | getName() { 12 | throw "All subclasses of DataSourceConnector must implement getName" 13 | } 14 | 15 | handleJob(job) { 16 | throw "All subclasses of DataSourceConnector must implement handleJob" 17 | } 18 | 19 | handleMemChanges(addedMems, removedMems) { 20 | throw "All subclasses of DataSourceConnector must implement handleMemChanges" 21 | } 22 | 23 | /** 24 | * Must return an object of the form: 25 | * {name: string, id: string, type: string, customData: object} 26 | * This object will later be used as the sole parameter 27 | * to constructors of implementing subclasses. 28 | */ 29 | export() { 30 | throw "All subclasses of DataSourceConnector must implement export" 31 | } 32 | 33 | watch() { 34 | throw "All subclasses of DataSourceConnector must implement watch" 35 | } 36 | 37 | handleQueueEmptiedEvent() { 38 | console.log("queue emptied") 39 | } 40 | } 41 | 42 | export default DataSourceConnector -------------------------------------------------------------------------------- /src/components/MemNode.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import './MemNode.css' 3 | import MiniAnnotations from './MiniAnnotations' 4 | import { Icon } from "@blueprintjs/core" 5 | import { IconNames } from "@blueprintjs/icons"; 6 | import DSConnectorRegistry from "../DSConnectorRegistry"; 7 | const path = require('path') 8 | 9 | export default function MemNode(props) { 10 | 11 | const thumbnailPath = "memexdata://" + path.join("thumbnails", props.mem._id) + ".png" 12 | 13 | const handleClick = e => { 14 | props.setAnnotationItem(null)  15 | props.openItemFunc(props.mem, false) 16 | } 17 | 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | {props.mem.name} 26 |
27 |
28 | 29 | {(props.mem.notes || props.mem.tags) && 30 | 31 | } 32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /public/electron.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const app = electron.app; 3 | const BrowserWindow = electron.BrowserWindow; 4 | const path = require('path'); 5 | const isDev = require('electron-is-dev'); 6 | 7 | let mainWindow; 8 | 9 | const protocol = electron.protocol 10 | 11 | global.NEDB = require('nedb-promises') 12 | 13 | protocol.registerSchemesAsPrivileged([{scheme: "memexdata", privileges: { standard: true, bypassCSP: true}}]) 14 | 15 | function createWindow() { 16 | 17 | protocol.registerFileProtocol('memexdata', (request, callback) => { 18 | const url = request.url.substr(12) 19 | callback({ path: path.join(app.getPath('userData'), url) }) 20 | }, (error) => { 21 | if (error) console.error('Failed to register protocol') 22 | }) 23 | 24 | // Setting 'nodeIntegration' true is a temporary solution and could be a security issue 25 | mainWindow = new BrowserWindow({ width: 1600, height: 900, webPreferences: { nodeIntegration: true, plugins: true} }) 26 | 27 | mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`) 28 | mainWindow.toggleDevTools(); 29 | mainWindow.on('closed', () => mainWindow = null); 30 | } 31 | 32 | app.on('ready', createWindow); 33 | 34 | app.on('window-all-closed', () => { 35 | app.quit(); 36 | }); 37 | 38 | app.on('activate', () => { 39 | if (mainWindow === null) { 40 | createWindow(); 41 | } 42 | }); -------------------------------------------------------------------------------- /src/components/AnnotationsPanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | import './AnnotationsPanel.css' 3 | import { TagInput, EditableText} from "@blueprintjs/core" 4 | import { IconNames } from "@blueprintjs/icons"; 5 | import DataStore from '../DataStore' 6 | 7 | export default function AnnotationsPanel(props) { 8 | const [tags, setTags] = useState([]) 9 | const [notes, setNotes] = useState("") 10 | 11 | useEffect(() => { 12 | const func = async () => { 13 | const mem = await DataStore.getMem(props.mem._id) 14 | setTags(mem.tags || []) 15 | setNotes(mem.notes || "") 16 | } 17 | func() 18 | }, [props.mem._id]) 19 | 20 | const handleTagChange = tags => { 21 | setTags(tags) 22 | DataStore.setMemTags(props.mem._id, tags) 23 | } 24 | const handleNotesTextChange = notes => { 25 | setNotes(notes) 26 | DataStore.setMemNotes(props.mem._id, notes) 27 | } 28 | 29 | return ( 30 |
31 | 40 | 45 |
46 | ) 47 | } -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | @import "~@blueprintjs/core/lib/css/blueprint-hi-contrast.css"; 3 | @import "~@blueprintjs/icons/lib/css/blueprint-icons.css"; 4 | 5 | #root { 6 | position: absolute; 7 | top: 0; 8 | bottom: 0; 9 | } 10 | 11 | body { 12 | background-color: #293742; 13 | } 14 | 15 | .App { 16 | position: relative; 17 | display: flex; 18 | flex-direction: column; 19 | height: 100vh; 20 | width: 100vw; 21 | overflow-x: hidden; 22 | } 23 | 24 | .app-top { 25 | position: relative; 26 | display: flex; 27 | flex-direction: row; 28 | flex-grow: 1; 29 | width: 100vw; 30 | height: 97vh; 31 | } 32 | 33 | .status-bar { 34 | display: flex; 35 | justify-content: flex-start; 36 | align-items: center; 37 | padding: 0.5rem; 38 | background-color: #eee; 39 | border-top: 2px solid #ccc; 40 | color: #666; 41 | width: 100vw; 42 | height: 3vh; 43 | flex-grow: 0; 44 | } 45 | 46 | .download-location-text { 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | } 51 | 52 | .prog-container { 53 | width: 10rem; 54 | margin-right: 2rem; 55 | margin-left: 2rem; 56 | } 57 | 58 | .right-column { 59 | position: relative; 60 | display: flex; 61 | flex-direction: column; 62 | text-align: center; 63 | overflow-y: scroll; 64 | overflow-x: hidden; 65 | flex-grow: 1; 66 | } 67 | 68 | .App-header { 69 | background-color: #ededed; 70 | height: 150px; 71 | color: white; 72 | } 73 | 74 | .no-select { 75 | -webkit-user-select: none; 76 | -moz-user-select: none; 77 | -ms-user-select: none; 78 | } -------------------------------------------------------------------------------- /src/components/MemNode.css: -------------------------------------------------------------------------------- 1 | .mem-node { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: space-between; 6 | align-items: center; 7 | background-color: #474756; 8 | padding: 0.75rem; 9 | box-shadow: 0 0 10px 4px rgba(0, 0, 0, 0.2); 10 | transition: all 0.2s; 11 | height: 18rem; 12 | width: 20rem; 13 | margin: 1rem; 14 | /* white-space: nowrap; */ 15 | /* overflow: hidden; */ 16 | text-overflow: ellipsis; 17 | cursor: pointer; 18 | } 19 | 20 | .mem-node:hover { 21 | background-color: #676776; 22 | transition: all 0.15s; 23 | } 24 | 25 | .mem-text { 26 | display: flex; 27 | justify-content: flex-start; 28 | width: 100%; 29 | font-size: 0.9rem; 30 | flex-shrink: 0; 31 | flex-grow: 0; 32 | height: 3rem; 33 | text-align: left; 34 | margin-bottom: 0.5rem; 35 | overflow: hidden; 36 | } 37 | 38 | .mem-thumbnail { 39 | background-color: #ccc; 40 | margin-bottom: 0.5rem; 41 | box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.5); 42 | opacity: 0.75; 43 | width: 100%; 44 | height: 100%; 45 | flex-shrink: 2; 46 | background-size: contain; 47 | background-repeat: no-repeat; 48 | background-position: center; 49 | } 50 | 51 | .ds-icon-container { 52 | display: inline-flex; 53 | flex-shrink: 0; 54 | justify-content: center; 55 | align-items: center; 56 | background-color: rgba(255, 255, 255, 0.2); 57 | border-radius: 5px; 58 | border: 1px solid #333; 59 | width: 2rem; 60 | height: 2rem; 61 | margin-right: 0.5rem; 62 | } -------------------------------------------------------------------------------- /src/DSConnectorRegistry.js: -------------------------------------------------------------------------------- 1 | import ChromeBookmarksDSC from "./ChromeBookmarksDSC" 2 | import LocalDirectoryDSC from "./LocalDirectoryDSC" 3 | import Const from './constants' 4 | import DataStore from './DataStore' 5 | import { IconNames } from "@blueprintjs/icons"; 6 | 7 | export default class DSConnectorRegistry { 8 | 9 | static dataSourceConnectors = {} 10 | static connectorClassMap = { [Const.DS_TYPE_CHROME]: ChromeBookmarksDSC, [Const.DS_TYPE_DIRECTORY]: LocalDirectoryDSC } 11 | static connectorIconMap = { [Const.DS_TYPE_CHROME]: IconNames.BOOKMARK, [Const.DS_TYPE_DIRECTORY]: IconNames.FOLDER_OPEN } 12 | static connectorIcons = {} 13 | 14 | static async getDataSourceConnector(dataSourceId) { 15 | let dsConnector = this.dataSourceConnectors[dataSourceId] 16 | 17 | if (dsConnector) 18 | return dsConnector 19 | 20 | const dataSource = await DataStore.getDataSource(dataSourceId) 21 | const connectorClass = this.connectorClassMap[dataSource.type] 22 | dsConnector = new connectorClass(dataSource) 23 | dsConnector.type = dataSource.type 24 | this.connectorIcons[dataSource._id] = this.connectorIconMap[dsConnector.type] 25 | this.dataSourceConnectors[dataSource._id] = dsConnector 26 | 27 | return dsConnector 28 | } 29 | 30 | static getDataSourceIcon(dataSourceId) { 31 | return this.connectorIcons[dataSourceId] 32 | } 33 | 34 | static getAllDataSourceConnectors() { 35 | const keys = Object.keys(this.dataSourceConnectors) 36 | return keys.map(key => this.dataSourceConnectors[key]) 37 | } 38 | } -------------------------------------------------------------------------------- /src/DataSourceWorkPump.js: -------------------------------------------------------------------------------- 1 | import DataStore from './DataStore' 2 | import DSConnectorRegistry from './DSConnectorRegistry' 3 | 4 | export default class DataSourceWorkPump { 5 | dataSources = {} 6 | jobChangeSubscribers = [] 7 | paused = false 8 | 9 | init(dataSourcesArray = []) { 10 | dataSourcesArray.forEach(dataSource => { 11 | this.dataSources[dataSource._id] = dataSource 12 | }) 13 | 14 | setInterval(() => { 15 | if (!this.working && !this.paused) { 16 | this.working = true 17 | 18 | this.doQueuedWork().then(result => { 19 | if (result.didSomeWork && result.jobCount === 0) { 20 | DataStore.getAllDataSourceConnectors(dsConnector => dsConnector.handleQueueEmptiedEvent()) 21 | } 22 | this.working = false 23 | }) 24 | } 25 | }, 3000) 26 | 27 | this.pause() 28 | } 29 | 30 | async doQueuedWork() { 31 | let didSomeWork = false 32 | let activeJob = await DataStore.nextJob() 33 | let jobCount = await DataStore.jobCount() 34 | 35 | while (activeJob && !this.paused) { 36 | didSomeWork = true 37 | this.jobChangeSubscribers.forEach(handler => handler(activeJob, jobCount)) 38 | 39 | const dsConnector = await DSConnectorRegistry.getDataSourceConnector(activeJob.dataSourceId) 40 | await dsConnector.handleJob(activeJob) 41 | 42 | await DataStore.removeJob(activeJob._id) 43 | activeJob = await DataStore.nextJob() 44 | jobCount = await DataStore.jobCount() 45 | } 46 | 47 | return { didSomeWork, jobCount} 48 | } 49 | 50 | pause() { 51 | this.paused = true 52 | } 53 | 54 | resume() { 55 | this.paused = false 56 | } 57 | 58 | subscribeToJobChangeEvents(handler) { 59 | this.jobChangeSubscribers.push(handler) 60 | } 61 | } -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | - Show full trees for each data source when there is no search text 2 | - "New Items" button/count indicator/dialog 3 | List of new mems for each data source (e.g. if a new item was added to their bookmark's file) 4 | Quick UI for adding notes/tags to items from here 5 | Button to "mark all as seen" 6 | 7 | - Clean up and document code 8 | -document DataStore/DataSourceWorkPump/DataSourceConnector etc. 9 | -fix warnings 10 | 11 | - Finish LocalDirectoryDSC 12 | - Be able to actually display local files 13 | - First, if it's local, you need to prepend "file://" to the url in ContentViewer 14 | - Second, this isn't going to work unless you set 'webSecurity' to false... 15 | - Think about sync workflow: maybe just do it every 10 seconds 16 | - Show pop-up when new items are discovered during sync 17 | - Make it optional to actually delete items removed from data source (e.g. if I delete a bookmark in Chrome, I should be able to keep it in this app [perhaps it should become associated with faux/native/default "data source" at that point]) 18 | - Get "download fresh version" to work 19 | - Begin TypeScript re-write 20 | 21 | Ensure database contents will be safely preserved: 22 | - Allow specifying a custom location (so it could e.g. be backed up by Dropbox) 23 | - Think about handling migrations 24 | 25 | 26 | Later: 27 | 28 | - Look into testing with Jest 29 | 30 | - Look into Storybook 31 | 32 | - Handle iframe page not loading errors 33 | 34 | - Need to either do some kind of filesystem watch for changes, or periodic re-scans or data sources to keep synced. 35 | 36 | - Highlight matched elements in search results, e.g. highlight the tags or title substrings that matched the search text. 37 | 38 | 39 | - Search results pagination 40 | 41 | For watching file system changes use https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener 42 | 43 | For initial recursive traversal use: https://www.npmjs.com/package/walkdir 44 | 45 | 46 | Ideas: 47 | - custom scrollbar component, showing a visual overview of results grouped into categories -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mymex 2 | 3 | Mymex[0] is an application designed for quick retreival of information from a variety of (web/local) data sources. One of its main goals is to make pieces of information located on the web easier and more reliable to return to. It does this by: 4 | 5 | - automatically storing local renderings of web resources 6 | - allowing tags and notes to be attached to web resources 7 | 8 | The first feature means you won't lose access to the resource if it becomes unavailable online. The second provides a more powerful/flexible way of searching for resources later (via tags), and remembering why you wanted the resource saved in the first place (via notes). 9 | 10 | Another goal of Mymex is to "unify" web / local information resources. It does this by working with a configurable set of "data sources," some of which may point to web resources, others of which point to local resources—the distinction is erased in-app. For web resources, you could (for example) point it to a Chrome bookmarks file and it would import all the URLs etc. For local resources you can point it to a directory, and all of its recursive contents will become accessible. In both cases Mymex continues watching the "data source" for changes (i.e. new items being added, old being removed). 11 | 12 | New types of "data sources" may be incorporated by writing custom DataSourceConnectors. 13 | 14 | This is my first solo React project: part of why I'm building it is to learn React more deeply. 15 | 16 | [0] The name is still under construction. The reference in the name is to the "proto-hypertext system" Memex 17 | 18 | ## Tech 19 | 20 | Mymex is being written using JS, React, Electron, webpack, BlueprintJS, and NeDB. 21 | 22 | ## To run 23 | 24 | You will need: 25 | - A version of node >= 10.x 26 | - Yarn (NPM may work fine, but I haven't tested it) 27 | 28 | After cloning the repo: 29 | 30 | ``` 31 | yarn install 32 | yarn start 33 | ``` 34 | ## Screenshots 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /src/components/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './SearchResults.css'; 3 | import FolderNode from './FolderNode' 4 | import MemNode from './MemNode' 5 | 6 | export default function SearchResults(props) { 7 | 8 | const sections = Object.keys(props.sections). 9 | map(key => props.sections[key]). 10 | filter(section => typeof section === "object") 11 | 12 | return ( 13 |
14 | {(props.visible) && 15 |
16 | 17 | {sections.filter(section => section.folders.length > 0 || section.mems.length > 0).map(section => ( 18 |
19 |
20 | {section.title} 21 |
22 | 23 |
24 | {section.folders.map(folder => ( 25 | 32 | ))} 33 |
34 | 35 |
36 | {section.mems.map(child => ( 37 | 44 | ))} 45 |
46 |
47 | ))} 48 |
49 | } 50 |
51 | ) 52 | } -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5' 2 | 3 | class Util { 4 | 5 | static walkDepthFirst(node, func, parent = null) { 6 | const stop = func(node, parent) 7 | 8 | if (stop) { 9 | return 10 | } else { 11 | (node.children || []).forEach(child => { 12 | Util.walkDepthFirst(child, func, node) 13 | }); 14 | } 15 | } 16 | 17 | static nodeToPath(node, startOmitCount, endOmitCount) { 18 | const parts = [node.name] 19 | 20 | let curNode = node.parent 21 | while(curNode) { 22 | parts.push(curNode.name) 23 | curNode = curNode.parent 24 | } 25 | 26 | const rootPart = parts[parts.length-1] 27 | parts.length -= startOmitCount 28 | parts.reverse() 29 | parts.length = Math.max(0, parts.length - endOmitCount) 30 | 31 | if (parts.length === 0) 32 | parts.push(rootPart) 33 | 34 | const path = parts.join("/") + "/" 35 | 36 | 37 | return path 38 | } 39 | 40 | static lineageLength(node) { 41 | let length = 1 42 | let curNode = node.parent 43 | while (curNode) { 44 | length++ 45 | curNode = curNode.parent 46 | } 47 | 48 | console.log("lineage length", length) 49 | 50 | return length 51 | } 52 | 53 | static isLeaf(node) { 54 | return !node.children || node.children.length === 0 55 | } 56 | 57 | static setTagNames(names) { 58 | Util.tagNames = names 59 | } 60 | 61 | static getRandomTagName() { 62 | return Util.tagNames[Math.floor((Util.tagNames.length-1)*Math.random())] 63 | } 64 | 65 | static idFromPath(path) { 66 | return md5(path.toLowerCase()) 67 | } 68 | 69 | static uniq(array, propName) { 70 | const seen = {} 71 | return array.filter(element => seen.hasOwnProperty(element[propName]) ? false : (seen[element[propName]] = true)) 72 | } 73 | 74 | /** 75 | * Returns the elements in array1 which are not in array2 76 | */ 77 | static arrayDifference(array1, array2, propName) { 78 | const ar2Set = new Set(array2.map(element => element[propName])) 79 | return array1.filter(element => !ar2Set.has(element[propName])) 80 | } 81 | } 82 | 83 | export default Util -------------------------------------------------------------------------------- /src/LocalDirectoryDSC.js: -------------------------------------------------------------------------------- 1 | import DataSourceConnector from './DataSourceConnector' 2 | 3 | import Const from './constants' 4 | import DataStore from './DataStore' 5 | import Util from './Util' 6 | const electron = window.require('electron').remote 7 | const dialog = electron.dialog 8 | const pathLib = electron.require('path') 9 | const fs = electron.require('fs') 10 | const walk = electron.require('walkdir') 11 | 12 | class LocalDirectoryDSC extends DataSourceConnector { 13 | 14 | name 15 | _id 16 | path 17 | 18 | constructor(config) { 19 | super() 20 | 21 | if (config) { 22 | this.name = config.name 23 | this._id = config._id 24 | this.path = config.customData.path 25 | } 26 | } 27 | 28 | async configureDataSource() { 29 | const defaultPath = electron.app.getPath('home') 30 | const result = await dialog.showOpenDialog({ title: "Select Directory", defaultPath, properties: ['openDirectory'] }) 31 | const selectedPath = result.filePaths[0] 32 | 33 | if (!result.canceled) { 34 | this.name = "Local File System DS" 35 | this._id = Util.idFromPath(selectedPath) 36 | this.path = selectedPath 37 | 38 | return true 39 | } else { 40 | 41 | return false 42 | } 43 | } 44 | 45 | watch() { 46 | 47 | } 48 | 49 | async pullLatest() { 50 | return walk.async(this.path, { return_object: true }).then((result, error) => { 51 | 52 | return Object.keys(result) 53 | 54 | }).then(paths => { 55 | const memNodes = paths.map(path => { 56 | const obj = pathLib.parse(path) 57 | const memNode = { _id: Util.idFromPath(path), name: obj.name, isLeaf: true, location: path, dataSourceId: this._id } 58 | 59 | return memNode 60 | }) 61 | 62 | return Util.uniq(memNodes, "_id") 63 | }) 64 | } 65 | 66 | getName() { 67 | throw "All subclasses of DataSourceConnector must implement getName" 68 | } 69 | 70 | handleJob(job) { 71 | throw "All subclasses of DataSourceConnector must implement handleJob" 72 | } 73 | 74 | async handleMemChanges(addedMems, removedMems) { 75 | 76 | const jobs = addedMems.filter(mem => mem.isLeaf).map(node => ({ _id: node._id, dataSourceId: this._id, priority: 10, customData: { location: node.location } })) 77 | await DataStore.addJobs(jobs) 78 | } 79 | 80 | export() { 81 | return { name: this.name, _id: this._id, type: Const.DS_TYPE_DIRECTORY, customData: { path: this.path } } 82 | } 83 | } 84 | 85 | export default LocalDirectoryDSC -------------------------------------------------------------------------------- /src/components/FolderNode.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import './FolderNode.css' 3 | import { Icon } from "@blueprintjs/core"; 4 | import { IconNames } from "@blueprintjs/icons"; 5 | import DataStore from '../DataStore' 6 | import MemNode from './MemNode' 7 | 8 | export default function FolderNode(props) { 9 | 10 | const [folderChildren, setFolderChildren] = useState([]) 11 | const [memChildren, setMemChildren] = useState([]) 12 | const [collapsed, setCollapsed] = useState(true) 13 | 14 | const handleClick = async e => { 15 | e.stopPropagation() 16 | 17 | if (collapsed) { 18 | const children = await DataStore.getMems(props.node.children) 19 | const memChildren = [] 20 | const folderChildren = [] 21 | 22 | children.forEach(child => { 23 | child.isLeaf ? memChildren.push(child) : folderChildren.push(child) 24 | }) 25 | 26 | setMemChildren(memChildren) 27 | setFolderChildren(folderChildren) 28 | } else { 29 | 30 | setMemChildren([]) 31 | setFolderChildren([]) 32 | } 33 | 34 | setCollapsed(!collapsed) 35 | } 36 | 37 | const nodeIcon = collapsed ? : 38 | 39 | return ( 40 |
41 |
42 |
43 | {nodeIcon} 44 |
{props.node.name}
45 |
46 |
47 | 48 |
49 | {folderChildren.map(folder => ( 50 | 57 | ))} 58 |
59 | 60 |
61 | {memChildren.map(folder => ( 62 | 69 | ))} 70 |
71 |
72 | ) 73 | } -------------------------------------------------------------------------------- /src/components/MiniAnnotations.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react" 2 | import './MiniAnnotations.css' 3 | import { Icon, OverflowList, Classes, Tag, Boundary } from "@blueprintjs/core" 4 | import { IconNames } from "@blueprintjs/icons" 5 | import { useSpring, animated } from 'react-spring' 6 | 7 | export default function MiniAnnotations(props) { 8 | const [hasMouse, setHasMouse] = useState(false) 9 | const [expanded, setExpanded] = useState(false) 10 | const [flexDirection, setFlexDirection] = useState("row") 11 | 12 | const panelProps = useSpring({ 13 | height: hasMouse ? "100%" : "3rem", 14 | backgroundColor: hasMouse ? "rgba(220, 220, 255, 0.3)" : "rgba(255, 255, 255, 0.2)", 15 | onRest: () => { setExpanded(hasMouse); setFlexDirection(hasMouse ? "column" : "row")}, 16 | config: {duration: 200} 17 | }) 18 | const notes = props.mem.notes 19 | const tags = props.mem.tags || [] 20 | 21 | const notesElements = expanded 22 | ?
{notes}

23 | : 24 | const tagsElements = !expanded 25 | ? { 29 | return 30 | }} 31 | visibleItemRenderer={(item, index) => { 32 | return {item} 33 | }} 34 | collapseFrom={Boundary.END} 35 | /> 36 | : 37 |
38 | {tags.map(tag => ({tag}))} 39 |
40 | 41 | const handleClick = e => { 42 | e.stopPropagation() 43 | props.setAnnotationItem(props.mem) 44 | } 45 | 46 | return ( 47 | setHasMouse(true)} 51 | onMouseLeave={e => { setHasMouse(false); setFlexDirection("row"); setExpanded(false)}} 52 | onClick={handleClick}> 53 | 54 | {notes && 55 | notesElements 56 | } 57 | {tags.length > 0 && 58 | tagsElements 59 | } 60 | 61 | ) 62 | } -------------------------------------------------------------------------------- /src/components/DataSourceDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import DataStore from "../DataStore" 3 | import { Icon, Button, Dialog, Classes, AnchorButton, NonIdealState, Divider, ControlGroup, RadioGroup, Radio } from "@blueprintjs/core"; 4 | import { IconNames } from "@blueprintjs/icons"; 5 | import './DataSourceDialog.css' 6 | import Const from '../constants' 7 | import ChromeBookmarksDSC from "../ChromeBookmarksDSC" 8 | import LocalDirectoryDSC from "../LocalDirectoryDSC" 9 | 10 | export default function DataSourceDialog(props) { 11 | const [selectedSourceType, setSelectedSourceType] = useState(Const.DS_TYPE_CHROME) 12 | const [dataSources, setDataSources] = useState([]) 13 | 14 | useEffect(() => { 15 | DataStore.getAllDataSources(dataSources => { 16 | setDataSources(dataSources) 17 | }) 18 | }, []) 19 | 20 | const handleSourceTypeChange = event => setSelectedSourceType(event.target.value) 21 | 22 | const handleAddSourceButton = async () => { 23 | // const connectorClass = connectorClassMap[selectedSourceType] 24 | // const dataSourceConnector = new connectorClass() 25 | // await dataSourceConnector.configureDataSource() 26 | // DataStore.addDataSource(dataSourceConnector.serialize(), dataSource => { 27 | // console.log("new data source", dataSource) 28 | // setDataSources(dataSources.concat(dataSource)) 29 | // }) 30 | 31 | if (selectedSourceType === Const.DS_TYPE_CHROME) { 32 | const chromeBookmarksDSC = new ChromeBookmarksDSC() 33 | const success = await chromeBookmarksDSC.configureDataSource() 34 | if (success) { 35 | DataStore.addDataSource(chromeBookmarksDSC.export(), dataSource => { 36 | console.log("dataSources", dataSource) 37 | setDataSources(dataSources.concat(dataSource)) 38 | }) 39 | } 40 | } else if (selectedSourceType === Const.DS_TYPE_DIRECTORY) { 41 | const localDirectoryDSC = new LocalDirectoryDSC() 42 | const success = await localDirectoryDSC.configureDataSource() 43 | if (success) { 44 | DataStore.addDataSource(localDirectoryDSC.export(), dataSource => { 45 | console.log("dataSources", dataSource) 46 | setDataSources(dataSources.concat(dataSource)) 47 | }) 48 | } 49 | } 50 | } 51 | 52 | let mainSection 53 | 54 | if (dataSources.length === 0) { 55 | mainSection = 60 | } else { 61 | mainSection = 62 |
63 | {dataSources.map(ds =>
{ds.name}:{ds.customData.bookmarksLocation}
)} 64 |
65 | } 66 | 67 | return ( 68 | 75 |
76 | {mainSection} 77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 |
92 |
93 | Done 94 |
95 |
96 |
97 | ) 98 | } -------------------------------------------------------------------------------- /src/components/ContentViewer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import './ContentViewer.css' 3 | import AnnotationsPanel from './AnnotationsPanel' 4 | import { Icon, Button, Tooltip, Position, NonIdealState } from "@blueprintjs/core"; 5 | import { IconNames } from "@blueprintjs/icons"; 6 | const path = require('path') 7 | const electron = window.require('electron').remote 8 | const fs = electron.require('fs') 9 | 10 | export default function ContentViewer(props) { 11 | 12 | const [viewingLocal, setViewingLocal] = useState(true) 13 | const [showingAnnotations, setshowingAnnotations] = useState(false) 14 | 15 | const handleGoBack = () => { 16 | props.setAnnotationItem(null) 17 | props.goBackFunc() 18 | } 19 | 20 | const handleToggleAnnotations = () => { 21 | if (!showingAnnotations) { 22 | props.setAnnotationItem(props.content) 23 | } else { 24 | props.setAnnotationItem(null) 25 | } 26 | 27 | setshowingAnnotations(!showingAnnotations) 28 | } 29 | const handleSwitchVersion = () => setViewingLocal(!viewingLocal) 30 | 31 | const localPath = "memexdata://" + path.join("local-mems", props.content._id) + ".pdf" 32 | const remotePath = props.content.location 33 | const localPathAbsolute = path.join(electron.app.getPath("userData"), "local-mems", props.content._id + ".pdf") 34 | const localExists = fs.existsSync(localPathAbsolute) 35 | const url = viewingLocal ? localPath : remotePath 36 | 37 | const switchButtonMessage = `Switch to ${viewingLocal ? "live" : "local"} version` 38 | const versionIcon = viewingLocal ? IconNames.DATABASE : IconNames.GLOBE_NETWORK 39 | 40 | return ( 41 |
42 |
43 |
44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 | Viewing {viewingLocal ? " local " : " live "}: {props.content.name} 67 |
68 | 69 |
70 |
71 | 72 | {(localExists || !viewingLocal) && 73 |