commit 4425615de4675212679362d07f3e41b18891b2f4 Author: = Date: Sat Nov 30 02:57:27 2019 -0600 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e19c7e5 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..70e2e2f --- /dev/null +++ b/README.md @@ -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 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. diff --git a/data/HTMLtemplate.html b/data/HTMLtemplate.html new file mode 100644 index 0000000..99deafb --- /dev/null +++ b/data/HTMLtemplate.html @@ -0,0 +1,43 @@ + + + + {} + diff --git a/namadino.py b/namadino.py new file mode 100644 index 0000000..8ba20d0 --- /dev/null +++ b/namadino.py @@ -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. (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 '{0}'.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 += " " + 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 += " " + 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 = "\n {0}\n\n

{0}

".format(data.title) + out = self.htmlExportRecurse(data, out) + out += "" + return HTMLtemplate.replace("{}",out) + + def htmlExportRecurse(self, data, out): + if isinstance(data, Folder): + for f in data.folders: + out += '
'.format(f.title) + out = self.htmlExportRecurse(f, out) + out += "
" + for n in data.notes: + out += '
'.format(n.title) + out += "
{}
".format(MD(n.markdown)) + out += "
" + else: + out += '
'.format(data.title) + out += "
{}
".format(MD(data.markdown)) + out += "
" + 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( + "".format( + *self.renderer.CSS + ) + self.renderer.HTML + "" + ) + + 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_()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2ed06ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyqt5 +pynacl +mistune