######################################################### ## @file : kmanapp.py ## @desc : kindle note managerment tool GUI ## @create : 2020/06/03 ## @author : Chengan ## @email : douboer@gmail.com ######################################################### import sys import os from time import sleep import pandas as pd import threading from PySide2.QtWidgets import * from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, QAbstractTableModel, QObject, QPoint, QRect, QSize, QTime, QUrl, Qt, QThread, Signal, QTimer) from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, QPixmap, QRadialGradient, QStandardItem, QStandardItemModel) from mainwindow import Ui_MainWindow from kman import * from parseweb import * # import binary resource file(kmanapp_rc.py) import kmanapp_rc notes_temp = """
《{bookname}》 {author} ({time}) 【{note}】


{content} 【P{position}】
""" words_temp = """
{usage}

{bookname} {author} {category} {timestamp} {position}


""" ONLY_TEST = 1 class kmanWindow(QMainWindow): """ def __init__(self, *args, **kwargs): super(kmanWindow, self).__init__(*args, **kwargs) """ flag = True def __init__(self, parent=None): super(kmanWindow, self).__init__(parent) # create ui and initial it ui = Ui_MainWindow() ui.setupUi(self) self.ui = ui self.add_ui_component() # initial tree selected self.tree_selected = 'note_root' self.km = kMan() self.spide = bookInfoSpide() # initial check order: # 1. backup file bk.data -> # 2. kindle(My Clippings.txt) -> # 3. local file(config) -> flg = 0 if os.path.exists(BACKUPNOTEFN) and os.path.exists(BACKUPWORDFN): self.books_data = self.km.json2dict(BACKUPNOTEFN) self.words_data = self.km.json2dict(BACKUPWORDFN) if (len(self.books_data)*len(self.words_data[0]))>=1: self.books_data = self.km.json2dict(BACKUPNOTEFN) self.words_data = self.km.json2dict(BACKUPWORDFN) flg = 1 if self.km.get_kindle_path() and (not flg): self.import_kindle() else: self.books_data = self.km.import_clips() self.words_data = self.km.import_words() [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data) #self.filter_list = self.km.filter_words(self.words_data) self.fill_treeview() self.refresh_ui_component(comp=1) # timer to check status of kindle self.timer = QTimer(self) self.timer.timeout.connect(self.show_status_info) self.timer.start(1000) # connect action/toolbutton to slot functions ui.actionimportkindle.triggered.connect(lambda: self.import_kindle()) ui.actionimportlocal.triggered.connect(lambda: self.import_local()) ui.actionconfig.triggered.connect(lambda: self.config()) ui.actionwords.triggered.connect(lambda: self.words()) ui.actionstatistic.triggered.connect(lambda: self.statistic()) ui.actionhomepage.triggered.connect(lambda: self.homepage()) ui.actionabout.triggered.connect(lambda: self.about()) ui.actionflush.triggered.connect(lambda: self.refresh()) ui.actionexport.triggered.connect(lambda: self.export()) #ui.searchLineEdit.returnPressed.connect(self.search_return_press()) ui.searchComboBox.currentIndexChanged.connect(self.search_scope_change) ui.searchToolButton.clicked.connect(self.search_button_clicked) ui.treeView.clicked.connect(self.tree_item_clicked) ui.tableView.clicked.connect(self.table_item_clicked) ui.tableView.horizontalHeader().setStretchLastSection(True) #ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) ui.tableView.verticalHeader().hide() ui.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) ui.tableView.setColumnWidth(0, 40) # type ui.tableView.setColumnWidth(2, 50) # author ui.tableView.selectRow(0) # initial tableView data = self.convert_to_panda(self.filter_list) ui.tablemodel = nTableModel(data) ui.tableView.verticalHeader().hide() ui.tableView.setModel(self.ui.tablemodel) def add_ui_component(self): self.ui.searchComboBox.addItems([u'ALL',u'BOOKNAME',u'CONTENT',u'AUTHOR']) self.ui.treeView.resize(200,200) # status bar self.ui.status_label = QLabel() self.ui.pe = QPalette() def refresh_ui_component(self, comp=0): """ refresh treeView, tableview, textedit information after import or open clips file Args: comp 0 - treeview + tablevew + textedit , initial, fill_treeview() move to __init__() 1 - tablevew + textedit , note tree view clicked 2 - textedit , note table view clicked 3 - tablevew + textedit , word tree view clicked 4 - textedit , word table view clicked """ # refresh tableview click note tree if comp in [1,2]: data = self.convert_to_panda(self.filter_list,0) # refresh tableview click word tree elif comp in [3,4]: data = self.convert_to_panda(self.filter_list,1) # refresh tableview content if hasattr(self.ui, 'tablemodel'): del self.ui.tablemodel self.ui.tablemodel = nTableModel(data) self.ui.tableView.verticalHeader().hide() self.ui.tableView.setModel(self.ui.tablemodel) #self.ui.tablemodel.tabledata_update.connect(self.tabledata_update_slot) # refresh textedit content if comp in [1,2]: if len(self.filter_list)>0: [stype,sbookname,sauthor,sposition,stime,scontent] = self.filter_list[0] self.ui.textEdit.setHtml(notes_temp.format( bookname=sbookname, author=sauthor, time=stime, note=stype, content=scontent, position=sposition)) elif comp in [3,4]: self.render_textedit_words(self.words_data) self.show_status_info() def render_textedit_words(self, wdata, mrow=100): [bookinfo, words, lookups] = wdata index = self.ui.tableView.currentIndex() if index.row() == -1: word = self.filter_list[0][0] else: word = index.sibling(index.row(), 0).data() txt = "" for row in lookups: if words[row[1]]['word'] == word: [susage, stimestamp, sbookname, sauthor, scategory, sposition] = \ [row[4],row[5],bookinfo[row[2]]['bookname'], \ bookinfo[row[2]]['author'], words[row[1]]['category'],row[3]] txt += words_temp.format( usage=susage,bookname=sbookname, author=sauthor,category=scategory, timestamp=stimestamp,position=sposition) self.ui.textEdit.setHtml(txt) def convert_to_panda(self, mlist, tp=0): if tp==0: pdframe = pd.DataFrame(mlist, \ columns = ['Type','Bookname','Author','Position','Date','Content']) else: pdframe = pd.DataFrame(mlist, \ columns = ['Word','Bookname','Author','Category']) return pdframe def tabledata_update_slot(self, s): print('call tabledata_update_slot() {}'.format(s)) def tree_item_clicked(self, modelidx): model_index_list = self.ui.treeView.selectedIndexes() # QModelIndexList model_index = model_index_list[0] # QModelIndex itemmodel = model_index.model() #QAbstractItemModel/QStandardItemModel item = itemmodel.itemFromIndex(modelidx) #QStandardItem self.tree_selected = item.accessibleDescription() print(self.tree_selected, item.text()) # filter clips self.filter_list = None info = re.split(r'\s+',item.text())[0] comp = 0 if self.tree_selected in ['note_root', 'note_bookname', 'note_author']: [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data) comp = 1 elif self.tree_selected in ['word_root', 'word_bookname']: self.filter_list = self.km.filter_words(self.words_data) comp = 3 elif self.tree_selected == 'note_bleaf': # bookname leaf comp = 1 [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data, info, 1) elif self.tree_selected == 'note_aleaf': # author leaf comp = 1 [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data, info, 2) elif self.tree_selected == 'word_leaf': # word bookname leaf comp = 3 self.filter_list = self.km.filter_words(self.words_data, info) else: return if comp == 3: self.ui.tableView.setColumnWidth(1, 50) # author self.ui.tableView.setColumnWidth(3, 50) # category # QHeaderView::Interactive 0 # QHeaderView::Stretch 1 # QHeaderView::Fixed 2 # QHeaderView::ResizeToContents 3 self.ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) if comp == 1: self.ui.tableView.setColumnWidth(0, 40) # type self.ui.tableView.setColumnWidth(2, 50) # author self.ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.refresh_ui_component(comp) def table_item_clicked(self, index): """ print('tableView.currentIndex().row() {} tableView.currentIndex().column() {}' .format(self.ui.tableView.currentIndex().row(), self.ui.tableView.currentIndex().column())) """ if not index.isValid(): return if self.tree_selected.split('_')[0]=='note': row = index.row() stype = index.sibling(row, 0).data() sbookname = index.sibling(row, 1).data() sauthor = index.sibling(row, 2).data() sposition = index.sibling(row, 3).data() stime = index.sibling(row, 4).data() scontent = index.sibling(row, 5).data() self.ui.textEdit.setHtml(notes_temp.format( bookname=sbookname, author=sauthor, time=stime, note=stype, content=scontent, position=sposition)) else: self.render_textedit_words(self.words_data) def search_button_clicked(self): search_word = self.ui.searchLineEdit.text() if search_word.strip() == '': self.messagebox(ico=2, info=u'\n\n search content is empty!') return 0 content_type = self.ui.searchComboBox.currentText() #content_idx = self.ui.searchComboBox.currentIndex() [nu, sbks] = self.km.search_clip(self.books_data, search_word, 'ALL', content_type) [self.filter_books, self.filter_list] = self.km.filter_clips(sbks) self.refresh_ui_component(1) print( 'call search_button_clicked()' ) def keyPressEvent(self, event): #print('hasfocus {} key {} {} {}'.format(self.ui.searchLineEdit.hasFocus(),\ # event.key(), Qt.Key_Return, event.key()==Qt.Key_Return)) if (self.ui.searchLineEdit.hasFocus() and event.key() == Qt.Key_Return): print('call keyPressEvent() {} '.format(event.key())) self.search_button_clicked() def search_return_press(self): self.search_button_clicked() print('call search_return_press()') # XXX def search_scope_change(self,t): p = {0:'ALL',1:'TITLE',2:'AUTHOR',3:'CONTENT'} s = self.ui.searchLineEdit.text() #print(self.books_data) #print(search_clip(self.books_data,s,'ALL',p[t])) print('call search_scope_change()') ## XXX ''' def check_kindle_status(self): while self.flag: self.show_status_info() sleep(2) ''' def show_status_info(self): """ show status information on statusbar Args: conn: 1 if kindle is connected else 0 Return: conn """ status = self.km.status_info() self.ui.statusbar.showMessage(status[0],0) self.ui.status_label.setText(status[1]) if not self.km.status: self.ui.pe.setColor(QPalette.WindowText,Qt.red) #self.ui.status_label.setAutoFillBackground(True) self.ui.status_label.setPalette(pe) self.ui.statusbar.addPermanentWidget(self.ui.status_label, stretch=0) # define slot functions def import_kindle(self): fp = self.km.get_kindle_path() if not fp: self.messagebox(ico=2, info='\n\n kindle is not connected') return 0 self.books_data = self.km.import_clips(fp+'documents/'+CLIPFN) self.words_data = self.km.import_words(fp+'system/vocabulary/'+WORDFN) [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data) self.filter_wordlist = self.km.filter_words(self.words_data) self.fill_treeview() self.refresh_ui_component(1) def import_local(self): fn, ft = QFileDialog.getOpenFileName(self, "choose file to import", './', # start path "All Files (*);;Text Files (*.txt)") # filter file type self.fn = fn self.books_data = self.km.import_clips(fn) [self.filter_books, self.filter_list] = self.km.filter_clips(self.books_data) self.fill_treeview() self.refresh_ui_component(1) #print('filename ', fn, 'filetype ', ft) if fn == "": return False def fill_treeview(self): self.ui.model = QStandardItemModel() rootItem = self.ui.model.invisibleRootItem() item = QStandardItem('All Notes ({})'.format(self.km.get_totalnum_nt(self.books_data))) icon = QIcon() icon.addFile(u":/icons/emblem_library.png", QSize(), QIcon.Normal, QIcon.Off) item.setIcon(icon) item.setAccessibleDescription('note_root') rootItem.appendRow(item) parent_item = item # add bookname tree [numbooks, booknum] = self.km.get_bookname_num(self.books_data) bookname_item = QStandardItem('Bookname ({})'.format(numbooks)) icon = QIcon() icon.addFile(u":/icons/book_open.png", QSize(), QIcon.Normal, QIcon.Off) bookname_item.setIcon(icon) bookname_item.setAccessibleDescription('note_bookname') parent_item.appendRow(bookname_item) if numbooks > 0: for k, v in booknum.items(): item = QStandardItem('{} ({})'.format(k, v)) icon = QIcon() icon.addFile(u":/icons/book_open_bookmark.png", QSize(), QIcon.Normal, QIcon.Off) item.setIcon(icon) item.setAccessibleDescription('note_bleaf') bookname_item.appendRow(item) # add author tree [numauthor, authnum] = self.km.get_author_num(self.books_data) author_item = QStandardItem('Author ({})'.format(numauthor)) icon = QIcon() icon.addFile(u":/icons/person.png", QSize(), QIcon.Normal, QIcon.Off) author_item.setIcon(icon) author_item.setAccessibleDescription('note_author') parent_item.appendRow(author_item) parent_item = author_item if numauthor > 0: for k, v in authnum.items(): item = QStandardItem('{} ({})'.format(k, v)) icon = QIcon() icon.addFile(u":/icons/user.png", QSize(), QIcon.Normal, QIcon.Off) item.setIcon(icon) item.setAccessibleDescription('note_aleaf') author_item.appendRow(item) # add words root word_rootItem = self.ui.model.invisibleRootItem() [numwords, wordnum] = self.km.get_book_word_num(self.words_data) item = QStandardItem('All Words({})'.format(numwords)) icon = QIcon() icon.addFile(u":/icons/emblem_library.png", QSize(), QIcon.Normal, QIcon.Off) item.setIcon(icon) item.setAccessibleDescription('word_root') word_rootItem.appendRow(item) word_parent_item = item # add word bookname tree word_bookname_item = QStandardItem('Bookname ({})'.format(numbooks)) icon = QIcon() icon.addFile(u":/icons/book_open.png", QSize(), QIcon.Normal, QIcon.Off) word_bookname_item.setIcon(icon) word_bookname_item.setAccessibleDescription('word_bookname') word_parent_item.appendRow(word_bookname_item) if numwords > 0: for k, v in wordnum.items(): item = QStandardItem('{} ({})'.format(k, v)) icon = QIcon() icon.addFile(u":/icons/book_open_bookmark.png", QSize(), QIcon.Normal, QIcon.Off) item.setIcon(icon) item.setAccessibleDescription('word_leaf') word_bookname_item.appendRow(item) self.ui.treeView.setModel(self.ui.model) self.ui.treeView.expandAll() def config(self): print("call slot config()") pass def words(self): print("call slot words()") pass def statistic(self): print("call slot statistic()") pass def homepage(self): import webbrowser webbrowser.open('https://gitee.com/douboer/kman') print("call slot homepage()") pass def about(self): self.messagebox(ico=1, info='\n'+ \ ' kindle management tool \n\n' + \ ' v1.0.4\n\n' + \ ' Author: chengan\n\n' + \ ' douboer@gmail.com') print("call slot about()") pass def refresh(self): self.import_kindle() print("call slot refresh()") pass def export(self): if self.tree_selected.split('_')[0]=='note': self.export_filter_notes() else: self.export_filter_words() print("call export()") def export_filter_notes(self): self.km.export_notes(self.filter_books, 'export', ft='MD') pass def export_filter_words(self): self.km.export_words(self.words_data, self.filter_list, 'export', ft='MD') pass def messagebox(self, ico=1, info=''): """ unify messagebox Args: ico - QMessageBox.NoIcon 0 QMessageBox.Information 1 QMessageBox.Warning 2 QMessageBox.Critical 3 QMessageBox.Question 4 """ icons = {0:QMessageBox.NoIcon, \ 1:QMessageBox.Information, \ 2:QMessageBox.Warning, \ 3:QMessageBox.Critical, \ 4:QMessageBox.Question } msgBox = QMessageBox() msgBox.setText(info) msgBox.setInformativeText("") msgBox.setIcon(icons[ico]) msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) msgBox.setBaseSize(QSize(600, 300)) r = msgBox.exec() # backup file when kman closed # read backup file when kman start def closeEvent(self, e): with open(BACKUPNOTEFN, 'w', encoding='utf8', errors='ignore') as fw: fw.write(self.km.dict2json(self.books_data)) with open(BACKUPWORDFN, 'w', encoding='utf8', errors='ignore') as fw: fw.write(self.km.dict2json(self.words_data)) # stop check thread self.flag = False def grab_all_book_info(self): for bkname in self.books_data.keys(): bkname = re.split(r'[\((\-\::_\s]',bkname.strip())[0] print(bkname) bkinfo = self.spide.grab_book_info(bkname) filter_bkinfo = self.spide.filter_spide_book(bkinfo) if filter_bkinfo: self.spide.down_book_img(filter_bkinfo) # thanks Martin Fitzpatrick ^_^ # https://www.learnpyqt.com/courses/model-views/qtableview-modelviews-numpy-pandas/ class nTableModel(QAbstractTableModel): tabledata_update = Signal(str) def __init__(self, data): super(nTableModel, self).__init__() self._data = data def data(self, index, role): if role == Qt.DisplayRole: value = self._data.iloc[index.row(), index.column()] self.tabledata_update[str].emit('{} {}'.format(index.row(),index.column())) return str(value) def rowCount(self, index): return self._data.shape[0] def columnCount(self, index): return self._data.shape[1] def headerData(self, section, orientation, role): # section is the index of the column/row. if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return str(self._data.columns[section]) if orientation == Qt.Vertical: return str(self._data.index[section]) if __name__ == "__main__": app = QApplication(sys.argv) kmw = kmanWindow() icon = QIcon() icon.addFile(u":/icons/Cbb20.png", QSize(), QIcon.Normal, QIcon.Off) kmw.setWindowIcon(icon) kmw.setWindowTitle("kindle management") kmw.resize(900, 600) #kmw.showFullScreen() kmw.show() trd = threading.Thread(target=kmw.grab_all_book_info) trd.setDaemon(True) trd.start() # loop check kindle is connected or not # BUG to be implement XXXX """ try: t = threading.Thread(target=kmw.check_kindle_status) t.start() except: print ("Error: can not start thread") """ app.exec_()