diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.yml b/.github/ISSUE_TEMPLATE/2_feature_request.yml index 3fb748c..f823933 100644 --- a/.github/ISSUE_TEMPLATE/2_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/2_feature_request.yml @@ -7,6 +7,7 @@ body: attributes: label: '👌 是否检查过没有类似issue' options: + - 就不检查 - 否 - 是 validations: diff --git a/app/DataBase/exporter_csv.py b/app/DataBase/exporter_csv.py index c19a1ee..ae463d8 100644 --- a/app/DataBase/exporter_csv.py +++ b/app/DataBase/exporter_csv.py @@ -13,7 +13,7 @@ class CSVExporter(ExporterBase): columns = ['localId', 'TalkerId', 'Type', 'SubType', 'IsSender', 'CreateTime', 'Status', 'StrContent', 'StrTime', 'Remark', 'NickName', 'Sender'] - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid, time_range=self.time_range) # 写入CSV文件 with open(filename, mode='w', newline='', encoding='utf-8-sig') as file: writer = csv.writer(file) diff --git a/app/DataBase/exporter_docx.py b/app/DataBase/exporter_docx.py index 22e3dad..7ef7db8 100644 --- a/app/DataBase/exporter_docx.py +++ b/app/DataBase/exporter_docx.py @@ -288,7 +288,7 @@ class DocxExporter(ExporterBase): doc = docx.Document() doc.styles['Normal'].font.name = u'Cambria' doc.styles['Normal']._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid, time_range=self.time_range) Me().save_avatar(os.path.join(f"{origin_docx_path}/avatar/{Me().wxid}.png")) if self.contact.is_chatroom: for message in messages: diff --git a/app/DataBase/exporter_html.py b/app/DataBase/exporter_html.py index b345f11..af506ed 100644 --- a/app/DataBase/exporter_html.py +++ b/app/DataBase/exporter_html.py @@ -275,7 +275,7 @@ class HtmlExporter(ExporterBase): ) def export(self): - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid, time_range=self.time_range) filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.html" file_path = './app/resources/data/template.html' if not os.path.exists(file_path): diff --git a/app/DataBase/exporter_txt.py b/app/DataBase/exporter_txt.py index f769633..f4df086 100644 --- a/app/DataBase/exporter_txt.py +++ b/app/DataBase/exporter_txt.py @@ -113,7 +113,7 @@ class TxtExporter(ExporterBase): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" os.makedirs(origin_docx_path, exist_ok=True) filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.txt" - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid, time_range=self.time_range) total_steps = len(messages) with open(filename, mode='w', newline='', encoding='utf-8') as f: for index, message in enumerate(messages): diff --git a/app/DataBase/msg.py b/app/DataBase/msg.py index faa8987..842784c 100644 --- a/app/DataBase/msg.py +++ b/app/DataBase/msg.py @@ -139,7 +139,7 @@ class Msg: new_messages.append(new_message) return new_messages - def get_messages(self, username_): + def get_messages(self, username_, time_range=None): """ return list a[0]: localId, @@ -157,10 +157,13 @@ class Msg: """ if not self.open_flag: return None - sql = ''' + if time_range: + start_time, end_time = time_range + sql = f''' select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent from MSG where StrTalker=? + {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''} order by CreateTime ''' try: @@ -230,14 +233,17 @@ class Msg: # result.sort(key=lambda x: x[5]) return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result - def get_messages_by_type(self, username_, type_, year_='all'): + def get_messages_by_type(self, username_, type_, year_='all',time_range=None): if not self.open_flag: return None + if time_range: + start_time, end_time = time_range if year_ == 'all': - sql = ''' + sql = f''' select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent from MSG where StrTalker=? and Type=? + {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''} order by CreateTime ''' try: diff --git a/app/DataBase/output.py b/app/DataBase/output.py index fda8dfa..4f00d22 100644 --- a/app/DataBase/output.py +++ b/app/DataBase/output.py @@ -3,29 +3,12 @@ import html import os import shutil import sys -import time -import traceback + import filecmp -from re import findall -import docx from PyQt5.QtCore import pyqtSignal, QThread -from PyQt5.QtWidgets import QFileDialog -from docx import shared -from docx.enum.table import WD_ALIGN_VERTICAL -from docx.enum.text import WD_COLOR_INDEX, WD_PARAGRAPH_ALIGNMENT -from docx.oxml.ns import qn -from .package_msg import PackageMsg -from ..DataBase import media_msg_db, hard_link_db, micro_msg_db, msg_db -from ..log import logger from ..person import Me, Contact -from ..util import path -from ..util.compress_content import parser_reply, music_share, share_card -from ..util.emoji import get_emoji_url -from ..util.file import get_file -from ..util.music import get_music_path -from ..util.image import get_image_path, get_image, get_image_abs_path os.makedirs('./data/聊天记录', exist_ok=True) @@ -107,7 +90,7 @@ class ExporterBase(QThread): CONTACT_CSV = 4 TXT = 5 - def __init__(self, contact, type_=DOCX, message_types={}, parent=None): + def __init__(self, contact, type_=DOCX, message_types={},time_range=None, parent=None): super().__init__(parent) self.message_types = message_types # 导出的消息类型 self.contact: Contact = contact # 联系人 @@ -115,6 +98,7 @@ class ExporterBase(QThread): self.total_num = 1 # 总的消息数量 self.num = 0 # 当前处理的消息数量 self.last_timestamp = 0 + self.time_range = time_range origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" makedirs(origin_docx_path) def run(self): diff --git a/app/DataBase/output_pc.py b/app/DataBase/output_pc.py index 1deb06c..d7dcfed 100644 --- a/app/DataBase/output_pc.py +++ b/app/DataBase/output_pc.py @@ -39,11 +39,12 @@ class Output(QThread): TXT = 5 Batch = 10086 - def __init__(self, contact, type_=DOCX, message_types={}, sub_type=[], parent=None): + def __init__(self, contact, type_=DOCX, message_types={}, sub_type=[], time_range=None,parent=None): super().__init__(parent) self.children = [] self.last_timestamp = 0 self.sub_type = sub_type + self.time_range = time_range self.message_types = message_types self.sec = 2 # 默认1000秒 self.contact = contact @@ -161,7 +162,7 @@ class Output(QThread): self.okSignal.emit(1) def to_docx(self, contact, message_types, is_batch=False): - Child = DocxExporter(contact, type_=self.DOCX, message_types=message_types) + Child = DocxExporter(contact, type_=self.DOCX, message_types=message_types,time_range=self.time_range) self.children.append(Child) Child.progressSignal.connect(self.progress) if not is_batch: @@ -170,7 +171,7 @@ class Output(QThread): Child.start() def to_txt(self, contact, message_types, is_batch=False): - Child = TxtExporter(contact, type_=self.TXT, message_types=message_types) + Child = TxtExporter(contact, type_=self.TXT, message_types=message_types,time_range=self.time_range) self.children.append(Child) Child.progressSignal.connect(self.progress) if not is_batch: @@ -179,7 +180,7 @@ class Output(QThread): Child.start() def to_html(self, contact, message_types, is_batch=False): - Child = HtmlExporter(contact, type_=self.output_type, message_types=message_types) + Child = HtmlExporter(contact, type_=self.output_type, message_types=message_types,time_range=self.time_range) self.children.append(Child) Child.progressSignal.connect(self.progress) if not is_batch: @@ -190,7 +191,7 @@ class Output(QThread): if message_types.get(34): # 语音消息单独的线程 self.total_num += 1 - output_media = OutputMedia(contact) + output_media = OutputMedia(contact,time_range=self.time_range) self.children.append(output_media) output_media.okSingal.connect(self.count_finish_num) output_media.progressSignal.connect(self.progressSignal) @@ -198,7 +199,7 @@ class Output(QThread): if message_types.get(47): # emoji消息单独的线程 self.total_num += 1 - output_emoji = OutputEmoji(contact) + output_emoji = OutputEmoji(contact,time_range=self.time_range) self.children.append(output_emoji) output_emoji.okSingal.connect(self.count_finish_num) output_emoji.progressSignal.connect(self.progressSignal) @@ -206,14 +207,14 @@ class Output(QThread): if message_types.get(3): # 图片消息单独的线程 self.total_num += 1 - output_image = OutputImage(contact) + output_image = OutputImage(contact,time_range=self.time_range) self.children.append(output_image) output_image.okSingal.connect(self.count_finish_num) output_image.progressSignal.connect(self.progressSignal) output_image.start() def to_csv(self, contact, message_types, is_batch=False): - Child = CSVExporter(contact, type_=self.CSV, message_types=message_types) + Child = CSVExporter(contact, type_=self.CSV, message_types=message_types,time_range=self.time_range) self.children.append(Child) Child.progressSignal.connect(self.progress) if not is_batch: @@ -263,13 +264,14 @@ class OutputMedia(QThread): okSingal = pyqtSignal(int) progressSignal = pyqtSignal(int) - def __init__(self, contact): + def __init__(self, contact,time_range=None): super().__init__() self.contact = contact + self.time_range = time_range def run(self): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" - messages = msg_db.get_messages_by_type(self.contact.wxid, 34) + messages = msg_db.get_messages_by_type(self.contact.wxid, 34,time_range=self.time_range) for message in messages: is_send = message[4] msgSvrId = message[9] @@ -289,13 +291,14 @@ class OutputEmoji(QThread): okSingal = pyqtSignal(int) progressSignal = pyqtSignal(int) - def __init__(self, contact): + def __init__(self, contact,time_range=None): super().__init__() self.contact = contact + self.time_range = time_range def run(self): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" - messages = msg_db.get_messages_by_type(self.contact.wxid, 47) + messages = msg_db.get_messages_by_type(self.contact.wxid, 47,time_range=self.time_range) for message in messages: str_content = message[7] try: @@ -315,10 +318,11 @@ class OutputImage(QThread): okSingal = pyqtSignal(int) progressSignal = pyqtSignal(int) - def __init__(self, contact): + def __init__(self, contact,time_range): super().__init__() self.contact = contact self.child_thread_num = 2 + self.time_range =time_range self.child_threads = [0] * (self.child_thread_num + 1) self.num = 0 @@ -331,7 +335,7 @@ class OutputImage(QThread): def run(self): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" - messages = msg_db.get_messages_by_type(self.contact.wxid, 3) + messages = msg_db.get_messages_by_type(self.contact.wxid, 3,time_range=self.time_range) for message in messages: str_content = message[7] BytesExtra = message[10] @@ -359,10 +363,11 @@ class OutputImageChild(QThread): okSingal = pyqtSignal(int) progressSignal = pyqtSignal(int) - def __init__(self, contact, messages): + def __init__(self, contact, messages,time_range): super().__init__() self.contact = contact self.messages = messages + self.time_range = time_range def run(self): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" diff --git a/app/components/calendar_dialog.py b/app/components/calendar_dialog.py new file mode 100644 index 0000000..9c177d2 --- /dev/null +++ b/app/components/calendar_dialog.py @@ -0,0 +1,46 @@ +import time + +from PyQt5 import QtWidgets +from PyQt5.QtCore import QTimer, QThread, pyqtSignal +from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget, QWidget, QVBoxLayout + + +class CalendarDialog(QDialog): + selected_date_signal = pyqtSignal(int) + def __init__(self, date_range=None, parent=None): + """ + + @param date_range: tuple[Union[QDate, datetime.date],Union[QDate, datetime.date]] + @param parent: + """ + super().__init__(parent) + self.calendar = QCalendarWidget(self) + self.calendar.clicked.connect(self.onDateChanged) + if date_range: + self.calendar.setDateRange(*date_range) + layout = QVBoxLayout(self) + layout.addWidget(self.calendar) + self.setLayout(layout) + + def onDateChanged(self): + # 获取选择的日期 + selected_date = self.calendar.selectedDate() + s_t = time.strptime(selected_date.toString("yyyy-MM-dd"), "%Y-%m-%d") # 返回元祖 + mkt = int(time.mktime(s_t)) + timestamp = mkt + self.selected_date_signal.emit(timestamp) + print("Selected Date:", selected_date.toString("yyyy-MM-dd"),timestamp) + self.close() + + +if __name__ == '__main__': + import sys + from datetime import datetime + app = QApplication(sys.argv) + # 设置日期范围 + start_date = datetime(2023, 12, 11) + end_date = datetime(2024, 1, 9) + date_range = (start_date.date(),end_date.date()) + ex = CalendarDialog(date_range=date_range) + ex.show() + sys.exit(app.exec_()) diff --git a/app/ui/menu/export.py b/app/ui/menu/export.py index 263cc98..d847da8 100644 --- a/app/ui/menu/export.py +++ b/app/ui/menu/export.py @@ -3,14 +3,16 @@ from typing import List from PyQt5 import QtWidgets from PyQt5.QtCore import QTimer, QThread, pyqtSignal -from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox +from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget from app.DataBase import micro_msg_db, misc_db from app.DataBase.output_pc import Output from app.components import ScrollBar +from app.components.calendar_dialog import CalendarDialog from app.components.export_contact_item import ContactQListWidgetItem from app.person import Contact from app.ui.menu.exportUi import Ui_Dialog +from app.ui.menu.export_time_range import TimeRangeDialog types = { '文本': 1, @@ -77,22 +79,46 @@ class ExportDialog(QDialog, Ui_Dialog): self.listWidget.itemClicked.connect(self.setCurrentIndex) self.visited = set() self.now_index = 0 + self.time_range = None def set_export_date(self): date_range = self.comboBox_time.currentText() if date_range == '全部时间': pass elif date_range == '最近三个月': - QMessageBox.warning(self, - "别急别急", - "马上就实现该功能" - ) - elif date_range == '自定义时间': - QMessageBox.warning(self, - "别急别急", - "马上就实现该功能" - ) + from datetime import datetime, timedelta + # 获取今天的日期和时间 + today = datetime.now() + + # 获取今天的日期 + today_date = today.date() + + # 获取今天的24:00:00的时间戳 + today_midnight = datetime.combine(today_date, datetime.min.time()) + timedelta(days=1) + today_midnight_timestamp = int(today_midnight.timestamp()) + + # 获取三个月前的日期 + three_months_ago = today - timedelta(days=90) + + # 获取三个月前的00:00:00的时间戳 + three_months_ago_date = three_months_ago.date() + three_months_ago_midnight = datetime.combine(three_months_ago_date, datetime.min.time()) + three_months_ago_midnight_timestamp = int(three_months_ago_midnight.timestamp()) + self.time_range = (three_months_ago_midnight_timestamp,today_midnight_timestamp) + + elif date_range == '自定义时间': + self.time_range_view = TimeRangeDialog(parent=self) + self.time_range_view.date_range_signal.connect(self.set_time_range) + self.time_range_view.show() + self.comboBox_time.setCurrentIndex(0) + # QMessageBox.warning(self, + # "别急别急", + # "马上就实现该功能" + # ) + def set_time_range(self,time_range): + self.time_range = time_range + self.comboBox_time.setCurrentIndex(2) def export_data(self): self.btn_start.setEnabled(False) # 在这里获取用户选择的导出数据类型 @@ -115,7 +141,7 @@ class ExportDialog(QDialog, Ui_Dialog): select_contacts.append(self.contacts[i]) # 在这里根据用户选择的数据类型执行导出操作 print("选择的文件格式:", file_types) - self.worker = Output(select_contacts, type_=Output.Batch, message_types=selected_types, sub_type=file_types) + self.worker = Output(select_contacts, type_=Output.Batch, message_types=selected_types, sub_type=file_types,time_range=self.time_range) # self.worker.progressSignal.connect(self.update_progress) self.worker.okSignal.connect(self.export_finished) self.worker.rangeSignal.connect(self.set_total_msg_num) diff --git a/app/ui/menu/export_time_range.py b/app/ui/menu/export_time_range.py new file mode 100644 index 0000000..9eade1f --- /dev/null +++ b/app/ui/menu/export_time_range.py @@ -0,0 +1,72 @@ +from datetime import datetime +from PyQt5 import QtWidgets +from PyQt5.QtCore import QTimer, QThread, pyqtSignal +from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget, QWidget, QVBoxLayout, QLabel + +from app.components.calendar_dialog import CalendarDialog +from .time_range import Ui_Dialog + +Stylesheet = ''' +QToolButton{ + color:#000000; +} +''' +class TimeRangeDialog(QDialog, Ui_Dialog): + date_range_signal = pyqtSignal(tuple) + + def __init__(self, date_range=None, parent=None): + """ + + @param date_range: tuple[Union[QDate, datetime.date],Union[QDate, datetime.date]] + @param parent: + """ + super().__init__(parent) + self.calendar = None + self.setupUi(self) + self.setStyleSheet(Stylesheet) + self.toolButton_start_time.clicked.connect(self.select_date_start) + self.toolButton_end_time.clicked.connect(self.select_date_end) + self.calendar = CalendarDialog(date_range=date_range, parent=self) + self.calendar.selected_date_signal.connect(self.set_date) + self.btn_ok.clicked.connect(self.ok) + self.btn_cancel.clicked.connect(lambda x:self.close()) + self.start_time_flag = True + self.start_timestamp = 0 + self.end_timestamp = 0 + + def set_date(self, timestamp): + if self.start_time_flag: + self.start_timestamp = timestamp + date_object = datetime.fromtimestamp(timestamp) + str_start =date_object.strftime("%Y-%m-%d") + self.toolButton_start_time.setText(str_start) + else: + date_object = datetime.fromtimestamp(timestamp) + str_start = date_object.strftime("%Y-%m-%d") + self.end_timestamp = timestamp + 86399 + self.toolButton_end_time.setText(str_start) + def ok(self): + date_range = (self.start_timestamp,self.end_timestamp) + self.date_range_signal.emit(date_range) + self.close() + def select_date_start(self): + self.start_time_flag = True + self.calendar.show() + + def select_date_end(self): + self.start_time_flag = False + self.calendar.show() + + +if __name__ == '__main__': + import sys + from datetime import datetime + + app = QApplication(sys.argv) + # 设置日期范围 + start_date = datetime(2023, 12, 11) + end_date = datetime(2024, 1, 9) + date_range = (start_date.date(), end_date.date()) + ex = CalendarDialog(date_range=date_range) + ex.show() + sys.exit(app.exec_()) diff --git a/app/ui/menu/time_range.py b/app/ui/menu/time_range.py new file mode 100644 index 0000000..2f640ad --- /dev/null +++ b/app/ui/menu/time_range.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'time_range.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(280, 200) + Dialog.setMinimumSize(QtCore.QSize(280, 200)) + Dialog.setMaximumSize(QtCore.QSize(280, 200)) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.label = QtWidgets.QLabel(Dialog) + self.label.setLayoutDirection(QtCore.Qt.LeftToRight) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.label_2 = QtWidgets.QLabel(Dialog) + self.label_2.setObjectName("label_2") + self.horizontalLayout_2.addWidget(self.label_2) + self.toolButton_start_time = QtWidgets.QToolButton(Dialog) + self.toolButton_start_time.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) + self.toolButton_start_time.setArrowType(QtCore.Qt.DownArrow) + self.toolButton_start_time.setObjectName("toolButton_start_time") + self.horizontalLayout_2.addWidget(self.toolButton_start_time) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label_3 = QtWidgets.QLabel(Dialog) + self.label_3.setObjectName("label_3") + self.horizontalLayout.addWidget(self.label_3) + self.toolButton_end_time = QtWidgets.QToolButton(Dialog) + self.toolButton_end_time.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) + self.toolButton_end_time.setArrowType(QtCore.Qt.DownArrow) + self.toolButton_end_time.setObjectName("toolButton_end_time") + self.horizontalLayout.addWidget(self.toolButton_end_time) + self.verticalLayout.addLayout(self.horizontalLayout) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.btn_ok = QtWidgets.QPushButton(Dialog) + self.btn_ok.setObjectName("btn_ok") + self.horizontalLayout_3.addWidget(self.btn_ok) + self.btn_cancel = QtWidgets.QPushButton(Dialog) + self.btn_cancel.setObjectName("btn_cancel") + self.horizontalLayout_3.addWidget(self.btn_cancel) + self.verticalLayout.addLayout(self.horizontalLayout_3) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "自定义时间")) + self.label_2.setText(_translate("Dialog", "开始日期")) + self.toolButton_start_time.setText(_translate("Dialog", "请选择时间")) + self.label_3.setText(_translate("Dialog", "结束日期")) + self.toolButton_end_time.setText(_translate("Dialog", "请选择时间")) + self.btn_ok.setText(_translate("Dialog", "确定")) + self.btn_cancel.setText(_translate("Dialog", "取消"))