Initial Commit
commit
043a8b8c30
|
@ -0,0 +1,4 @@
|
||||||
|
app.build
|
||||||
|
tabletoplibrary
|
||||||
|
minified
|
||||||
|
pdf.js
|
|
@ -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/
|
|
@ -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_())
|
|
@ -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
|
|
@ -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 ..
|
Loading…
Reference in New Issue