├── .npmrc ├── .babelrc ├── resources ├── icon │ ├── icon.ico │ ├── icon.png │ └── icon.psd ├── demo │ ├── creation.gif │ └── switching.gif ├── dmg_background │ ├── background.png │ ├── background.psd │ └── background@2x.png └── syntax.txt ├── src ├── main │ ├── index.ts │ ├── utils │ │ └── menu.ts │ ├── windows │ │ ├── route.ts │ │ ├── about.ts │ │ ├── window.ts │ │ └── main.ts │ └── app.ts ├── renderer │ ├── static │ │ ├── images │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ └── fonts │ │ │ └── FiraCode-Regular.woff2 │ ├── template │ │ ├── error_boundary.scss │ │ ├── app.scss │ │ ├── base.scss │ │ ├── index.scss │ │ ├── button.scss │ │ ├── about.scss │ │ ├── variables.scss │ │ ├── titlebar.scss │ │ └── codemirror.scss │ ├── routes.ts │ ├── components │ │ ├── main │ │ │ ├── code │ │ │ │ ├── items │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tag.ts │ │ │ │ │ ├── project.ts │ │ │ │ │ ├── link.ts │ │ │ │ │ ├── font.ts │ │ │ │ │ └── todo.ts │ │ │ │ ├── codemirror.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── index.tsx │ │ │ │ └── addons │ │ │ │ │ └── dialog.js │ │ │ ├── wrapper.tsx │ │ │ ├── titlebar.tsx │ │ │ ├── index.tsx │ │ │ └── extra │ │ │ │ └── ipc.tsx │ │ ├── about │ │ │ └── index.tsx │ │ └── error_boundary.tsx │ ├── index.ts │ ├── containers │ │ └── main │ │ │ ├── index.ts │ │ │ ├── notes.ts │ │ │ ├── window.ts │ │ │ ├── editor.ts │ │ │ └── note.ts │ ├── debugging.ts │ └── render.tsx └── common │ ├── environment.ts │ ├── settings.ts │ └── types.ts ├── .editorconfig ├── .todo ├── bump.json ├── CHANGELOG.md ├── .gitignore ├── tsconfig.json ├── webpack.js ├── LICENSE ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "./src/renderer/static" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /resources/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/icon/icon.ico -------------------------------------------------------------------------------- /resources/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/icon/icon.png -------------------------------------------------------------------------------- /resources/icon/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/icon/icon.psd -------------------------------------------------------------------------------- /resources/demo/creation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/demo/creation.gif -------------------------------------------------------------------------------- /resources/demo/switching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/demo/switching.gif -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import App from './app'; 5 | 6 | /* MAIN */ 7 | 8 | new App (); 9 | -------------------------------------------------------------------------------- /src/renderer/static/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/src/renderer/static/images/icon.ico -------------------------------------------------------------------------------- /src/renderer/static/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/src/renderer/static/images/icon.png -------------------------------------------------------------------------------- /resources/dmg_background/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/dmg_background/background.png -------------------------------------------------------------------------------- /resources/dmg_background/background.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/dmg_background/background.psd -------------------------------------------------------------------------------- /resources/dmg_background/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/resources/dmg_background/background@2x.png -------------------------------------------------------------------------------- /src/renderer/static/fonts/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/noty/HEAD/src/renderer/static/fonts/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /src/renderer/template/error_boundary.scss: -------------------------------------------------------------------------------- 1 | 2 | #error-boundary { 3 | 4 | text-align: center; 5 | 6 | pre { 7 | text-align: left; 8 | margin: 0 auto; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/renderer/template/app.scss: -------------------------------------------------------------------------------- 1 | 2 | #app { 3 | height: 100%; 4 | } 5 | 6 | #app-wrapper { 7 | height: 100%; 8 | } 9 | 10 | #app-content { 11 | position: fixed; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | top: $titlebar-height; 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/routes.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import About from './components/about'; 5 | import Main from './components/main'; 6 | 7 | /* ROUTES */ 8 | 9 | const Routes = { 10 | about: About, 11 | main: Main 12 | }; 13 | 14 | /* EXPORT */ 15 | 16 | export default Routes; 17 | -------------------------------------------------------------------------------- /.todo: -------------------------------------------------------------------------------- 1 | 2 | Bugs: 3 | ☐ Cmd-+ (zoom in) is broken (Electron's bug?) 4 | ☐ A new note can sometimes have the previous one's content (react-codemirror2's bug?) 5 | ☐ Find & Replace is broken (react-codemirror2's bug?) 6 | 7 | Future: 8 | ☐ Support nested styles 9 | ☐ Add `Print...` support 10 | -------------------------------------------------------------------------------- /bump.json: -------------------------------------------------------------------------------- 1 | { 2 | "tag": { 3 | "enabled": false 4 | }, 5 | "release": { 6 | "github": { 7 | "enabled": true, 8 | "files": [ 9 | "releases/*", 10 | "!releases/builder-effective-config.yaml" 11 | ] 12 | } 13 | }, 14 | "scripts": { 15 | "prerelease": "npm run build:all" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/template/base.scss: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | padding: 0; 5 | margin: 0; 6 | height: 100%; 7 | width: 100%; 8 | font-family: sans-serif; 9 | background-color: $color-main; 10 | color: $color-text; 11 | user-select: none; 12 | } 13 | 14 | pre { 15 | max-width: 100%; 16 | overflow: auto; 17 | padding: $gutter; 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Font from './font'; 5 | import Link from './link'; 6 | import Project from './project'; 7 | import Tag from './tag'; 8 | import Todo from './todo'; 9 | 10 | /* ITEMS */ 11 | 12 | const Items = [Font, Link, Project, Tag, Todo]; 13 | 14 | /* EXPORT */ 15 | 16 | export default Items; 17 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/tag.ts: -------------------------------------------------------------------------------- 1 | 2 | /* TAG */ 3 | 4 | const Tag = { 5 | 6 | tagRe: /(?:^|[^a-zA-Z0-9`])(@[^\s*~(]+(?:\([^)]*\))?)/, 7 | 8 | getTokens () { 9 | 10 | return { 11 | start: [ 12 | { regex: Tag.tagRe, token: 'tag' } 13 | ] 14 | }; 15 | 16 | } 17 | 18 | }; 19 | 20 | /* EXPORT */ 21 | 22 | export default Tag; 23 | -------------------------------------------------------------------------------- /src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import debugging from './debugging'; 5 | import render from './render'; 6 | 7 | /* RENDERER */ 8 | 9 | debugging (); 10 | render (); 11 | 12 | /* HOT MODULE REPLACEMENT */ 13 | 14 | if ( module.hot ) { 15 | 16 | module.hot.accept ( './render', () => { 17 | require ( './render' ).default (); 18 | }); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/template/index.scss: -------------------------------------------------------------------------------- 1 | 2 | @charset "UTF-8"; 3 | 4 | @import "~codemirror/lib/codemirror.css"; 5 | @import "~codemirror/addon/dialog/dialog.css"; 6 | @import "variables.scss"; 7 | @import "base.scss"; 8 | @import "app.scss"; 9 | @import "about.scss"; 10 | @import "error_boundary.scss"; 11 | @import "button.scss"; 12 | @import "titlebar.scss"; 13 | @import "codemirror.scss"; 14 | -------------------------------------------------------------------------------- /src/common/environment.ts: -------------------------------------------------------------------------------- 1 | 2 | /* ENVIRONMENT */ 3 | 4 | const Environment = { 5 | environment: process.env.NODE_ENV, 6 | isDevelopment: ( process.env.NODE_ENV !== 'production' ), 7 | wds: { // Webpack Development Server 8 | protocol: 'http', 9 | hostname: 'localhost', 10 | port: process.env.ELECTRON_WEBPACK_WDS_PORT 11 | } 12 | }; 13 | 14 | /* EXPORT */ 15 | 16 | export default Environment; 17 | -------------------------------------------------------------------------------- /resources/syntax.txt: -------------------------------------------------------------------------------- 1 | 2 | *Bold* *Bold* 3 | `code` `code` 4 | _Italic_ _Italic_ 5 | ~Strikethrough~ ~Strikethrough~ 6 | http://asdasd.com 7 | http://ex.com 8 | example.com 9 | www.example.com 10 | asdasd: 11 | asdasd: @asdasd 12 | @asd 13 | @asd(asd) 14 | ☐ asdasd 15 | ☐ asdasd @asdasd 16 | ✔ asdasd @asdasd asdasd 17 | ✘ asdasd @asdasd asdasd 18 | ☐ asdasd *Bold* 19 | ☐ asdasd `code` 20 | ☐ asdasd _Italic_ 21 | ☐ asdasd ~Strikethrough~ 22 | -------------------------------------------------------------------------------- /src/renderer/template/button.scss: -------------------------------------------------------------------------------- 1 | 2 | .button { 3 | 4 | display: inline-block; 5 | background-color: $color-button; 6 | color: $color-button-text; 7 | padding: $gutter-half $gutter; 8 | border-radius: $gutter-half; 9 | font-weight: bold; 10 | cursor: pointer; 11 | 12 | &:hover { 13 | background-color: $color-button-hover; 14 | } 15 | 16 | &:active { 17 | background-color: $color-button-active; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 2.2.0 2 | - Readme: added a link to Notable 3 | - Updated template 4 | - Hiding close button on non-macOS systems 5 | - Improved CodeMirror configuration 6 | - Properly managing the state 7 | 8 | ### Version 2.1.0 9 | - Added a shortcut for deleting the current note 10 | 11 | ### Version 2.0.0 12 | - Complete rewrite 13 | - Many bug fixes 14 | - Some minor improvements 15 | - It's now truly cross-platform 16 | 17 | ### Version 1.0.0 18 | - Initial release. 19 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/project.ts: -------------------------------------------------------------------------------- 1 | 2 | /* PROJECT */ 3 | 4 | const Project = { 5 | 6 | projectRe: /^(?![^\S\n]*(?!--|––|——)(?:[-❍❑■⬜□☐▪▫–—≡→›✘xX✔✓☑+]|\[[ xX+-]?\])\s[^\n]*)[^\S\n]*(.+:)[^\S\n]*(?:(?=@[^\s*~(]+(?:\([^)]*\))?)|$)/, 7 | 8 | getTokens () { 9 | 10 | return { 11 | start: [ 12 | { sol: true, regex: Project.projectRe, token: 'project' } 13 | ] 14 | }; 15 | 16 | } 17 | 18 | }; 19 | 20 | /* EXPORT */ 21 | 22 | export default Project; 23 | -------------------------------------------------------------------------------- /src/renderer/containers/main/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Container, compose} from 'overstated'; 5 | import Editor from './editor'; 6 | import Note from './note'; 7 | import Notes from './notes'; 8 | import Window from './window'; 9 | 10 | /* MAIN */ 11 | 12 | class Main extends Container {} 13 | 14 | /* EXPORT */ 15 | 16 | export default compose ({ 17 | editor: Editor, 18 | note: Note, 19 | notes: Notes, 20 | window: Window 21 | })( Main ) as IMain; 22 | -------------------------------------------------------------------------------- /src/renderer/debugging.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Environment from '@common/environment'; 5 | 6 | /* DEBUGGING */ 7 | 8 | async function debugging () { 9 | 10 | if ( !Environment.isDevelopment ) return; 11 | 12 | const {debug, HMR} = await import ( 'overstated' ); 13 | 14 | debug.isEnabled = Environment.isDevelopment; 15 | debug.logStateChanges = false; 16 | 17 | HMR.isEnabled = Environment.isDevelopment; 18 | 19 | } 20 | 21 | /* EXPORT */ 22 | 23 | export default debugging; 24 | -------------------------------------------------------------------------------- /src/main/utils/menu.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | 6 | /* MENU */ 7 | 8 | const Menu = { 9 | 10 | filterTemplate ( template ) { // Removes items with `visible == false` 11 | 12 | return _.cloneDeepWith ( template, val => { 13 | 14 | if ( !_.isArray ( val ) ) return; 15 | 16 | return val.filter ( ele => !_.isObject ( ele ) || !ele.hasOwnProperty ( 'visible' ) || ele.visible ).map ( Menu.filterTemplate ); 17 | 18 | }); 19 | 20 | } 21 | 22 | }; 23 | 24 | /* EXPORT */ 25 | 26 | export default Menu; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *.zip 11 | *~ 12 | *.sass-cache 13 | *.ruby-version 14 | *.rbenv-version 15 | 16 | # OS or Editor folders 17 | ._* 18 | .cache 19 | .DS_Store 20 | .idea 21 | .project 22 | .settings 23 | .tmproj 24 | *.esproj 25 | *.sublime-project 26 | *.sublime-workspace 27 | nbproject 28 | Thumbs.db 29 | .fseventsd 30 | .DocumentRevisions* 31 | .TemporaryItems 32 | .Trashes 33 | 34 | # Other paths to ignore 35 | bower_components 36 | node_modules 37 | dist 38 | releases 39 | -------------------------------------------------------------------------------- /src/renderer/components/about/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as React from 'react'; 5 | import * as path from 'path'; 6 | import pkg from '@root/package.json'; 7 | 8 | /* ABOUT */ 9 | 10 | const About = () => ( 11 |
12 | 13 |

