Repository: zonetti/zonote
Branch: master
Commit: 706be61b3679
Files: 16
Total size: 49.1 KB
Directory structure:
gitextract_2n15ldnx/
├── .github/
│ └── workflows/
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── build/
│ └── icon.icns
├── package.json
├── src/
│ ├── main/
│ │ ├── formatter.js
│ │ ├── main.js
│ │ └── state_manager.js
│ └── renderer/
│ ├── notes.js
│ ├── script.js
│ ├── tabs.js
│ ├── top_menu.js
│ └── util.js
└── static/
├── index.html
└── style.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: Release binaries
on:
release:
types: [published]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macOS-latest]
include:
- os: windows-latest
zip_name: zonote-win
- os: ubuntu-latest
zip_name: zonote-linux
- os: macOS-latest
zip_name: zonote-mac
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm i
- name: Build Windows
if: matrix.os == 'windows-latest'
shell: bash
run: npm run build:windows
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
shell: bash
run: npm run build:linux
- name: Build Mac
if: matrix.os == 'macOS-latest'
shell: bash
run: npm run build:mac
- name: Zip
shell: bash
env:
ZIP_NAME: ${{ matrix.zip_name }}
run: |
cd dist
7z a ../${ZIP_NAME}.zip .
- name: Upload release asset
if: github.event.action == 'published'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./${{ matrix.zip_name }}.zip
asset_name: ${{ matrix.zip_name }}.zip
asset_content_type: application/zip
================================================
FILE: .gitignore
================================================
node_modules
dist
================================================
FILE: LICENSE
================================================
Copyright (c) 2020 Osvaldo Zonetti
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
Cross-platform desktop note-taking app
<br>
Sticky notes + Markdown + Tabs
<br>
All in one .txt file
<br>
<br>
<img src="https://github.com/zonetti/zonote/raw/master/preview.gif" width="95%">
</p>
### Download
You can find the latest version to download on the [release page](https://github.com/zonetti/zonote/releases/latest).
### Development
```
git clone git@github.com:zonetti/zonote.git
cd zonote
npm install && npm start
```
================================================
FILE: package.json
================================================
{
"name": "zonote",
"version": "0.4.4",
"main": "src/main/main.js",
"scripts": {
"start": "electron . --dev",
"build:windows": "electron-packager . --out=dist --asar --overwrite --icon=build/icon.ico --version-string.CompanyName=zonetti --version-string.FileDescription=zonetti --version-string.ProductName=zonote",
"build:linux": "electron-packager . --out=dist --asar --overwrite --icon=build/icon.png",
"build:mac": "electron-packager . --out=dist --asar --overwrite --icon=build/icon.icns"
},
"repository": {
"type": "git",
"url": "https://github.com/zonetti/zonote.git"
},
"dependencies": {
"dompurify": "^2.2.6",
"electron-store": "^5.2.0",
"events": "^3.2.0",
"hotkeys-js": "^3.8.1",
"marked": "^1.1.0",
"shortid": "^2.2.15"
},
"devDependencies": {
"electron": "^9.0.0",
"electron-packager": "^15.0.0"
}
}
================================================
FILE: src/main/formatter.js
================================================
const NOTE_MIN_WIDTH = 100
const NOTE_MIN_HEIGHT = 100
const NOTE_MAX_WIDTH = 5000
const NOTE_MAX_HEIGHT = 5000
const isTabs = /^znt-tabs\|.+/
const isNote = /^znt-note\|\d+,\d+,\d+,\d+,\d+/
const colors = ['default', 'white', 'black', 'primary', 'warning', 'danger']
function newNote ({ howManyTabs, t = 0, x = 0, y = 0, w = 500, h = 300, c = 0 }) {
const note = {
t: t >= 0 ? t : 0,
x: x >= 0 ? x : 0,
y: y >= 0 ? y : 0,
w: w >= NOTE_MIN_WIDTH && w <= NOTE_MAX_WIDTH ? w : NOTE_MIN_WIDTH,
h: h >= NOTE_MIN_HEIGHT && h <= NOTE_MAX_HEIGHT ? h : NOTE_MIN_HEIGHT,
text: '',
color: (c < 0 || c > colors.length - 1) ? colors[0] : colors[c]
}
if (t > 0 && (!howManyTabs || t > howManyTabs - 1)) note.t = 0
return note
}
function fromText (text) {
text = text.replace(/\r/g, '')
if (typeof text !== 'string') {
throw new Error('"text" must be a string')
}
let tabs = []
const notes = []
const lines = text.split('\n')
let currentNote = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (!line && !currentNote) continue
if (i === 0 && isTabs.test(line)) {
tabs = line.split('|').pop().split(',').map(tab => {
return tab.length <= 100 ? tab : tab.substring(0, 100)
})
continue
}
if (isNote.test(line)) {
if (currentNote) {
notes.push(currentNote)
currentNote = null
}
const [t, x, y, w, h, c] = line.split('|').pop().split(',').map(n => parseInt(n, 10))
currentNote = newNote({ howManyTabs: tabs.length, t, x, y, w, h, c })
continue
}
if (!currentNote) {
currentNote = newNote({ howManyTabs: tabs.length })
}
currentNote.text += line + '\n'
}
if (currentNote) notes.push(currentNote)
return { tabs, notes }
}
function toText ({ tabs, notes }) {
let text = ''
if (tabs.length) {
text += 'znt-tabs|'
tabs.forEach(tab => {
text += `${tab},`
})
text = text.substring(0, text.length - 1) + '\r\n'
}
for (let i = 0; i < notes.length; i++) {
const note = notes[i]
const color = note.color || 'default'
note.c = colors.indexOf(color)
text += `znt-note|${note.t},${note.x},${note.y},${note.w},${note.h},${note.c}\r\n`
text += note.text.replace(/\r/g, '').replace(/\n/g, '\r\n') + '\r\n'
}
return text
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = { fromText, toText }
} else {
window.fromText = fromText
window.toText = toText
}
================================================
FILE: src/main/main.js
================================================
const path = require('path')
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const stateManager = require('./state_manager')
let state = stateManager.getInitialState()
function close () {
if (process.platform === 'darwin') return
app.quit()
}
function start () {
const minWidth = 800
const minHeight = 600
const browserWindowOptions = {
minWidth,
minHeight,
width: minWidth,
height: minHeight,
autoHideMenuBar: true,
frame: false,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
}
const previousWindowState = stateManager.getWindowState()
if (previousWindowState) {
browserWindowOptions.x = previousWindowState.x
browserWindowOptions.y = previousWindowState.y
browserWindowOptions.width = previousWindowState.w
browserWindowOptions.height = previousWindowState.h
}
const win = new BrowserWindow(browserWindowOptions)
if (previousWindowState && previousWindowState.isMaximized) {
win.maximize()
}
win.on('close', () => {
stateManager.saveWindowState({
isMaximized: win.isMaximized(),
x: win.getPosition()[0],
y: win.getPosition()[1],
w: win.getSize()[0],
h: win.getSize()[1]
})
})
ipcMain.on('minimize', () => win.isMinimized() ? win.restore() : win.minimize())
ipcMain.on('maximize', () => win.isMaximized() ? win.restore() : win.maximize())
ipcMain.on('get state', event => event.reply('update state', state))
ipcMain.on('close', close)
ipcMain.on('update state', (event, guiState) => {
state = guiState
stateManager.saveState(state)
})
ipcMain.on('new file', event => {
state = stateManager.getNewState()
event.reply('update state', state)
})
ipcMain.on('open file', (event, filePath) => {
try {
state = stateManager.getStateFromFile(filePath)
event.reply('update state', state)
} catch (err) {
console.log(err)
dialog.showMessageBoxSync({ message: 'Oh no... something is not right' })
event.reply('done')
}
})
ipcMain.on('save file', (event, savePath) => {
try {
const filePath = savePath || state.path
stateManager.saveStateToFile(filePath, state)
event.reply('done', filePath, path.basename(filePath))
} catch (err) {
if (err.message && err.message.includes('ENOENT')) {
delete state.path
return event.reply('done', 'save as')
}
console.log(err)
dialog.showMessageBoxSync({ message: 'Oh no... something is not right' })
event.reply('done')
}
})
win.setMenu(null)
win.loadFile(path.join(__dirname, '..', '..', 'static', 'index.html'))
if (process.argv.includes('--dev')) {
win.webContents.openDevTools()
}
}
app.on('window-all-closed', close)
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length !== 0) return
start()
})
app.whenReady().then(start)
================================================
FILE: src/main/state_manager.js
================================================
const fs = require('fs')
const path = require('path')
const Store = require('electron-store')
const formatter = require('./formatter')
const store = new Store()
const NEW_STATE = {
file: 'new file',
path: null,
isDirty: false,
activeTab: 0,
tabs: [],
notes: []
}
function saveState (state) {
store.set('state', state)
}
function getNewState () {
return JSON.parse(JSON.stringify(NEW_STATE))
}
function getInitialState () {
const state = store.get('state')
if (!state) return getNewState()
state.notes = state.notes.map(note => {
delete note.id
return note
})
if (state.path) {
try {
fs.readFileSync(state.path)
} catch (err) {
state.isDirty = true
}
}
return state
}
function saveStateToFile (filePath, state) {
fs.writeFileSync(filePath, formatter.toText(state))
}
function getStateFromFile (filePath) {
const parsed = formatter.fromText(fs.readFileSync(filePath).toString())
return {
file: path.basename(filePath),
path: filePath,
isDirty: false,
activeTab: 0,
tabs: parsed.tabs,
notes: parsed.notes
}
}
function saveWindowState (data) {
store.set('win-state', data)
}
function getWindowState () {
return store.get('win-state')
}
module.exports = {
saveState,
getNewState,
getInitialState,
saveStateToFile,
getStateFromFile,
saveWindowState,
getWindowState
}
================================================
FILE: src/renderer/notes.js
================================================
const shortid = require('shortid')
const marked = require('marked')
const DOMPurify = require('dompurify')
marked.setOptions({
breaks: true,
gfm: true,
smartypants: true
})
const contentElm = document.getElementById('content')
const editMessageElm = document.getElementById('edit-message')
const backMessageElm = document.getElementById('back-message')
const removeNoteElm = document.getElementById('remove-note')
const colorPickerElm = document.getElementById('color-picker')
function generateId () {
const id = shortid().replace(/\W/g, '') + new Date().getTime()
return id.split('').sort(() => 0.5 - Math.random()).join('')
}
let editMessageTimeout = null
EVENTS.on('render', () => {
editMessageElm.style.display = 'none'
const visibleNotes = STATE.notes.filter(note => note.t === STATE.activeTab)
backMessageElm.style.display = visibleNotes.length ? 'none' : 'block'
contentElm.style.top = TOP_OFFSET() + 'px'
document.querySelectorAll('.note').forEach(e => {
if (e.isSameNode(GUI_STATE.elementBeingDraggedElm)) return
e.remove()
})
STATE.notes.forEach(note => {
EVENTS.emit('render note', note)
})
})
function enableResizing (noteElm) {
let resizeStartX, resizeStartY, resizeStartWidth, resizeStartHeight
function resize (resizerClass) {
return function (event) {
if (['resizer-right', 'resizer-both'].includes(resizerClass)) {
let resizeAmount = resizeStartWidth + event.pageX - resizeStartX
if (resizeAmount < NOTE_MIN_WIDTH) {
resizeAmount = NOTE_MIN_WIDTH
}
GET_NOTE_BY_ID(GUI_STATE.elementBeingResizedElm.dataset.id).w = resizeAmount
GUI_STATE.elementBeingResizedElm.style.width = resizeAmount + 'px'
}
if (['resizer-bottom', 'resizer-both'].includes(resizerClass)) {
let resizeAmount = resizeStartHeight + event.pageY - resizeStartY
if (resizeAmount < NOTE_MIN_HEIGHT) {
resizeAmount = NOTE_MIN_HEIGHT
}
GET_NOTE_BY_ID(GUI_STATE.elementBeingResizedElm.dataset.id).h = resizeAmount
GUI_STATE.elementBeingResizedElm.style.height = resizeAmount + 'px'
}
}
}
function stopResizing () {
GUI_STATE.isResizing = false
UTIL.removeClass(GUI_STATE.elementBeingResizedElm, 'selected')
GUI_STATE.elementBeingResizedElm = null
document.documentElement.onmousemove = null
document.documentElement.onmouseup = null
EVENTS.emit('touch state')
EVENTS.emit('render')
}
function startResizing (event) {
EVENTS.emit('hide menu')
event.stopPropagation()
GUI_STATE.isResizing = true
GUI_STATE.elementBeingResizedElm = this.parentNote
resizeStartX = event.pageX
resizeStartWidth = parseInt(document.defaultView.getComputedStyle(GUI_STATE.elementBeingResizedElm).width, 10)
resizeStartY = event.pageY
resizeStartHeight = parseInt(document.defaultView.getComputedStyle(GUI_STATE.elementBeingResizedElm).height, 10)
UTIL.addClass(GUI_STATE.elementBeingResizedElm, 'selected')
document.documentElement.onmousemove = resize(this.className)
document.documentElement.onmouseup = stopResizing
}
const rightElm = document.createElement('div')
rightElm.className = 'resizer-right'
noteElm.appendChild(rightElm)
rightElm.onmousedown = startResizing
rightElm.parentNote = noteElm
const bottomElm = document.createElement('div')
bottomElm.className = 'resizer-bottom'
noteElm.appendChild(bottomElm)
bottomElm.onmousedown = startResizing
bottomElm.parentNote = noteElm
const bothElm = document.createElement('div')
bothElm.className = 'resizer-both'
noteElm.appendChild(bothElm)
bothElm.onmousedown = startResizing
bothElm.parentNote = noteElm
}
function enableDragging (noteElm) {
const draggingMouseDifference = [0, 0]
function dragElement (event) {
if (!GUI_STATE.elementBeingDraggedElm) return
event = event || window.event
let noteNewPosition = [
event.pageX + draggingMouseDifference[0],
event.pageY - TOP_OFFSET() + draggingMouseDifference[1]
]
if (noteNewPosition[0] <= CANVAS_PADDING()) {
noteNewPosition[0] = CANVAS_PADDING()
}
if (noteNewPosition[1] <= CANVAS_PADDING()) {
noteNewPosition[1] = CANVAS_PADDING()
}
const note = GET_NOTE_BY_ID(GUI_STATE.elementBeingDraggedElm.dataset.id)
note.x = noteNewPosition[0] - CANVAS_PADDING()
note.y = noteNewPosition[1] - CANVAS_PADDING()
GUI_STATE.elementBeingDraggedElm.style.left = noteNewPosition[0] + 'px'
GUI_STATE.elementBeingDraggedElm.style.top = noteNewPosition[1] + 'px'
UTIL.addClass(GUI_STATE.elementBeingDraggedElm, 'selected')
if (STATE.isInRemoveNoteArea) {
UTIL.addClass(GUI_STATE.elementBeingDraggedElm, 'remove')
} else {
UTIL.removeClass(GUI_STATE.elementBeingDraggedElm, 'remove')
}
}
function stopDragging () {
if (STATE.isInRemoveNoteArea) {
const confirmationDialogResponse = DIALOG.showMessageBoxSync({
message: 'Are you sure you want to remove this note?',
buttons: ['Yes', 'No']
})
const removeConfirmed = confirmationDialogResponse === 0
if (removeConfirmed) {
const noteIdToRemove = GUI_STATE.elementBeingDraggedElm.dataset.id
let noteIndex = 0
for (let i = 0; i < STATE.notes.length; i++) {
if (STATE.notes[i].id !== noteIdToRemove) continue
noteIndex = i
break
}
STATE.notes.splice(noteIndex, 1)
GUI_STATE.elementBeingDraggedElm.remove()
EVENTS.emit('render')
}
}
UTIL.removeClasses(GUI_STATE.elementBeingDraggedElm, ['selected', 'remove'])
GUI_STATE.isDragging = false
removeNoteElm.style.opacity = 0
GUI_STATE.elementBeingDraggedElm = null
document.onmouseup = null
document.onmousemove = null
EVENTS.emit('touch state')
EVENTS.emit('render')
}
function startDragging (event) {
EVENTS.emit('hide menu')
GUI_STATE.elementBeingDraggedElm = this
GUI_STATE.elementBeingDraggedElm.style.zIndex = NEXT_ZINDEX()
removeNoteElm.style.zIndex = NEXT_ZINDEX()
event = event || window.event
draggingMouseDifference[0] = GUI_STATE.elementBeingDraggedElm.offsetLeft - event.pageX
draggingMouseDifference[1] = GUI_STATE.elementBeingDraggedElm.offsetTop - (event.pageY - TOP_OFFSET())
GUI_STATE.isDragging = true
UTIL.addClass(GUI_STATE.elementBeingDraggedElm, 'selected')
removeNoteElm.style.opacity = 1
removeNoteElm.style.top = (TOP_OFFSET() + removeNoteElm.offsetHeight) + 'px'
document.onmouseup = stopDragging
document.onmousemove = dragElement
}
noteElm.onmousedown = startDragging
}
EVENTS.on('render note text', (note, noteElm) => {
const noteTextElm = document.createElement('div')
noteTextElm.className = 'text'
noteTextElm.innerHTML = DOMPurify.sanitize(marked(note.text))
noteTextElm.onclick = event => {
EVENTS.emit('hide menu')
event.stopPropagation()
}
noteTextElm.ondblclick = () => {
EVENTS.emit('hide menu')
note.isEditing = true
editMessageElm.style.display = 'none'
EVENTS.emit('render note', note)
}
noteTextElm.onmouseenter = function () {
this.style.userSelect = 'text'
editMessageElm.style.display = 'block'
editMessageElm.style.top = (noteElm.offsetTop + noteElm.offsetHeight - 15) + 'px'
editMessageElm.style.left = (noteElm.offsetLeft + (noteElm.offsetWidth / 2) - (editMessageElm.offsetWidth / 2)) + 'px'
editMessageElm.style.zIndex = '' + (parseInt(noteElm.style.zIndex, 10) + 5)
clearTimeout(editMessageTimeout)
editMessageTimeout = setTimeout(() => {
editMessageElm.style.display = 'none'
}, 1000)
}
noteTextElm.onmouseleave = function () {
this.style.userSelect = 'none'
editMessageElm.style.display = 'none'
}
noteTextElm.onmousedown = event => event.stopPropagation()
noteTextElm.onmouseout = event => event.stopPropagation()
noteTextElm.querySelectorAll('a').forEach(linkElm => {
linkElm.onclick = event => {
event.preventDefault()
require('electron').shell.openExternal(event.target.href)
}
})
noteElm.appendChild(noteTextElm)
})
EVENTS.on('render note', note => {
if (!note.id) {
note.id = generateId()
}
let noteElm = document.querySelector(`.note[data-id="${note.id}"]`)
if (noteElm && !noteElm.isSameNode(GUI_STATE.elementBeingDraggedElm)) {
noteElm.remove()
noteElm = null
}
if (note.t !== STATE.activeTab) return
if (!noteElm) {
noteElm = document.createElement('div')
}
noteElm.dataset.id = note.id
noteElm.dataset.tab = note.t
noteElm.className = 'note ' + (note.color || 'default')
noteElm.style.display = note.t === STATE.activeTab ? 'block' : 'none'
noteElm.style.top = note.y + CANVAS_PADDING() + 'px'
noteElm.style.left = note.x + CANVAS_PADDING() + 'px'
noteElm.style.width = note.w + 'px'
noteElm.style.height = note.h + 'px'
noteElm.style.zIndex = NEXT_ZINDEX()
enableResizing(noteElm)
enableDragging(noteElm)
noteElm.ondblclick = event => event.stopPropagation()
noteElm.onmouseenter = function (event) {
if (GUI_STATE.isDragging || GUI_STATE.isResizing) return
this.style.zIndex = NEXT_ZINDEX()
}
if (note.isEditing) {
const noteTextareaElm = document.createElement('textarea')
noteTextareaElm.onblur = function () {
delete note.isEditing
this.remove()
EVENTS.emit('render note text', note, noteElm)
}
noteTextareaElm.onclick = event => event.stopPropagation()
noteTextareaElm.onmousedown = event => event.stopPropagation()
noteTextareaElm.onkeydown = function (event) {
if (event.keyCode === KEY_ESC) return EVENTS.emit('render note', note)
if (event.keyCode === KEY_TAB) {
event.preventDefault()
const selectionStart = this.selectionStart
this.value = this.value.substring(0, selectionStart) + ' ' + this.value.substring(this.selectionEnd)
this.selectionEnd = selectionStart + 2
}
note.text = this.value
EVENTS.emit('touch state')
}
noteTextareaElm.oninput = function (event) {
note.text = this.value
EVENTS.emit('touch state')
}
noteElm.appendChild(noteTextareaElm)
setTimeout(() => {
noteTextareaElm.value = note.text
noteTextareaElm.focus()
}, 0)
} else if (!noteElm.isSameNode(GUI_STATE.elementBeingDraggedElm)) {
EVENTS.emit('render note text', note, noteElm)
}
noteElm.addEventListener('contextmenu', e => {
e.preventDefault()
colorPickerElm.style.display = 'block'
colorPickerElm.style.top = (e.pageY - 15) + 'px'
colorPickerElm.style.left = (e.pageX - 15) + 'px'
colorPickerElm.style.zIndex = NEXT_ZINDEX()
GUI_STATE.noteRightClicked = note
})
contentElm.appendChild(noteElm)
})
removeNoteElm.onmouseenter = () => { STATE.isInRemoveNoteArea = true }
removeNoteElm.onmouseleave = () => { STATE.isInRemoveNoteArea = false }
function hideColorPicker () {
colorPickerElm.style.display = 'none'
GUI_STATE.noteRightClicked = null
}
colorPickerElm.onmouseleave = hideColorPicker
colorPickerElm.querySelectorAll('span').forEach(spanElm => {
spanElm.onclick = e => {
e.preventDefault()
GUI_STATE.noteRightClicked.color = spanElm.className.split(' ')[0]
EVENTS.emit('touch state')
hideColorPicker()
}
})
contentElm.ondblclick = event => {
const newNote = {
t: STATE.activeTab,
x: event.pageX - CANVAS_PADDING(),
y: event.pageY - TOP_OFFSET() - CANVAS_PADDING(),
w: 200,
h: 200,
text: '',
isEditing: true,
color: 'default'
}
if (newNote.x < 0) newNote.x = 0
if (newNote.y < 0) newNote.y = 0
STATE.notes.push(newNote)
EVENTS.emit('touch state')
EVENTS.emit('render')
}
================================================
FILE: src/renderer/script.js
================================================
const EventEmitter = require('events')
const electron = require('electron')
window.STATE = {}
window.GUI_STATE = { zIndex: 1 }
window.EVENTS = new EventEmitter()
window.DIALOG = electron.remote.dialog
window.IPC = electron.ipcRenderer
window.NEXT_ZINDEX = () => '' + ++GUI_STATE.zIndex
// REFACTOR ->
window.CANVAS_PADDING = () => 20
window.TOP_OFFSET = () => {
const topBarElm = document.getElementById('top')
const tabsElm = document.getElementById('tabs')
return topBarElm.offsetHeight + tabsElm.offsetHeight
}
window.ALLOWED_EXTENSIONS = ['znt', 'txt']
window.KEY_ESC = 27
window.KEY_TAB = 9
window.NOTE_MIN_HEIGHT = 100
window.NOTE_MIN_WIDTH = 100
window.GET_NOTE_BY_ID = id => {
for (let i = 0; i < STATE.notes.length; i++) {
if (STATE.notes[i].id === id) return STATE.notes[i]
}
return null
}
// <- REFACTOR
require('../src/renderer/util')
require('../src/renderer/top_menu')
require('../src/renderer/tabs')
require('../src/renderer/notes')
document.body.onclick = () => EVENTS.emit('lose focus')
document.body.onresize = () => EVENTS.emit('render tab scroll')
EVENTS.on('render', () => IPC.send('update state', STATE))
IPC.on('update state', (event, newState) => {
STATE = newState
EVENTS.emit('render')
})
IPC.send('get state')
================================================
FILE: src/renderer/tabs.js
================================================
const hotkeys = require('hotkeys-js')
const tabsElm = document.getElementById('tabs')
const tabScrollLeftElm = document.getElementById('tab-scroll-left')
const tabScrollRightElm = document.getElementById('tab-scroll-right')
const removeNoteElm = document.getElementById('remove-note')
EVENTS.on('toggle tabs', () => {
EVENTS.emit('hide menu')
if (STATE.tabs.length > 0) {
const tabsWithNotes = STATE.notes
.map(note => note.t)
.filter((value, index, self) => self.indexOf(value) === index)
if (tabsWithNotes.length === 1) {
STATE.notes = STATE.notes.map(note => ({ ...note, t: 0 }))
} else if (tabsWithNotes.length > 1) {
const chosenAction = DIALOG.showMessageBoxSync({
message: 'All your existing notes will be merged into one canvas.\nAre you sure you want to proceed?',
buttons: ['Yes', 'No']
})
if (chosenAction === 1) return
STATE.notes = STATE.notes.map(note => ({ ...note, t: 0 }))
}
STATE.tabs = []
} else {
STATE.tabs = ['new tab']
EVENTS.emit('touch state')
}
STATE.activeTab = 0
EVENTS.emit('touch state')
EVENTS.emit('render')
})
EVENTS.on('render tab scroll', () => {
const newTabButtonElm = document.getElementById('new-tab-button')
const showLeft = tabsElm.scrollLeft > 0
const scrollRight = tabsElm.scrollWidth - tabsElm.scrollLeft - window.innerWidth > 0
tabScrollLeftElm.style.display = showLeft ? 'block' : 'none'
tabScrollRightElm.style.display = scrollRight ? 'block' : 'none'
})
EVENTS.on('render', () => {
tabsElm.innerText = ''
tabsElm.style.display = STATE.tabs.length > 0 ? 'inline-flex' : 'none'
tabScrollLeftElm.style.display = 'none'
tabScrollRightElm.style.display = 'none'
if (!STATE.tabs.length) return
let tabBeingDragged = null
STATE.tabs.forEach((tabText, tabIndex) => {
const isActiveTab = STATE.activeTab === tabIndex
const tabElm = document.createElement('li')
tabElm.dataset.index = '' + tabIndex
tabElm.draggable = 'true'
tabElm.ondrag = function (event) {
tabBeingDragged = this
}
tabElm.ondragover = event => event.preventDefault()
tabElm.ondragenter = function (event) {
if (this.isSameNode(tabBeingDragged)) return
this.style.backgroundColor = '#4ECDC4'
this.style.color = '#FFF'
}
tabElm.ondragleave = function (event) {
if (this.isSameNode(tabBeingDragged)) return
if (this.contains(document.elementFromPoint(event.pageX, event.pageY))) return
this.style.backgroundColor = ''
this.style.color = ''
}
tabElm.ondrop = function (event) {
event.preventDefault()
this.style.backgroundColor = ''
const fromIndex = parseInt(tabBeingDragged.dataset.index, 10)
const toIndex = parseInt(this.dataset.index, 10)
STATE.notes = STATE.notes.map(note => {
if (![fromIndex, toIndex].includes(note.t)) return note
note.t = note.t === fromIndex ? toIndex : fromIndex
return note
})
const tempTab = STATE.tabs[fromIndex]
STATE.tabs[fromIndex] = STATE.tabs[toIndex]
STATE.tabs[toIndex] = tempTab
tabBeingDragged = null
if (STATE.activeTab === fromIndex) {
STATE.activeTab = toIndex
}
EVENTS.emit('render')
}
const spanElm = document.createElement('span')
spanElm.ondragover = event => event.preventDefault()
spanElm.innerText = tabText
if (isActiveTab) {
spanElm.contentEditable = 'true'
spanElm.spellcheck = false
}
const removeButtonElm = document.createElement('button')
removeButtonElm.ondragover = event => event.preventDefault()
removeButtonElm.type = 'button'
removeButtonElm.style.opacity = isActiveTab ? 1 : 0
removeButtonElm.innerText = '𝗑'
removeButtonElm.onclick = event => {
event.stopPropagation()
EVENTS.emit('remove tab', tabIndex)
}
tabElm.append(spanElm)
tabElm.append(removeButtonElm)
if (isActiveTab) {
tabElm.className = 'active'
tabElm.onclick = event => event.stopPropagation()
spanElm.oninput = function () {
EVENTS.emit('touch state')
if (this.innerText.length <= 100) return
this.innerText = this.innerText.substring(0, 100)
}
spanElm.onblur = function () {
const value = this.innerText
.replace(/ /g, ' ')
.replace(/[,|]/g, '')
.replace(/\s+/g, ' ')
.trim()
this.innerText = value || 'new tab'
STATE.tabs[tabIndex] = this.innerText
}
} else {
tabElm.style.userSelect = 'none'
tabElm.onclick = () => {
STATE.activeTab = tabIndex
EVENTS.emit('render')
}
tabElm.onmouseenter = () => {
removeButtonElm.style.opacity = 1
if (GUI_STATE.isDragging) {
STATE.activeTab = tabIndex
GET_NOTE_BY_ID(GUI_STATE.elementBeingDraggedElm.dataset.id).t = tabIndex
EVENTS.emit('render')
GUI_STATE.elementBeingDraggedElm.style.zIndex = NEXT_ZINDEX()
removeNoteElm.style.zIndex = NEXT_ZINDEX()
}
}
tabElm.onmouseleave = () => {
removeButtonElm.style.opacity = 0
}
}
tabsElm.append(tabElm)
})
const newTabButtonElm = document.createElement('li')
newTabButtonElm.id = 'new-tab-button'
newTabButtonElm.innerText = '+'
newTabButtonElm.onmouseenter = function () { this.innerText = '+ new tab' }
newTabButtonElm.onmouseleave = function () { this.innerText = '+' }
newTabButtonElm.onclick = () => {
STATE.tabs.push('new tab')
STATE.activeTab = STATE.tabs.length - 1
EVENTS.emit('touch state')
EVENTS.emit('render')
}
tabsElm.append(newTabButtonElm)
EVENTS.emit('render tab scroll')
})
EVENTS.on('new tab', () => {
STATE.tabs.push('new tab')
STATE.activeTab = STATE.tabs.length - 1
EVENTS.emit('render')
EVENTS.emit('touch state')
})
EVENTS.on('remove tab', tabIndex => {
EVENTS.emit('touch state')
if (STATE.tabs.length === 1) {
STATE.tabs = []
return EVENTS.emit('render')
}
if (STATE.notes.filter(note => note.t === tabIndex).length) {
const confirmationDialogResponse = DIALOG.showMessageBoxSync({
message: 'If you remove this tab all your notes inside of it will be removed as well.\nAre you sure you want to proceed?',
buttons: ['Yes', 'No']
})
const removeConfirmed = confirmationDialogResponse === 0
if (!removeConfirmed) return
document.querySelectorAll(`.note[data-tab="${tabIndex}"]`).forEach(noteElm => noteElm.remove())
STATE.notes = STATE.notes.filter(note => note.t !== tabIndex)
}
STATE.notes.filter(note => note.t > tabIndex).forEach(note => { note.t-- })
STATE.tabs.splice(tabIndex, 1)
if (STATE.activeTab > 0 && STATE.activeTab >= tabIndex) {
STATE.activeTab--
}
EVENTS.emit('render')
})
EVENTS.on('switch tab', direction => {
const tabsLength = STATE.tabs.length
if (tabsLength === 0 || tabsLength === 1) return
let newActiveTab = STATE.activeTab
if (direction === 'next') {
newActiveTab++
if (newActiveTab === tabsLength) {
newActiveTab = 0
}
} else {
newActiveTab--
if (newActiveTab < 0) {
newActiveTab = tabsLength - 1
}
}
STATE.activeTab = newActiveTab
EVENTS.emit('render')
})
tabsElm.onwheel = event => tabsElm.scrollLeft += event.deltaY
tabsElm.onscroll = () => EVENTS.emit('render tab scroll')
let scrollTabsInterval = null
function scrollTabs (amount) {
tabsElm.scrollLeft += amount
}
tabScrollLeftElm.onmouseenter = () => scrollTabsInterval = setInterval(() => scrollTabs(-10), 50)
tabScrollLeftElm.onmouseleave = () => clearInterval(scrollTabsInterval)
tabScrollRightElm.onmouseenter = () => scrollTabsInterval = setInterval(() => scrollTabs(10), 50)
tabScrollRightElm.onmouseleave = () => clearInterval(scrollTabsInterval)
hotkeys('ctrl+t,command+t', () => EVENTS.emit('new tab'))
hotkeys('ctrl+w,command+w', () => {
if (!STATE.tabs.length) return
EVENTS.emit('remove tab', STATE.activeTab)
})
hotkeys('ctrl+tab,command+tab', () => EVENTS.emit('switch tab', 'next'))
hotkeys('ctrl+shift+tab,command+shift+tab', () => EVENTS.emit('switch tab', 'previous'))
================================================
FILE: src/renderer/top_menu.js
================================================
const hotkeys = require('hotkeys-js')
const menuElm = document.getElementById('menu')
const menuButtonElm = document.getElementById('menu-button')
const topButtonCloseAppElm = document.getElementById('close')
const fileElm = document.getElementById('file')
function createMenuButton (text, key) {
const shortcut = process.platform === 'darwin' ? 'command' : 'ctrl'
const buttonElm = document.createElement('li')
buttonElm.innerText = text
const span = document.createElement('span')
span.innerText = `${shortcut} + ${key}`
buttonElm.appendChild(span)
return buttonElm
}
const menuButtonNewElm = createMenuButton('new', 'n')
const menuButtonOpenElm = createMenuButton('open...', 'o')
const menuButtonSaveElm = createMenuButton('save', 's')
const menuButtonTabsElm = document.createElement('li')
menuButtonTabsElm.id = 'tabs-menu-button'
const menuButtonExitElm = document.createElement('li')
menuButtonExitElm.innerText = 'exit'
menuElm.appendChild(menuButtonNewElm)
menuElm.appendChild(menuButtonOpenElm)
menuElm.appendChild(menuButtonSaveElm)
menuElm.appendChild(menuButtonTabsElm)
menuElm.appendChild(menuButtonExitElm)
EVENTS.on('render', () => {
document.title = STATE.path || 'zonote'
fileElm.innerText = `${STATE.isDirty ? '*' : ''}${STATE.file}`
menuButtonTabsElm.innerText = STATE.tabs.length > 0 ? 'disable tabs' : 'enable tabs'
})
EVENTS.on('hide menu', () => {
menuElm.style.display = 'none'
menuButtonElm.className = ''
})
EVENTS.on('toggle menu', () => {
if (menuButtonElm.className === 'open') return EVENTS.emit('hide menu')
menuElm.style.display = 'block'
menuButtonElm.className = 'open'
menuElm.style.zIndex = 999 + NEXT_ZINDEX()
})
EVENTS.on('touch state', () => {
STATE.isDirty = true
fileElm.innerText = `*${STATE.file}`
})
EVENTS.on('lose focus', () => {
EVENTS.emit('hide menu')
EVENTS.emit('render')
})
async function handleCurrentState () {
EVENTS.emit('hide menu')
if (!STATE.isDirty) return 'continue'
const chosenAction = DIALOG.showMessageBoxSync({
message: 'You have unsaved changes in your current file.\nWhat do you want to do?',
buttons: ['Save changes', 'Discard changes', 'Cancel']
})
if (chosenAction === 0) return EVENTS.emit('save file')
if (chosenAction === 1) return 'continue'
if (chosenAction === 2) return 'cancel'
}
EVENTS.on('new file', async () => {
const action = await handleCurrentState()
if (action === 'cancel') return
IPC.send('new file')
})
EVENTS.on('open file', async () => {
const action = await handleCurrentState()
if (action === 'cancel') return
let filePath = DIALOG.showOpenDialogSync({
filters: [ { name: 'zonote files', extensions: ALLOWED_EXTENSIONS } ]
})
if (!filePath) return
filePath = filePath[0]
IPC.send('open file', filePath)
})
EVENTS.on('save file', async () => {
EVENTS.emit('hide menu')
if (!STATE.isDirty) return
if (STATE.path) {
await new Promise(resolve => {
IPC.once('done', (event, filePath) => {
if (filePath === 'save as') {
delete STATE.path
EVENTS.emit('save file')
return resolve()
}
if (filePath) {
STATE.path = filePath
STATE.isDirty = false
}
resolve()
})
IPC.send('save file')
})
return EVENTS.emit('render')
}
const savePath = DIALOG.showSaveDialogSync({
filters: [ { name: 'zonote files', extensions: ALLOWED_EXTENSIONS } ],
properties: [ 'createDirectory' ]
})
if (!savePath) return
await new Promise(resolve => {
IPC.once('done', (event, filePath, fileName) => {
if (filePath) {
STATE.file = fileName
STATE.path = filePath
STATE.isDirty = false
}
resolve()
})
IPC.send('save file', savePath)
})
EVENTS.emit('render')
})
EVENTS.on('close', () => {
EVENTS.emit('hide menu')
IPC.send('close', STATE)
})
menuElm.onclick = event => event.stopPropagation()
menuButtonElm.onclick = event => {
event.stopPropagation()
EVENTS.emit('toggle menu')
}
menuButtonNewElm.onclick = () => EVENTS.emit('new file')
menuButtonOpenElm.onclick = () => EVENTS.emit('open file')
menuButtonSaveElm.onclick = () => EVENTS.emit('save file')
menuButtonTabsElm.onclick = () => EVENTS.emit('toggle tabs')
menuButtonExitElm.onclick = () => EVENTS.emit('close')
topButtonCloseAppElm.onclick = () => EVENTS.emit('close')
hotkeys('esc', () => EVENTS.emit('lose focus'))
hotkeys('ctrl+n,command+n', () => EVENTS.emit('new file'))
hotkeys('ctrl+o,command+o', () => EVENTS.emit('open file'))
hotkeys('ctrl+s,command+s', () => EVENTS.emit('save file'))
================================================
FILE: src/renderer/util.js
================================================
window.UTIL = {}
window.UTIL.addClass = (elm, value) => {
const classes = elm.className.split(' ')
if (classes.includes(value)) return
elm.className = classes.concat(value).join(' ')
}
window.UTIL.removeClass = (elm, value) => {
const classes = elm.className.split(' ')
if (!classes.includes(value)) return
classes.splice(classes.indexOf(value), 1)
elm.className = classes.join(' ')
}
window.UTIL.addClasses = (elm, values) => values.forEach(v => UTIL.addClass(elm, v))
window.UTIL.removeClasses = (elm, values) => values.forEach(v => UTIL.removeClass(elm, v))
================================================
FILE: static/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>zonote</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
<link href="style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="top">
<div id="menu-button">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div id="file"></div>
<button id="close" type="button">𝗑</button>
</div>
<div id="remove-note">drag here to remove</div>
<ul id="tabs"></ul>
<button id="tab-scroll-left" class="tab-scroll" type="button">«</button>
<button id="tab-scroll-right" class="tab-scroll" type="button">»</button>
<div id="content">
<p id="edit-message">double click to edit</p>
<p id="back-message">double click to create a new note</p>
<div id="color-picker">
<span class="default mb-5 mr-5"></span>
<span class="white mb-5 mr-5"></span>
<span class="black mb-5"></span>
<span class="primary mr-5"></span>
<span class="warning mr-5"></span>
<span class="danger"></span>
</div>
</div>
<ul id="menu"></ul>
<script src="../src/renderer/script.js"></script>
</body>
</html>
================================================
FILE: static/style.css
================================================
@import url('https://fonts.googleapis.com/css?family=Merriweather:300');
* {
margin: 0;
padding: 0;
}
:focus {
outline: 0;
}
body {
font-size: 16px;
font-family: "Merriweather", "PT Serif", Georgia, "Times New Roman", "STSong", Serif;
}
#top {
position: fixed;
top: 0;
left: 0;
width: 100%;
-webkit-app-region: drag;
height: 60px;
background: #556270;
border: 1px solid #000;
box-sizing: border-box;
}
#remove-note {
position: fixed;
left: 50%;
list-style-type: none;
font-size: 13px;
line-height: 26px;
z-index: 100;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
border: 2px dashed #FF6B6B;
background: #FAFAFA;
color: #FF6B6B;
border-radius: 5px;
padding: 2px 10px;
transition: opacity .2s;
opacity: 0;
}
#remove-note:hover {
border: 2px solid #FF6B6B;
}
#tabs {
position: fixed;
box-sizing: border-box;
top: 60px;
left: 0;
background: #E5E5E5;
width: 100%;
border: 1px solid #000;
border-top: none;
border-bottom: none;
z-index: 100;
list-style-type: none;
overflow: hidden;
display: none;
}
#tabs li {
display: inline-flex;
padding: 10px;
line-height: 20px;
white-space: nowrap;
cursor: pointer;
border-bottom: 1px solid #000;
}
#tabs li:hover {
background: #EEE;
}
#tabs li span {
display: flex;
}
#tabs li button {
float: right;
opacity: 0;
transition: .1s;
top: 10px;
right: 10px;
background: #FF6B6B;
text-align: center;
font-size: 10px;
color: #FFF;
height: 20px;
width: 20px;
border-radius: 10px;
border: 1px solid #C44D58;
margin-left: 10px;
cursor: pointer;
}
#tabs li button:hover {
box-shadow: 1px 1px #333;
background: #EE5A5A;
}
#tabs li#new-tab-button {
min-width: 160px;
width: 100%;
}
#tabs li.active {
background: #FAFAFA;
border-bottom: 1px solid #FAFAFA;
}
#tabs li.active span {
cursor: text;
}
#tabs li:not(:first-child) {
border-left: 1px solid #000;
}
button.tab-scroll {
position: fixed;
z-index: 999;
top: 66px;
background: #4ECDC4;
border: none;
color: #FFF;
opacity: 1;
padding: 5px 0;
width: 30px;
font-size: 18px;
text-align: center;
line-height: 18px;
font-weight: bold;
text-shadow: 1px 1px #333;
display: none;
}
button.tab-scroll:hover {
background: #5FDED5;
cursor: pointer;
opacity: 1;
}
button#tab-scroll-left {
border-left: 1px solid #000;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
left: 0;
}
button#tab-scroll-left:hover {
text-align: left;
padding-left: 5px;
}
button#tab-scroll-right {
border-right: 1px solid #000;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
right: 0;
}
button#tab-scroll-right:hover {
text-align: right;
padding-right: 5px;
}
#content {
position: absolute;
bottom: 0;
left: 0;
top: 60px;
width: 100%;
background: #FAFAFA;
border: 1px solid #000;
border-top: none;
box-sizing: border-box;
padding: 20px;
font-size: 1.3em;
}
#menu-button,
#menu li,
#file,
#close,
#back-message,
.note div,
#tabs button,
#tabs #new-tab-button,
button.tab-scroll,
#remove-note,
#edit-message {
user-select: none;
}
#menu-button, #close, #back-message {
-webkit-app-region: no-drag;
}
#close {
position: fixed;
top: 15px;
right: 20px;
background: #FF6B6B;
transition: .1s;
color: #FFF;
height: 30px;
width: 30px;
border-radius: 15px;
font-size: 18px;
border: 1px solid #C44D58;
cursor: pointer;
}
#close:hover {
box-shadow: 1px 1px #333;
background: #EE5A5A;
}
#menu-button {
position: fixed;
top: 20px;
left: 20px;
height: 20px;
width: 30px;
cursor: pointer;
}
#file {
position: fixed;
top: 20px;
left: 70px;
font-size: 18px;
line-height: 20px;
color: #FFF;
text-shadow: 1px 1px #333;
}
#menu-button span {
display: block;
position: absolute;
height: 4px;
width: 100%;
background: #4ECDC4;
opacity: 1;
left: 0;
transform: rotate(0deg);
transition: .1s;
}
#menu-button span:nth-child(1) {
top: 0px;
}
#menu-button span:nth-child(2), #menu-button span:nth-child(3) {
top: 8px;
}
#menu-button span:nth-child(4) {
top: 16px;
}
#menu-button.open span:nth-child(1), #menu-button.open span:nth-child(4) {
top: 8px;
width: 0%;
left: 50%;
}
#menu-button.open span:nth-child(2) {
transform: rotate(45deg);
}
#menu-button.open span:nth-child(3) {
transform: rotate(-45deg);
}
#edit-message {
position: absolute;
border: 2px dashed #777;
border-radius: 5px;
background: #FAFAFA;
font-size: 12px;
line-height: 14px;
padding: 6px 10px;
display: none;
}
#back-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
border: 2px dashed #888;
color: #556270;
border-radius: 5px;
padding: 6px 10px;
}
#menu {
position: fixed;
display: none;
top: 59px;
left: 0;
padding-top: 1px;
background: #556270;
z-index: 999;
border: 1px solid #000;
border-top: none;
color: #FFF;
text-shadow: 1px 1px #333;
}
#menu li {
width: 200px;
padding: 20px;
list-style-type: none;
font-size: 16px;
line-height: 16px;
cursor: pointer;
}
#menu li:hover {
background: #4ECDC4;
text-shadow: none;
}
#menu li span {
float: right;
margin-top: 1px;
font-size: 12px;
}
#content {
overflow: auto;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
background: #EEE;
}
*::-webkit-scrollbar-track, *::-webkit-scrollbar-corner {
background: #EEE;
}
*::-webkit-scrollbar-thumb {
background: #556270;
}
#color-picker {
display: none;
position: fixed;
background: #EEE;
border: 1px solid #777;
box-shadow: 2px 2px #BBB;
width: 84px;
height: 54px;
padding: 5px;
}
#color-picker span {
display: inline-block;
height: 20px;
width: 20px;
border: 1px solid #777;
padding: 0;
margin: 0;
cursor: pointer;
}
#color-picker span.mr-5 { margin-right: 4px; }
#color-picker span.mb-5 { margin-bottom: 4px; }
#color-picker span.default { background: #EEE; }
#color-picker span.white { background: #FAFAFA; }
#color-picker span.black { background: #222; }
#color-picker span.primary { background: #4ECDC4; }
#color-picker span.warning { background: #FEE074; }
#color-picker span.danger { background: #FF6B6B; }
.note {
position: absolute;
border: 1px solid #777;
box-shadow: 2px 2px #BBB;
transition: box-shadow .5s, opacity .5s, background .5s;
padding: 20px;
min-width: 100px;
min-height: 100px;
max-width: 2000px;
max-height: 2000px;
font-size: 0.8em;
opacity: 1;
}
.note.default { background: #EEE; color: #333; }
.note.white { background: #FAFAFA; color: #333; }
.note.black { background: #222; color: #FAFAFA; }
.note.primary { background: #4ECDC4; color: #333; }
.note.warning { background: #FEE074; color: #333; }
.note.danger { background: #FF6B6B; color: #333; }
.note.selected {
border: 2px dashed #4ECDC4;
opacity: 0.9;
/* background: #EFEFEF; */
box-shadow: none !important;
}
.note:hover {
cursor: move;
box-shadow: 4px 4px #CCC;
}
.note.selected.remove {
border: 2px solid #FF6B6B;
box-shadow: none !important;
}
.note textarea {
width: 100%;
height: 100%;
resize: none;
background: #EEE;
border: 2px dashed #333;
color: #333;
font-family: monospace;
font-size: 1.2em;
}
.note.white textarea { background: #FAFAFA; }
.note.black textarea { background: #222; color: #FAFAFA; border-color: #FAFAFA; }
.note.primary textarea { background: #4ECDC4; color: #333; border-color: #333; }
.note.warning textarea { background: #FEE074; color: #333; border-color: #333; }
.note.danger textarea { background: #FF6B6B; color: #333; border-color: #333; }
.note .resizer-right {
width: 5px;
height: 100%;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: e-resize;
}
.note .resizer-bottom {
width: 100%;
height: 5px;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: n-resize;
}
.note .resizer-both {
width: 5px;
height: 5px;
background: transparent;
z-index: 10;
position: absolute;
right: 0;
bottom: 0;
cursor: nw-resize;
}
.note .text {
height: 100%;
width: 100%;
overflow: auto;
}
.note .text:hover {
cursor: text;
}
.note .text *:not(:first-child) {
margin-top: 15px;
}
.note .text hr {
border: 0;
border-top: 1px solid #000;
}
.note.black .text hr { border-color: #FAFAFA }
.note .text hr:not(:first-child) {
margin: 20px 0 !important;
}
.note .text ul,
.note .text ol {
padding-left: 10px;
margin-left: 10px;
}
.note .text li {
margin-top: 8px !important;
}
.note .text h1:not(:first-child),
.note .text h2:not(:first-child) {
padding: 10px 0;
}
.note .text h3:not(:first-child),
.note .text h4:not(:first-child) {
padding: 5px 0;
}
.note .text h5:not(:first-child),
.note .text h6:not(:first-child) {
padding: 3px 0;
}
.note .text h1 {
font-size: 2.5em;
}
.note .text h2 {
font-size: 2em;
}
.note .text h3 {
font-size: 1.6em;
}
.note .text h4 {
font-size: 1.3em;
}
.note .text h5 {
font-size: 1.2em;
}
.note .text h6 {
font-size: 1.1em;
}
.note .text a {
color: #4ECDC4;
border-bottom: 1px solid #4ECDC4;
text-decoration: none;
}
.note.danger .text a { color: #FAFAFA; border-color: #EEE; }
.note .text a:visited,
.note .text a:active,
.note .text a:hover {
color: #3DBCB3;
border-bottom: 1px solid #3DBCB3;
}
.note.danger .text a:visited,
.note.danger .text a:active,
.note.danger .text a:hover { color: #CCC; border-color: #CCC; }
.note .text blockquote {
padding: 10px 20px;
margin: 0 0 20px;
border-left: 5px solid #CCC;
width: 90%;
}
.note.black .text blockquote { background: #222; color: #FAFAFA; border-color: #FAFAFA; }
.note.primary .text blockquote { border-color: #FAFAFA; }
.note.warning .text blockquote { border-color: #333; }
.note.danger .text blockquote { border-color: #333; }
.note .text code {
background: #FAFAFA;
border: 1px solid #333;
padding: 0 2px;
margin: 0 2px;
}
.note.white .text code { background: #EEE; }
.note.black .text code { background: #FAFAFA; color: #222; border-color: #EEE; }
.note.primary .text code { background: #FAFAFA; }
.note.warning .text code { background: #FAFAFA; }
.note.danger .text code { background: #FAFAFA; }
.note .text pre {
display: block;
padding: 10px;
margin: 0 0 10px;
width: 95%;
color: #333;
word-break: break-all;
word-wrap: break-word;
background: #FAFAFA;
border: 1px solid #CCC;
border-radius: 4px;
}
.note.white .text pre { background: #EEE; }
.note.black .text pre { background: #FAFAFA; color: #222; }
.note.primary .text pre { background: #FAFAFA; }
.note.warning .text pre { background: #FAFAFA; }
.note.danger .text pre { background: #FAFAFA; }
.note .text pre code {
background: #FAFAFA;
border: none;
padding: 0;
}
.note .text table {
display: table;
width: 90%;
border-spacing: 0;
border-collapse: collapse;
border: 1px solid #CCC;
background: #FAFAFA;
}
.note.white .text table { background: #EEE; }
.note.black .text table { background: #FAFAFA; color: #333; }
.note.primary .text table { background: #FAFAFA; color: #333; }
.note.warning .text table { background: #FAFAFA; color: #333; }
.note.danger .text table { background: #FAFAFA; color: #333; }
.note .text thead {
display: table-header-group;
vertical-align: middle;
border-color: inherit;
}
.note .text tr {
display: table-row;
vertical-align: inherit;
border-color: inherit;
}
.note .text tr th,
.note .text tr td {
vertical-align: bottom;
padding: 8px;
vertical-align: top;
border-top: 1px solid #CCC;
}
.note .text tr th {
border-bottom: 2px solid #CCC;
}
.note .text img {
display: inline-block;
max-width: 33%;
}
gitextract_2n15ldnx/
├── .github/
│ └── workflows/
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── build/
│ └── icon.icns
├── package.json
├── src/
│ ├── main/
│ │ ├── formatter.js
│ │ ├── main.js
│ │ └── state_manager.js
│ └── renderer/
│ ├── notes.js
│ ├── script.js
│ ├── tabs.js
│ ├── top_menu.js
│ └── util.js
└── static/
├── index.html
└── style.css
SYMBOL INDEX (24 symbols across 6 files)
FILE: src/main/formatter.js
constant NOTE_MIN_WIDTH (line 1) | const NOTE_MIN_WIDTH = 100
constant NOTE_MIN_HEIGHT (line 2) | const NOTE_MIN_HEIGHT = 100
constant NOTE_MAX_WIDTH (line 3) | const NOTE_MAX_WIDTH = 5000
constant NOTE_MAX_HEIGHT (line 4) | const NOTE_MAX_HEIGHT = 5000
function newNote (line 11) | function newNote ({ howManyTabs, t = 0, x = 0, y = 0, w = 500, h = 300, ...
function fromText (line 25) | function fromText (text) {
function toText (line 72) | function toText ({ tabs, notes }) {
FILE: src/main/main.js
function close (line 7) | function close () {
function start (line 12) | function start () {
FILE: src/main/state_manager.js
constant NEW_STATE (line 8) | const NEW_STATE = {
function saveState (line 17) | function saveState (state) {
function getNewState (line 21) | function getNewState () {
function getInitialState (line 25) | function getInitialState () {
function saveStateToFile (line 46) | function saveStateToFile (filePath, state) {
function getStateFromFile (line 50) | function getStateFromFile (filePath) {
function saveWindowState (line 62) | function saveWindowState (data) {
function getWindowState (line 66) | function getWindowState () {
FILE: src/renderer/notes.js
function generateId (line 17) | function generateId () {
function enableResizing (line 41) | function enableResizing (noteElm) {
function enableDragging (line 108) | function enableDragging (noteElm) {
function hideColorPicker (line 311) | function hideColorPicker () {
FILE: src/renderer/tabs.js
function scrollTabs (line 240) | function scrollTabs (amount) {
FILE: src/renderer/top_menu.js
function createMenuButton (line 8) | function createMenuButton (text, key) {
function handleCurrentState (line 62) | async function handleCurrentState () {
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (53K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 1537,
"preview": "name: Release binaries\n\non:\n release:\n types: [published]\n\njobs:\n\n build:\n\n runs-on: ${{ matrix.os }}\n strate"
},
{
"path": ".gitignore",
"chars": 18,
"preview": "node_modules\ndist\n"
},
{
"path": "LICENSE",
"chars": 1058,
"preview": "Copyright (c) 2020 Osvaldo Zonetti\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this"
},
{
"path": "README.md",
"chars": 470,
"preview": "<p align=\"center\">\n Cross-platform desktop note-taking app\n <br>\n Sticky notes + Markdown + Tabs\n <br>\n All in one "
},
{
"path": "package.json",
"chars": 891,
"preview": "{\n \"name\": \"zonote\",\n \"version\": \"0.4.4\",\n \"main\": \"src/main/main.js\",\n \"scripts\": {\n \"start\": \"electron . --dev\""
},
{
"path": "src/main/formatter.js",
"chars": 2540,
"preview": "const NOTE_MIN_WIDTH = 100\nconst NOTE_MIN_HEIGHT = 100\nconst NOTE_MAX_WIDTH = 5000\nconst NOTE_MAX_HEIGHT = 5000\n\nconst i"
},
{
"path": "src/main/main.js",
"chars": 2930,
"preview": "const path = require('path')\nconst { app, BrowserWindow, ipcMain, dialog } = require('electron')\nconst stateManager = re"
},
{
"path": "src/main/state_manager.js",
"chars": 1387,
"preview": "const fs = require('fs')\nconst path = require('path')\nconst Store = require('electron-store')\nconst formatter = require("
},
{
"path": "src/renderer/notes.js",
"chars": 11796,
"preview": "const shortid = require('shortid')\nconst marked = require('marked')\nconst DOMPurify = require('dompurify')\n\nmarked.setOp"
},
{
"path": "src/renderer/script.js",
"chars": 1264,
"preview": "const EventEmitter = require('events')\nconst electron = require('electron')\n\nwindow.STATE = {}\nwindow.GUI_STATE = { zInd"
},
{
"path": "src/renderer/tabs.js",
"chars": 8218,
"preview": "const hotkeys = require('hotkeys-js')\n\nconst tabsElm = document.getElementById('tabs')\nconst tabScrollLeftElm = document"
},
{
"path": "src/renderer/top_menu.js",
"chars": 4638,
"preview": "const hotkeys = require('hotkeys-js')\n\nconst menuElm = document.getElementById('menu')\nconst menuButtonElm = document.ge"
},
{
"path": "src/renderer/util.js",
"chars": 578,
"preview": "window.UTIL = {}\n\nwindow.UTIL.addClass = (elm, value) => {\n const classes = elm.className.split(' ')\n if (classes.incl"
},
{
"path": "static/index.html",
"chars": 1224,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <title>zonote</title>\n <meta http-equiv=\"Content-Security-Poli"
},
{
"path": "static/style.css",
"chars": 11758,
"preview": "@import url('https://fonts.googleapis.com/css?family=Merriweather:300');\n\n* {\n margin: 0;\n padding: 0;\n}\n\n:focus {\n o"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the zonetti/zonote GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (49.1 KB), approximately 14.5k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.