Commit 19d0fbab authored by Rinigus Saar's avatar Rinigus Saar

keep bookmarks and history in SQLite database

parent dd393760
......@@ -28,7 +28,7 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
################# Find dependencies #################
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2)
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2 Sql)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 Purpose I18n)
# Necessary to support QtWebEngine installed in a different prefix than the rest of Qt (e.g flatpak)
......@@ -37,6 +37,7 @@ find_package(Qt5WebEngine REQUIRED)
################# Definitions to pass to the compiler #################
add_definitions(-DQT_NO_FOREACH)
add_definitions(-fexceptions)
################# build and install #################
add_subdirectory(src)
......
include(ECMAddTests)
find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test)
find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Sql)
include_directories(../src)
......@@ -14,12 +14,12 @@ ecm_add_test(useragenttest.cpp ../src/useragent.cpp
LINK_LIBRARIES Qt5::Test
)
ecm_add_test(browsermanagertest.cpp ../src/browsermanager.cpp ../src/urlmodel.cpp ../src/urlutils.cpp
ecm_add_test(browsermanagertest.cpp ../src/browsermanager.cpp ../src/dbmanager.cpp ../src/urlmodel.cpp ../src/urlutils.cpp
TEST_NAME browsermanagertest
LINK_LIBRARIES Qt5::Test
LINK_LIBRARIES Qt5::Test Qt5::Sql
)
ecm_add_test(tabsmodeltest.cpp ../src/tabsmodel.cpp ../src/browsermanager.cpp ../src/urlmodel.cpp
ecm_add_test(tabsmodeltest.cpp ../src/tabsmodel.cpp ../src/browsermanager.cpp ../src/dbmanager.cpp ../src/urlmodel.cpp
TEST_NAME tabsmodeltest
LINK_LIBRARIES Qt5::Test
LINK_LIBRARIES Qt5::Test Qt5::Sql
)
set(angelfish_SRCS
main.cpp
browsermanager.cpp
dbmanager.cpp
bookmarkshistorymodel.cpp
sqlquerymodel.cpp
urlmodel.cpp
urlfilterproxymodel.cpp
urlutils.cpp
......@@ -11,6 +14,6 @@ set(angelfish_SRCS
qt5_add_resources(RESOURCES resources.qrc)
add_executable(angelfish ${angelfish_SRCS} ${RESOURCES})
target_link_libraries(angelfish Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg Qt5::WebEngine KF5::I18n)
target_link_libraries(angelfish Qt5::Core Qt5::Qml Qt5::Quick Qt5::Sql Qt5::Svg Qt5::WebEngine KF5::I18n)
install(TARGETS angelfish ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
#include "bookmarkshistorymodel.h"
#include "browsermanager.h"
#include <QDateTime>
#include <QDebug>
#include <QSqlError>
#define QUERY_LIMIT 1000
using namespace AngelFish;
BookmarksHistoryModel::BookmarksHistoryModel()
{
connect(BrowserManager::instance(), &BrowserManager::databaseTableChanged,
this, &BookmarksHistoryModel::onDatabaseChanged);
}
void BookmarksHistoryModel::setActive(bool a)
{
if (m_active == a) return;
m_active = a;
if (m_active)
setQuery();
else
clear();
emit activeChanged();
}
void BookmarksHistoryModel::setBookmarks(bool b)
{
if (m_bookmarks == b) return;
m_bookmarks = b;
setQuery();
emit bookmarksChanged();
}
void BookmarksHistoryModel::setHistory(bool h)
{
if (m_history == h) return;
m_history = h;
setQuery();
emit historyChanged();
}
void BookmarksHistoryModel::setFilter(const QString &f)
{
if (m_filter == f) return;
m_filter = f;
setQuery();
emit filterChanged();
}
void BookmarksHistoryModel::onDatabaseChanged(const QString &table)
{
if ( (table == "bookmarks" && m_bookmarks) ||
(table == "history" && m_history) )
setQuery();
}
void BookmarksHistoryModel::setQuery()
{
QString command;
const char *b = "SELECT rowid AS id, url, title, icon, :now - lastVisited AS lastVisited, %1 AS bookmarked FROM %2 ";
QString filter = m_filter.isEmpty() ? QString() : "WHERE url LIKE '%' || :filter || '%' OR title LIKE '%' || :filter || '%'";
bool include_history = m_history && !(m_bookmarks && m_filter.isEmpty());
if (m_bookmarks)
command = QString(b).arg(1).arg("bookmarks") + filter;
if (m_bookmarks && include_history)
command += "\n UNION \n";
if (include_history)
command += QString(b).arg(0).arg("history") + filter;
command += "\n ORDER BY bookmarked, lastVisited ASC";
if (include_history)
command += QStringLiteral("\n LIMIT %1").arg(QUERY_LIMIT);
qint64 ref = QDateTime::currentSecsSinceEpoch();
QSqlQuery query;
if (!query.prepare(command)) {
qWarning() << Q_FUNC_INFO << "Failed to prepare SQL statement";
qWarning() << query.lastQuery();
qWarning() << query.lastError();
return;
}
if (!m_filter.isEmpty())
query.bindValue(":filter", m_filter);
query.bindValue(":now", ref);
if (!query.exec()) {
qWarning() << Q_FUNC_INFO << "Failed to execute SQL statement";
qWarning() << query.lastQuery();
qWarning() << query.lastError();
return;
}
SqlQueryModel::setQuery(query);
}
#ifndef BOOKMARKSHISTORYMODEL_H
#define BOOKMARKSHISTORYMODEL_H
#include "sqlquerymodel.h"
namespace AngelFish {
/**
* @class BookmarksHistoryModel
* @short Model for listing Bookmarks and History items.
*/
class BookmarksHistoryModel : public SqlQueryModel
{
Q_OBJECT
// while active, data is shown and changes in the used database table(s)
// will trigger new query
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
// set to true for including bookmarks
Q_PROPERTY(bool bookmarks READ bookmarks WRITE setBookmarks NOTIFY bookmarksChanged)
// set to true for including history
Q_PROPERTY(bool history READ history WRITE setHistory NOTIFY historyChanged)
// set to string to filter url or title by it. without filter set, only
// bookmarks are shown
Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged)
public:
BookmarksHistoryModel();
bool active() const { return m_active; }
void setActive(bool a);
bool bookmarks() const { return m_bookmarks; }
void setBookmarks(bool b);
bool history() const { return m_history; }
void setHistory(bool h);
QString filter() const { return m_filter; }
void setFilter(const QString &f);
signals:
void activeChanged();
void bookmarksChanged();
void historyChanged();
void filterChanged();
private:
void onDatabaseChanged(const QString &table);
void setQuery();
private:
bool m_active = true;
bool m_bookmarks = false;
bool m_history = false;
QString m_filter;
};
} // namespace
#endif // BOOKMARKSHISTORYMODEL_H
......@@ -32,6 +32,7 @@ BrowserManager *BrowserManager::s_instance = nullptr;
BrowserManager::BrowserManager(QObject *parent) : QObject(parent), m_settings(new QSettings(this))
{
connect(&m_dbmanager, &DBManager::databaseTableChanged, this, &BrowserManager::databaseTableChanged);
}
BrowserManager::~BrowserManager()
......@@ -65,11 +66,13 @@ void BrowserManager::addBookmark(const QVariantMap &bookmarkdata)
qDebug() << "Add bookmark";
qDebug() << " data: " << bookmarkdata;
bookmarks()->add(QJsonObject::fromVariantMap(bookmarkdata));
m_dbmanager.addBookmark(bookmarkdata);
}
void BrowserManager::removeBookmark(const QString &url)
{
bookmarks()->remove(url);
m_dbmanager.removeBookmark(url);
}
void BrowserManager::addToHistory(const QVariantMap &pagedata)
......@@ -78,12 +81,24 @@ void BrowserManager::addToHistory(const QVariantMap &pagedata)
// qDebug() << " data: " << pagedata;
history()->add(QJsonObject::fromVariantMap(pagedata));
emit historyChanged();
m_dbmanager.addToHistory(pagedata);
}
void BrowserManager::removeFromHistory(const QString &url)
{
history()->remove(url);
emit historyChanged();
m_dbmanager.removeFromHistory(url);
}
void BrowserManager::lastVisited(const QString &url)
{
m_dbmanager.lastVisited(url);
}
void BrowserManager::updateIcon(const QString &url, const QString &iconSource)
{
m_dbmanager.updateIcon(url, iconSource);
}
void BrowserManager::setHomepage(const QString &homepage)
......
......@@ -24,6 +24,7 @@
#include <QObject>
#include "dbmanager.h"
#include "urlmodel.h"
class QSettings;
......@@ -73,6 +74,8 @@ signals:
void searchBaseUrlChanged();
void initialUrlChanged();
void databaseTableChanged(QString table);
public slots:
void addBookmark(const QVariantMap &bookmarkdata);
void removeBookmark(const QString &url);
......@@ -80,6 +83,9 @@ public slots:
void addToHistory(const QVariantMap &pagedata);
void removeFromHistory(const QString &url);
void lastVisited(const QString &url);
void updateIcon(const QString &url, const QString &iconSource);
void setHomepage(const QString &homepage);
void setSearchBaseUrl(const QString &searchBaseUrl);
......@@ -87,6 +93,8 @@ private:
// BrowserManager should only be createdd by calling the instance() function
BrowserManager(QObject *parent = nullptr);
DBManager m_dbmanager;
UrlModel *m_bookmarks = nullptr;
UrlModel *m_history = nullptr;
QSettings *m_settings;
......
......@@ -47,7 +47,9 @@ Kirigami.ScrollablePage {
interactive: height < contentHeight
clip: true
model: BrowserManager.bookmarks
model: BookmarksHistoryModel {
bookmarks: true
}
delegate: Kirigami.DelegateRecycler {
width: parent.width
......
......@@ -45,10 +45,11 @@ Kirigami.ScrollablePage {
interactive: height < contentHeight
clip: true
model: UrlFilterProxyModel {
sourceModel: BrowserManager.history
model: BookmarksHistoryModel {
history: true
}
delegate: Kirigami.DelegateRecycler {
width: parent.width
sourceComponent: delegateComponent
......
......@@ -82,7 +82,7 @@ Controls.Drawer {
onAccepted: applyUrl()
onTextChanged: {
if (!openedState) return; // avoid filtering
urlFilter.setFilterFixedString(text);
urlFilter.filter = text;
justOpened = false;
}
Keys.onEscapePressed: if (overlay.sheetOpen) overlay.close()
......@@ -129,9 +129,11 @@ Controls.Drawer {
width: parent.width
}
model: UrlFilterProxyModel {
model: BookmarksHistoryModel {
id: urlFilter
sourceModel: (!openedState || justOpened || !urlInput.text) ? BrowserManager.bookmarks : BrowserManager.history
active: openedState
bookmarks: true
history: true
}
}
}
......@@ -144,7 +146,7 @@ Controls.Drawer {
urlInput.forceActiveFocus();
urlInput.selectAll();
justOpened = true;
urlFilter.setFilterFixedString("");
urlFilter.filter = "";
openedState = true;
listView.positionViewAtBeginning();
}
......
......@@ -57,10 +57,6 @@ Kirigami.SwipeListItem {
source: model.icon ? model.icon : ""
}
Image {
source: preview == undefined ? "" : preview
}
}
ColumnLayout {
......
......@@ -165,7 +165,13 @@ WebEngineView {
}
if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
if (!privateMode) {
addHistoryEntry();
var request = new Object;// FIXME
request.url = currentWebView.url;
request.title = currentWebView.title;
request.icon = currentWebView.icon;
request.lastVisited = new Date();
BrowserManager.addToHistory(request);
BrowserManager.lastVisited(currentWebView.url);
}
grabThumb();
}
......@@ -192,8 +198,8 @@ WebEngineView {
}
onIconChanged: {
if (icon)
BrowserManager.history.updateIcon(url, icon)
if (icon && !privateMode)
BrowserManager.updateIcon(url, icon)
}
onNewViewRequested: {
......
......@@ -55,19 +55,6 @@ Kirigami.ApplicationWindow {
width: Kirigami.Units.gridUnit * 20
height: Kirigami.Units.gridUnit * 30
/**
* Add page of currently active webview to history
*/
function addHistoryEntry() {
//print("Adding history");
var request = new Object;// FIXME
request.url = currentWebView.url;
request.title = currentWebView.title;
request.icon = currentWebView.icon;
request.lastVisited = new Date();
BrowserManager.addToHistory(request);
}
pageStack.globalToolBar.showNavigationButtons: {
if (pageStack.depth <= 1)
return Kirigami.ApplicationHeaderStyle.None;
......
/***************************************************************************
* *
* Copyright 2020 Jonah Brüchert <jbb@kaidan.im> *
* 2020 Rinigus <rinigus.git@gmail.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
* *
***************************************************************************/
#include "dbmanager.h"
#include <QDateTime>
#include <QDebug>
#include <QStandardPaths>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QVariant>
#include <exception>
#define DB_USER_VERSION 1
#define MAX_BROWSER_HISTORY_SIZE 3000
using namespace AngelFish;
DBManager::DBManager(QObject *parent) : QObject(parent)
{
QString dbname = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
+ QStringLiteral("/angelfish/angelfish.sqlite");
QSqlDatabase database = QSqlDatabase::addDatabase("QSQLITE");
database.setDatabaseName(dbname);
if (!database.open()) {
throw std::runtime_error("Failed to open database " + dbname.toStdString());
}
if (!migrate()) {
throw std::runtime_error("Failed to initialize or migrate the schema in " + dbname.toStdString());
}
trimHistory();
}
int DBManager::version()
{
QSqlQuery query("PRAGMA user_version");
if (query.next()) {
bool ok;
int value = query.value(0).toInt(&ok);
if (ok)
return value;
}
return -1;
}
void DBManager::setVersion(int v)
{
QSqlQuery query;
query.prepare( QStringLiteral("PRAGMA user_version = %1").arg(v) );
query.exec();
}
bool DBManager::execute(const QString &command)
{
QSqlQuery query;
if (!query.exec(command)) {
qWarning() << Q_FUNC_INFO << "Failed to execute SQL statement";
qWarning() << query.lastQuery();
qWarning() << query.lastError();
return false;
}
return true;
}
bool DBManager::execute(QSqlQuery &query)
{
if (!query.exec()) {
qWarning() << Q_FUNC_INFO << "Failed to execute SQL statement";
qWarning() << query.lastQuery();
qWarning() << query.lastError();
return false;
}
return true;
}
bool DBManager::migrate()
{
for (int v = version(); v != DB_USER_VERSION; v = version()) {
if (v < 0 || v > DB_USER_VERSION) {
qCritical() << "Don't know what to do with the database schema version" << v << ". Bailing out.";
return false;
}
if (v == 0) {
if (!migrateTo1())
return false;
}
}
return true;
}
bool DBManager::migrateTo1()
{
// Starting from empty database, let's create the tables.
QString bookmarks = "CREATE TABLE bookmarks (url TEXT UNIQUE, title TEXT, icon TEXT, lastVisited INT)";
QString history = "CREATE TABLE history (url TEXT UNIQUE, title TEXT, icon TEXT, lastVisited INT)";
QString idx_bookmarks = "CREATE UNIQUE INDEX idx_bookmarks_url ON bookmarks(url)";
QString idx_history = "CREATE UNIQUE INDEX idx_history_url ON history(url)";
if (!execute(bookmarks) || !execute(history) || !execute(idx_bookmarks) || !execute(idx_history))
return false;
setVersion(1);
qDebug() << "Migrated database schema to version 1";
return true;
}
void DBManager::trimHistory()
{
execute(QStringLiteral("DELETE FROM history WHERE rowid NOT IN (SELECT rowid from history" \
" ORDER BY lastVisited DESC LIMIT %1)").arg(MAX_BROWSER_HISTORY_SIZE));
}
void DBManager::addRecord(const QString &table, const QVariantMap &pagedata)
{
QString url = pagedata.value("url").toString();
QString title = pagedata.value("title").toString();
QString icon = pagedata.value("icon").toString();
qint64 lastVisited = QDateTime::currentSecsSinceEpoch();
if (url.isEmpty()) return;
QSqlQuery query;
query.prepare(QStringLiteral("INSERT OR REPLACE INTO %1 (url, title, icon, lastVisited) " \
"VALUES (:url, :title, :icon, :lastVisited)").arg(table));
query.bindValue(":url", url);
query.bindValue(":title", title);
query.bindValue(":icon", icon);
query.bindValue(":lastVisited", lastVisited);
execute(query);
emit databaseTableChanged(table);
}
void DBManager::removeRecord(const QString &table, const QString &url)
{
if (url.isEmpty()) return;
QSqlQuery query;
query.prepare(QStringLiteral("DELETE FROM %1 WHERE url = :url").arg(table));
query.bindValue(":url", url);
execute(query);
emit databaseTableChanged(table);
}
void DBManager::updateIconRecord(const QString &table, const QString &url, const QString &iconSource)
{
if (url.isEmpty()) return;
QSqlQuery query;
query.prepare(QStringLiteral("UPDATE %1 SET icon = :icon WHERE url = :url").arg(table));
query.bindValue(":url", url);
query.bindValue(":icon", iconSource);
execute(query);
emit databaseTableChanged(table);
}
void DBManager::lastVisitedRecord(const QString &table, const QString &url)
{
if (url.isEmpty()) return;
qint64 lastVisited = QDateTime::currentSecsSinceEpoch();
QSqlQuery query;
query.prepare(QStringLiteral("UPDATE %1 SET lastVisited = :lv WHERE url = :url").arg(table));
query.bindValue(":url", url);
query.bindValue(":lv", lastVisited);
execute(query);
emit databaseTableChanged(table);
}
void DBManager::addBookmark(const QVariantMap &bookmarkdata)
{
addRecord("bookmarks", bookmarkdata);
}
void DBManager::removeBookmark(const QString &url)
{
removeRecord("bookmarks", url);
}
void DBManager::addToHistory(const QVariantMap &pagedata)
{
addRecord("history", pagedata);
}
void DBManager::removeFromHistory(const QString &url)
{
removeRecord("history", url);
}
void DBManager::updateIcon(const QString &url, const QString &iconSource)
{
updateIconRecord("bookmarks", url, iconSource);
updateIconRecord("history", url, iconSource);
}
void DBManager::lastVisited(const QString &url)
{
lastVisitedRecord("bookmarks", url);
lastVisitedRecord("history", url);
}
/***************************************************************************
* *