'update'
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
# 改代码后续丢弃
|
||||
# 不使用matplotlib库来实现绘图,使用QT6自带的绘图功能
|
||||
|
||||
# --- IGNORE ---
|
||||
import sys, os, re, datetime, math
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QListWidget,
|
||||
QFileDialog, QMessageBox, QLineEdit, QFormLayout, QDialog, QDialogButtonBox
|
||||
QApplication, QWidget, QLabel, QListWidget, QMessageBox, QLineEdit,
|
||||
QFormLayout, QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout
|
||||
)
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtGui import QIcon, QPixmap
|
||||
from PyQt6.QtCore import QSettings, QSize, QByteArray
|
||||
from PyQt6 import uic
|
||||
import config
|
||||
from exportbooknotes import BookNotesExporter
|
||||
from booklist_parse import BookListManager
|
||||
from review_worker import BookReviewWorker
|
||||
from cover_mixin import CoverMixin
|
||||
from finished_books_mixin import FinishedBooksMixin
|
||||
|
||||
|
||||
class ConfigDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
@@ -25,288 +31,422 @@ class ConfigDialog(QDialog):
|
||||
layout.addRow(attr, inp)
|
||||
self.inputs[attr] = inp
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.accepted.connect(self._accept_and_apply)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def _accept_and_apply(self):
|
||||
# 写回 config 变量
|
||||
updated = self.get_config()
|
||||
for k, v in updated.items():
|
||||
if not k.isupper():
|
||||
continue
|
||||
old = getattr(config, k, None)
|
||||
if isinstance(old, int):
|
||||
try:
|
||||
setattr(config, k, int(v)); continue
|
||||
except Exception:
|
||||
pass
|
||||
setattr(config, k, v)
|
||||
if self.parent() and hasattr(self.parent(), '_apply_global_font'):
|
||||
try: self.parent()._apply_global_font()
|
||||
except Exception: pass
|
||||
self.accept()
|
||||
|
||||
def get_config(self):
|
||||
return {k: v.text() for k, v in self.inputs.items()}
|
||||
|
||||
class IBookExportApp(QWidget):
|
||||
|
||||
class IBookExportApp(CoverMixin, FinishedBooksMixin, QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 加载 UI 文件
|
||||
ui_file = os.path.join(os.path.dirname(__file__), 'ibook_export_app.ui')
|
||||
uic.loadUi(ui_file, self)
|
||||
|
||||
# 设置窗口标题
|
||||
self.setWindowTitle("notesExporter")
|
||||
|
||||
# 设置窗口图标
|
||||
self.setWindowTitle("notesExporter (matplot)")
|
||||
if os.path.exists(config.APP_ICON):
|
||||
self.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
# 启动前同步一次源数据到本地,确保后续 AnnotationManager 读取的是最新副本
|
||||
# 数据同步
|
||||
try:
|
||||
from exportbooknotes import sync_source_files
|
||||
sync_source_files(config)
|
||||
except Exception as e:
|
||||
print(f"警告: 初始同步源数据失败: {e}")
|
||||
|
||||
# 初始化数据
|
||||
print('警告: 初始同步源数据失败:', e)
|
||||
# 数据加载
|
||||
self.exporter = BookNotesExporter(config)
|
||||
self.manager = BookListManager(plist_path=config.LOCAL_BOOKS_PLIST, db_path=config.LOCAL_LIBRARY_DB)
|
||||
self.booksinfo = self.manager.get_books_info()
|
||||
self.last_open_times = self.manager.get_books_last_open()
|
||||
self.assetid2name = {}
|
||||
self.assetid2lastopen = {}
|
||||
for assetid, info in self.booksinfo.items():
|
||||
name = info.get('displayname') or info.get('itemname') or assetid
|
||||
self.assetid2name, self.assetid2lastopen = {}, {}
|
||||
for aid, info in self.booksinfo.items():
|
||||
name = info.get('displayname') or info.get('itemname') or aid
|
||||
if '-' in name:
|
||||
name = name.split('-', 1)[0].strip()
|
||||
self.assetid2name[assetid] = name
|
||||
ts = self.last_open_times.get(assetid, {}).get('last_open', 0)
|
||||
self.assetid2lastopen[assetid] = ts
|
||||
sorted_assetids = sorted(self.assetid2name.keys(), key=lambda aid: self.assetid2lastopen[aid], reverse=True)
|
||||
self.sorted_assetids = sorted_assetids
|
||||
|
||||
# 填充书籍列表
|
||||
for aid in sorted_assetids:
|
||||
self.assetid2name[aid] = name
|
||||
self.assetid2lastopen[aid] = self.last_open_times.get(aid, {}).get('last_open', 0)
|
||||
self.sorted_assetids = sorted(self.assetid2name.keys(), key=lambda a: self.assetid2lastopen[a], reverse=True)
|
||||
for aid in self.sorted_assetids:
|
||||
self.listwidget.addItem(f"{self.assetid2name[aid]} [{self.assetid2lastopen[aid]}]")
|
||||
|
||||
# 连接信号
|
||||
self.export_btn.clicked.connect(self.export_notes)
|
||||
self.config_btn.clicked.connect(self.show_config)
|
||||
|
||||
# 回车直接导出(使用事件过滤器)
|
||||
# 左侧提示标签
|
||||
try:
|
||||
total_books = len(self.sorted_assetids)
|
||||
if hasattr(self, 'label') and isinstance(self.label, QLabel):
|
||||
self.label.setText(f"请选择要导出的书籍【{total_books}本】:")
|
||||
self.label.setStyleSheet("QLabel { background-color:#4c221b;color:#fff;font-weight:bold;padding:4px 6px;border-radius:4px; }")
|
||||
except Exception: pass
|
||||
# 按钮胶囊样式
|
||||
try:
|
||||
if hasattr(self, 'export_btn') and hasattr(self, 'config_btn'):
|
||||
pill_css = (
|
||||
"QPushButton { border:none; color:#ffffff; padding:6px 22px; font-size:14px; font-weight:600; border-radius:22px; }"
|
||||
"QPushButton#export_btn { background: qlineargradient(spread:pad,x1:0,y1:0,x2:1,y2:0,stop:0 #63a9ff, stop:1 #388bff); }"
|
||||
"QPushButton#config_btn { background: qlineargradient(spread:pad,x1:0,y1:0,x2:1,y2:0,stop:0 #7ed957, stop:1 #4caf50); }"
|
||||
)
|
||||
self.export_btn.setObjectName('export_btn')
|
||||
self.config_btn.setObjectName('config_btn')
|
||||
self.export_btn.setStyleSheet(pill_css)
|
||||
self.config_btn.setStyleSheet(pill_css)
|
||||
except Exception: pass
|
||||
# 信号
|
||||
if hasattr(self, 'export_btn'):
|
||||
self.export_btn.clicked.connect(self.export_notes)
|
||||
if hasattr(self, 'config_btn'):
|
||||
self.config_btn.clicked.connect(self.show_config)
|
||||
if hasattr(self, 'ipad_export_btn'):
|
||||
try: self.ipad_export_btn.clicked.connect(self.export_ipad_bundle)
|
||||
except Exception: pass
|
||||
self.listwidget.currentRowChanged.connect(self.update_book_info)
|
||||
self.listwidget.installEventFilter(self)
|
||||
# 封面标签
|
||||
if all(hasattr(self, n) for n in ('cover_label_1','cover_label_2','cover_label_3')):
|
||||
self._cover_labels = [self.cover_label_1, self.cover_label_2, self.cover_label_3]
|
||||
else:
|
||||
self._cover_labels = [getattr(self,'book_cover_label', QLabel('封面', self))]
|
||||
for lab in self._cover_labels:
|
||||
lab.setMinimumWidth(180); lab.setMaximumWidth(180)
|
||||
lab.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
self.cover_ratio = 1.2
|
||||
self.book_toc_textedit.setPlainText("书籍信息 / 简评")
|
||||
self._review_worker = None
|
||||
self._current_bookname = None
|
||||
self._active_workers = []
|
||||
self._cover_pixmaps_original = []
|
||||
self._restore_window_geometry()
|
||||
self._load_initial()
|
||||
self._apply_global_font()
|
||||
|
||||
# 事件过滤:回车导出
|
||||
def eventFilter(self, obj, event):
|
||||
from PyQt6.QtCore import QEvent, Qt
|
||||
from PyQt6.QtCore import QEvent
|
||||
if obj == self.listwidget and event.type() == QEvent.Type.KeyPress:
|
||||
# 检查回车键(Enter/Return)
|
||||
if event.key() in (0x01000004, 0x01000005): # Qt.Key_Return, Qt.Key_Enter
|
||||
self.export_notes()
|
||||
return True
|
||||
if event.key() in (0x01000004, 0x01000005):
|
||||
self.export_notes(); return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def show_config(self):
|
||||
dlg = ConfigDialog(self)
|
||||
if dlg.exec():
|
||||
new_config = dlg.get_config()
|
||||
# 这里只是演示,实际可写入config.py或动态加载
|
||||
QMessageBox.information(self, "提示", "配置已更新(仅本次运行有效)")
|
||||
|
||||
# 导出
|
||||
def export_notes(self):
|
||||
idx = self.listwidget.currentRow()
|
||||
if idx < 0:
|
||||
QMessageBox.warning(self, "提示", "请先选择一本书")
|
||||
return
|
||||
assetid = self.sorted_assetids[idx]
|
||||
selected_booksnote = self.exporter.build_booksnote(bookid=assetid)
|
||||
selected_booksinfo = {assetid: self.booksinfo.get(assetid, {})}
|
||||
bookname = selected_booksinfo[assetid].get("displayname") or selected_booksinfo[assetid].get("itemname") or assetid
|
||||
QMessageBox.warning(self, '提示', '请先选择一本书'); return
|
||||
aid = self.sorted_assetids[idx]
|
||||
selected_booksnote = self.exporter.build_booksnote(bookid=aid)
|
||||
selected_booksinfo = {aid: self.booksinfo.get(aid, {})}
|
||||
bookname = selected_booksinfo[aid].get('displayname') or selected_booksinfo[aid].get('itemname') or aid
|
||||
ts = datetime.datetime.now().strftime('%m%d%H%M')
|
||||
shortname = re.split(r'[.::_\【\[\((]', bookname)[0].strip()
|
||||
export_dir = getattr(config, "EXPORT_NOTES_DIR", os.getcwd())
|
||||
if not os.path.exists(export_dir):
|
||||
os.makedirs(export_dir)
|
||||
out_path = os.path.join(export_dir, f"notes_{shortname}-{ts}.md")
|
||||
export_dir = getattr(config,'EXPORT_NOTES_DIR', os.getcwd())
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
out_path = os.path.join(export_dir, f'notes_{shortname}-{ts}.md')
|
||||
self.exporter.export_booksnote_to_md(selected_booksnote, selected_booksinfo, out_path)
|
||||
QMessageBox.information(self, "导出成功", f"已导出到:{out_path}")
|
||||
QMessageBox.information(self,'导出成功', f'已导出到:{out_path}')
|
||||
|
||||
# ---------------- 图表相关 -----------------
|
||||
# iPad 数据包
|
||||
def export_ipad_bundle(self):
|
||||
try:
|
||||
from ipad_bundle_export import export_ipad_bundle
|
||||
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
out_dir = getattr(config,'EXPORT_NOTES_DIR', os.getcwd())
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = os.path.join(out_dir, f'ipad_bundle_{ts}.zip')
|
||||
export_ipad_bundle(out_path, self.manager, self.exporter)
|
||||
QMessageBox.information(self,'完成', f'iPad 数据包已生成:\n{out_path}')
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self,'错误', f'导出失败: {e}')
|
||||
|
||||
# 配置
|
||||
def show_config(self):
|
||||
dlg = ConfigDialog(self)
|
||||
if dlg.exec():
|
||||
QMessageBox.information(self,'提示','配置已更新(仅本次运行有效)')
|
||||
|
||||
# 初始封面 + 首本书信息
|
||||
def _load_initial(self):
|
||||
try:
|
||||
if not hasattr(self,'_cover_labels') or not self.sorted_assetids:
|
||||
return
|
||||
from PyQt6.QtGui import QPixmap
|
||||
self._cover_pixmaps_original = []
|
||||
first_indices = list(range(min(3, len(self.sorted_assetids))))
|
||||
for pos in range(3):
|
||||
if pos < len(first_indices):
|
||||
aid = self.sorted_assetids[first_indices[pos]]
|
||||
info = self.booksinfo.get(aid, {})
|
||||
cpath = self.find_book_cover(aid, info)
|
||||
if cpath:
|
||||
pm = QPixmap(cpath)
|
||||
if not pm.isNull():
|
||||
self._cover_pixmaps_original.append(pm)
|
||||
self._cover_labels[pos].setPixmap(pm); continue
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('无封面')
|
||||
else:
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('')
|
||||
self._apply_cover_scale()
|
||||
# 首本书信息 & AI 书评
|
||||
aid0 = self.sorted_assetids[0]
|
||||
info0 = self.booksinfo.get(aid0, {})
|
||||
bookname_display = info0.get('displayname') or info0.get('itemname') or aid0
|
||||
author = info0.get('author',''); btype = info0.get('type',''); datev = info0.get('date','')
|
||||
self._base_info_cache = {'bookname': bookname_display,'author':author,'type':btype,'date':datev}
|
||||
self._current_bookname = bookname_display
|
||||
import json
|
||||
json_path = os.path.join(os.path.dirname(__file__), 'bookintro.json')
|
||||
try:
|
||||
with open(json_path,'r',encoding='utf-8') as f: intro_dict = json.load(f)
|
||||
except Exception: intro_dict = {}
|
||||
review = intro_dict.get(bookname_display)
|
||||
if review:
|
||||
self.book_toc_textedit.setHtml(self._build_book_html(review))
|
||||
else:
|
||||
self.book_toc_textedit.setHtml(self._build_book_html('简评获取中...'))
|
||||
prompt = f"{bookname_display} 400字书评 三段 简洁精炼"
|
||||
worker = BookReviewWorker(bookname_display, prompt, json_path, parent=self)
|
||||
worker.finished.connect(lambda b,r: self._on_review_finished(b,r))
|
||||
worker.finished.connect(lambda _b,_r,w=worker: self._remove_worker(w))
|
||||
self._review_worker = worker; self._active_workers.append(worker); worker.start()
|
||||
except Exception as e:
|
||||
print('初始加载失败:', e)
|
||||
|
||||
# 更新书籍信息(列表选中)
|
||||
def update_book_info(self, row):
|
||||
try:
|
||||
if row < 0:
|
||||
self.book_toc_textedit.clear(); return
|
||||
aid = self.sorted_assetids[row]
|
||||
info = self.booksinfo.get(aid, {})
|
||||
total = len(self.sorted_assetids)
|
||||
indices = [(row + i) % total for i in range(min(3,total))]
|
||||
from PyQt6.QtGui import QPixmap
|
||||
self._cover_pixmaps_original = []
|
||||
for lab in self._cover_labels: lab.clear(); lab.setText('加载中')
|
||||
for pos, idx in enumerate(indices):
|
||||
aid_show = self.sorted_assetids[idx]; binfo = self.booksinfo.get(aid_show, {})
|
||||
cpath = self.find_book_cover(aid_show, binfo); label = self._cover_labels[pos]
|
||||
if cpath:
|
||||
pm = QPixmap(cpath)
|
||||
if not pm.isNull(): self._cover_pixmaps_original.append(pm); label.setText(''); continue
|
||||
self._cover_pixmaps_original.append(None); label.setText('无封面')
|
||||
for pos in range(len(indices),3): self._cover_labels[pos].setText('无'); self._cover_pixmaps_original.append(None)
|
||||
self._apply_cover_scale()
|
||||
bookname_display = info.get('displayname') or info.get('itemname') or aid
|
||||
author = info.get('author',''); btype = info.get('type',''); datev = info.get('date','')
|
||||
self._base_info_cache = {'bookname':bookname_display,'author':author,'type':btype,'date':datev}
|
||||
self._current_bookname = bookname_display
|
||||
import json
|
||||
json_path = os.path.join(os.path.dirname(__file__),'bookintro.json')
|
||||
try:
|
||||
with open(json_path,'r',encoding='utf-8') as f: intro_dict = json.load(f)
|
||||
except Exception: intro_dict = {}
|
||||
review = intro_dict.get(bookname_display)
|
||||
if review:
|
||||
self.book_toc_textedit.setHtml(self._build_book_html(review))
|
||||
else:
|
||||
prompt = f"{bookname_display} 400字书评 三段 简洁精炼"
|
||||
self.book_toc_textedit.setHtml(self._build_book_html('简评获取中...'))
|
||||
worker = BookReviewWorker(bookname_display, prompt, json_path, parent=self)
|
||||
worker.finished.connect(lambda b,r: self._on_review_finished(b,r))
|
||||
worker.finished.connect(lambda _b,_r,w=worker: self._remove_worker(w))
|
||||
self._review_worker = worker; self._active_workers.append(worker); worker.start()
|
||||
except Exception as e:
|
||||
print('更新书籍信息失败:', e)
|
||||
|
||||
def _on_review_finished(self, bookname, review):
|
||||
if bookname != getattr(self,'_current_bookname',None): return
|
||||
self.book_toc_textedit.setHtml(self._build_book_html(review))
|
||||
|
||||
def _remove_worker(self, worker):
|
||||
try:
|
||||
if worker in self._active_workers: self._active_workers.remove(worker)
|
||||
except Exception: pass
|
||||
|
||||
# HTML 构建
|
||||
def _build_book_html(self, review_text: str) -> str:
|
||||
info = getattr(self,'_base_info_cache',{})
|
||||
magenta = '#C71585'
|
||||
def line(t,v): return f"<p><span style='color:{magenta};font-weight:bold;'>{t}</span> {v}</p>"
|
||||
def review_lines(txt):
|
||||
if not txt: return ['']
|
||||
segs = [s.strip() for s in re.split(r'\n{2,}|\r{2,}|\n|\r|\s{2,}', txt) if s.strip()]
|
||||
return [f"<p>{s}</p>" for s in segs]
|
||||
parts = [
|
||||
line('书名:', f"{info.get('author','')} - {info.get('bookname','')}"),
|
||||
line('作者:', info.get('author','')),
|
||||
line('类型:', info.get('type','')),
|
||||
line('获取时间:', info.get('date','')),
|
||||
f"<span style='color:{magenta};font-weight:bold;'>书籍简评:</span>"
|
||||
]
|
||||
parts += review_lines(review_text)
|
||||
return ''.join(parts)
|
||||
|
||||
# 字体
|
||||
def _apply_global_font(self):
|
||||
try:
|
||||
from PyQt6.QtGui import QFontDatabase, QFont
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
fam_cfg = getattr(config,'FONT_FAMILY',None)
|
||||
size = int(getattr(config,'FONT_SIZE',14))
|
||||
cands = list(getattr(config,'FONT_CANDIDATES',[]))
|
||||
extra_pf = ['PingFang SC','PingFang TC','PingFang HK'] if fam_cfg and fam_cfg.lower().startswith('pingfang') else []
|
||||
ordered = []
|
||||
seen=set()
|
||||
for f in [fam_cfg]+extra_pf+cands:
|
||||
if f and f not in seen:
|
||||
seen.add(f); ordered.append(f)
|
||||
avail = set(QFontDatabase.families())
|
||||
chosen = None
|
||||
for f in ordered:
|
||||
if f in avail: chosen=f; break
|
||||
if not chosen:
|
||||
print('[字体] 未找到可用字体'); return
|
||||
font = QFont(chosen, size)
|
||||
QApplication.instance().setFont(font); self.setFont(font)
|
||||
print(f'[字体] 应用 {chosen} {size}px')
|
||||
except Exception as e:
|
||||
print('字体应用失败:', e)
|
||||
|
||||
# 窗口尺寸持久化
|
||||
def _restore_window_geometry(self):
|
||||
settings = QSettings('iBookTools','notesExporterMatplot')
|
||||
geo = settings.value('mainWindowGeometry')
|
||||
if isinstance(geo, QByteArray) and not geo.isEmpty():
|
||||
try: self.restoreGeometry(geo); return
|
||||
except Exception: pass
|
||||
self.resize(1500,900)
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
settings = QSettings('iBookTools','notesExporterMatplot')
|
||||
settings.setValue('mainWindowGeometry', self.saveGeometry())
|
||||
except Exception: pass
|
||||
super().closeEvent(event)
|
||||
|
||||
# 覆盖尺寸变化(封面缩放)
|
||||
def resizeEvent(self, event):
|
||||
try: self._apply_cover_scale()
|
||||
except Exception: pass
|
||||
super().resizeEvent(event)
|
||||
|
||||
# ---------------- 图表(matplotlib 保留并加“已读”) -----------------
|
||||
def _init_charts(self):
|
||||
"""初始化并渲染统计标签页内的四个图表。若 matplotlib 不可用或 frame 不存在则忽略。"""
|
||||
# 延迟导入 matplotlib,避免无图形依赖时阻塞主功能
|
||||
try:
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
# 中文字体设置:尝试常见中文字体,找到即设置
|
||||
import matplotlib, json
|
||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||
# 优先使用苹方
|
||||
zh_pref = ['PingFang SC','PingFang','Heiti SC','STHeiti','Hiragino Sans GB','Songti SC','SimHei','Microsoft YaHei']
|
||||
try:
|
||||
import matplotlib
|
||||
from matplotlib import font_manager
|
||||
candidate_fonts = [
|
||||
'PingFang SC', 'Heiti SC', 'STHeiti', 'Hiragino Sans GB', 'Songti SC',
|
||||
'SimHei', 'SimSun', 'Microsoft YaHei', 'WenQuanYi Zen Hei'
|
||||
]
|
||||
available = set(f.name for f in font_manager.fontManager.ttflist)
|
||||
zh_font = None
|
||||
for f in candidate_fonts:
|
||||
if f in available:
|
||||
zh_font = f
|
||||
break
|
||||
if zh_font:
|
||||
matplotlib.rcParams['font.family'] = zh_font
|
||||
# 解决负号显示问题
|
||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||
except Exception as fe:
|
||||
print(f"信息: 中文字体配置失败: {fe}")
|
||||
except Exception as e: # matplotlib 可能未安装
|
||||
print(f"信息: 未加载统计图表(matplotlib不可用):{e}")
|
||||
return
|
||||
|
||||
# 检查 frame 是否存在
|
||||
required_frames = [
|
||||
('frame_week', 'weekLayout'),
|
||||
('frame_month', 'monthLayout'),
|
||||
('frame_year', 'yearLayout'),
|
||||
('frame_bubble', 'bubbleLayout'),
|
||||
]
|
||||
for attr, _ in required_frames:
|
||||
if not hasattr(self, attr):
|
||||
print("信息: 缺少统计容器", attr)
|
||||
return
|
||||
|
||||
# 获取数据
|
||||
try:
|
||||
week_data = self.manager.get_total_readtime(days=7) # 索引0=今天
|
||||
month_data = self.manager.get_total_readtime(days=30)
|
||||
year_data = self.manager.get_total_readtime12m() # 12个月
|
||||
year_total_minutes = self.manager.get_total_readtime_year()
|
||||
avail = set(f.name for f in font_manager.fontManager.ttflist)
|
||||
for nm in zh_pref:
|
||||
if nm in avail:
|
||||
matplotlib.rcParams['font.family'] = nm; break
|
||||
except Exception: pass
|
||||
except Exception as e:
|
||||
print(f"警告: 统计数据获取失败: {e}")
|
||||
print('信息: 未加载统计图表:', e); return
|
||||
frames = [('frame_week','weekLayout'),('frame_month','monthLayout'),('frame_year','yearLayout'),('frame_bubble','bubbleLayout')]
|
||||
if any(not hasattr(self,a) for a,_ in frames):
|
||||
print('信息: 缺少统计容器'); return
|
||||
try:
|
||||
week_data = self.manager.get_total_readtime(days=7)
|
||||
month_data = self.manager.get_total_readtime(days=30)
|
||||
year_data = self.manager.get_total_readtime12m()
|
||||
year_total_minutes = self.manager.get_total_readtime_year()
|
||||
finished_books = self.manager.get_finished_books_this_year()
|
||||
finished_count = len(finished_books)
|
||||
except Exception as e:
|
||||
print('统计数据获取失败:', e); return
|
||||
if all(v==0 for v in week_data+month_data+year_data):
|
||||
for _,layout in frames: getattr(self,layout).addWidget(QLabel('暂无阅读数据'))
|
||||
return
|
||||
|
||||
# 处理无数据情况
|
||||
if all(v == 0 for v in week_data + month_data + year_data):
|
||||
for frame_name, layout_name in required_frames:
|
||||
lbl = QLabel("暂无阅读数据")
|
||||
getattr(self, layout_name).addWidget(lbl)
|
||||
return
|
||||
|
||||
# 工具函数:添加图到 frame
|
||||
def add_figure(frame_layout, fig):
|
||||
canvas = FigureCanvas(fig)
|
||||
frame_layout.addWidget(canvas)
|
||||
return canvas
|
||||
|
||||
# 周图(最近7天) - 倒序显示使左侧为7天前? 按需求索引0=今天 -> 我们希望x轴从右到左还是左到右? 采用左=今天的一致性
|
||||
def plot_bar(data, title, xlabel_list):
|
||||
fig = Figure(figsize=(3.2, 2.4), tight_layout=True)
|
||||
ax = fig.add_subplot(111)
|
||||
bars = ax.bar(range(len(data)), data, color="#4c72b0")
|
||||
ax.set_title(title, fontsize=10)
|
||||
def add_fig(layout_name, fig):
|
||||
canvas = FigureCanvas(fig); getattr(self, layout_name).addWidget(canvas); return canvas
|
||||
def plot_bar(data, ylabel, color):
|
||||
fig = Figure(figsize=(3.2,2.4), tight_layout=True); ax = fig.add_subplot(111)
|
||||
bars = ax.bar(range(len(data)), data, color=color)
|
||||
ax.set_xticks(range(len(data)))
|
||||
ax.set_xticklabels(xlabel_list, rotation=0, fontsize=8)
|
||||
ax.set_ylabel("分钟", fontsize=8)
|
||||
# 在柱子顶部加简单数值(若非0)
|
||||
for rect, val in zip(bars, data):
|
||||
if val > 0:
|
||||
ax.text(rect.get_x() + rect.get_width()/2, rect.get_height(), str(val), ha='center', va='bottom', fontsize=7)
|
||||
return fig
|
||||
|
||||
# x 轴标签
|
||||
week_labels = ["今", "昨", "2", "3", "4", "5", "6"] # 索引0=今天
|
||||
month_labels = [str(i) for i in range(30)] # 0..29 天前
|
||||
year_labels = [f"{i+1}月" for i in range(12)]
|
||||
|
||||
# 绘制三个柱状图
|
||||
week_fig = plot_bar(week_data, "", week_labels)
|
||||
month_fig = plot_bar(month_data, "", month_labels)
|
||||
# 年数据转为小时用于展示
|
||||
year_hours_data = [round(m / 60.0, 1) for m in year_data]
|
||||
def plot_bar_hours(data, title, xlabel_list):
|
||||
fig = Figure(figsize=(3.2, 2.4), tight_layout=True)
|
||||
ax = fig.add_subplot(111)
|
||||
bars = ax.bar(range(len(data)), data, color="#8c6bb1")
|
||||
ax.set_title(title, fontsize=10)
|
||||
ax.set_xticks(range(len(data)))
|
||||
ax.set_xticklabels(xlabel_list, rotation=0, fontsize=8)
|
||||
ax.set_ylabel("小时", fontsize=8)
|
||||
for rect, val in zip(bars, data):
|
||||
if val > 0:
|
||||
ax.text(rect.get_x() + rect.get_width()/2, rect.get_height(), str(val), ha='center', va='bottom', fontsize=7)
|
||||
return fig
|
||||
year_fig = plot_bar_hours(year_hours_data, "", year_labels)
|
||||
|
||||
add_figure(self.weekLayout, week_fig)
|
||||
add_figure(self.monthLayout, month_fig)
|
||||
add_figure(self.yearLayout, year_fig)
|
||||
|
||||
# 气泡图数据计算
|
||||
import math
|
||||
# 全年阅读小时数
|
||||
year_hours = year_total_minutes / 60.0
|
||||
# 月均阅读小时数
|
||||
month_avg_hours = (sum(year_data) / 12.0) / 60.0 if year_data else 0
|
||||
# 近7天阅读小时数
|
||||
week_hours = sum(week_data) / 60.0
|
||||
# 日均阅读分钟数(最近30天)
|
||||
day_avg_minutes = (sum(month_data) / 30.0) if month_data else 0
|
||||
|
||||
ax.set_ylabel(ylabel, fontsize=8)
|
||||
for r,v in zip(bars,data):
|
||||
if v>0: ax.text(r.get_x()+r.get_width()/2, r.get_height(), str(int(v if ylabel=='分钟' else v)), ha='center', va='bottom', fontsize=7)
|
||||
return fig, ax
|
||||
week_labels = ['今','昨','2','3','4','5','6']
|
||||
month_labels = [str(i) for i in range(30)]
|
||||
year_labels = [f'{i+1}月' for i in range(12)]
|
||||
week_fig,_ = plot_bar(week_data,'分钟','#4c72b0'); add_fig('weekLayout', week_fig)
|
||||
month_fig,_ = plot_bar(month_data,'分钟','#4c72b0'); add_fig('monthLayout', month_fig)
|
||||
year_hours = [round(m/60.0,1) for m in year_data]
|
||||
year_fig = Figure(figsize=(3.2,2.4), tight_layout=True); ax_y = year_fig.add_subplot(111)
|
||||
bars = ax_y.bar(range(len(year_hours)), year_hours, color='#8c6bb1')
|
||||
ax_y.set_xticks(range(len(year_hours))); ax_y.set_ylabel('小时', fontsize=8)
|
||||
for r,v in zip(bars,year_hours):
|
||||
if v>0: ax_y.text(r.get_x()+r.get_width()/2, r.get_height(), v, ha='center', va='bottom', fontsize=7)
|
||||
add_fig('yearLayout', year_fig)
|
||||
# 气泡
|
||||
year_hours_total = year_total_minutes/60.0
|
||||
month_avg_hours = (sum(year_data)/12.0)/60.0 if year_data else 0
|
||||
week_hours = sum(week_data)/60.0
|
||||
day_avg_minutes = (sum(month_data)/30.0) if month_data else 0
|
||||
bubble_metrics = [
|
||||
("全年", year_hours, 'h', '#5b6ee1'),
|
||||
("月均", month_avg_hours, 'h', '#c9b2d9'),
|
||||
("近7天", week_hours, 'h', '#f4b2c2'),
|
||||
("日均", day_avg_minutes, 'm', '#b9b542'),
|
||||
('全年', year_hours_total, 'h', '#5b6ee1'),
|
||||
('月均', month_avg_hours, 'h', '#c9b2d9'),
|
||||
('近7天', week_hours, 'h', '#f4b2c2'),
|
||||
('日均', day_avg_minutes, 'm', '#b9b542'),
|
||||
('已读', finished_count, 'book', '#6aa84f'),
|
||||
]
|
||||
# 归一化确定半径(防止过大/过小)。将值全部转为分钟再归一化。
|
||||
minute_values = []
|
||||
for label, val, unit, color in bubble_metrics:
|
||||
if unit == 'h':
|
||||
minute_values.append(val * 60)
|
||||
else:
|
||||
minute_values.append(val)
|
||||
for label,val,unit,_ in bubble_metrics:
|
||||
minute_values.append(val*60 if unit=='h' else (val if unit!='book' else val*60))
|
||||
max_minutes = max(minute_values) if minute_values else 1
|
||||
radii = []
|
||||
radii=[]
|
||||
for mv in minute_values:
|
||||
# 半径在 [0.3, 1.0] 之间的平方放大到 marker size
|
||||
norm = mv / max_minutes if max_minutes > 0 else 0
|
||||
radii.append(0.3 + 0.7 * math.sqrt(norm))
|
||||
|
||||
fig_b = Figure(figsize=(3.6, 2.6), tight_layout=True)
|
||||
axb = fig_b.add_subplot(111)
|
||||
#axb.set_title("阅读指标气泡")
|
||||
axb.axis('off')
|
||||
# 采用归一化坐标使气泡左右均匀填充 (x 0~1)
|
||||
# 布局:最大在 0.2,另外两个上方/右方,一个在下方,形成视觉平衡
|
||||
label2pos = {
|
||||
'全年': (0.20, 0.00),
|
||||
'月均': (0.55, 0.52),
|
||||
'近7天': (0.85, 0.05),
|
||||
'日均': (0.55, -0.52)
|
||||
}
|
||||
# 若有新增指标则线性平铺
|
||||
if any(l not in label2pos for l, *_ in bubble_metrics):
|
||||
step = 1.0 / max(1, len(bubble_metrics)-1)
|
||||
label2pos = {m[0]: (i*step, 0.0) for i, m in enumerate(bubble_metrics)}
|
||||
|
||||
for (label, val, unit, color), r in zip(bubble_metrics, radii):
|
||||
x, y = label2pos.get(label, (0.5, 0.0))
|
||||
size = (r * 1150) ** 2 * 0.012
|
||||
axb.scatter(x, y, s=size, color=color, alpha=0.70, edgecolors='white', linewidths=1.0)
|
||||
if unit == 'h':
|
||||
text_val = f"{val:.0f} 小时" if val >= 10 else f"{val:.1f} 小时"
|
||||
norm = mv/max_minutes if max_minutes>0 else 0
|
||||
radii.append(0.3+0.7*math.sqrt(norm))
|
||||
fig_b = Figure(figsize=(3.6,2.6), tight_layout=True); axb = fig_b.add_subplot(111); axb.axis('off')
|
||||
label2pos = {'全年':(0.18,0.02),'月均':(0.60,0.55),'近7天':(0.90,0.05),'日均':(0.60,-0.55),'已读':(0.34,-0.45)}
|
||||
if any(l not in label2pos for l,_,_,_ in bubble_metrics):
|
||||
step = 1.0/max(1,len(bubble_metrics)-1); label2pos={m[0]:(i*step,0.0) for i,m in enumerate(bubble_metrics)}
|
||||
for (label,val,unit,color),r in zip(bubble_metrics,radii):
|
||||
x,y = label2pos.get(label,(0.5,0.0)); size=(r*1150)**2*0.012
|
||||
axb.scatter(x,y,s=size,color=color,alpha=0.70,edgecolors='white',linewidths=1.0)
|
||||
if unit=='h':
|
||||
text_val = f"{val:.0f} 小时" if val>=10 else f"{val:.1f} 小时"
|
||||
elif unit=='book':
|
||||
text_val = f"{int(val)} 本"
|
||||
else:
|
||||
text_val = f"{val:.0f} 分钟"
|
||||
axb.text(x, y, f"{text_val}\n{label}", ha='center', va='center', fontsize=11, color='white', weight='bold')
|
||||
axb.text(x,y,f"{text_val}\n{label}",ha='center',va='center',fontsize=11,color='white',weight='bold')
|
||||
axb.set_xlim(-0.02,1.02); axb.set_ylim(-0.95,0.95); axb.set_aspect('auto')
|
||||
add_fig('bubbleLayout', fig_b)
|
||||
|
||||
axb.set_xlim(-0.02, 1.02)
|
||||
axb.set_ylim(-0.95, 0.95)
|
||||
axb.set_aspect('auto')
|
||||
add_figure(self.bubbleLayout, fig_b)
|
||||
|
||||
# ------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置应用程序名称和组织信息
|
||||
app.setApplicationName("notesExporter")
|
||||
app.setApplicationDisplayName("notesExporter")
|
||||
app.setOrganizationName("iBook Tools")
|
||||
|
||||
# 设置应用程序图标
|
||||
if os.path.exists(config.APP_ICON):
|
||||
app.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
app.setApplicationName('notesExporterMatplot')
|
||||
app.setApplicationDisplayName('notesExporterMatplot')
|
||||
app.setOrganizationName('iBook Tools')
|
||||
if os.path.exists(config.APP_ICON): app.setWindowIcon(QIcon(config.APP_ICON))
|
||||
win = IBookExportApp()
|
||||
try:
|
||||
win._init_charts()
|
||||
except Exception:
|
||||
pass
|
||||
# 启动即全屏
|
||||
win.showFullScreen()
|
||||
try: win._init_charts()
|
||||
except Exception: pass
|
||||
win.show() # 不全屏,根据需求 3
|
||||
sys.exit(app.exec())
|
||||
|
||||
Reference in New Issue
Block a user