Initial commit

This commit is contained in:
= 2019-11-30 02:57:27 -06:00
commit 4425615de4
Signed by: kiichan
GPG key ID: 619DFD67F0976616
5 changed files with 921 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 kiichan@git.kiichan.space
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 (including the next
paragraph) 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.

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# Namadino
## The **Na**meless **Ma**rkdown **Di**rectory-organized **No**tation software
Namadino is a notation software that uses Markdown and directories to organize
your thoughts. It was originally designed to help with the translation of
written works.
## Features
### Markdown
Basic Markdown support is inplemented using the `mistune` library. Additionally,
one custom pattern has been added for what is called "hover text." Hover text
will add HTML `title` text to a root word. This is useful for adding definitions
to unknown words or hiding answers to problems when reviewing later.
<This is an example of hover text>(This text would be hidden)
<日本>(にっぽん | Japan)
### Exporting
All notes can be exported into an interactive HTML file. Each directory will
appear as a button that hides the content beneath it. These HTML files can
easily be distributed to others and viewed in any web browser with JavaScript
enabled.
### Integration with Anki
All instancecs of hover text can be exported into a file that can be imported
directly into Anki. There are options available to get all instances of hover
text on a single page, in a single folder, or across a whole project
### Password Protection
Notes can have a password set to limit who has access to a set of notes.
Exported files are saved in clear text however, so it is not recommended to use
those features if your notes contain sensitive information.

43
data/HTMLtemplate.html Normal file
View file

@ -0,0 +1,43 @@
<HTML>
<script>
function toggle (elem) {
if (elem.className === "vis") {
elem.className = "hid"
} else {
elem.className = "vis";
}
x = elem.nextSibling;
if(x.style.display === "block") {
x.style.display = "none";
} else {
x.style.display = "block";
}
}
</script>
<style>
div {
display: block;
padding: 1vw;
}
.vis {
font-style: normal;
background: lightgray;
}
.hid {
font-style: italic;
background: none;
}
.hid + div {
display: None;
}
.ht {
}
button {
width: 100%;
}
h1 {
text-align: center;
}
</style>
{}
</HTML>

816
namadino.py Normal file
View file

