diff --git a/resources/js/headerAnchor.js b/resources/js/headerAnchor.js index 044d9cc5..dcec14e2 100644 --- a/resources/js/headerAnchor.js +++ b/resources/js/headerAnchor.js @@ -1,23 +1,23 @@ -function isHeaderElement(elem) -{ +// Track the current anchor for history state +let currentAnchor = null; +let isNavigating = false; + +function isHeaderElement(elem) { return elem.nodeName.match(/^H\d+$/) && elem.textContent; } -function getDOMElementsPreorderDFS(elem, pred) -{ +function getDOMElementsPreorderDFS(elem, pred) { var result = []; if (pred(elem)) result.push(elem); - for ( const child of elem.children) + for (const child of elem.children) result = result.concat(getDOMElementsPreorderDFS(child, pred)); return result; } -function anchorHeaderElements(headers) -{ - return Array.from(headers, function(elem, i) - { +function anchorHeaderElements(headers) { + return Array.from(headers, function(elem, i) { const text = elem.textContent.trim().replace(/"/g, '\\"'); const level = parseInt(elem.nodeName.substr(1)); const anchor = `kiwix-toc-${i}`; @@ -25,29 +25,140 @@ function anchorHeaderElements(headers) const anchorElem = document.createElement("a"); anchorElem.id = anchor; - /* Mark header content with something we can reference. */ + // Mark header content with something we can reference elem.insertAdjacentElement("afterbegin", anchorElem); return { text, level, anchor }; }); } -function getHeadersJSONStr() -{ +function getHeadersJSONStr() { const headerInfo = { url: window.location.href.replace(location.hash,""), headers: [] }; - if (document.body !== undefined) - { + if (document.body !== undefined) { const headers = getDOMElementsPreorderDFS(document.body, isHeaderElement); headerInfo.headers = anchorHeaderElements(headers); } return JSON.stringify(headerInfo); } -new QWebChannel(qt.webChannelTransport, function(channel) { - var kiwixObj = channel.objects.kiwixChannelObj; - kiwixObj.sendHeadersJSONStr(getHeadersJSONStr()); - kiwixObj.navigationRequested.connect(function(url, anchor) { - if (window.location.href.replace(location.hash,"") == url) - document.getElementById(anchor).scrollIntoView(); - }); -}); +function scrollToAnchor(anchor, updateHistory = false) { + if (!anchor || typeof anchor !== 'string') { + return false; + } + + // Skip if already navigating to the same anchor, unless triggered by history + if (isNavigating && anchor === currentAnchor && !updateHistory) { + // Continue if from history navigation + const isFromHistory = document.referrer === '' || + (window.history.state && window.history.state.anchor === anchor); + if (!isFromHistory) { + return true; + } + } + + try { + isNavigating = true; + const element = document.getElementById(anchor); + if (element) { + setTimeout(() => { + element.scrollIntoView({behavior: 'smooth'}); + currentAnchor = anchor; + + // Update the URL in history if requested + if (updateHistory && window.history && window.history.pushState) { + try { + const baseUrl = window.location.href.replace(location.hash, ""); + window.history.pushState({ anchor: anchor }, "", baseUrl + "#" + anchor); + } catch (e) { } + } + + // Reset navigation flag after a short delay + setTimeout(() => { + isNavigating = false; + }, 150); + }, 10); + + return true; + } + isNavigating = false; + return false; + } catch (e) { + isNavigating = false; + return false; + } +} + +function initializeWebChannel() { + try { + if (typeof qt === 'undefined' || typeof qt.webChannelTransport === 'undefined') { + return; + } + + new QWebChannel(qt.webChannelTransport, function(channel) { + if (!channel || !channel.objects || !channel.objects.kiwixChannelObj) { + return; + } + + var kiwixObj = channel.objects.kiwixChannelObj; + + try { + kiwixObj.sendHeadersJSONStr(getHeadersJSONStr()); + } catch (e) { } + + // Handle navigation requests from Qt + kiwixObj.navigationRequested.connect(function(url, anchor) { + // Skip if already navigating to the same anchor + if (isNavigating && anchor === currentAnchor) { + return; + } + + if (window.location.href.replace(location.hash, "") == url) { + scrollToAnchor(anchor, false); + } + }); + + // Handle browser history navigation (back/forward buttons) + window.addEventListener('popstate', function(event) { + if (isNavigating) { + return; + } + + // Handle navigation from history + let anchorFound = false; + if (location.hash) { + const anchor = location.hash.substring(1); + anchorFound = scrollToAnchor(anchor, false); + } else if (event.state && event.state.anchor) { + anchorFound = scrollToAnchor(event.state.anchor, false); + } + + // Notify Qt about the navigation to update TOC + if (anchorFound && kiwixObj) { + setTimeout(function() { + try { + const currentHash = location.hash ? location.hash.substring(1) : + (event.state && event.state.anchor ? event.state.anchor : null); + + if (currentHash) { + kiwixObj.sendConsoleMessage(JSON.stringify({ + type: "history-navigation", + message: "Browser history navigation event", + anchor: currentHash, + url: window.location.href, + timestamp: Date.now() + })); + } + } catch (e) { } + }, 150); + } + }); + }); + } catch (e) { } +} + +// Initialize web channel when document is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeWebChannel); +} else { + initializeWebChannel(); +} \ No newline at end of file diff --git a/src/kiwixwebchannelobject.h b/src/kiwixwebchannelobject.h index 9e37d38d..ba383252 100644 --- a/src/kiwixwebchannelobject.h +++ b/src/kiwixwebchannelobject.h @@ -11,10 +11,12 @@ class KiwixWebChannelObject : public QObject explicit KiwixWebChannelObject(QObject *parent = nullptr) : QObject(parent) {}; Q_INVOKABLE void sendHeadersJSONStr(const QString& headersJSONStr) { emit headersChanged(headersJSONStr); }; + Q_INVOKABLE void sendConsoleMessage(const QString& message) { emit consoleMessageReceived(message); }; signals: void headersChanged(const QString& headersJSONStr); void navigationRequested(const QString& url, const QString& anchor); + void consoleMessageReceived(const QString& message); }; -#endif // KIWIXWEBCHANNELOBJECT_H +#endif // KIWIXWEBCHANNELOBJECT_H \ No newline at end of file diff --git a/src/tabbar.cpp b/src/tabbar.cpp index 3c7632e0..01128bd7 100644 --- a/src/tabbar.cpp +++ b/src/tabbar.cpp @@ -4,6 +4,7 @@ class QMenu; #include "kiwixapp.h" #include "css_constants.h" +#include "tableofcontentbar.h" #include #include #include @@ -11,6 +12,7 @@ class QMenu; #include #include #include +#include #define QUITIFNULL(VIEW) if (nullptr==(VIEW)) { return; } #define CURRENTIFNULL(VIEW) if(nullptr==VIEW) { VIEW = currentZimView();} @@ -269,8 +271,43 @@ void TabBar::triggerWebPageAction(QWebEnginePage::WebAction action, ZimView *wid { CURRENTIFNULL(widget); QUITIFNULL(widget); - widget->getWebView()->triggerPageAction(action); - widget->getWebView()->setFocus(); + + auto webView = widget->getWebView(); + + // For back/forward navigation, use direct JavaScript history methods + if (action == QWebEnginePage::Back || action == QWebEnginePage::Forward) { + // Instead of using Qt's action, directly use JavaScript to manipulate browser history + // This ensures consistent behavior across platforms and better TOC synchronization + if (action == QWebEnginePage::Back) { + webView->page()->runJavaScript("window.history.back();"); + } else { + webView->page()->runJavaScript("window.history.forward();"); + } + + // Focus the webview + webView->setFocus(); + + // Set up a check to ensure the TOC is updated after navigation + QTimer::singleShot(300, widget, [widget]() { + auto webView = widget->getWebView(); + if (!webView) return; + + QString fragment = webView->url().fragment(); + if (!fragment.isEmpty()) { + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + tocBar->updateSelectionFromFragment(fragment); + } + } + }); + + return; + } + + // For other actions, just trigger them normally + webView->triggerPageAction(action); + webView->setFocus(); } void TabBar::closeTabsByZimId(const QString &id) diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index 78531db3..6fc2d00d 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -3,6 +3,8 @@ #include "kiwixapp.h" #include #include +#include +#include TableOfContentBar::TableOfContentBar(QWidget *parent) : QFrame(parent), @@ -24,6 +26,10 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) : ui->tree->setItemsExpandable(false); connect(ui->tree, &QTreeWidget::itemClicked, this, &TableOfContentBar::onTreeItemActivated); connect(ui->tree, &QTreeWidget::itemActivated, this, &TableOfContentBar::onTreeItemActivated); + + // Setup debounce timer + m_clickDebounceTimer.setSingleShot(true); + m_clickDebounceTimer.setInterval(300); // 300ms debounce } TableOfContentBar::~TableOfContentBar() @@ -33,7 +39,37 @@ TableOfContentBar::~TableOfContentBar() void TableOfContentBar::onTreeItemActivated(QTreeWidgetItem *item) { - emit navigationRequested(m_url, item->data(0, Qt::UserRole).toString()); + //Safety check + if (!item) { + return; + } + + // Get the anchor from the item + QVariant anchorVariant = item->data(0, Qt::UserRole); + if (!anchorVariant.isValid()) { + return; + } + + QString anchor = anchorVariant.toString(); + if (anchor.isEmpty() || m_url.isEmpty()) { + return; + } + + if (m_isNavigating || (anchor == m_lastAnchor && m_clickDebounceTimer.isActive())) { + return; + } + + m_isNavigating = true; + m_lastAnchor = anchor; + m_clickDebounceTimer.start(); + + QTimer::singleShot(10, this, [this, anchor]() { + emit navigationRequested(m_url, anchor); + + QTimer::singleShot(300, this, [this]() { + m_isNavigating = false; + }); + }); } namespace @@ -93,9 +129,40 @@ void TableOfContentBar::setupTree(const QJsonObject& headers) const auto currentUrl = webView->url().url(QUrl::RemoveFragment); if (headerUrl != currentUrl) return; - + m_url = headerUrl; ui->tree->clear(); QJsonArray headerArr = headers["headers"].toArray(); createSubTree(ui->tree->invisibleRootItem(), "", headerArr); + + // Update selection based on current URL fragment + updateSelectionFromFragment(webView->url().fragment()); } + +void TableOfContentBar::updateSelectionFromFragment(const QString& fragment) +{ + if (fragment.isEmpty() || !ui || !ui->tree) { + return; + } + + // Find the item with the matching anchor + QTreeWidgetItemIterator it(ui->tree); + while (*it) { + QVariant anchorVariant = (*it)->data(0, Qt::UserRole); + if (!anchorVariant.isValid()) { + ++it; + continue; + } + + QString anchor = anchorVariant.toString(); + if (anchor == fragment) { + // Select the item without triggering navigation + ui->tree->blockSignals(true); + ui->tree->setCurrentItem(*it); + ui->tree->scrollToItem(*it); + ui->tree->blockSignals(false); + break; + } + ++it; + } +} \ No newline at end of file diff --git a/src/tableofcontentbar.h b/src/tableofcontentbar.h index c2a2356a..c019f45b 100644 --- a/src/tableofcontentbar.h +++ b/src/tableofcontentbar.h @@ -2,6 +2,7 @@ #define TABLEOFCONTENTBAR_H #include +#include namespace Ui { class tableofcontentbar; @@ -20,6 +21,7 @@ class TableOfContentBar : public QFrame public slots: void setupTree(const QJsonObject& headers); void onTreeItemActivated(QTreeWidgetItem* item); + void updateSelectionFromFragment(const QString& fragment); signals: void navigationRequested(const QString& url, const QString& anchor); @@ -27,6 +29,9 @@ public slots: private: Ui::tableofcontentbar *ui; QString m_url; + QTimer m_clickDebounceTimer; + bool m_isNavigating = false; + QString m_lastAnchor; }; -#endif // TABLEOFCONTENTBAR_H +#endif // TABLEOFCONTENTBAR_H \ No newline at end of file diff --git a/src/webview.cpp b/src/webview.cpp index 19cd84e1..30bc5174 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -21,6 +21,7 @@ class QMenu; #include #include "kiwixwebchannelobject.h" #include "tableofcontentbar.h" +#include zim::Entry getArchiveEntryFromUrl(const zim::Archive& archive, const QUrl& url); QString askForSaveFilePath(const QString& suggestedName); @@ -85,6 +86,32 @@ WebView::WebView(QWidget *parent) { setPage(new WebPage(this)); QObject::connect(this, &QWebEngineView::urlChanged, this, &WebView::onUrlChanged); + QObject::connect(this, &QWebEngineView::urlChanged, this, &WebView::handleTocHistoryNavigation); + + // Inject JavaScript to handle popstate events for better history navigation + connect(page(), &QWebEnginePage::loadStarted, this, [=]() { + // Inject a popstate event handler to ensure smooth navigation and scrolling + QString js = + "if (!window._kiwixHandlerInstalled) {" + " window._kiwixHandlerInstalled = true;" + " try {" + " window.addEventListener('popstate', function(event) {" + " if (location.hash) {" + " var anchorId = location.hash.substring(1);" + " var element = document.getElementById(anchorId);" + " if (element) {" + " element.scrollIntoView({behavior: 'smooth'});" + " }" + " }" + " });" + " } catch(e) {" + " console.error('Error in popstate handler:', e);" + " }" + "}"; + + page()->runJavaScript(js); + }); + connect(this->page(), &QWebEnginePage::linkHovered, this, [=] (const QString& url) { m_linkHovered = url; }); @@ -107,15 +134,54 @@ WebView::WebView(QWidget *parent) const auto kiwixChannelObj = new KiwixWebChannelObject; page()->setWebChannel(channel, QWebEngineScript::UserWorld); channel->registerObject("kiwixChannelObj", kiwixChannelObj); - + const auto tabbar = KiwixApp::instance()->getTabWidget(); connect(tabbar, &TabBar::currentTitleChanged, this, &WebView::onCurrentTitleChanged); connect(kiwixChannelObj, &KiwixWebChannelObject::headersChanged, this, &WebView::onHeadersReceived); + connect(kiwixChannelObj, &KiwixWebChannelObject::navigationRequested, + this, &WebView::onNavigationRequested); + connect(kiwixChannelObj, &KiwixWebChannelObject::consoleMessageReceived, + this, &WebView::onConsoleMessageReceived); const auto tocbar = KiwixApp::instance()->getMainWindow()->getTableOfContentBar(); connect(this, &WebView::headersChanged, tocbar, &TableOfContentBar::setupTree); connect(tocbar, &TableOfContentBar::navigationRequested, this, &WebView::onNavigationRequested); connect(this, &WebView::navigationRequested, kiwixChannelObj, &KiwixWebChannelObject::navigationRequested); + + // Add this in the WebView constructor + connect(this, &QWebEngineView::loadFinished, this, [this](bool success) { + if (success) { + // Update history action states + auto app = KiwixApp::instance(); + app->getAction(KiwixApp::HistoryBackAction)->setEnabled(history()->canGoBack()); + app->getAction(KiwixApp::HistoryForwardAction)->setEnabled(history()->canGoForward()); + + // This will catch ALL successful page loads, including history navigation + // Update TOC selection if URL has fragment + if (url().hasFragment()) { + QString fragment = url().fragment(); + qDebug() << "Page load finished - Updating TOC selection for fragment:" << fragment; + + // Use a slightly longer delay for more reliability + QTimer::singleShot(250, this, [this, fragment]() { + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + qDebug() << "Applying TOC selection update for fragment:" << fragment; + tocBar->updateSelectionFromFragment(fragment); + + // Force another scroll to the anchor to ensure it's visible + page()->runJavaScript(QString( + "if (document.getElementById('%1')) {" + " console.log('Page loaded - Ensuring scroll to anchor: %1');" + " document.getElementById('%1').scrollIntoView({behavior: 'smooth'});" + "}" + ).arg(fragment)); + } + }); + } + } + }); } WebView::~WebView() @@ -224,18 +290,133 @@ void WebView::onCurrentTitleChanged() void WebView::onHeadersReceived(const QString& headersJSONStr) { - const auto tabbar = KiwixApp::instance()->getTabWidget(); - m_headers = QJsonDocument::fromJson(headersJSONStr.toUtf8()).object(); - - if (tabbar->currentWebView() == this) + QJsonDocument doc = QJsonDocument::fromJson(headersJSONStr.toUtf8()); + if (!doc.isObject()) + return; + + m_headers = doc.object(); + if (KiwixApp::instance()->getTabWidget()->currentWebView() == this) emit headersChanged(m_headers); } +void WebView::onConsoleMessageReceived(const QString& message) +{ + // Parse the JSON message + QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); + if (!doc.isObject()) + return; + + QJsonObject obj = doc.object(); + QString type = obj["type"].toString(); + QString msg = obj["message"].toString(); + + // Special handling for history navigation messages + if (type == "history-navigation") { + // Check if we have an anchor in the JSON + if (obj.contains("anchor") && obj["anchor"].isString()) { + QString anchor = obj["anchor"].toString(); + + if (!anchor.isEmpty()) { + // Sync TOC selection with the current fragment + QTimer::singleShot(10, this, [this, anchor]() { + syncTOCWithFragment(anchor); + }); + } + } + } +} + void WebView::onNavigationRequested(const QString &url, const QString &anchor) { + // Safety checks const auto tabbar = KiwixApp::instance()->getTabWidget(); - if (tabbar->currentWebView() == this) - emit navigationRequested(url, anchor); + if (!tabbar || tabbar->currentWebView() != this) { + return; + } + + // Create a QUrl with fragment for history + QUrl historyUrl(url); + historyUrl.setFragment(anchor); + + // Update TOC selection + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + tocBar->updateSelectionFromFragment(anchor); + } + + // Check if we're already at this anchor + if (this->url().hasFragment() && this->url().fragment() == anchor) { + return; + } + + // Properly escape the URL and anchor for JavaScript + QString escapedUrl = url.toHtmlEscaped(); + QString escapedAnchor = anchor.toHtmlEscaped(); + + // JS code to avoid Unexpected end of input error + QString js = QString( + "try {" + " if (window.history && window.history.pushState) {" + " var elem = document.getElementById('%1');" + " if (elem) {" + " window.history.pushState({anchor: '%1'}, '', '%2#%1');" + " elem.scrollIntoView({behavior: 'smooth'});" + " } else {" + " console.error('Anchor not found: %1');" + " }" + " }" + "} catch(e) {" + " console.error('Navigation error:', e);" + "}" + ).arg(escapedAnchor).arg(escapedUrl); + // Execute JavaScript safely with a callback + page()->runJavaScript(js, [this, url, anchor](const QVariant &result) { + Q_UNUSED(result); + // Emit the navigation signal to the JavaScript after the history is updated + // Use a small delay to prevent navigation loops + QTimer::singleShot(50, this, [this, url, anchor]() { + emit navigationRequested(url, anchor); + }); + }); +} + +// Add a method to handle history navigation for TOC entries +void WebView::handleTocHistoryNavigation(const QUrl &url) +{ + // Safety check + if (!url.isValid()) { + return; + } + + // For any URL with a fragment, update TOC selection + if (url.hasFragment()) { + QString anchor = url.fragment(); + + // Safety check for empty anchor + if (anchor.isEmpty()) { + return; + } + + // Update TOC selection for any URL with a fragment + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + tocBar->updateSelectionFromFragment(anchor); + } + + // Only for URLs on the current page, emit navigation signal + if (url.url(QUrl::RemoveFragment) == this->url().url(QUrl::RemoveFragment)) { + // Only emit navigation signal if we're not already at this anchor + if (!(this->url().hasFragment() && this->url().fragment() == anchor)) { + // Use a small delay to prevent navigation loops + QTimer::singleShot(50, this, [this, url, anchor]() { + // Emit navigation signal instead of loading the page + emit navigationRequested(url.url(QUrl::RemoveFragment), anchor); + }); + } + } + } } void WebView::addHistoryItemAction(QMenu *menu, @@ -258,7 +439,25 @@ void WebView::gotoTriggeredHistoryItemAction() if (n < 0 || n >= h->count()) return; - h->goToItem(h->itemAt(n)); + // Store the item before navigating + QWebEngineHistoryItem item = h->itemAt(n); + + // If the target URL has a fragment, update TOC after navigation + if (item.url().hasFragment()) { + QString fragment = item.url().fragment(); + + // After navigation, update the TOC selection + QTimer::singleShot(200, this, [this, fragment]() { + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + tocBar->updateSelectionFromFragment(fragment); + } + }); + } + + // Go to the history item + h->goToItem(item); } @@ -278,6 +477,15 @@ void WebView::onUrlChanged(const QUrl& url) { auto app = KiwixApp::instance(); app->saveListOfOpenTabs(); if (m_currentZimId == zimId ) { + // Even if the ZIM ID hasn't changed, we still need to update TOC selection + if (url.hasFragment()) { + QString fragment = url.fragment(); + + // Sync TOC selection after a short delay to ensure the page has loaded + QTimer::singleShot(100, this, [this, fragment]() { + syncTOCWithFragment(fragment); + }); + } return; } m_currentZimId = zimId; @@ -286,6 +494,10 @@ void WebView::onUrlChanged(const QUrl& url) { auto zoomFactor = app->getSettingsManager()->getZoomFactorByZimId(zimId); this->setZoomFactor(zoomFactor); emit iconChanged(m_icon); + + // Update history action states + app->getAction(KiwixApp::HistoryBackAction)->setEnabled(history()->canGoBack()); + app->getAction(KiwixApp::HistoryForwardAction)->setEnabled(history()->canGoForward()); } void WebView::wheelEvent(QWheelEvent *event) { @@ -430,3 +642,35 @@ bool WebView::event(QEvent *event) } return true; } + +void WebView::syncTOCWithFragment(const QString& fragment) +{ + if (fragment.isEmpty()) { + return; + } + + // Get the TOC bar and update selection + auto app = KiwixApp::instance(); + auto tocBar = app->getMainWindow()->getTableOfContentBar(); + if (tocBar) { + // Update the TOC selection to match the current fragment + tocBar->updateSelectionFromFragment(fragment); + + // Ensure the fragment is properly scrolled into view + QString escapedFragment = fragment.toHtmlEscaped(); + QString js = QString( + "try {" + " var element = document.getElementById('%1');" + " if (element) {" + " element.scrollIntoView({behavior: 'smooth'});" + " setTimeout(function() {" + " element.scrollIntoView({behavior: 'smooth'});" + " }, 100);" + " }" + "} catch(e) { console.error('Error in syncTOCWithFragment:', e); }" + ).arg(escapedFragment); + + // Execute the JavaScript + page()->runJavaScript(js); + } +} \ No newline at end of file diff --git a/src/webview.h b/src/webview.h index d6563920..84c5b2a1 100644 --- a/src/webview.h +++ b/src/webview.h @@ -50,6 +50,15 @@ class WebView : public QWebEngineView public slots: void onUrlChanged(const QUrl& url); + + /** + * @brief Synchronizes TOC selection with the given fragment + * @param fragment The anchor ID to select in the TOC + * + * This method updates the TOC selection and ensures the selected element + * is properly scrolled into view in the browser. + */ + void syncTOCWithFragment(const QString& fragment); signals: void iconChanged(const QIcon& icon); @@ -72,7 +81,9 @@ private slots: void gotoTriggeredHistoryItemAction(); void onCurrentTitleChanged(); void onHeadersReceived(const QString& headersJSONStr); + void onConsoleMessageReceived(const QString& message); void onNavigationRequested(const QString& url, const QString& anchor); + void handleTocHistoryNavigation(const QUrl& url); private: void addHistoryItemAction(QMenu *menu, const QWebEngineHistoryItem &item, int n) const; @@ -82,4 +93,4 @@ private slots: QJsonObject m_headers; }; -#endif // WEBVIEW_H +#endif // WEBVIEW_H \ No newline at end of file