Rabbit Remote Control 0.1.0-bate8
Loading...
Searching...
No Matches
FrmHistory.cpp
1// Author: Kang Lin <kl222@126.com>
2
3#include <QFileDialog>
4#include <QApplication>
5#include <QClipboard>
6#include <QMessageBox>
7#include <QMenu>
8#include <QLoggingCategory>
9#include <QToolBar>
10#include <QLayout>
11#include <QVBoxLayout>
12#include <QComboBox>
13#include <QDateEdit>
14#include <QSpinBox>
15#include <QLabel>
16#include <QLineEdit>
17
18#include "FrmHistory.h"
19#include "RabbitCommonDir.h"
20#include "ui_FrmHistory.h"
21
22static Q_LOGGING_CATEGORY(log, "WebBrowser.History")
24 QWidget *parent)
25 : QWidget(parent)
26 , ui(new Ui::CFrmHistory)
27 , m_pModelHistory(nullptr)
28 , m_pPara(pPara)
29 , m_pDateStart(nullptr)
30 , m_pDateEnd(nullptr)
31{
32 bool check = false;
33 ui->setupUi(this);
34 ui->treeView->hide();
35
36 setWindowTitle(tr("History"));
37
38 QToolBar* pToolBar = ui->toolBar;
39
40 QDate curDate = QDate::currentDate();
41 m_pDateStart = new QDateEdit(curDate.addDays(-7), pToolBar);
42 if(m_pDateStart)
43 m_pDateStart->setToolTip(tr("Start date"));
44 m_pDateEnd = new QDateEdit(curDate, pToolBar);
45 if(m_pDateEnd)
46 m_pDateEnd->setToolTip(tr("End date"));
47 QComboBox* pCB = new QComboBox(pToolBar);
48 pCB->addItem(tr("One day"), 1);
49 pCB->addItem(tr("Two days"), 2);
50 pCB->addItem(tr("One Week"), 7);
51 pCB->addItem(tr("One month"), curDate.daysInMonth());
52 pCB->setCurrentIndex(2);
53 check = connect(pCB, SIGNAL(currentIndexChanged(int)),
54 this, SLOT(slotComboxIndexChanged(int)));
55 Q_ASSERT(check);
56
57 pToolBar->addWidget(pCB);
58 pToolBar->addWidget(m_pDateStart);
59 pToolBar->addWidget(m_pDateEnd);
60
61 QAction* pRefresh = pToolBar->addAction(
62 QIcon::fromTheme("view-refresh"), tr("Refresh"),
63 this, &CFrmHistory::slotRefresh);
64 if(pRefresh) {
65 pRefresh->setToolTip(pRefresh->text());
66 pRefresh->setStatusTip(pRefresh->text());
67 }
68
69 pToolBar->addSeparator();
70 QSpinBox* pSBLimit = new QSpinBox(pToolBar);
71 if(pSBLimit) {
72 pToolBar->addWidget(pSBLimit);
73 pSBLimit->setToolTip(tr("Limit"));
74 int nMax = 1000;
75 if(m_pPara)
76 nMax = qMax(nMax, m_pPara->GetDatabaseViewLimit());
77 pSBLimit->setRange(-1, nMax);
78 if(m_pPara)
79 pSBLimit->setValue(m_pPara->GetDatabaseViewLimit());
80 check = connect(pSBLimit, SIGNAL(valueChanged(int)), this, SLOT(slotLimit(int)));
81 Q_ASSERT(check);
82 }
83
84 pToolBar->addSeparator();
85 pToolBar->addAction(
86 QIcon::fromTheme("import"), tr("Import"), this, SLOT(slotImport()));
87 pToolBar->addAction(
88 QIcon::fromTheme("export"), tr("Export"), this, SLOT(slotExport()));
89 pToolBar->addSeparator();
90 /*
91 QAction* pSettings = pToolBar->addAction(
92 QIcon::fromTheme("system-settings"), tr("Settings"),
93 this, [&]() {
94 //TODO: add settings ui
95 });
96 if(pSettings) {
97 pSettings->setToolTip(pSettings->text());
98 pSettings->setStatusTip(pSettings->text());
99 }//*/
100
101 pToolBar->addAction(QIcon::fromTheme("window-close"), tr("Close"), this, SLOT(close()));
102
103 check = connect(ui->tableView, &QTableView::customContextMenuRequested,
104 this, &CFrmHistory::slotCustomContextMenuRequested);
105 //ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
106 ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
107 ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
108
109 m_pModelHistory = new CHistoryModel(m_pPara, this);
110 if(m_pModelHistory) {
111 ui->tableView->setModel(m_pModelHistory);
112 }
113
114 resize(m_pPara->GetWindowSize());
115
116 slotRefresh();
117}
118
119CFrmHistory::~CFrmHistory()
120{
121 m_pPara->SetWindowSize(this->size());
122 delete ui;
123}
124
125void CFrmHistory::slotRefresh()
126{
127 if(m_pModelHistory) {
128 m_pModelHistory->refresh(m_pDateStart->date(), m_pDateEnd->date());
129 ui->tableView->resizeColumnsToContents();
130 }
131}
132
133void CFrmHistory::on_tableView_doubleClicked(const QModelIndex &index)
134{
135 if(!index.isValid() || !m_pModelHistory) return;
136 auto item = m_pModelHistory->getItem(index);
137 emit sigOpenUrl(item.url);
138}
139
140void CFrmHistory::slotCustomContextMenuRequested(const QPoint &pos)
141{
142 auto index = ui->tableView->indexAt(pos);
143 QItemSelectionModel *selectionModel = ui->tableView->selectionModel();
144 QModelIndexList selectedIndexes;
145 if(selectionModel)
146 selectedIndexes = selectionModel->selectedRows();
147
148 QMenu menu(this);
149
150 if (selectedIndexes.count() > 1) {
151 // 有选中行的情况
152 int selectedCount = selectedIndexes.size();
153
154 // 批量打开
155 QAction *openSelectedAction = menu.addAction(
156 QIcon::fromTheme("document-open"), tr("Open the selected %1 urls").arg(selectedCount));
157 if(openSelectedAction) {
158 connect(openSelectedAction, &QAction::triggered, this, [this, selectedIndexes]() {
159 onOpenSelectedUrls(selectedIndexes);
160 });
161 }
162
163 // 批量删除
164 QAction *deleteSelectedAction = menu.addAction(
165 QIcon::fromTheme("edit-delete"), tr("Delete the selected %1 urls").arg(selectedCount));
166 if(deleteSelectedAction) {
167 connect(deleteSelectedAction, &QAction::triggered, this, [this, selectedIndexes]() {
168 onDeleteSelectedItems(selectedIndexes);
169 });
170 }
171
172 menu.addSeparator();
173
174 // 全选
175 QAction *selectAllAction = menu.addAction(
176 QIcon(":/icons/select_all.png"), tr("All selected"));
177 if(selectAllAction)
178 connect(selectAllAction, &QAction::triggered, ui->tableView, &QTableView::selectAll);
179
180 // 取消选择
181 QAction *deselectAction = menu.addAction(
182 QIcon(":/icons/deselect.png"), tr("Cancel selected"));
183 if(deselectAction)
184 connect(deselectAction, &QAction::triggered,
185 selectionModel, &QItemSelectionModel::clearSelection);
186 } else if (index.isValid()) {
187 // 获取当前行的数据
188 QString url = ui->tableView->model()->data(
189 index.siblingAtColumn(CHistoryModel::ColumnUrl)).toString();
190 QString title = ui->tableView->model()->data(
191 index.siblingAtColumn(CHistoryModel::ColumnTitle)).toString();
192
193 // 打开操作
194 QAction *openAction = menu.addAction(QIcon::fromTheme("document-open"),
195 tr("Open"));
196 if(openAction)
197 connect(openAction, &QAction::triggered, this, [this, url]() {
198 emit sigOpenUrl(url);
199 });
200
201 // 在新标签页中打开
202 QAction *openNewTabAction = menu.addAction(
203 QIcon::fromTheme("document-open"),
204 tr("Open in new tab"));
205 if(openNewTabAction)
206 connect(openNewTabAction, &QAction::triggered, this, [this, url]() {
207 emit sigOpenUrlInNewTab(url);
208 });
209
210 // 复制URL
211 QAction *copyUrlAction = menu.addAction(QIcon::fromTheme("edit-copy"),
212 tr("Copy url"));
213 if(copyUrlAction)
214 connect(copyUrlAction, &QAction::triggered, this, [url]() {
215 QApplication::clipboard()->setText(url);
216 });
217
218 // 复制标题
219 QAction *copyTitleAction = menu.addAction(
220 QIcon::fromTheme("edit-copy"),
221 tr("Copy title"));
222 if(copyTitleAction)
223 connect(copyTitleAction, &QAction::triggered, this, [title]() {
224 QApplication::clipboard()->setText(title);
225 });
226
227 menu.addSeparator();
228
229 // 删除操作
230 QAction *deleteAction = menu.addAction(
231 QIcon::fromTheme("edit-delete"), tr("Delete"));
232 if(deleteAction)
233 connect(deleteAction, &QAction::triggered, this, [this, index]() {
234 onDeleteHistoryItem(index);
235 });
236
237 QAction *deleteAllAction = menu.addAction(
238 QIcon::fromTheme("edit-delete"),
239 tr("Delete all urls %1").arg(title.left(30)));
240 if(deleteAllAction)
241 connect(deleteAllAction, &QAction::triggered, this, [this, url]() {
242 m_pModelHistory->removeItems(url);
243 });
244
245 // 删除所有类似网站
246 QString domain = extractDomain(url);
247 if (!domain.isEmpty()) {
248 QAction *deleteDomainAction = menu.addAction(
249 QIcon::fromTheme("edit-delete"),
250 tr("Delete all urls from %1").arg(domain));
251 if(deleteDomainAction)
252 connect(deleteDomainAction, &QAction::triggered, this, [this, domain]() {
253 onDeleteHistoryByDomain(domain);
254 });
255 }
256
257 menu.addSeparator();
258
259 // 属性/详细信息
260 QAction *propertiesAction = menu.addAction(
261 QIcon::fromTheme("document-properties"), tr("Properties"));
262 if(propertiesAction)
263 connect(propertiesAction, &QAction::triggered, this, [this, index]() {
264 onShowHistoryProperties(index);
265 });
266 }
267
268 menu.addSeparator();
269
270 // 总是显示的菜单项
271 QAction *refreshAction = menu.addAction(
272 QIcon::fromTheme("view-refresh"), tr("Refresh"));
273 if(refreshAction)
274 connect(refreshAction, &QAction::triggered, this, [&]() {
275 if(m_pModelHistory)
276 m_pModelHistory->refresh();
277 });
278
279 QAction *clearAllAction = menu.addAction(
280 QIcon::fromTheme("edit-clear"), tr("Clear all urls"));
281 if(clearAllAction)
282 connect(clearAllAction, &QAction::triggered, this, [&]() {
283 if(m_pModelHistory->removeRows(0, m_pModelHistory->rowCount()));
284 });
285
286 // 设置菜单
287 // QAction *settingsAction = menu.addAction(
288 // QIcon::fromTheme("system-settings"), tr("Settings"));
289 // connect(settingsAction, &QAction::triggered, this, &CFrmHistory::showHistorySettings);
290
291 // 4. 显示菜单
292 menu.exec(ui->tableView->viewport()->mapToGlobal(pos));
293}
294
295QString CFrmHistory::extractDomain(const QString &url)
296{
297 QUrl qurl(url);
298 if (qurl.isValid()) {
299 QString host = qurl.host();
300 // 移除 www. 前缀
301 if (host.startsWith("www.")) {
302 host = host.mid(4);
303 }
304 return host;
305 }
306 return QString();
307}
308
309void CFrmHistory::onDeleteHistoryItem(const QModelIndex &index)
310{
311 if (!index.isValid() || !m_pModelHistory) {
312 return;
313 }
314
315 QString title = ui->tableView->model()->data(
316 index.siblingAtColumn(CHistoryModel::ColumnTitle)).toString();
317 if(title.isEmpty()) {
318 title = ui->tableView->model()->data(
319 index.siblingAtColumn(CHistoryModel::ColumnUrl)).toString();
320 }
321 // 确认对话框
322 QMessageBox::StandardButton reply = QMessageBox::question(
323 this,
324 tr("Delete the url"),
325 tr("Are you sure you want to delete the url \"%1\"?").arg(title),
326 QMessageBox::Yes | QMessageBox::No,
327 QMessageBox::No
328 );
329
330 if (reply == QMessageBox::Yes) {
331 // 从模型中删除
332 if (m_pModelHistory->removeRow(index.row())) {
333 qDebug(log) << "History item deleted";
334 }
335 }
336}
337
338void CFrmHistory::onDeleteHistoryByDomain(const QString &domain)
339{
340 if (domain.isEmpty() || !m_pModelHistory) {
341 return;
342 }
343
344 // 确认对话框
345 QMessageBox::StandardButton reply = QMessageBox::question(
346 this,
347 tr("Delete the url"),
348 tr("Are you sure you want to delete all url from \"%1\"?").arg(domain),
349 QMessageBox::Yes | QMessageBox::No,
350 QMessageBox::No
351 );
352
353 if (reply == QMessageBox::Yes) {
354 m_pModelHistory->removeDomainItems(domain);
355 }
356}
357
358void CFrmHistory::onShowHistoryProperties(const QModelIndex &index)
359{
360 if (!index.isValid() || !m_pModelHistory) {
361 return;
362 }
363
364 // 获取详细数据
365 auto item = m_pModelHistory->getItem(index);
366 // 创建详细信息对话框
367 QString details = QString(
368 "<h3>%1</h3>"
369 "<table border='0' cellspacing='5'>"
370 "<tr><td><b>%2</b></td><td>%3</td></tr>"
371 "<tr><td><b>%4</b></td><td>%5</td></tr>"
372 "</table>")
373 .arg(item.title.toHtmlEscaped())
374 .arg(tr("Url:"))
375 .arg(item.url.toHtmlEscaped())
376 .arg(tr("Visit Time:"))
377 .arg(item.visitTime.toString(QLocale::system().dateFormat()))
378 ;
379
380 QMessageBox::information(this, tr("Properties"), details);
381}
382
383void CFrmHistory::onOpenSelectedUrls(const QModelIndexList &indexes)
384{
385 if (indexes.isEmpty()) return;
386 for (const QModelIndex &index : indexes) {
387 QString url = ui->tableView->model()->data(
388 index.siblingAtColumn(CHistoryModel::ColumnUrl)).toString();
389 if (!url.isEmpty()) {
390 emit sigOpenUrlInNewTab(url);
391 }
392 }
393}
394
395void CFrmHistory::onDeleteSelectedItems(const QModelIndexList &indexes)
396{
397 if (indexes.isEmpty() || !m_pModelHistory) return;
398
399 QMessageBox::StandardButton reply = QMessageBox::question(
400 this,
401 tr("Delete the urls"),
402 tr("Are you sure you want to delete the selected %1 urls?").arg(indexes.size()),
403 QMessageBox::Yes | QMessageBox::No,
404 QMessageBox::No
405 );
406
407 if (reply == QMessageBox::Yes) {
408 // 按行号降序排序,这样删除时索引不会乱
409 QList<int> rows;
410 for (const QModelIndex &index : indexes) {
411 rows.append(index.row());
412 }
413
414 // 降序排序
415 std::sort(rows.begin(), rows.end(), std::greater<int>());
416
417 // 批量删除
418 for (int row : rows) {
419 m_pModelHistory->removeRow(row);
420 }
421
422 qDebug(log) << "Deleted" << indexes.size() << "history items";
423 }
424}
425
426void CFrmHistory::slotImport()
427{
428 QString filename = QFileDialog::getOpenFileName(
429 this, tr("Import histories"),
430 RabbitCommon::CDir::Instance()->GetDirUserDocument(),
431 tr("JSON (*.json);; All files (*.*)"));
432
433 if (filename.isEmpty()) return;
434
435 QFileInfo fi(filename);
436 if(0 == fi.suffix().compare("json", Qt::CaseInsensitive)) {
437 if (m_pModelHistory->importFromJson(filename)) {
438 slotRefresh();
439 QMessageBox::information(this, tr("Import histories"),
440 tr("Successfully imported histories from JSON file: %1").arg(filename));
441 } else {
442 QMessageBox::critical(this, tr("Import histories"),
443 tr("Failed to import histories from JSON file: %1").arg(filename));
444 }
445 return;
446 }
447
448 QMessageBox::critical(this, tr("Import histories"),
449 tr("Invalid file: %1").arg(filename) + "\n\n"
450 + tr("Please use JSON or CSV file"));
451}
452
453void CFrmHistory::slotExport()
454{
455 QString filename = QFileDialog::getSaveFileName(
456 this, tr("Export histories"),
457 RabbitCommon::CDir::Instance()->GetDirUserDocument(),
458 tr("JSON (*.json);; All files (*.*)"));
459
460 if (filename.isEmpty()) return;
461
462 QFileInfo fi(filename);
463 if(0 == fi.suffix().compare("json", Qt::CaseInsensitive)) {
464 if (m_pModelHistory->exportToJson(filename)) {
465 QMessageBox::information(this, tr("Export histories"),
466 tr("Histories successfully exported to JSON file: %1").arg(filename));
467 } else {
468 QMessageBox::critical(this, tr("Export histories"),
469 tr("Failed to export histories to JSON file: %1").arg(filename));
470 }
471 return;
472 }
473
474 QMessageBox::critical(this, tr("Export histories"),
475 tr("Invalid file: %1").arg(filename) + "\n\n"
476 + tr("Please use JSON or CSV file"));
477}
478
479void CFrmHistory::slotComboxIndexChanged(int index)
480{
481 qDebug(log) << "Change days";
482 QComboBox* pCB = qobject_cast<QComboBox*>(sender());
483 if(!pCB) return;
484 int d = pCB->itemData(index).toInt();
485 QDate curDate = QDate::currentDate();
486 if(m_pDateEnd) {
487 m_pDateEnd->setDate(curDate);
488 }
489 switch(d) {
490 case -1:
491 break;
492 default:
493 if(m_pDateStart)
494 m_pDateStart->setDate(curDate.addDays(-1 * d));
495 break;
496 }
497 slotRefresh();
498}
499
500void CFrmHistory::slotLimit(int v)
501{
502 if(m_pPara)
503 m_pPara->SetDatabaseViewLimit(v);
504 slotRefresh();
505}
506
507void CFrmHistory::on_leSearch_textChanged(const QString &keyword)
508{
509 m_pModelHistory->search(keyword);
510}