{pkg.productName}

14 |

Version {pkg.version}

15 |

{pkg.license} © {pkg.author.name}

16 |
17 | ); 18 | 19 | /* EXPORT */ 20 | 21 | export default About; 22 | -------------------------------------------------------------------------------- /src/renderer/containers/main/notes.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Container} from 'overstated'; 5 | import Settings from '@common/settings'; 6 | 7 | /* NOTES */ 8 | 9 | class Notes extends Container { 10 | 11 | /* STATE */ 12 | 13 | state = { 14 | notes: Settings.get ( 'notes' ) 15 | }; 16 | 17 | /* API */ 18 | 19 | get = (): NoteObj[] => { 20 | 21 | return this.state.notes; 22 | 23 | } 24 | 25 | set = ( notes: NoteObj[] ) => { 26 | 27 | Settings.set ( 'notes', notes ); 28 | 29 | return this.setState ({ notes }); 30 | 31 | } 32 | 33 | } 34 | 35 | /* EXPORT */ 36 | 37 | export default Notes; 38 | -------------------------------------------------------------------------------- /src/renderer/components/main/wrapper.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as React from 'react'; 5 | import {connect} from 'overstated'; 6 | import Main from '@renderer/containers/main'; 7 | 8 | /* WRAPPER */ 9 | 10 | const Wrapper = ({ children, isFocus, isScroll }) => ( 11 |
12 | {children} 13 |
14 | ); 15 | 16 | /* EXPORT */ 17 | 18 | export default connect ({ 19 | container: Main, 20 | selector: ({ children, container }) => ({ 21 | children, 22 | isFocus: container.window.isFocus (), 23 | isScroll: container.editor.isScroll () 24 | }) 25 | })( Wrapper ); 26 | -------------------------------------------------------------------------------- /src/renderer/template/about.scss: -------------------------------------------------------------------------------- 1 | 2 | #about { 3 | 4 | background-color: $color-about; 5 | position: fixed; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate(-50%, -50%); 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | & > *:not(:last-child) { 17 | margin-top: 0; 18 | margin-bottom: $gutter-half; 19 | } 20 | 21 | & > *:last-child { 22 | margin-top: 0; 23 | margin-bottom: 0; 24 | } 25 | 26 | & .title { 27 | font-weight: bold; 28 | margin-top: 0; 29 | } 30 | 31 | & .desc { 32 | font-size: $about-desc-font-size; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/electron-webpack/tsconfig-base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "experimentalDecorators": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "lib": ["dom", "scripthost", "es2015", "es2016", "es2017"], 9 | "noUnusedParameters": false, 10 | "resolveJsonModule": true, 11 | "strict": false, 12 | "strictNullChecks": true, 13 | "target": "es5", 14 | "paths": { 15 | "@common/*": ["common/*"], 16 | "@main/*": ["main/*"], 17 | "@renderer/*": ["renderer/*"], 18 | "@root/*": ["../*"], 19 | "@static/*": ["renderer/static/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/containers/main/window.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {remote} from 'electron'; 5 | import {Container} from 'overstated'; 6 | 7 | /* WINDOW */ 8 | 9 | class Window extends Container { 10 | 11 | /* STATE */ 12 | 13 | state = { 14 | focus: !!remote.BrowserWindow.getFocusedWindow () 15 | }; 16 | 17 | /* API */ 18 | 19 | isFocus = (): boolean => { 20 | 21 | return this.state.focus; 22 | 23 | } 24 | 25 | setFocus = ( focus: boolean ) => { 26 | 27 | return this.setState ({ focus }); 28 | 29 | } 30 | 31 | close = () => { 32 | 33 | return remote.getCurrentWindow ().close (); 34 | 35 | } 36 | 37 | } 38 | 39 | /* EXPORT */ 40 | 41 | export default Window; 42 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | const TerserPlugin = require ( 'terser-webpack-plugin' ), 5 | TSConfigPathsPlugin = require ( 'tsconfig-paths-webpack-plugin' ), 6 | webpack = require ( 'webpack' ); 7 | 8 | /* CONFIG */ 9 | 10 | const config = { 11 | resolve: { 12 | plugins: [ 13 | new TSConfigPathsPlugin () 14 | ] 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin ({ 18 | 'Environment.isDevelopment': JSON.stringify ( process.env.NODE_ENV !== 'production' ) 19 | }) 20 | ], 21 | optimization: { 22 | minimizer: [ 23 | new TerserPlugin ({ 24 | parallel: true, 25 | sourceMap: true, 26 | terserOptions: { 27 | keep_fnames: true 28 | } 29 | }) 30 | ] 31 | } 32 | }; 33 | 34 | /* EXPORT */ 35 | 36 | module.exports = config; 37 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/codemirror.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import 'codemirror/addon/mode/simple.js'; 5 | import 'codemirror/addon/search/search.js'; 6 | import 'codemirror/keymap/sublime.js'; 7 | import './addons/dialog.js'; 8 | import * as _ from 'lodash'; 9 | import * as CodeMirrorLib from 'codemirror/lib/codemirror'; 10 | import * as CodeMirror from 'codemirror'; 11 | 12 | /* WEIRD FIX */ //UGLY: Why the hell is this required? Why are `codemirror` and `codemirror/lib/codemirror` separate beasts? do they get cached on they own or something? 13 | 14 | _.extend ( CodeMirror['keyMap'], CodeMirrorLib.keyMap ); 15 | _.extend ( CodeMirror['commands'], CodeMirrorLib.commands ); 16 | _.extend ( CodeMirror, CodeMirrorLib ); 17 | 18 | ( CodeMirror as any ).prototype = CodeMirrorLib.prototype; //TSC 19 | 20 | /* EXPORT */ 21 | 22 | export default CodeMirrorLib; 23 | -------------------------------------------------------------------------------- /src/common/settings.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as Store from 'electron-store'; 5 | import pkg from '@root/package.json'; 6 | 7 | /* SETTINGS */ 8 | 9 | const Settings = new Store ({ 10 | defaults: { 11 | note: pkg.productName, 12 | notes: [ 13 | { 14 | title: pkg.productName, 15 | content: `Welcome to ${pkg.productName}\n\nSince we are using the FiraCode font you can type many glyphs like: -> ->> => ==> ~~> <-< <=< |> <| <>\n\nWe support To-Do lists by default:\n ✔ Read the readme\n ☐ Star the repository\n ☐ Share with friends\n\nLinks: www.example.com\n\nFont styles: *Bold*, _Italic_ and ~Strikethrough~\n\nAnd multiple notes, try clicking the title to switch note.` 16 | }, 17 | { 18 | title: 'Another note', 19 | content: 'Pretty cool, huh?' 20 | } 21 | ] 22 | } 23 | }); 24 | 25 | /* EXPORT */ 26 | 27 | export default Settings; 28 | -------------------------------------------------------------------------------- /src/main/windows/route.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as path from 'path'; 5 | import {format as formatURL} from 'url'; 6 | import Environment from '@common/environment'; 7 | import Window from './window'; 8 | 9 | /* ROUTE */ 10 | 11 | class Route extends Window { 12 | 13 | /* API */ 14 | 15 | load () { 16 | 17 | const route = this.name; 18 | 19 | if ( Environment.isDevelopment ) { 20 | 21 | const {protocol, hostname, port} = Environment.wds; 22 | 23 | this.win.loadURL ( `${protocol}://${hostname}:${port}?route=${route}` ); 24 | 25 | } else { 26 | 27 | this.win.loadURL ( formatURL ({ 28 | pathname: path.join ( __dirname, 'index.html' ), 29 | protocol: 'file', 30 | slashes: true, 31 | query: { 32 | route 33 | } 34 | })); 35 | 36 | } 37 | 38 | } 39 | 40 | } 41 | 42 | /* EXPORT */ 43 | 44 | export default Route; 45 | -------------------------------------------------------------------------------- /src/renderer/render.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import '@renderer/template/index.scss'; 5 | 6 | import * as React from 'react'; 7 | import {render as reactRender} from 'react-dom'; 8 | import Identity from 'react-component-identity'; 9 | import {Router} from 'react-router-static'; 10 | import {Provider} from 'overstated'; 11 | import Environment from '@common/environment'; 12 | import Routes from './routes'; 13 | import ErrorBoundary from './components/error_boundary'; 14 | 15 | /* RENDER */ 16 | 17 | async function render () { 18 | 19 | const AppContainer = Environment.isDevelopment ? ( await import ( 'react-hot-loader' ) ).AppContainer : Identity; 20 | 21 | reactRender ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | , 29 | document.getElementById ( 'app' ) 30 | ); 31 | 32 | } 33 | 34 | /* EXPORT */ 35 | 36 | export default render; 37 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* GLOBALS */ 3 | 4 | declare const __static: string; 5 | 6 | /* BASE OBJECTS */ 7 | 8 | type NoteObj = { 9 | title: string, 10 | content: string 11 | }; 12 | 13 | /* MAIN CONTAINERS STATES */ 14 | 15 | type EditorState = { 16 | editor: import ( 'codemirror' ).Editor | undefined, 17 | scroll: boolean 18 | }; 19 | 20 | type NoteState = { 21 | note: NoteObj | undefined 22 | }; 23 | 24 | type NotesState = { 25 | notes: NoteObj[] 26 | }; 27 | 28 | type WindowState = { 29 | focus: boolean 30 | }; 31 | 32 | /* MAIN */ 33 | 34 | type MainState = { 35 | editor: EditorState, 36 | note: NoteState, 37 | notes: NotesState, 38 | window: WindowState 39 | }; 40 | 41 | type MainCTX = { 42 | editor: import ( '@renderer/containers/main/editor' ).default, 43 | note: import ( '@renderer/containers/main/note' ).default, 44 | notes: import ( '@renderer/containers/main/notes' ).default, 45 | window: import ( '@renderer/containers/main/window' ).default 46 | }; 47 | 48 | type IMain = MainCTX & { ctx: MainCTX }; 49 | -------------------------------------------------------------------------------- /src/main/windows/about.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Menu, MenuItemConstructorOptions} from 'electron'; 5 | import pkg from '@root/package.json'; 6 | import UMenu from '@main/utils/menu'; 7 | import Route from './route'; 8 | 9 | /* ABOUT */ 10 | 11 | class About extends Route { 12 | 13 | /* CONSTRUCTOR */ 14 | 15 | constructor ( name = 'about', options = { frame: true, autoHideMenuBar: true, minimizable: false, maximizable: false, resizable: false, backgroundColor: '#ececec', title: 'About', titleBarStyle: 'default', minWidth: 284, minHeight: 160 }, stateOptions = { defaultWidth: 284, defaultHeight: 160 } ) { 16 | 17 | super ( name, options, stateOptions ); 18 | 19 | } 20 | 21 | /* SPECIAL */ 22 | 23 | initMenu () { 24 | 25 | const template: MenuItemConstructorOptions[] = UMenu.filterTemplate ([ 26 | { 27 | label: pkg.productName, 28 | submenu: [ 29 | { role: 'close' } 30 | ] 31 | } 32 | ]); 33 | 34 | const menu = Menu.buildFromTemplate ( template ); 35 | 36 | Menu.setApplicationMenu ( menu ); 37 | 38 | } 39 | 40 | } 41 | 42 | /* EXPORT */ 43 | 44 | export default About; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/renderer/components/error_boundary.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {shell} from 'electron'; 5 | import * as React from 'react'; 6 | import pkg from '@root/package.json'; 7 | 8 | /* ERROR BOUNDARY */ 9 | 10 | class ErrorBoundary extends React.Component { 11 | 12 | /* STATE */ 13 | 14 | state = { 15 | error: undefined as Error | undefined 16 | }; 17 | 18 | /* SPECIAL */ 19 | 20 | componentDidCatch ( error: Error ) { 21 | 22 | this.setState ({ error }); 23 | 24 | } 25 | 26 | /* API */ 27 | 28 | report = () => { 29 | 30 | shell.openExternal ( pkg.bugs.url ); 31 | 32 | } 33 | 34 | /* RENDER */ 35 | 36 | render () { 37 | 38 | const {error} = this.state; 39 | 40 | if ( !error ) return this.props.children; 41 | 42 | return ( 43 |
44 |
45 |
An Error Occurred!
46 |
47 |
{error.stack}
48 |
Report It
49 |
50 | ); 51 | 52 | } 53 | 54 | } 55 | 56 | /* EXPORT */ 57 | 58 | export default ErrorBoundary; 59 | -------------------------------------------------------------------------------- /src/renderer/components/main/titlebar.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as React from 'react'; 5 | import * as is from 'electron-is'; 6 | import {connect} from 'overstated'; 7 | import Main from '@renderer/containers/main'; 8 | 9 | /* TITLEBAR */ 10 | 11 | const Titlebar = ({ title, titles, onChange, close }) => ( 12 |
13 | {!is.macOS () ? null : ( 14 |
15 | 16 | 17 | 18 |
19 | )} 20 |
21 | {title} 22 | 27 |
28 |
29 | ); 30 | 31 | /* EXPORT */ 32 | 33 | export default connect ({ 34 | container: Main, 35 | selector: ({ container, title, titles, onChange }) => ({ 36 | title, titles, onChange, 37 | close: container.window.close 38 | }) 39 | })( Titlebar ); 40 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | import merge from 'conf-merge'; 6 | import CodeMirror from './codemirror'; 7 | import Items from './items'; 8 | 9 | /* UTILS */ 10 | 11 | const Utils = { 12 | 13 | defineMode () { 14 | 15 | const tokensAll = merge ( {}, ...Items.map ( item => item.getTokens () ) ); 16 | 17 | CodeMirror.defineSimpleMode ( 'noty', tokensAll ); 18 | 19 | }, 20 | 21 | addSelection ( cm, pos ) { 22 | 23 | cm.getDoc ().addSelection ( pos ); 24 | 25 | }, 26 | 27 | walkSelections ( cm, callback ) { 28 | 29 | cm.listSelections ().forEach ( selection => { 30 | 31 | const lineNr = Math.min ( selection.anchor.line, selection.head.line ), 32 | line = cm.getLine ( lineNr ); 33 | 34 | callback ( line, lineNr ); 35 | 36 | }); 37 | 38 | }, 39 | 40 | replace ( cm, lineNr, replacement, fromCh, toCh? ) { 41 | 42 | const from = { line: lineNr, ch: fromCh }; 43 | 44 | if ( _.isUndefined ( toCh ) ) { 45 | 46 | cm.replaceRange ( replacement, from ); 47 | 48 | } else { 49 | 50 | const to = { line: lineNr, ch: toCh }; 51 | 52 | cm.replaceRange ( replacement, from, to ); 53 | 54 | } 55 | 56 | } 57 | 58 | }; 59 | 60 | /* EXPORT */ 61 | 62 | export default Utils; 63 | -------------------------------------------------------------------------------- /src/renderer/template/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $gutter: 10px; 3 | $gutter-half: $gutter / 2; 4 | 5 | $color-about: #ececec; 6 | $color-main: #fef3a1; 7 | $color-text: #1f1f1f; 8 | 9 | $color-link: #1543F9; 10 | $color-link-hover: lighten( $color-link, 10% ); 11 | $color-link-active: darken( $color-link, 15% ); 12 | 13 | $color-titlebar: darken( $color-main, 15% ); 14 | $color-titlebar-title: transparent; 15 | $color-titlebar-title-hover: darken( $color-titlebar, 15% ); 16 | $color-titlebar-title-active: darken( $color-titlebar, 20% ); 17 | 18 | $color-code-cursor: $color-text; 19 | $color-code-selected: $color-titlebar; 20 | $color-code-match: transparentize( $color-code-selected, .52 ); 21 | $color-code-dialog: $color-titlebar; 22 | 23 | $color-code: $color-text; 24 | $color-project: #268FE1; 25 | $color-tag: #f97f26; 26 | $color-todo-done: #10C413; 27 | $color-todo-cancel: #f92672; 28 | 29 | $color-button: $color-todo-cancel; 30 | $color-button-hover: lighten( $color-button, 10% ); 31 | $color-button-active: darken( $color-button, 15% ); 32 | $color-button-text: #ffffff; 33 | 34 | $code-font-size: 13px; 35 | $code-dialog-font-size: $code-font-size; 36 | $code-dialog-height: 27px; //FIXME: Hacky 37 | 38 | $shadow-animation-duration: .15s; 39 | $shadow-color: #00000022; 40 | 41 | $titlebar-height: 22px; 42 | $titlebar-font-size: 12px; 43 | 44 | $font-token-opacity: .25; 45 | 46 | $about-desc-font-size: .5946142301em; 47 | -------------------------------------------------------------------------------- /src/renderer/components/main/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as React from 'react'; 5 | import {connect} from 'overstated'; 6 | import MainContainer from '@renderer/containers/main'; 7 | import IPC from './extra/ipc'; 8 | import Code from './code'; 9 | import Titlebar from './titlebar'; 10 | import Wrapper from './wrapper'; 11 | 12 | /* MAIN */ 13 | 14 | const Main = ({ index, note, notes, save, set, setEditor }) => { 15 | 16 | if ( !note ) { 17 | set ( notes[0].title ); 18 | return null; 19 | } 20 | 21 | const titles = notes.map ( note => note.title ), 22 | id = `${note.title}-${index}`; 23 | 24 | return ( 25 | <> 26 | 27 | 28 | set ( e.target.value )} /> 29 |
30 | save ( note, content )} onEditor={setEditor} /> 31 |
32 |
33 | 34 | ); 35 | 36 | }; 37 | 38 | /* EXPORT */ 39 | 40 | export default connect ({ 41 | container: MainContainer, 42 | selector: ({ container }) => ({ 43 | index: container.note.getIndex (), 44 | note: container.note.get (), 45 | notes: container.notes.get (), 46 | save: container.note.save, 47 | set: container.note.set, 48 | setEditor: container.editor.set 49 | }) 50 | })( Main ); 51 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/link.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as $ from 'cash-dom'; 5 | import {shell} from 'electron'; 6 | 7 | /* LINK */ 8 | 9 | const Link = { 10 | 11 | urlRe: /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i, 12 | 13 | getTokens () { 14 | 15 | return { 16 | start: [ 17 | { regex: Link.urlRe, token: 'link' } 18 | ] 19 | }; 20 | 21 | }, 22 | 23 | onClick () { 24 | 25 | $(document).off ( 'click' ).on ( 'click', '.cm-link', event => { 26 | if ( !event.metaKey ) return; 27 | let url = $(event.target).text (); 28 | if ( !/^https?:\/\//i.test ( url ) ) { 29 | url = `http://${url}`; 30 | } 31 | shell.openExternal ( url ); 32 | }); 33 | 34 | }, 35 | 36 | onMeta () { 37 | 38 | let meta = false; 39 | 40 | $(document).off ( 'keydown keyup' ).on ( 'keydown keyup', event => { 41 | if ( meta === event.metaKey ) return; 42 | meta = event.metaKey; 43 | $('html').toggleClass ( 'meta', meta ); 44 | }); 45 | 46 | } 47 | 48 | } 49 | 50 | /* INIT */ 51 | 52 | Link.onClick (); 53 | Link.onMeta (); 54 | 55 | /* EXPORT */ 56 | 57 | export default Link; 58 | -------------------------------------------------------------------------------- /src/renderer/containers/main/editor.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as $ from 'cash-dom'; 5 | import {Editor as EditorType} from 'codemirror'; 6 | import {Container} from 'overstated'; 7 | 8 | /* EDITOR */ 9 | 10 | class Editor extends Container { 11 | 12 | /* STATE */ 13 | 14 | state = { 15 | editor: undefined as EditorType | undefined, 16 | scroll: false 17 | }; 18 | 19 | /* API */ 20 | 21 | get = (): EditorType | undefined => { 22 | 23 | return this.state.editor; 24 | 25 | } 26 | 27 | set = ( editor: EditorType ) => { 28 | 29 | return this.setState ({ editor }); 30 | 31 | } 32 | 33 | reset = () => { 34 | 35 | const editor = this.ctx.editor.get (); 36 | 37 | if ( editor ) { 38 | editor['focus'](); 39 | editor['setSelection']({ line: 0, ch: 0 }); 40 | editor['doc'].clearHistory (); 41 | } 42 | 43 | const $scroll = $('.CodeMirror-scroll'); 44 | 45 | if ( $scroll.length ) { 46 | $scroll[0].scrollTop = 0; 47 | } 48 | 49 | } 50 | 51 | dialog = ( label: string, options? ): Promise => { 52 | 53 | return new Promise ( res => { 54 | 55 | const editor = this.get (); 56 | 57 | if ( !editor ) return res (); 58 | 59 | const template = `${label} `; 60 | 61 | editor['openDialog']( template, res, options ); 62 | 63 | }); 64 | 65 | } 66 | 67 | isScroll = (): boolean => { 68 | 69 | return this.state.scroll; 70 | 71 | } 72 | 73 | setScroll = ( scroll ) => { 74 | 75 | return this.setState ({ scroll }); 76 | 77 | } 78 | 79 | onScroll = ( editor, {top} ) => { 80 | 81 | const isScroll = !!top; 82 | 83 | if ( this.isScroll () === isScroll ) return; 84 | 85 | this.setScroll ( isScroll ); 86 | 87 | } 88 | 89 | } 90 | 91 | /* EXPORT */ 92 | 93 | export default Editor; 94 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/font.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | 6 | /* FONT */ 7 | 8 | const Font = { 9 | 10 | boldRe: /(\*)([^\n*]+)(\*)/, 11 | codeRe: /(`)([^\n`]*)(`)/, 12 | italicRe: /(_)([^\n_]+)(_)/, 13 | strikethroughRe: /(~)([^\n~]+)(~)/, 14 | 15 | getTokens () { 16 | 17 | const repeat = ( arr, times ) => _.concat ( [], ..._.range ( times ).map ( () => _.cloneDeep ( arr ) ) ), 18 | makeTokens = ( classNames, times = 1 ) => { 19 | const tokens = [`${classNames} font-token`, classNames, `${classNames} font-token`]; 20 | return repeat ( tokens, times ); 21 | }; 22 | 23 | return { 24 | start: [ 25 | { regex: Font.boldRe, token: makeTokens ( 'bold' ) }, 26 | { regex: Font.codeRe, token: makeTokens ( 'code' ) }, 27 | { regex: Font.italicRe, token: makeTokens ( 'italic' ) }, 28 | { regex: Font.strikethroughRe, token: makeTokens ( 'strikethrough' ) } 29 | ] 30 | }; 31 | 32 | }, 33 | 34 | toggleToken ( cm, prefix, suffix = prefix, content = '' ) { 35 | 36 | cm.doc.replaceSelections ( cm.doc.getSelections ().map ( selection => { 37 | 38 | const hasToken = selection.slice ( 0, prefix.length ) === prefix && selection.slice ( - suffix.length ) === suffix; 39 | 40 | return hasToken ? selection.slice ( prefix.length, - suffix.length ) : `${prefix}${selection || content}${suffix}`; 41 | 42 | }), 'around' ); 43 | 44 | }, 45 | 46 | toggleBold ( cm ) { 47 | 48 | Font.toggleToken ( cm, '*', '*', 'Bold' ); 49 | 50 | }, 51 | 52 | toggleCode ( cm ) { 53 | 54 | Font.toggleToken ( cm, '`', '`', 'Code' ); 55 | 56 | }, 57 | 58 | toggleItalic ( cm ) { 59 | 60 | Font.toggleToken ( cm, '_', '_', 'Italic' ); 61 | 62 | }, 63 | 64 | toggleStrikethrough ( cm ) { 65 | 66 | Font.toggleToken ( cm, '~', '~', 'Strikethrough' ); 67 | 68 | } 69 | 70 | }; 71 | 72 | /* EXPORT */ 73 | 74 | export default Font; 75 | -------------------------------------------------------------------------------- /src/renderer/template/titlebar.scss: -------------------------------------------------------------------------------- 1 | 2 | #titlebar { 3 | 4 | -webkit-app-region: drag; 5 | display: flex; 6 | align-items: center; 7 | background-color: $color-titlebar; 8 | height: $titlebar-height; 9 | font-size: $titlebar-font-size; 10 | transition: box-shadow $shadow-animation-duration; 11 | 12 | #app-wrapper.scrolled & { 13 | box-shadow: 0 2px 3px 0 $shadow-color; 14 | } 15 | 16 | } 17 | 18 | #titlebar-close { 19 | 20 | position: relative; 21 | width: $gutter; 22 | height: $gutter; 23 | border-radius: $gutter; 24 | margin-left: $gutter; 25 | background-color: #ff5f57; 26 | border: 1px solid #e2463f; 27 | -webkit-app-region: no-drag; 28 | 29 | &:active { 30 | border-color: #ad3934; 31 | background-color: #bf4943; 32 | } 33 | 34 | svg { 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | transform: translate(-50%, -50%); 39 | width: 6px; 40 | height: 6px; 41 | -webkit-app-region: no-drag; 42 | } 43 | 44 | &:not(:hover) svg { 45 | display: none; 46 | } 47 | 48 | #app-wrapper:not(.focused) & { 49 | border-color: lighten( $color-titlebar, 25% ); 50 | background-color: lighten( $color-titlebar, 25% ); 51 | } 52 | 53 | } 54 | 55 | #titlebar-title { 56 | 57 | position: fixed; 58 | top: 0; 59 | left: 50%; 60 | right: auto; 61 | transform: translateX(-50%); 62 | display: flex; 63 | align-items: center; 64 | padding: 0 $gutter; 65 | height: $titlebar-height; 66 | background: $color-titlebar-title; 67 | -webkit-app-region: no-drag; 68 | 69 | &:hover { 70 | background: $color-titlebar-title-hover; 71 | } 72 | 73 | &:active { 74 | background: $color-titlebar-title-active; 75 | } 76 | 77 | } 78 | 79 | #titlebar-select { 80 | -webkit-appearance: none; 81 | position: absolute; 82 | top: 0; 83 | right: 0; 84 | bottom: 0; 85 | left: 0; 86 | width: 100%; 87 | height: 100%; 88 | margin: 0; 89 | opacity: 0; 90 | border: 0; 91 | border-radius: inherit; 92 | z-index: 1; 93 | } 94 | -------------------------------------------------------------------------------- /src/main/app.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {app} from 'electron'; 5 | import {autoUpdater} from 'electron-updater'; 6 | import * as contextMenu from 'electron-context-menu'; 7 | import * as is from 'electron-is'; 8 | import Environment from '@common/environment'; 9 | import Main from './windows/main'; 10 | import Window from './windows/window'; 11 | 12 | /* APP */ 13 | 14 | class App { 15 | 16 | /* VARIABLES */ 17 | 18 | win: Window; 19 | 20 | /* CONSTRUCTOR */ 21 | 22 | constructor () { 23 | 24 | this.init (); 25 | this.events (); 26 | 27 | } 28 | 29 | /* SPECIAL */ 30 | 31 | init () { 32 | 33 | this.initContextMenu (); 34 | 35 | } 36 | 37 | initContextMenu () { 38 | 39 | contextMenu (); 40 | 41 | } 42 | 43 | async initDebug () { 44 | 45 | if ( !Environment.isDevelopment ) return; 46 | 47 | const {default: installExtension, REACT_DEVELOPER_TOOLS} = await import ( 'electron-devtools-installer' ); 48 | 49 | installExtension ( REACT_DEVELOPER_TOOLS ); 50 | 51 | } 52 | 53 | events () { 54 | 55 | this.___windowAllClosed (); 56 | this.___activate (); 57 | this.___ready (); 58 | 59 | } 60 | 61 | /* WINDOW ALL CLOSED */ 62 | 63 | ___windowAllClosed () { 64 | 65 | app.on ( 'window-all-closed', this.__windowAllClosed.bind ( this ) ); 66 | 67 | } 68 | 69 | __windowAllClosed () { 70 | 71 | if ( is.macOS () ) return; 72 | 73 | app.quit (); 74 | 75 | } 76 | 77 | /* ACTIVATE */ 78 | 79 | ___activate () { 80 | 81 | app.on ( 'activate', this.__activate.bind ( this ) ); 82 | 83 | } 84 | 85 | __activate () { 86 | 87 | if ( this.win && this.win.win ) return; 88 | 89 | this.load (); 90 | 91 | } 92 | 93 | /* READY */ 94 | 95 | ___ready () { 96 | 97 | app.on ( 'ready', this.__ready.bind ( this ) ); 98 | 99 | } 100 | 101 | __ready () { 102 | 103 | this.initDebug (); 104 | 105 | autoUpdater.checkForUpdatesAndNotify (); 106 | 107 | this.load (); 108 | 109 | } 110 | 111 | /* API */ 112 | 113 | load () { 114 | 115 | this.win = new Main (); 116 | 117 | } 118 | 119 | } 120 | 121 | /* EXPORT */ 122 | 123 | export default App; 124 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/items/todo.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | import Utils from '../utils'; 6 | 7 | /* TODO */ 8 | 9 | const Todo = { 10 | 11 | boxSymbol: '☐', 12 | doneSymbol: '✔', 13 | cancelledSymbol: '✘', 14 | 15 | boxRe: /^[^\S\n]*((?!--|––|——)(?:[-❍❑■⬜□☐▪▫–—≡→›]|\[ ?\])\s[^\n@]*)/, 16 | doneRe: /^[^\S\n]*((?!--|––|——)(?:(?:[✔✓☑+]|\[[xX+]\])|\[ ?\])\s[^\n@]*)/, 17 | cancelledRe: /^[^\S\n]*((?!--|––|——)(?:(?:[✘xX]|\[-\])|\[ ?\])\s[^\n@]*)/, 18 | 19 | getTokens () { 20 | 21 | return { 22 | start: [ 23 | { sol: true, regex: Todo.boxRe, token: 'todo-box' }, 24 | { sol: true, regex: Todo.doneRe, token: 'todo-done' }, 25 | { sol: true, regex: Todo.cancelledRe, token: 'todo-cancel' } 26 | ] 27 | }; 28 | 29 | }, 30 | 31 | toggleToken ( cm, token, removeToken, insertToken ) { 32 | 33 | Utils.walkSelections ( cm, ( line, lineNr ) => { 34 | 35 | const tokenIndex = line.indexOf ( `${token} ` ), 36 | isPrevTokenEmpty = !_.trim ( line.slice ( 0, tokenIndex ) ).length, 37 | otherIndex = line.search ( `${Todo.boxSymbol}|${Todo.doneSymbol}|${Todo.cancelledSymbol} ` ), 38 | isPrevOtherEmpty = !_.trim ( line.slice ( 0, otherIndex ) ).length; 39 | 40 | if ( tokenIndex >= 0 && isPrevTokenEmpty ) { 41 | 42 | const replacement = removeToken ? `${removeToken} ` : ''; 43 | 44 | Utils.replace ( cm, lineNr, replacement, tokenIndex, tokenIndex + 2 ); 45 | 46 | } else if ( otherIndex >= 0 && isPrevOtherEmpty ) { 47 | 48 | Utils.replace ( cm, lineNr, `${token} `, otherIndex, otherIndex + 2 ); 49 | 50 | } else if ( insertToken ) { 51 | 52 | let spaceIndex = line.search ( /\S/ ); 53 | 54 | if ( spaceIndex === -1 ) spaceIndex = line.length; 55 | 56 | Utils.replace ( cm, lineNr, `${insertToken} `, spaceIndex ); 57 | 58 | } 59 | 60 | }); 61 | 62 | }, 63 | 64 | toggleBox ( cm ) { 65 | 66 | Todo.toggleToken ( cm, Todo.boxSymbol, '', Todo.boxSymbol ); 67 | 68 | }, 69 | 70 | toggleDone ( cm ) { 71 | 72 | Todo.toggleToken ( cm, Todo.doneSymbol, Todo.boxSymbol, Todo.doneSymbol ); 73 | 74 | }, 75 | 76 | toggleCancelled ( cm ) { 77 | 78 | Todo.toggleToken ( cm, Todo.cancelledSymbol, Todo.boxSymbol, Todo.cancelledSymbol ); 79 | 80 | } 81 | 82 | }; 83 | 84 | /* EXPORT */ 85 | 86 | export default Todo; 87 | -------------------------------------------------------------------------------- /src/renderer/components/main/extra/ipc.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {ipcRenderer as ipc} from 'electron'; 5 | import {connect} from 'overstated'; 6 | import {Component} from 'react-component-renderless'; 7 | import Main from '@renderer/containers/main'; 8 | 9 | /* IPC */ 10 | 11 | class IPC extends Component<{ container: IMain}, undefined> { 12 | 13 | /* SPECIAL */ 14 | 15 | componentDidMount () { 16 | 17 | ipc.on ( 'note-add', this.__noteAdd ); 18 | ipc.on ( 'note-rename', this.__noteRename ); 19 | ipc.on ( 'note-delete', this.__noteDelete ); 20 | ipc.on ( 'note-select-number', this.__noteSelectNumber ); 21 | ipc.on ( 'note-select-previous', this.__noteSelectPrevious ); 22 | ipc.on ( 'note-select-next', this.__noteSelectNext ); 23 | ipc.on ( 'window-focus', this.__windowFocus ); 24 | ipc.on ( 'window-blur', this.__windowBlur ); 25 | 26 | } 27 | 28 | componentWillUnmount () { 29 | 30 | ipc.removeListener ( 'note-add', this.__noteAdd ); 31 | ipc.removeListener ( 'note-rename', this.__noteRename ); 32 | ipc.removeListener ( 'note-delete', this.__noteDelete ); 33 | ipc.removeListener ( 'note-select-number', this.__noteSelectNumber ); 34 | ipc.removeListener ( 'note-select-previous', this.__noteSelectPrevious ); 35 | ipc.removeListener ( 'note-select-next', this.__noteSelectNext ); 36 | ipc.removeListener ( 'window-focus', this.__windowFocus ); 37 | ipc.removeListener ( 'window-blur', this.__windowBlur ); 38 | 39 | } 40 | 41 | /* HANDLERS */ 42 | 43 | __noteAdd = () => { 44 | 45 | this.props.container.note.add (); 46 | 47 | } 48 | 49 | __noteRename = () => { 50 | 51 | this.props.container.note.rename (); 52 | 53 | } 54 | 55 | __noteDelete = () => { 56 | 57 | this.props.container.note.delete (); 58 | 59 | } 60 | 61 | __noteSelectNumber = ( event, nr: number ) => { 62 | 63 | this.props.container.note.selectNumber ( nr ); 64 | 65 | } 66 | 67 | __noteSelectPrevious = () => { 68 | 69 | this.props.container.note.selectPrevious (); 70 | 71 | } 72 | 73 | __noteSelectNext = () => { 74 | 75 | this.props.container.note.selectNext (); 76 | 77 | } 78 | 79 | __windowFocus = () => { 80 | 81 | this.props.container.window.setFocus ( true ); 82 | 83 | } 84 | 85 | __windowBlur = () => { 86 | 87 | this.props.container.window.setFocus ( false ); 88 | 89 | } 90 | 91 | } 92 | 93 | /* EXPORT */ 94 | 95 | export default connect ({ 96 | container: Main, 97 | shouldComponentUpdate: false 98 | })( IPC ); 99 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as is from 'electron-is'; 5 | import {connect} from 'overstated'; 6 | import * as React from 'react'; 7 | import {UnControlled as CodeMirror} from 'react-codemirror2'; 8 | import Main from '@renderer/containers/main'; 9 | import Utils from './utils'; 10 | import Font from './items/font'; 11 | import Todo from './items/todo'; 12 | 13 | /* OPTIONS */ 14 | 15 | const CTMD = is.macOS () ? 'Cmd' : 'Ctrl', // `Cmd` on macOS, `Ctrl` otherwise 16 | ALMD = is.macOS () ? 'Cmd' : 'Alt'; // `Cmd` on macOS, `Alt` otherwise 17 | 18 | const options: any = { //TSC 19 | autofocus: true, 20 | electricChars: false, 21 | indentUnit: 2, 22 | indentWithTabs: false, 23 | lineNumbers: false, 24 | lineWrapping: true, 25 | mode: 'noty', 26 | scrollbarStyle: 'native', 27 | smartIndent: false, 28 | tabSize: 2, 29 | undoDepth: 1000, 30 | keyMap: 'sublime', 31 | viewportMargin: Infinity, 32 | extraKeys: { 33 | 'Backspace': 'delCharBefore', 34 | [`${CTMD}-Z`]: 'undo', 35 | [`${CTMD}-Shift-Z`]: 'redo', 36 | 'Tab': 'indentMore', 37 | 'Shift-Tab': 'indentLess', 38 | [`${CTMD}-F`]: 'findPersistent', 39 | [`${CTMD}-G`]: 'findPersistentNext', 40 | [`${CTMD}-Shift-G`]: 'findPersistentPrev', 41 | [`${CTMD}-Shift-H`]: 'replace', 42 | [`${CTMD}-Shift-Alt-H`]: 'replaceAll', 43 | 'Esc': 'clearSearch', 44 | [`${ALMD}-Ctrl-Up`]: 'swapLineUp', 45 | [`${ALMD}-Ctrl-Down`]: 'swapLineDown', 46 | 'Alt-LeftClick': Utils.addSelection, 47 | [`${CTMD}-Enter`]: Todo.toggleBox, 48 | 'Alt-D': Todo.toggleDone, 49 | 'Alt-C': Todo.toggleCancelled, 50 | [`${CTMD}-B`]: Font.toggleBold, 51 | [`${CTMD}-\``]: Font.toggleCode, 52 | [`${CTMD}-I`]: Font.toggleItalic, 53 | [`${CTMD}-S`]: Font.toggleStrikethrough, 54 | 'F2': false, 55 | [`${CTMD}-M`]: false, 56 | [`${CTMD}-H`]: false, 57 | [`${CTMD}-LeftClick`]: false 58 | } 59 | }; 60 | 61 | Utils.defineMode (); 62 | 63 | /* CODE */ 64 | 65 | class Code extends React.PureComponent { 66 | 67 | componentDidMount () { 68 | 69 | this.props.reset (); 70 | 71 | } 72 | 73 | componentDidUpdate () { 74 | 75 | this.props.reset (); 76 | 77 | } 78 | 79 | render () { 80 | 81 | const {value, onChange, onEditor, onScroll} = this.props; 82 | 83 | return ; 84 | 85 | } 86 | 87 | } 88 | 89 | /* EXPORT */ 90 | 91 | export default connect ({ 92 | container: Main, 93 | shouldComponentUpdate: 'id', 94 | selector: ({ container, value, onChange, onEditor }) => ({ 95 | value, onChange, onEditor, 96 | reset: container.editor.reset, 97 | onScroll: container.editor.onScroll 98 | }) 99 | })( Code ); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning**: This app is now deprecated, you should use [Notable](https://notable.md) instead, which is a much better note-taking app overall, its zen mode can replace Noty's UI pretty effectively and it is very actively developed. 2 | 3 | # Noty ([DOWNLOAD](https://github.com/fabiospampinato/noty/releases)) 4 | 5 |

6 | Logo 7 |

8 | 9 | Autosaving sticky note with support for multiple notes without needing multiple windows. 10 | 11 | ## Features 12 | 13 | - Supports multiple notes without needing multiple windows. 14 | - Auto-saves your notes. 15 | - To-Do functionalities built-in. 16 | - Links support. 17 | - Bold/code/italic/strikethrough support. 18 | - Multiple cursors. 19 | - Find and Replace. 20 | - Programmers shortcuts. 21 | - Uses the [FiraCode](https://github.com/tonsky/FiraCode) font. 22 | 23 | ## Shortcuts 24 | 25 | > **Note:** The following are macOS shortcuts, if you're using a different OS replace Cmd with Ctrl, or Alt if Ctrl is already used. 26 | 27 | - Cmd+N - Create a new note. 28 | - F2 - Rename the current note. 29 | - Cmd+Alt+Backspace - Delete the current note. 30 | - Tab - Indent current line. 31 | - Shift+Tab - Outdent current line. 32 | - Cmd+F - Find. 33 | - Cmd+G - Find next. 34 | - Cmd+Shift+G - Find previous. 35 | - Cmd+Shift+H - Replace. 36 | - Cmd+Shift+Alt+H - Replace all. 37 | - Cmd+Ctrl+Up - Move current line up. 38 | - Cmd+Ctrl+Down - Move current line down. 39 | - Alt+Click - Add a new cursor. 40 | - Cmd+Click - Open the clicked link. 41 | - Cmd+Enter - Toggle a todo's box symbol. 42 | - Alt+D - Toggle a todo's done symbol. 43 | - Alt+C - Toggle a todo's cancelled symbol. 44 | - Cmd+B - Toggle bold. 45 | - Cmd+` - Toggle code. 46 | - Cmd+I - Toggle italic. 47 | - Cmd+S - Toggle strikethrough. 48 | - Cmd+1/9 - Select the 1st/9th note. 49 | - Cmd+Alt+Right - Select the next note. 50 | - Ctrl+Tab - Select the next note. 51 | - Cmd+Alt+Left - Select the previous note. 52 | - Ctrl+Shift+Tab - Select the previous note. 53 | 54 | ## Demo 55 | 56 | Switching note: 57 | 58 | ![Switching note](resources/demo/switching.gif) 59 | 60 | New note and rename: 61 | 62 | ![New note and rename](resources/demo/creation.gif) 63 | 64 | ## Contributing 65 | 66 | If you have an idea, or found an problem, please open an [issue](https://github.com/fabiospampinato/noty/issues) about it. 67 | 68 | If you want to make a pull request, or fork the app, you should: 69 | 70 | ```bash 71 | git clone https://github.com/fabiospampinato/noty.git 72 | cd noty 73 | npm install 74 | npm run dev 75 | ``` 76 | 77 | ## Related 78 | 79 | - **[vscode-todo-plus](https://marketplace.visualstudio.com/items?itemName=fabiospampinato.vscode-todo-plus)**: Visual Studio Code extension that implements the same To-Do functionalities, and much more. 80 | - **[Notable](https://github.com/fabiospampinato/notable)**: The markdown-based note-taking app that doesn't suck. 81 | 82 | ## License 83 | 84 | MIT © Fabio Spampinato 85 | -------------------------------------------------------------------------------- /src/main/windows/window.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | import * as path from 'path'; 6 | import {BrowserWindow} from 'electron'; 7 | import * as is from 'electron-is'; 8 | import * as windowStateKeeper from 'electron-window-state'; 9 | import pkg from '@root/package.json'; 10 | import Environment from '@common/environment'; 11 | 12 | /* WINDOW */ 13 | 14 | class Window { 15 | 16 | /* VARIABLES */ 17 | 18 | name: string; 19 | win: BrowserWindow; 20 | options: object; 21 | stateOptions: object; 22 | 23 | /* CONSTRUCTOR */ 24 | 25 | constructor ( name, options = {}, stateOptions = {} ) { 26 | 27 | this.name = name; 28 | this.options = options; 29 | this.stateOptions = stateOptions; 30 | 31 | this.init (); 32 | this.events (); 33 | 34 | } 35 | 36 | /* SPECIAL */ 37 | 38 | init () { 39 | 40 | this.initWindow (); 41 | this.initDebug (); 42 | this.initLocalShortcuts (); 43 | this.initMenu (); 44 | this.load (); 45 | 46 | } 47 | 48 | initWindow () { 49 | 50 | this.win = this.make (); 51 | 52 | } 53 | 54 | initDebug () { 55 | 56 | if ( !Environment.isDevelopment ) return; 57 | 58 | this.win.webContents.openDevTools (); 59 | 60 | this.win.webContents.on ( 'devtools-opened', () => { 61 | 62 | this.win.focus (); 63 | 64 | setImmediate ( () => this.win.focus () ); 65 | 66 | }); 67 | 68 | } 69 | 70 | initMenu () {} 71 | 72 | initLocalShortcuts () {} 73 | 74 | events () { 75 | 76 | this.___readyToShow (); 77 | this.___closed (); 78 | this.___focused (); 79 | 80 | } 81 | 82 | /* READY TO SHOW */ 83 | 84 | ___readyToShow () { 85 | 86 | this.win.on ( 'ready-to-show', this.__readyToShow.bind ( this ) ); 87 | 88 | } 89 | 90 | __readyToShow () { 91 | 92 | this.win.show (); 93 | this.win.focus (); 94 | 95 | } 96 | 97 | /* CLOSED */ 98 | 99 | ___closed () { 100 | 101 | this.win.on ( 'closed', this.__closed.bind ( this ) ); 102 | 103 | } 104 | 105 | __closed () { 106 | 107 | delete this.win; 108 | 109 | } 110 | 111 | /* FOCUSED */ 112 | 113 | ___focused () { 114 | 115 | this.win.on ( 'focus', this.__focused.bind ( this ) ); 116 | 117 | } 118 | 119 | __focused () { 120 | 121 | this.initMenu (); 122 | 123 | } 124 | 125 | /* API */ 126 | 127 | make ( id = this.name, options = this.options, stateOptions = this.stateOptions ) { 128 | 129 | stateOptions = _.merge ({ 130 | file: `${id}.json`, 131 | defaultWidth: 600, 132 | defaultHeight: 600 133 | }, stateOptions ); 134 | 135 | const state = windowStateKeeper ( stateOptions ), 136 | dimensions = _.pick ( state, ['x', 'y', 'width', 'height'] ); 137 | 138 | options = _.merge ( dimensions, { 139 | frame: !is.macOS (), 140 | backgroundColor: '#fef3a1', 141 | icon: path.join ( __static, 'images', `icon.${is.windows () ? 'ico' : 'png'}` ), 142 | show: false, 143 | title: pkg.productName, 144 | webPreferences: { 145 | webSecurity: false 146 | } 147 | }, options ); 148 | 149 | const win = new BrowserWindow ( options ); 150 | 151 | state.manage ( win ); 152 | 153 | return win; 154 | 155 | } 156 | 157 | load () {} 158 | 159 | } 160 | 161 | /* EXPORT */ 162 | 163 | export default Window; 164 | -------------------------------------------------------------------------------- /src/renderer/template/codemirror.scss: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: "FiraCode"; 4 | src: url("~@static/fonts/FiraCode-Regular.woff2") format("woff2"); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | .react-codemirror2 { 10 | 11 | height: 100%; 12 | box-sizing: border-box; 13 | 14 | .CodeMirror { 15 | 16 | background: transparent; 17 | display: flex; 18 | flex-direction: column; 19 | height: 100%; 20 | line-height: 1.25; 21 | font-size: $code-font-size; 22 | font-family: "FiraCode"; 23 | color: $color-text; 24 | -webkit-overflow-scrolling: touch; 25 | padding: 0 $gutter; 26 | 27 | pre { 28 | padding: 0; 29 | z-index: 1; 30 | text-rendering: optimizeLegibility; 31 | font-variant-ligatures: contextual; 32 | } 33 | 34 | .CodeMirror-scroll { 35 | padding: $gutter-half 0; 36 | overflow-x: hidden !important; 37 | overflow-y: visible !important; 38 | } 39 | 40 | .CodeMirror-crosshair { 41 | cursor: text; 42 | } 43 | 44 | .CodeMirror-selected { 45 | background: $color-code-selected; 46 | } 47 | 48 | .CodeMirror-cursor { 49 | border-left: 1px solid $color-code-cursor; 50 | } 51 | 52 | .CodeMirror-lines { 53 | padding: 0; 54 | } 55 | 56 | .CodeMirror-line span { 57 | max-width: 100%; 58 | padding-right: 0; 59 | } 60 | 61 | .CodeMirror-dialog { 62 | 63 | display: flex; 64 | align-items: center; 65 | position: fixed; 66 | top: $titlebar-height; 67 | left: 0; 68 | right: 0; 69 | background: $color-code-dialog; 70 | padding: $gutter-half $gutter; 71 | border-bottom-width: 0; 72 | font-family: sans-serif; 73 | transition: box-shadow $shadow-animation-duration; 74 | 75 | #app-wrapper.scrolled & { 76 | box-shadow: 0 2px 3px 0 $shadow-color; 77 | } 78 | 79 | ~ .CodeMirror-scroll { 80 | padding-top: $code-dialog-height + $gutter-half; 81 | } 82 | 83 | } 84 | 85 | .CodeMirror-search-label { 86 | white-space: nowrap; 87 | padding-right: $gutter; 88 | } 89 | 90 | .CodeMirror-search-field { 91 | width: 100% !important; 92 | font-size: $code-dialog-font-size; 93 | font-family: inherit; 94 | } 95 | 96 | .CodeMirror-search-hint { 97 | display: none; 98 | } 99 | 100 | .cm-searching { 101 | background: $color-code-match !important; 102 | } 103 | 104 | .cm-link { 105 | 106 | color: $color-link; 107 | 108 | html.meta & { // Meta key pressed 109 | 110 | &:hover { 111 | cursor: pointer; 112 | color: $color-link-hover; 113 | } 114 | 115 | &:active { 116 | color: $color-link-active; 117 | } 118 | 119 | } 120 | 121 | } 122 | 123 | .cm-bold { 124 | font-weight: bold; 125 | } 126 | 127 | .cm-code { 128 | color: $color-code; 129 | font-family: monospace; 130 | } 131 | 132 | .cm-italic { 133 | font-style: italic; 134 | } 135 | 136 | .cm-strikethrough { 137 | text-decoration: line-through; 138 | } 139 | 140 | .cm-font-token { 141 | opacity: $font-token-opacity; 142 | } 143 | 144 | .cm-project { 145 | color: $color-project; 146 | } 147 | 148 | .cm-todo-done { 149 | color: $color-todo-done; 150 | } 151 | 152 | .cm-todo-cancel { 153 | color: $color-todo-cancel; 154 | } 155 | 156 | .cm-tag { 157 | color: $color-tag; 158 | } 159 | 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/renderer/containers/main/note.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | import Dialog from 'electron-dialog'; 6 | import {Container} from 'overstated'; 7 | import pkg from '@root/package.json'; 8 | import Settings from '@common/settings'; 9 | 10 | /* NOTE */ 11 | 12 | class Note extends Container { 13 | 14 | /* STATE */ 15 | 16 | state = { 17 | note: Settings.get ( 'notes' ).find ( note => note.title === Settings.get ( 'note' ) ) //FIXME: Ugly and slow 18 | }; 19 | 20 | /* API */ 21 | 22 | get = ( title?: string ): NoteObj | undefined => { 23 | 24 | if ( !title ) return this.state.note; 25 | 26 | const notes = this.ctx.notes.get (); 27 | 28 | return notes.find ( note => note.title === title ); 29 | 30 | } 31 | 32 | getIndex = ( note: NoteObj | undefined = this.state.note ) => { 33 | 34 | const notes = this.ctx.notes.get (); 35 | 36 | return notes.findIndex ( n => n === note || n.title === note.title ); 37 | 38 | } 39 | 40 | add = async ( title?: string, content: string = '' ) => { 41 | 42 | title = title || await this.ctx.editor.dialog ( 'Note name:' ); 43 | 44 | if ( !title ) return; 45 | 46 | if ( this.get ( title ) ) return Dialog.alert ( 'Note names must be unique' ); //TODO: Use a notification dialog instead 47 | 48 | const notes = _.clone ( this.ctx.notes.get () ), 49 | note = { title, content }; 50 | 51 | notes.push ( note ); 52 | 53 | this.ctx.notes.set ( notes ); 54 | this.set ( title ); 55 | 56 | } 57 | 58 | rename = async ( note: NoteObj | undefined = this.state.note ) => { 59 | 60 | if ( !note ) return; 61 | 62 | const title = await this.ctx.editor.dialog ( 'Note name:', { value: note.title } ); 63 | 64 | if ( !title || note.title === title ) return; 65 | 66 | if ( this.get ( title ) ) return Dialog.alert ( 'Note names must be unique' ); //TODO: Use a notification dialog instead 67 | 68 | const noteNext = _.clone ( note ); 69 | 70 | noteNext.title = title; 71 | 72 | return this.replace ( note, noteNext ); 73 | 74 | } 75 | 76 | delete = ( note: NoteObj | undefined = this.state.note ) => { 77 | 78 | if ( !note ) return; 79 | 80 | if ( !Dialog.confirm ( `Are you sure you want to delete "${note.title}"?` ) ) return; //TODO: Use a confirmation dialog instead 81 | 82 | const notes = this.ctx.notes.get (), 83 | notesNext = notes.filter ( n => n.title !== note.title ); 84 | 85 | this.ctx.notes.set ( notesNext ); 86 | 87 | if ( !notesNext.length ) { 88 | 89 | this.add ( pkg.productName ); 90 | 91 | } else if ( !this.get ( note.title ) ) { 92 | 93 | this.set ( notesNext[0].title ); 94 | 95 | } 96 | 97 | } 98 | 99 | save = ( note: NoteObj | undefined = this.state.note, content: string ) => { 100 | 101 | if ( !note ) return; 102 | 103 | if ( note !== this.get () ) return; //Ugly: we only switched note 104 | 105 | const noteNext = _.clone ( note ); 106 | 107 | noteNext.content = content; 108 | 109 | return this.replace ( note, noteNext ); 110 | 111 | } 112 | 113 | replace = ( note: NoteObj, noteNext: NoteObj ) => { 114 | 115 | const notes = this.ctx.notes.get (), 116 | notesNext = notes.map ( n => n.title === note.title ? noteNext : n ); 117 | 118 | this.ctx.notes.set ( notesNext ); 119 | 120 | if ( note.title === noteNext.title ) return; 121 | 122 | this.set ( noteNext.title ); 123 | 124 | } 125 | 126 | set = ( title: string ) => { 127 | 128 | const note = this.get ( title ); 129 | 130 | if ( !note ) return; 131 | 132 | Settings.set ( 'note', title ); 133 | 134 | return this.setState ({ note }); 135 | 136 | } 137 | 138 | selectNumber = ( nr: number ) => { 139 | 140 | const notes = this.ctx.notes.get (), 141 | note = notes[nr - 1]; 142 | 143 | if ( !note ) return; 144 | 145 | return this.set ( note.title ); 146 | 147 | } 148 | 149 | selectNavigate = ( modifier: number ) => { 150 | 151 | const notes = this.ctx.notes.get (), 152 | note = this.get (); 153 | 154 | if ( !note ) return; 155 | 156 | const minNr = 0, 157 | maxNr = notes.length - 1, 158 | currNr = this.getIndex ( note ); 159 | 160 | let nextNr = currNr + modifier; 161 | 162 | if ( nextNr > maxNr ) nextNr = minNr; 163 | if ( nextNr < minNr ) nextNr = maxNr; 164 | 165 | return this.selectNumber ( nextNr + 1 ); 166 | 167 | } 168 | 169 | selectPrevious = () => { 170 | 171 | return this.selectNavigate ( -1 ); 172 | 173 | } 174 | 175 | selectNext = () => { 176 | 177 | return this.selectNavigate ( 1 ); 178 | 179 | } 180 | 181 | } 182 | 183 | /* EXPORT */ 184 | 185 | export default Note; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noty", 3 | "productName": "Noty", 4 | "description": "Autosaving sticky note with support for multiple notes without needing multiple windows.", 5 | "version": "2.2.0", 6 | "scripts": { 7 | "clean:deps": "npx del 'node_modules/**/{README,LICENSE,license,.travis.yml,tsconfig.json,*.{md,MD,map,png,svg,ts}}' '!node_modules/**/*.d.ts'", 8 | "clean:dist": "npx del dist", 9 | "clean:releases": "npx del releases", 10 | "clean": "npm run clean:dist && npm run clean:releases", 11 | "compile": "npm run clean:deps && electron-webpack app --env.minify=false", 12 | "build:mac": "npm run compile && electron-builder --mac", 13 | "build:win": "npm run compile && electron-builder --win", 14 | "build:linux": "npm run compile && electron-builder --linux", 15 | "build:all": "npm run clean:releases && npm run compile && electron-builder -mwl", 16 | "dev": "electron-webpack dev", 17 | "prod": "npm run compile && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dir && open releases/mac/*.app" 18 | }, 19 | "electronWebpack": { 20 | "staticSourceDirectory": "src/renderer/static", 21 | "main": { 22 | "webpackConfig": "webpack.js" 23 | }, 24 | "renderer": { 25 | "webpackConfig": "webpack.js" 26 | } 27 | }, 28 | "build": { 29 | "appId": "com.fabiospampinato.noty", 30 | "copyright": "Copyright © 2017-present Fabio Spampinato", 31 | "directories": { 32 | "output": "releases" 33 | }, 34 | "mac": { 35 | "target": [ 36 | "dmg", 37 | "pkg", 38 | "zip" 39 | ], 40 | "category": "public.app-category.utilities", 41 | "icon": "resources/icon/icon.png", 42 | "type": "distribution" 43 | }, 44 | "dmg": { 45 | "background": "resources/dmg_background/background.png", 46 | "iconSize": 160, 47 | "iconTextSize": 12, 48 | "window": { 49 | "width": 660, 50 | "height": 400 51 | }, 52 | "contents": [ 53 | { 54 | "x": 180, 55 | "y": 170, 56 | "type": "file" 57 | }, 58 | { 59 | "x": 480, 60 | "y": 170, 61 | "type": "link", 62 | "path": "/Applications" 63 | } 64 | ] 65 | }, 66 | "pkg": { 67 | "license": "LICENSE" 68 | }, 69 | "win": { 70 | "target": [ 71 | "nsis", 72 | "portable", 73 | "zip" 74 | ], 75 | "icon": "resources/icon/icon.ico" 76 | }, 77 | "nsis": { 78 | "installerIcon": "resources/icon/icon.ico", 79 | "license": "LICENSE", 80 | "warningsAsErrors": false 81 | }, 82 | "linux": { 83 | "target": [ 84 | "AppImage", 85 | "deb", 86 | "rpm", 87 | "snap" 88 | ], 89 | "icon": "resources/icon", 90 | "category": "Utility" 91 | }, 92 | "snap": { 93 | "grade": "stable", 94 | "summary": "Autosaving sticky note with support for multiple notes without needing multiple windows." 95 | }, 96 | "publish": { 97 | "provider": "github", 98 | "owner": "fabiospampinato", 99 | "releaseType": "release", 100 | "publishAutoUpdate": true 101 | } 102 | }, 103 | "license": "MIT", 104 | "author": { 105 | "name": "Fabio Spampinato", 106 | "email": "spampinabio@gmail.com" 107 | }, 108 | "homepage": "https://github.com/fabiospampinato/noty", 109 | "repository": { 110 | "type": "git", 111 | "url": "https://github.com/fabiospampinato/noty.git" 112 | }, 113 | "bugs": { 114 | "url": "https://github.com/fabiospampinato/noty/issues" 115 | }, 116 | "keywords": [ 117 | "electron", 118 | "react", 119 | "webpack", 120 | "codemirror", 121 | "sticky", 122 | "note" 123 | ], 124 | "dependencies": { 125 | "cash-dom": "^2.3.5", 126 | "codemirror": "^5.40.0", 127 | "conf-merge": "^1.0.0", 128 | "electron-context-menu": "^0.10.0", 129 | "electron-dialog": "^1.0.0", 130 | "electron-is": "^3.0.0", 131 | "electron-localshortcut": "^3.1.0", 132 | "electron-store": "^2.0.0", 133 | "electron-updater": "^4.0.6", 134 | "electron-window-state": "^4.1.1", 135 | "lodash": "^4.17.10", 136 | "overstated": "^1.1.2", 137 | "react": "^16.4.2", 138 | "react-codemirror2": "^5.1.0", 139 | "react-component-identity": "^1.0.1", 140 | "react-component-renderless": "^1.0.2", 141 | "react-dom": "^16.4.2", 142 | "react-router-static": "^1.0.0" 143 | }, 144 | "devDependencies": { 145 | "@babel/preset-react": "^7.0.0", 146 | "@types/codemirror": "0.0.60", 147 | "@types/lodash": "^4.14.116", 148 | "@types/react": "^16.4.13", 149 | "@types/react-dom": "^16.0.7", 150 | "del-cli": "^1.1.0", 151 | "electron": "3.0.0", 152 | "electron-builder": "^20.38.4", 153 | "electron-builder-squirrel-windows": "^20.38.3", 154 | "electron-devtools-installer": "^2.2.4", 155 | "electron-webpack": "git://github.com/fabiospampinato/electron-webpack.git#package-electron-webpack", 156 | "electron-webpack-ts": "^3.1.0", 157 | "node-sass": "^4.11.0", 158 | "react-hot-loader": "^4.3.5", 159 | "sass-loader": "^7.1.0", 160 | "terser-webpack-plugin": "^1.2.0", 161 | "tsconfig-paths-webpack-plugin": "^3.2.0", 162 | "typescript": "^3.0.3", 163 | "webpack": "^4.17.1" 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/renderer/components/main/code/addons/dialog.js: -------------------------------------------------------------------------------- 1 | 2 | //FIXME: Super ugly override, we shouldn't need to do this just to inject the dialog before the editor 3 | 4 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 5 | // Distributed under an MIT license: http://codemirror.net/LICENSE 6 | 7 | // Open simple dialogs on top of an editor. Relies on dialog.css. 8 | 9 | (function(mod) { 10 | if (typeof exports == "object" && typeof module == "object") // CommonJS 11 | mod(require("codemirror/lib/codemirror")); 12 | else if (typeof define == "function" && define.amd) // AMD 13 | define(["codemirror/lib/codemirror"], mod); 14 | else // Plain browser env 15 | mod(CodeMirror); 16 | })(function(CodeMirror) { 17 | function dialogDiv(cm, template, bottom) { 18 | var wrap = cm.getWrapperElement(); 19 | var dialog; 20 | // dialog = wrap.appendChild(document.createElement("div")); 21 | dialog = wrap.insertBefore(document.createElement("div"),wrap.firstChild); 22 | if (bottom) 23 | dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; 24 | else 25 | dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; 26 | 27 | if (typeof template == "string") { 28 | dialog.innerHTML = template; 29 | } else { // Assuming it's a detached DOM element. 30 | dialog.appendChild(template); 31 | } 32 | return dialog; 33 | } 34 | 35 | function closeNotification(cm, newVal) { 36 | if (cm.state.currentNotificationClose) 37 | cm.state.currentNotificationClose(); 38 | cm.state.currentNotificationClose = newVal; 39 | } 40 | 41 | CodeMirror.defineExtension("openDialog", function(template, callback, options) { 42 | if (!options) options = {}; 43 | 44 | closeNotification(this, null); 45 | 46 | var dialog = dialogDiv(this, template, options.bottom); 47 | var closed = false, me = this; 48 | function close(newVal) { 49 | if (typeof newVal == 'string') { 50 | inp.value = newVal; 51 | } else { 52 | if (closed) return; 53 | closed = true; 54 | dialog.parentNode.removeChild(dialog); 55 | me.focus(); 56 | 57 | if (options.onClose) options.onClose(dialog); 58 | } 59 | } 60 | 61 | var inp = dialog.getElementsByTagName("input")[0], button; 62 | if (inp) { 63 | inp.focus(); 64 | 65 | if (options.value) { 66 | inp.value = options.value; 67 | if (options.selectValueOnOpen !== false) { 68 | inp.select(); 69 | } 70 | } 71 | 72 | if (options.onInput) 73 | CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); 74 | if (options.onKeyUp) 75 | CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); 76 | 77 | CodeMirror.on(inp, "keydown", function(e) { 78 | if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } 79 | if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { 80 | inp.blur(); 81 | CodeMirror.e_stop(e); 82 | close(); 83 | } 84 | if (e.keyCode == 13) return callback(inp.value, e); 85 | if (e.keyCode == 27) return callback(undefined, e); 86 | }); 87 | 88 | if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); 89 | } else if (button = dialog.getElementsByTagName("button")[0]) { 90 | CodeMirror.on(button, "click", function() { 91 | close(); 92 | me.focus(); 93 | }); 94 | 95 | if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); 96 | 97 | button.focus(); 98 | } 99 | return close; 100 | }); 101 | 102 | CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { 103 | closeNotification(this, null); 104 | var dialog = dialogDiv(this, template, options && options.bottom); 105 | var buttons = dialog.getElementsByTagName("button"); 106 | var closed = false, me = this, blurring = 1; 107 | function close() { 108 | if (closed) return; 109 | closed = true; 110 | dialog.parentNode.removeChild(dialog); 111 | me.focus(); 112 | } 113 | buttons[0].focus(); 114 | for (var i = 0; i < buttons.length; ++i) { 115 | var b = buttons[i]; 116 | (function(callback) { 117 | CodeMirror.on(b, "click", function(e) { 118 | CodeMirror.e_preventDefault(e); 119 | close(); 120 | if (callback) callback(me); 121 | }); 122 | })(callbacks[i]); 123 | CodeMirror.on(b, "blur", function() { 124 | --blurring; 125 | setTimeout(function() { if (blurring <= 0) close(); }, 200); 126 | }); 127 | CodeMirror.on(b, "focus", function() { ++blurring; }); 128 | } 129 | }); 130 | 131 | /* 132 | * openNotification 133 | * Opens a notification, that can be closed with an optional timer 134 | * (default 5000ms timer) and always closes on click. 135 | * 136 | * If a notification is opened while another is opened, it will close the 137 | * currently opened one and open the new one immediately. 138 | */ 139 | CodeMirror.defineExtension("openNotification", function(template, options) { 140 | closeNotification(this, close); 141 | var dialog = dialogDiv(this, template, options && options.bottom); 142 | var closed = false, doneTimer; 143 | var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; 144 | 145 | function close() { 146 | if (closed) return; 147 | closed = true; 148 | clearTimeout(doneTimer); 149 | dialog.parentNode.removeChild(dialog); 150 | } 151 | 152 | CodeMirror.on(dialog, 'click', function(e) { 153 | CodeMirror.e_preventDefault(e); 154 | close(); 155 | }); 156 | 157 | if (duration) 158 | doneTimer = setTimeout(close, duration); 159 | 160 | return close; 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/main/windows/main.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import * as _ from 'lodash'; 5 | import {Menu, MenuItemConstructorOptions, shell} from 'electron'; 6 | import * as is from 'electron-is'; 7 | import * as localShortcut from 'electron-localshortcut'; 8 | import Environment from '@common/environment'; 9 | import Settings from '@common/settings'; 10 | import pkg from '@root/package.json'; 11 | import UMenu from '@main/utils/menu'; 12 | import About from './about'; 13 | import Route from './route'; 14 | 15 | /* MAIN */ 16 | 17 | class Main extends Route { 18 | 19 | /* CONSTRUCTOR */ 20 | 21 | constructor ( name = 'main', options = { minWidth: 150, minHeight: 100 }, stateOptions = { defaultWidth: 250, defaultHeight: 450 } ) { 22 | 23 | super ( name, options, stateOptions ); 24 | 25 | } 26 | 27 | /* SPECIAL */ 28 | 29 | events () { 30 | 31 | super.events (); 32 | 33 | this.___focus (); 34 | this.___blur (); 35 | 36 | } 37 | 38 | /* FOCUS */ 39 | 40 | ___focus () { 41 | 42 | this.win.on ( 'focus', this.__focus.bind ( this ) ); 43 | 44 | } 45 | 46 | __focus () { 47 | 48 | this.win.webContents.send ( 'window-focus' ); 49 | 50 | } 51 | 52 | /* BLUR */ 53 | 54 | ___blur () { 55 | 56 | this.win.on ( 'blur', this.__blur.bind ( this ) ); 57 | 58 | } 59 | 60 | __blur () { 61 | 62 | this.win.webContents.send ( 'window-blur' ); 63 | 64 | } 65 | 66 | /* SPECIAL */ 67 | 68 | initLocalShortcuts () { 69 | 70 | /* CmdOrCtrl + 1-9 */ 71 | 72 | _.range ( 1, 10 ).forEach ( nr => { 73 | localShortcut.register ( this.win, `CmdOrCtrl+${nr}`, () => { 74 | this.win.webContents.send ( 'note-select-number', nr ); 75 | }); 76 | }); 77 | 78 | } 79 | 80 | initMenu () { 81 | 82 | const template: MenuItemConstructorOptions[] = UMenu.filterTemplate ([ 83 | { 84 | label: pkg.productName, 85 | submenu: [ 86 | { 87 | label: `About ${pkg.productName}`, 88 | click: () => new About () 89 | }, 90 | { 91 | type: 'separator' 92 | }, 93 | { 94 | role: 'services', 95 | submenu: [] , 96 | visible: is.macOS () 97 | }, 98 | { 99 | type: 'separator', 100 | visible: is.macOS () 101 | }, 102 | { 103 | role: 'hide', 104 | visible: is.macOS () 105 | }, 106 | { 107 | role: 'hideothers', 108 | visible: is.macOS () 109 | }, 110 | { 111 | role: 'unhide', 112 | visible: is.macOS () 113 | }, 114 | { 115 | type: 'separator', 116 | visible: is.macOS () 117 | }, 118 | { role: 'quit' } 119 | ] 120 | }, 121 | { 122 | label: 'Note', 123 | submenu: [ 124 | { 125 | label: 'New', 126 | accelerator: 'CmdOrCtrl+N', 127 | click: () => this.win.webContents.send ( 'note-add' ) 128 | }, 129 | { 130 | label: 'Rename', 131 | accelerator: 'f2', 132 | click: () => this.win.webContents.send ( 'note-rename' ) 133 | }, 134 | { 135 | label: 'Delete', 136 | accelerator: 'CmdOrCtrl+Alt+Backspace', 137 | click: () => this.win.webContents.send ( 'note-delete' ) 138 | }, 139 | { type: 'separator' }, 140 | { 141 | label: 'Open Configuration', 142 | click: () => Settings.openInEditor () 143 | } 144 | ] 145 | }, 146 | { 147 | label: 'Edit', 148 | submenu: [ 149 | // { role: 'undo' }, 150 | // { role: 'redo' }, 151 | // { type: 'separator' }, 152 | { role: 'cut' }, 153 | { role: 'copy' }, 154 | { role: 'paste' }, 155 | { role: 'pasteandmatchstyle' }, 156 | { role: 'delete' }, 157 | { role: 'selectall' }, 158 | { 159 | type: 'separator', 160 | visible: is.macOS () 161 | }, 162 | { 163 | label: 'Speech', 164 | submenu: [ 165 | { role: 'startspeaking' }, 166 | { role: 'stopspeaking' } 167 | ], 168 | visible: is.macOS () 169 | } 170 | ] 171 | }, 172 | { 173 | label: 'View', 174 | submenu: [ 175 | { 176 | role: 'reload', 177 | visible: Environment.isDevelopment 178 | }, 179 | { 180 | role: 'forcereload', 181 | visible: Environment.isDevelopment 182 | }, 183 | { 184 | role: 'toggledevtools', 185 | visible: Environment.isDevelopment 186 | }, 187 | { 188 | type: 'separator', 189 | visible: Environment.isDevelopment 190 | }, 191 | { role: 'resetzoom' }, 192 | { role: 'zoomin' }, 193 | { role: 'zoomout' }, 194 | { type: 'separator' }, 195 | { role: 'togglefullscreen' } 196 | ] 197 | }, 198 | { 199 | role: 'window', 200 | submenu: [ 201 | { role: 'close' }, 202 | { role: 'minimize' }, 203 | { 204 | role: 'zoom', 205 | visible: is.macOS () 206 | }, 207 | { type: 'separator' }, 208 | { 209 | label: 'Select Previous Note', 210 | accelerator: 'CmdOrCtrl+Alt+Left', 211 | click: () => this.win.webContents.send ( 'note-select-previous' ) 212 | }, 213 | { 214 | label: 'Select Previous Note', 215 | accelerator: 'Shift+Ctrl+Tab', 216 | click: () => this.win.webContents.send ( 'note-select-previous' ) 217 | }, 218 | { 219 | label: 'Select Next Note', 220 | accelerator: 'CmdOrCtrl+Alt+Right', 221 | click: () => this.win.webContents.send ( 'note-select-next' ) 222 | }, 223 | { 224 | label: 'Select Next Note', 225 | accelerator: 'Ctrl+Tab', 226 | click: () => this.win.webContents.send ( 'note-select-next' ) 227 | }, 228 | { type: 'separator' }, 229 | { 230 | type: 'checkbox', 231 | label: 'Float on Top', 232 | checked: !!this.win && this.win.isAlwaysOnTop (), 233 | click: () => this.win.setAlwaysOnTop ( !this.win.isAlwaysOnTop () ) 234 | }, 235 | { 236 | type: 'separator', 237 | visible: is.macOS () 238 | }, 239 | { 240 | role: 'front', 241 | visible: is.macOS () 242 | } 243 | ] 244 | }, 245 | { 246 | role: 'help', 247 | submenu: [ 248 | { 249 | label: 'Learn More', 250 | click: () => shell.openExternal ( pkg.homepage ) 251 | }, 252 | { 253 | label: 'Support', 254 | click: () => shell.openExternal ( pkg.bugs.url ) 255 | }, 256 | { type: 'separator' }, 257 | { 258 | label: 'View Changelog', 259 | click: () => shell.openExternal ( `${pkg.homepage}/blob/master/CHANGELOG.md` ) 260 | }, 261 | { 262 | label: 'View License', 263 | click: () => shell.openExternal ( `${pkg.homepage}/blob/master/LICENSE` ) 264 | } 265 | ] 266 | } 267 | ]); 268 | 269 | const menu = Menu.buildFromTemplate ( template ); 270 | 271 | Menu.setApplicationMenu ( menu ); 272 | 273 | } 274 | 275 | } 276 | 277 | /* EXPORT */ 278 | 279 | export default Main; 280 | --------------------------------------------------------------------------------