From 043a8b8c30d374fb2f95962d2a1b851456c59dc5 Mon Sep 17 00:00:00 2001 From: knotteye Date: Mon, 1 Mar 2021 12:57:27 -0600 Subject: [PATCH] Initial Commit --- .gitignore | 4 + Makefile | 20 +++++ app.py | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ configure | 23 +++++ makeviewer.sh | 9 ++ 5 files changed, 287 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 app.py create mode 100755 configure create mode 100755 makeviewer.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88ebe76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +app.build +tabletoplibrary +minified +pdf.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bad7b11 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +ifeq ($(PREFIX),) + PREFIX := /usr/local +endif +ifeq ($(LIBDIR),) + LIBDIR := /lib +endif + + +all: build +build: viewer + nuitka3 --follow-imports app.py -o tabletoplibrary +clean: + rm -f tabletoplibrary + rm -rf pdf.js minified app.build +viewer: + @sh ./makeviewer.sh +install: + install tabletoplibrary $(PREFIX)/bin/ + mkdir -p $(LIBDIR)/tabletoplibrary + cp -r minified $(LIBDIR)/tabletoplibrary/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..c6a637e --- /dev/null +++ b/app.py @@ -0,0 +1,231 @@ +#!/bin/env python +import os, sys, urllib.parse, re, magic, shutil +from appdirs import AppDirs +from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets +from PyQt5.QtCore import QSettings, QCoreApplication, QUrl, QTimer +from PyQt5.QtWidgets import QApplication, QWidget, qApp, QMainWindow, QStyle, QAction, QTabWidget, QInputDialog, QFileDialog, QLineEdit +from PyQt5.QtGui import QIcon +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineDownloadItem +from PyQt5.QtWebChannel import QWebChannel + +DATA_DIR = AppDirs("TabletopLibrary", "knotteye").user_data_dir +LIB_DIR = '/lib' + +def _mkdir(_dir): + if os.path.isdir(_dir): pass + elif os.path.isfile(_dir): + raise OSError("%s exists as a regular file." % _dir) + else: + parent, directory = os.path.split(_dir) + if parent and not os.path.isdir(parent): _mkdir(parent) + if directory: os.mkdir(_dir) + + +class TabBar(QtWidgets.QTabBar): + def tabSizeHint(self, index): + s = QtWidgets.QTabBar.tabSizeHint(self, index) + s.transpose() + return s + + def paintEvent(self, event): + painter = QtWidgets.QStylePainter(self) + opt = QtWidgets.QStyleOptionTab() + + for i in range(self.count()): + self.initStyleOption(opt, i) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, opt) + painter.save() + + s = opt.rect.size() + s.transpose() + r = QtCore.QRect(QtCore.QPoint(), s) + r.moveCenter(opt.rect.center()) + opt.rect = r + + c = self.tabRect(i).center() + painter.translate(c) + painter.rotate(90) + painter.translate(-c) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, opt); + painter.restore() + + +class TabWidget(QtWidgets.QTabWidget): + def __init__(self, *args, **kwargs): + QtWidgets.QTabWidget.__init__(self, *args, **kwargs) + self.setTabBar(TabBar(self)) + self.setTabPosition(QtWidgets.QTabWidget.West) + + +class App(QMainWindow): + settings = QSettings() + currentDownloads = [] + def __init__(self): + self.vertTabDict = {} + super().__init__() + self.title = 'Tabletop Library' + self.initUI() + + def initUI(self): + self.setWindowTitle(self.title) + self.setGeometry(self.settings.value('left', type=int) or 10, self.settings.value('top', type=int) or 10, self.settings.value('width', type=int) or 640, self.settings.value('height', type=int) or 480) + + exitAction = QAction('&Exit', self) + exitAction.setShortcut('Ctrl+Q') + exitAction.setStatusTip('Exit application') + exitAction.triggered.connect(self.close) + + saveAction = QAction('Save', self, triggered=self.saveCurrent) + saveAction.setShortcut('Ctrl+S') + saveAction.setStatusTip('Save current tab') + + saveAllAction = QAction('Save All', self, triggered=self.saveAll) + saveAllAction.setShortcut('Ctrl+Shift+S') + saveAllAction.setStatusTip('Save all tabs') + + newCharAction = QAction("New Collection", self, triggered=self.newCharDialog) + newCharAction.setShortcut('Ctrl+Shift+N') + + newFileAction = QAction("New File", self, triggered=self.newFileDialog) + newFileAction.setShortcut('Ctrl+N') + + menubar = self.menuBar() + + filemenu = menubar.addMenu("File") + filemenu.addAction(newFileAction) + filemenu.addAction(newCharAction) + filemenu.insertSeparator(QAction()) + filemenu.addAction(saveAction) + filemenu.addAction(saveAllAction) + filemenu.insertSeparator(QAction()) + filemenu.addAction(exitAction) + + self.saveOnExitPref = QAction('Save On Exit', self, checkable=True) + self.saveOnExitPref.setChecked(self.settings.value('saveOnExit', type=bool)) + self.saveOnExitPref.setToolTip('May cause the app to be sluggish when closing') + + editmenu = menubar.addMenu("Edit") + editmenu.setToolTipsVisible(True) + editmenu.addAction(self.saveOnExitPref) + + self.vtabs = TabWidget() + + self.getChars() + + self.setCentralWidget(self.vtabs) + self.show() + + QtWebEngineWidgets.QWebEngineProfile.defaultProfile().downloadRequested.connect( self.on_downloadRequested ) + + def closeEvent(self, event): + if self.saveOnExitPref.isChecked(): + self.saveAll() + if not self.currentDownloads and self.saveOnExitPref.isChecked(): + QTimer.singleShot(200, self.close) + event.ignore() + return + self.settings.setValue("left", self.x()) + self.settings.setValue("top", self.y()) + self.settings.setValue("width", self.width()) + self.settings.setValue("height", self.height()) + self.settings.setValue('saveOnExit', self.saveOnExitPref.isChecked()) + event.accept() + + def getChars(self): + _mkdir(DATA_DIR+"/collections") + for _, dirs, _ in os.walk(DATA_DIR+"/collections"): + for direc in dirs: + self.addChar(direc) + break + + def addChar(self, name): + _mkdir(DATA_DIR+"/collections/"+name) + self.vertTabDict[name] = CTab(QTabWidget(), name) + self.vtabs.addTab(self.vertTabDict[name].qobject, name) + for _, _, files in os.walk(DATA_DIR+"/collections/"+name): + for file in files: + mime = magic.from_file(DATA_DIR+"/collections/"+name+"/"+file, mime=True) + if(mime == "application/pdf"): + self.vertTabDict[name].makeChild(file) + return + break + + def saveCurrent(self): + self.vtabs.currentWidget().currentWidget().page().runJavaScript('document.getElementById("download").click()') + + @QtCore.pyqtSlot("QWebEngineDownloadItem*") + def on_downloadRequested(self, download): + regex = re.compile('^file://[\\/\w:]+viewer\.html\?file=') + #strip out the file://whatever/viewer.html?file= and we're left with the file path + download.setPath(regex.sub('', download.page().url().toString())) + download.accept() + self.currentDownloads.append(download) + download.finished.connect(self.finishDownload) + + def finishDownload(self): + for d in self.currentDownloads: + if d.isFinished(): + del d + return + + def saveAll(self): + for vt in self.vertTabDict: + for t in self.vertTabDict[vt].children: + self.vertTabDict[vt].children[t].page().runJavaScript('document.getElementById("download").click()') + + def newCharDialog(self): + text, ok = QInputDialog.getText(self, "New Collection", "Name:", QLineEdit.Normal, "") + if ok and text != '': + self.addChar(text) + + def newFileDialog(self): + qfd = QFileDialog(self) + qfd.setFileMode(QFileDialog.ExistingFile) + qfd.setNameFilters(["TabletopLibrary Documents (*.pdf)", "All Files (*)"]) + item, ok = QInputDialog.getItem(self, "Add to which collection?","Collection:", self.vertTabDict.keys(), self.vtabs.currentIndex(), False) + if not ok: + return + qfd.filesSelected.connect(self.vertTabDict[item].addFile) + qfd.show() + + +class CTab(object): + def __init__(self, qobject, name, children={}): + self.qobject = qobject + self.children = children + self.name = name + + def makeChild(self, filename): + print('makechild') + self.children[filename] = QWebEngineView() + self.qobject.addTab(self.children[filename], filename) + try: + viewerfile = sys.argv[1] + except: + if os.path.isfile(DATA_DIR+'/minified/web/viewer.html'): + viewerfile = DATA_DIR+'/minified/web/viewer.html' + elif os.path.isfile(LIB_DIR+'/tabletoplibrary/minified/web/viewer.html'): + viewerfile = LIB_DIR+'/tabletoplibrary/minified/web/viewer.html' + else: + print("No viewer file") + exit(1) + self.children[filename].load(QUrl('file://'+viewerfile+'?file='+urllib.parse.quote(DATA_DIR+'/collections/'+self.name+'/'+filename))) + self.children[filename].show() + + def addFile(self, pathlist): + # Check for duplicates because of this bug + # https://forum.qt.io/topic/41356/qfiledialog-signal-fileselected-is-triggered-twice-for-one-user-choice-changed-in-recent-qt/ + for path in pathlist: + _mkdir(DATA_DIR+"/collections/"+self.name) + if os.path.isfile(DATA_DIR+'/collections/'+self.name+'/'+os.path.basename(path)): + return + shutil.copyfile(path, DATA_DIR+'/collections/'+self.name+'/'+os.path.basename(path)) + self.makeChild(os.path.basename(path)) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + app.setOrganizationName("knotteye") + app.setApplicationName("Tabletop Library") + ex = App() + sys.exit(app.exec_()) diff --git a/configure b/configure new file mode 100755 index 0000000..c36b210 --- /dev/null +++ b/configure @@ -0,0 +1,23 @@ +#!/bin/sh +for arg in "$@" +do + case $arg in +# Install bin to PREFIX/bin +# specify with --prefix "/whatever" + --prefix) + export PREFIX="$2" + sed "s#PREFIX := /usr/local#PREFIX := $2#g" Makefile | tee Makefile + shift # Remove argument name from processing + shift # Remove argument value from processing + ;; +# Install viewer files to LIBDIR/tabletoplibrary +# specify with --libdir "/libwhatver" + --libdir) + export LIBDIR="$2" + sed "s#LIBDIR := /lib#LIBDIR := $2#g" Makefile | tee Makefile + sed "s#LIB_DIR = \'/lib\'#LIB_DIR = \'$2\'#g" app.py | tee app.py + shift + shift + ;; + esac +done diff --git a/makeviewer.sh b/makeviewer.sh new file mode 100755 index 0000000..b2f2243 --- /dev/null +++ b/makeviewer.sh @@ -0,0 +1,9 @@ +#!/bin/sh +if [[ -s ./minified/web/viewer.html ]]; then + exit +fi +git clone https://github.com/mozilla/pdf.js/ +cd pdf.js +npm i +./node_modules/.bin/gulp minified +mv build/minified ..