@ -0,0 +1,816 @@
from sys import argv
from os import mkdir, remove
from os.path import exists, join, dirname
from uuid import uuid4
from re import compile, findall
from tempfile import gettempdir
from webbrowser import open as wbopen
from PyQt5.QtGui import QIcon, QPalette, QColor
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QApplication, QWidget, QMainWindow, QLineEdit,
QVBoxLayout, QHBoxLayout, QSplitter, QAction, qApp, QLabel, QDesktopWidget,
QPlainTextEdit, QMenu, QMessageBox, QFileDialog, QColorDialog, QInputDialog,
QMessageBox,
)
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
from mistune import Markdown, Renderer, InlineLexer, InlineGrammar
from nacl.secret import SecretBox
from nacl.hash import generichash
from nacl.exceptions import CryptoError
from pickle import dump, load, dumps, loads
myPath = dirname(__file__)
DATADIR = join(myPath,"data")
TMPDIR = join(gettempdir(),"namadino_"+str(uuid4()))
mkdir(TMPDIR)
HTMLtemplate = ""
with open(join(DATADIR, "HTMLtemplate.html")) as f:
HTMLtemplate = f.read()
################################################################################
class Note(object):
def __init__(self, title="New note", markdown="Enter your notes here\n\n**Markdown** is supported. <Hover notes work like this>(Hello world)"):
super().__init__()
self.title = title
self.markdown = markdown
def toDict(self):
return self.__dict__.copy()
@classmethod
def fromDict(cls, state):
return cls(**state)
def setMarkdown(self, markdown):
self.markdown = markdown
def setTitle(self, title):
self.title = title
def copy(self):
return Note(self.title, self.markdown)
class Folder(object):
def __init__(self, title="New Folder"):
super().__init__()
self.title = title
self.folders = []
self.notes = []
def __getstate__(self):
return self.toDict()
def __setstate__(self, state):
self.__dict__.update( self.fromDict(state).__dict__ )
def toDict(self):
"""
Allow for folders to be pickled for saving
"""
state = {
"title": self.title,
"folders": [f.toDict() for f in self.folders],
"notes": [n.toDict() for n in self.notes]
}
return state
@classmethod
def fromDict(cls, state):
f = cls(state['title'])
f.folders = [Folder.fromDict(fstate) for fstate in state["folders"]]
f.notes = [Note.fromDict(nstate) for nstate in state["notes"]]
return f
def setTitle(self, title):
self.title = title
def addFolder(self, folder=None):
if not folder:
folder = Folder()
elif isinstance(folder, str):
folder = Folder(folder)
self.folders.append(folder)
return folder
def addNote(self, note=None):
if not note:
note = Note()
self.notes.append(note)
return note
def concatNotes(self):
out = []
for f in self.folders:
out += f.concatNotes()
for n in self.notes:
out.append(n)
return out
def deleteFolder(self, folder):
self.folders = [f for f in self.folders if f != folder]
def deleteNote(self, note):
self.notes = [n for n in self.notes if n != note]
def copy(self):
"""
Returns a copy of the folder, used for defaults
"""
out = Folder(self.title)
out.folders = [f.copy() for f in self.folders]
out.notes = [n.copy() for n in self.notes]
return out
################################################################################
hoverNoteRe = compile(r'<([^}]+?)>\(([^)]+?)\)')
class CustomRenderer(Renderer):
def hover_note(self, base, hover):
return '<a class="ht" href="" title="{1}">{0}</a>'.format(base, hover)
class CustomInlineGrammar(InlineGrammar):
hover_note = hoverNoteRe
class CustomInlineLexer(InlineLexer):
grammar_class = CustomInlineGrammar
def enable_hover_note(self):
self.rules.hoverNote = hoverNoteRe
self.default_rules.insert(0, 'hover_note')
def output_hover_note(self, m):
base = m.group(1)
hover = m.group(2)
return self.renderer.hover_note(base, hover)
renderer = CustomRenderer()
inline = CustomInlineLexer(renderer)
inline.enable_hover_note()
MD = Markdown(renderer, inline=inline)
################################################################################
DEFAULT_DATA = Folder("Untitled Project")
DEFAULT_DATA.addNote()
DEFAULT_DATA.notes[0].setTitle("Welcome")
DEFAULT_DATA.notes[0].setMarkdown(
"""Welcome to the **Na**meless **Ma**rkdown **Di**rectory-organized **No**tation
software, a.k.a. **Namadino** *(nah-mah-dee-no)*
Here you can organize your thoughts into folders and notes. This program was
originally written to aid in translating different media, however it can be
applied to nearly any study.
Notes support basic **Markdown** with one additional, custom pattern for "hover
text". Hover text allows you to put hidden definitions or solutions directly
above the relavent information when the note is rendered, like so: <日本>(にっぽん
| Japan). It also acts as a quick template for making Anki
flashcards. All instances of hover text in a project can be exported into a
`txt` file that can be imported into an Anki deck by using the `Export Project
To` menu at the top of the screen. The same can be done for all children in a
folder or all hover text in a single note by using the `Export To` menu in the
right-click menu.
Projects, folders, and notes can also be exported to an interactive HTML file.
These files can be shared easily and viewed with any web browser. Alternatively,
the HTML file can be opened directly in your browser.
"""
)
class GUI(QMainWindow):
def __init__(self):
super().__init__()
self.defaults()
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.render)
self.autosaveTime = 5*60*1000
self.autosavetimer = QTimer()
self.autosavetimer.timeout.connect(self.autoSave)
self.autosavetimer.start(self.autosaveTime)
self.loadSettings()
self.buildGUI()
def defaults(self):
self.data = DEFAULT_DATA.copy()
self.lastSave = Folder()
self.filepath = None
self.selectedNote = None
self.hashword = None
def buildGUI(self):
#self.setWindowIcon(QIcon(join(DATADIR,"d20_32x32.png")))
self.center()
self.statusBar().showMessage('Welcome to the thing with no name',3000)
self.setPalette(QPalette(QColor(self.settings["theme"])))
########################################################################
menubar = self.menuBar()
# File
fileMenu = menubar.addMenu('&File')
# File > New
new = QAction(QIcon.fromTheme("document-new"), "&New", self)
new.setShortcut("Ctrl+N")
new.setStatusTip("New notes")
new.triggered.connect(self.newProject)
fileMenu.addAction(new)
# File > Open
op = QAction(QIcon.fromTheme("document-open"), "&Open", self)
op.setShortcut("Ctrl+O")
op.setStatusTip("Open notes")
op.triggered.connect(self.openProject)
fileMenu.addAction(op)
# File > Save
sav = QAction(QIcon.fromTheme("document-save"), "&Save", self)
sav.setShortcut("Ctrl+S")
sav.setStatusTip("Save notes")
sav.triggered.connect(self.saveProject)
fileMenu.addAction(sav)
# File > Save As
sas = QAction(QIcon.fromTheme("document-save-as"), "Save &As", self)
sas.setShortcut("Ctrl+Shift+S")
sas.setStatusTip("Save notes")
sas.triggered.connect(self.saveProjectAs)
fileMenu.addAction(sas)
# File > View source code
fileMenu.addSeparator()
source = QAction(QIcon.fromTheme("applications-internet"),"View source code",self)
source.triggered.connect(self.openGitLab)
source.setStatusTip('Open source code in browser')
fileMenu.addAction(source)
fileMenu.addSeparator()
# File > Quit
exitAct = QAction(QIcon.fromTheme("application-exit"), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(qApp.quit)
fileMenu.addAction(exitAct)
# Edit
edtMenu = menubar.addMenu('&Edit')
# Edit > Color
co = QAction(QIcon.fromTheme("applications-graphics"), "Theme", self)
co.setStatusTip("Pick a color to generate a theme from")
co.triggered.connect(self.setTheme)
edtMenu.addAction(co)
# Export
expMenu = menubar.addMenu('&Export Project To')
expMenu.REF = self.data
self.makeExportMenu(expMenu, expMenu)
########################################################################
self.main = QSplitter()
self.main.setChildrenCollapsible(False)
self.tree = QTreeWidget(self.main)
self.tree.setColumnCount(1)
self.tree.setHeaderHidden(True)
self.traverseTree()
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.treeContext)
self.tree.currentItemChanged.connect(self.updateSel)
self.tree.itemChanged.connect(self.endRename)
self.note = QPlainTextEdit(self.main)
self.note.textChanged.connect(self.noteUpdate)
self.note.setEnabled(False)
self.note.setPlaceholderText("Select a note...")
self.renderer = QWebEngineView(self.main)
self.getCSSInversion()
self.renderer.setZoomFactor(1.5)
self.renderer.settings().setAttribute(
QWebEngineSettings.FocusOnNavigationEnabled, False)
self.renderer.keyPressEvent = None
self.renderHTML("")
self.main.setStretchFactor(0,1)
self.main.setStretchFactor(1,1)
self.main.setStretchFactor(2,1)
self.main.restoreState(self.settings["frame"])
# There is no event for when the handles are released
# without keeping track of all of the handles' `mouseReleaseEvent`s
# So instead we watch the constant update event with a timer.
self.main.splitterMoved.connect(self.frameResizing)
self.main.timer = QTimer()
self.main.timer.setSingleShot(True)
self.main.timer.timeout.connect(self.frameResized)
# Finished loading GUI.
self.setCentralWidget(self.main)
self.show()
def center(self):
"""
Resizes the window to be 80% of the available space and centers it
"""
space = QDesktopWidget().availableGeometry()
self.resize(
space.width() * 0.80,
space.height() * 0.80
)
qr = self.frameGeometry()
qr.moveCenter(space.center())
self.move(qr.topLeft())
def checkUnsaved(self):
self.autosavetimer.stop()
if self.data.toDict() == DEFAULT_DATA.toDict():
return True
if self.data.toDict() != self.lastSave.toDict():
alert = QMessageBox.question(self, "Unsaved changes",
"There are unsaved changes. Save before closing?".format(self.data.title),
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, QMessageBox.No)
if alert == QMessageBox.Yes:
self.saveProject()
elif alert == QMessageBox.Cancel:
return False
return True
def closeEvent(self, event):
_ = self.checkUnsaved()
def newProject(self):
if self.checkUnsaved():
self.defaults()
self.traverseTree()
self.resetNoteField()
self.statusBar().showMessage("Blank project opened",3000)
self.autosavetimer.start(self.autosaveTime)
def openProject(self):
if self.checkUnsaved():
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filepath, OK = QFileDialog.getOpenFileName(self,
"Open...",
self.data.title+".notes",
"Notes Files (*.notes);;Encrypted Notes Files *.enotes)",
options=options)
if not OK:
self.autosavetimer.start(self.autosaveTime)
return None
if filepath[-6:] == "enotes":
if not self.encOpen(filepath):
self.autosavetimer.start(self.autosaveTime)
return self.showError("Error opening project")
else:
self.hashword = None
self.openAction(filepath)
self.filepath = filepath
self.lastSave = self.data.copy()
self.traverseTree()
self.resetNoteField()
self.statusBar().showMessage("Opened "+self.data.title,3000)
self.autosavetimer.start(self.autosaveTime)
def openAction(self, fp):
with open(fp, "rb") as f:
self.data = load(f)
def encOpen(self, fp):
pwd, OK = QInputDialog.getText(self,
"Input password",
"Type the password for this project",
QLineEdit.Password)
if not OK:
return
hashword = generichash(bytes(pwd, "utf-8"), digest_size=16)
return self.encOpenAction(fp, hashword)
def encOpenAction(self, fp, hashword):
box = SecretBox(hashword)
data = None
with open(fp, "rb") as f:
try:
data = loads(box.decrypt(load(f)))
except CryptoError:
return self.showError("Invalid password")
except Exception as e:
return self.showError("Unknown error: "+str(e))
self.hashword = hashword
self.data = data
return True
def saveProject(self):
if not self.filepath:
self.saveProjectAs()
return None
self.saveActionWrap()
def saveProjectAs(self):
self.autosavetimer.stop()
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filepath, OK = QFileDialog.getSaveFileName(self,
"Save as...",
self.data.title+".notes",
"Notes File (*.notes);;"+
"Encrypted Notes File (*.enotes)",
options=options)
if OK:
self.filepath = filepath
self.saveActionWrap(resetHash=True)
self.autosavetimer.start(self.autosaveTime)
def saveActionWrap(self, resetHash=False):
if self.filepath[-6:].lower() == "enotes":
if not self.encSave(resetHash):
return
else:
self.hashword = None
self.saveAction(self.filepath, self.data)
self.lastSave = self.data.copy()
windowTitle = self.data.title
if self.hashword:
windowTitle += " <Secure>"
self.setWindowTitle(windowTitle + " - Namadino")
self.statusBar().showMessage("Saved {0} to {1}".format(self.data.title, self.filepath),3000)
def autoSave(self):
if not self.filepath:
return
if self.hashword:
self.encSaveAction(self.filepath, self.hashword)
else:
self.saveAction(self.filepath, self.data)
self.statusBar().showMessage("Project autosaved", 3000)
def saveAction(self, fp, data, full=True):
with open(fp, "wb") as f:
dump(data, f)
def encSave(self, resetHash):
success = False
if resetHash:
success = self.encValidate(None)
else:
success = self.encValitade(self.hashword)
if success:
self.encSaveAction(self.filepath, self.hashword)
def encValidate(self, pwd1):
if not pwd1:
pwd1, OK = QInputDialog.getText(self,
"Input password",
"Type a password for this project",
QLineEdit.Password)
if not OK:
return self.showError("Save aborted")
pwd1 = generichash(bytes(pwd1, "utf-8"), digest_size=16)
pwd2, OK = QInputDialog.getText(self,
"Confirm password",
"Reype the password for this project",
QLineEdit.Password)
if not OK:
return self.showError("Save aborted")
pwd2 = generichash(bytes(pwd2, "utf-8"), digest_size=16)
if pwd1 != pwd2:
return self.showError("Passwords do not match")
self.hashword = pwd1
return True
def encSaveAction(self, fp, hashword):
box = SecretBox(hashword)
self.saveAction(fp, box.encrypt(dumps(self.data)))
return True
def showError(self, error):
QMessageBox(QMessageBox.Critical,
"Error",
error
).exec_()
return
def traverseTree(self, parent=None, folder=None):
"""
Parse the data and populate the tree
"""
if not parent:
self.tree.clear()
trunk = QTreeWidgetItem([self.data.title])
trunk.REF = self.data
if self.hashword:
trunk.setIcon(0, QIcon.fromTheme("security-high"))
else:
trunk.setIcon(0, QIcon.fromTheme("package-x-generic"))
self.tree.addTopLevelItem(self.traverseTree(trunk, self.data))
windowTitle = self.data.title
if self.hashword:
windowTitle += " <Secure>"
self.setWindowTitle(windowTitle + " - Namadino")
return
for f in folder.folders:
branch = QTreeWidgetItem([f.title])
branch.REF = f
branch.setIcon(0, QIcon.fromTheme("folder"))
parent.addChild(self.traverseTree(branch, f))
for n in folder.notes:
leaf = QTreeWidgetItem([n.title])
leaf.REF = n
leaf.setIcon(0, QIcon.fromTheme("text-x-generic"))
parent.addChild(leaf)
return parent
def noteUpdate(self):
"""
Called every time the text box changes
Starts timer to save markdown
"""
self.timer.start(1000)
def render(self):
"""
Saves note text to active Note object
"""
if self.selectedNote:
self.selectedNote.setMarkdown(self.note.toPlainText())
self.renderHTML(MD(self.note.toPlainText()))
def treeContext(self, point):
sel = self.tree.currentItem()
if sel:
popMenu = QMenu(self)
if isinstance(sel.REF, Folder):
popMenu.new = QMenu("New...")
popMenu.new.f = QAction("Folder")
popMenu.new.f.triggered.connect(self.newFolder)
popMenu.new.f.PARENT = sel
popMenu.new.addAction(popMenu.new.f)
popMenu.new.n = QAction("Note")
popMenu.new.n.triggered.connect(self.newNote)
popMenu.new.n.PARENT = sel
popMenu.new.addAction(popMenu.new.n)
popMenu.addMenu(popMenu.new)
popMenu.rename = QAction("Rename")
popMenu.rename.PARENT = sel
popMenu.rename.triggered.connect(self.renameThing)
popMenu.addAction(popMenu.rename)
popMenu.export = QMenu("Export to...")
self.makeExportMenu(popMenu.export, sel)
popMenu.addMenu(popMenu.export)
popMenu.addSeparator()
if sel.REF != self.data:
popMenu.delete = QAction("Delete")
popMenu.delete.PARENT = sel
popMenu.delete.triggered.connect(self.deleteThing)
popMenu.addAction(popMenu.delete)
popMenu.exec_(self.tree.mapToGlobal(point))
def makeExportMenu(self, menu, parent):
menu.anki = QAction(QIcon.fromTheme("text-x-generic"), "Anki Text")
menu.anki.triggered.connect(self.hoverTextAnkiExport)
menu.anki.setStatusTip("Export all hover text pairs to Anki-importable file")
menu.anki.PARENT = parent
menu.addAction(menu.anki)
menu.html = QAction(QIcon.fromTheme("text-html"), "HTML")
menu.html.triggered.connect(self.htmlExportFile)
menu.html.setStatusTip("Export to interactive HTML file")
menu.html.PARENT = parent
menu.addAction(menu.html)
menu.web = QAction(QIcon.fromTheme("applications-internet"),"Web Browser")
menu.web.triggered.connect(self.htmlExportWebpage)
menu.web.setStatusTip("Open render directly in your browser")
menu.web.PARENT = parent
menu.addAction(menu.web)
def updateSel(self, sel, old):
if not sel:
# This doesn't seem to be possible but I don't trust it
self.resetNoteField()
elif isinstance(sel.REF, Note):
self.note.setEnabled(True)
self.note.setPlainText(sel.REF.markdown)
self.selectedNote = sel.REF
self.timer.stop()
self.render()
else:
self.resetNoteField()
if self.tree.isPersistentEditorOpen(old,0):
self.endRename(old)
def renameThing(self, thing=None):
if not thing:
thing = self.sender().PARENT
self.tree.openPersistentEditor(thing,0)
self.tree.focusNextChild()
def endRename(self, thing):
self.tree.closePersistentEditor(thing)
new = thing.data(0,thing.type())
if new:
thing.REF.setTitle(new)
else:
thing.setText(0, thing.REF.title)
def resetNoteField(self):
self.selectedNote = None
self.note.setEnabled(False)
self.note.setPlainText("")
self.note.setPlaceholderText("Select a note...")
self.timer.stop()
self.render()
def newFolder(self):
parent = self.sender().PARENT
f = parent.REF.addFolder()
self.newFinish(parent, f, "folder")
def newNote(self):
parent = self.sender().PARENT
n = parent.REF.addNote()
self.newFinish(parent, n, "text-x-generic")
def newFinish(self, parent, item, icon):
# This is janky but...
oldKids = [c.REF for c in parent.takeChildren()]
self.traverseTree(parent=parent, folder=parent.REF)
newKids = parent.takeChildren()
parent.addChildren(newKids)
newKid = [k for k in newKids if k.REF not in oldKids][0]
if not parent.isExpanded():
parent.setExpanded(True)
self.renameThing(newKid)
def deleteThing(self):
thing = self.sender().PARENT
if isinstance(thing.REF,Note):
confirm = QMessageBox.question(self, "Confirm Deletion",
"Permanently delete {}?".format(thing.REF.title),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
thing.parent().REF.deleteNote(thing.REF)
thing.parent().removeChild(thing)
else:
confirm = QMessageBox.question(self, "Confirm Deletion",
"Permanently delete {} and everything in it?".format(thing.REF.title),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
thing.parent().REF.deleteFolder(thing.REF)
thing.parent().removeChild(thing)
def allowExport(self):
if self.hashword:
confirm = QMessageBox.question(self, "Allow cleartext export",
("This project is password protected. Exporting it will allow "
"its contents to be read in clear text. Proceed with export?"),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.No:
return False
if not encValidate(self.hashword):
return False
return True
def hoverTextAnkiExport(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fp, OK = QFileDialog.getSaveFileName(self,
"Export to...",
data.title+".txt","Anki Import Files (*.txt)",
options=options)
if not OK:
return None
out = ""
if isinstance(data, Folder):
notes = data.concatNotes()
else:
notes = [data]
for n in notes:
for front, back in findall(hoverNoteRe, n.markdown):
out += front.replace("\t"," ")+"\t"+back.replace("\t"," ")+"\n"
with open(fp,"w") as f:
f.write(out)
self.statusBar().showMessage("Exported {0} to {1}".format(self.data.title, fp))
def htmlExportWebpage(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
tmp = join(TMPDIR, str(uuid4())+".html")
html = self.htmlExporter(data)
with open(tmp,"w") as f:
# format doesn't like the extra brackets from the CSS/JS
f.write(html)
wbopen(tmp)
def htmlExportFile(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fp, OK = QFileDialog.getSaveFileName(self,
"Export to...",
data.title+".html","HTML Files (*.html)",
options=options)
if not OK:
return None
html = self.htmlExporter(data)
with open(fp,"w") as f:
# format doesn't like the extra brackets from the CSS/JS
f.write(html)
self.statusBar().showMessage("Exported {0} to {1}".format(data.title, fp))
def htmlExporter(self, data):
out = "<head>\n <title>{0}</title>\n</head>\n<body><h1>{0}</h1>".format(data.title)
out = self.htmlExportRecurse(data, out)
out += "</body>"
return HTMLtemplate.replace("{}",out)
def htmlExportRecurse(self, data, out):
if isinstance(data, Folder):
for f in data.folders:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(f.title)
out = self.htmlExportRecurse(f, out)
out += "</div>"
for n in data.notes:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(n.title)
out += "<div>{}</div>".format(MD(n.markdown))
out += "</div>"
else:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(data.title)
out += "<div>{}</div>".format(MD(data.markdown))
out += "</div>"
return out
def setTheme(self):
col = QColorDialog.getColor()
if col.isValid():
p = QPalette(col)
self.setPalette(p)
self.getCSSInversion()
self.settings["theme"] = p.window().color().name()
self.renderHTML(self.renderer.HTML)
self.saveSettings()
def getCSSInversion(self):
"""
If the palette says the background should be black, invert colors
"""
self.renderer.CSS = (self.palette().base().color().name(),
self.palette().text().color().name())
def renderHTML(self, html):
self.renderer.HTML = html
self.renderer.setHtml(
"<style> body {{ background-color: {0}; color: {1};}}</style><body>".format(
*self.renderer.CSS
) + self.renderer.HTML + "</body>"
)
def defaultSettings(self):
self.settings = {
"theme": "#ffffff",
"frame": b'\x00\x00\x00\xff\x00\x00\x00\x01\x00\x00\x00\x03\x00\x01\x86\x9f\x00\x01\x86\x9f\x00\x01\x86\x9f\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00'
}
self.saveSettings()
def saveSettings(self):
with open(join(DATADIR, "preferences.pkl"),"wb") as f:
dump(self.settings, f)
def loadSettings(self):
prefs = join(DATADIR, "preferences.pkl")
if exists(prefs):
with open(prefs, "rb") as f:
self.settings = load(f)
else:
self.defaultSettings()
def frameResizing(self, ix, pos):
self.main.timer.start(3000)
def frameResized(self):
self.settings["frame"] = self.main.saveState()
self.saveSettings()
def openGitLab(self):
# Launch web browser pointed towards this script's source code.
wbopen("https://gitlab.com/WolfgangAxel/namadino")
if __name__ == '__main__':
app = 0
if QApplication.instance():
app = QApplication.instance()
else:
app = QApplication(argv)
G = GUI()
exit(app.exec_())

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
pyqt5
pynacl
mistune